Refactor to MVVM architecture

This commit is contained in:
世界
2025-11-26 22:25:48 +08:00
parent 61f1036f57
commit b71cbae382
31 changed files with 1551 additions and 1229 deletions
@@ -5,10 +5,7 @@ import SwiftUI
@MainActor
public struct ServiceLogView: View {
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var content = ""
@State private var alert: Alert?
@StateObject private var viewModel = ServiceLogViewModel()
private let logFont = Font.system(.caption, design: .monospaced)
@@ -16,18 +13,18 @@ public struct ServiceLogView: View {
public var body: some View {
viewBuilder {
if isLoading {
if viewModel.isLoading {
ProgressView().onAppear {
Task {
await loadContent()
await viewModel.loadContent()
}
}
} else {
if content.isEmpty {
if viewModel.isEmpty {
Text("Empty content")
} else {
ScrollView {
Text(content)
Text(viewModel.content)
.font(logFont)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
@@ -36,17 +33,17 @@ public struct ServiceLogView: View {
}
}
.toolbar {
if !content.isEmpty {
if !viewModel.isEmpty {
#if !os(tvOS)
ShareButtonCompat($alert) {
ShareButtonCompat($viewModel.alert) {
Label("Export", systemImage: "square.and.arrow.up.fill")
} itemURL: {
try content.generateShareFile(name: "service.log")
try viewModel.generateShareFile()
}
#endif
Button(role: .destructive) {
Task {
await deleteContent()
await viewModel.deleteContent(dismiss: dismiss)
}
} label: {
#if !os(tvOS)
@@ -58,56 +55,10 @@ public struct ServiceLogView: View {
}
}
}
.alertBinding($alert)
.alertBinding($viewModel.alert)
.navigationTitle("Service Log")
#if os(tvOS)
.focusable()
#endif
}
private nonisolated func loadContent() async {
var content = ""
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
} catch {}
if content.isEmpty {
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
} catch {}
}
#if DEBUG
if content.isEmpty {
content = "Empty content"
}
#endif
if !content.isEmpty {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let machineName = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
var deviceInfo = String("Machine: ") + machineName + "\n"
#if os(iOS)
await deviceInfo += String("System: ") + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
#elseif os(macOS)
deviceInfo += String("System: ") + "macOS " + ProcessInfo().operatingSystemVersionString + "\n"
#endif
content = deviceInfo + "\n" + content
}
await MainActor.run { [content] in
self.content = content
isLoading = false
}
}
private nonisolated func deleteContent() async {
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
await MainActor.run {
dismiss()
isLoading = true
}
}
}
@@ -0,0 +1,64 @@
import Foundation
import Library
import SwiftUI
@MainActor
final class ServiceLogViewModel: ObservableObject {
@Published var isLoading = true
@Published var content = ""
@Published var alert: Alert?
var isEmpty: Bool {
content.isEmpty
}
nonisolated func loadContent() async {
var content = ""
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
} catch {}
if content.isEmpty {
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
} catch {}
}
#if DEBUG
if content.isEmpty {
content = "Empty content"
}
#endif
if !content.isEmpty {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let machineName = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
var deviceInfo = String("Machine: ") + machineName + "\n"
#if os(iOS)
await deviceInfo += String("System: ") + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
#elseif os(macOS)
deviceInfo += String("System: ") + "macOS " + ProcessInfo().operatingSystemVersionString + "\n"
#endif
content = deviceInfo + "\n" + content
}
await MainActor.run { [content] in
self.content = content
isLoading = false
}
}
nonisolated func deleteContent(dismiss: DismissAction) async {
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
await MainActor.run {
dismiss()
isLoading = true
}
}
func generateShareFile() throws -> URL {
try content.generateShareFile(name: "service.log")
}
}
@@ -92,8 +92,7 @@ public struct SettingView: View {
}
}
@State private var isLoading = true
@State private var taiwanFlagAvailable = false
@StateObject private var viewModel = SettingViewModel()
public init() {}
public var body: some View {
@@ -146,24 +145,15 @@ public struct SettingView: View {
Label("Service Log", systemImage: "doc.on.clipboard")
}
FormTextItem("Taiwan Flag Available", "touchid") {
if isLoading {
if viewModel.isLoading {
Text("Loading...")
.onAppear {
Task.detached {
let available: Bool
if ApplicationLibrary.inPreview {
available = true
} else {
available = !DeviceCensorship.isChinaDevice()
}
await MainActor.run {
taiwanFlagAvailable = available
isLoading = false
}
await viewModel.checkTaiwanFlagAvailability()
}
}
} else {
Text(taiwanFlagAvailable.toString())
Text(viewModel.taiwanFlagAvailable.toString())
}
}
}
@@ -0,0 +1,21 @@
import Library
import SwiftUI
@MainActor
final class SettingViewModel: ObservableObject {
@Published var isLoading = true
@Published var taiwanFlagAvailable = false
nonisolated func checkTaiwanFlagAvailability() async {
let available: Bool
if ApplicationLibrary.inPreview {
available = true
} else {
available = !DeviceCensorship.isChinaDevice()
}
await MainActor.run {
taiwanFlagAvailable = available
isLoading = false
}
}
}