Refactor ViewModel

This commit is contained in:
世界
2025-12-01 17:11:34 +08:00
parent 30492c1191
commit 8e764c56eb
34 changed files with 136 additions and 160 deletions
@@ -0,0 +1,31 @@
import SwiftUI
@MainActor
open class BaseViewModel: ObservableObject {
@Published public var alert: Alert?
@Published public var isLoading = false
public init() {}
public func showError(_ error: Error) {
alert = Alert(error)
}
public func execute(_ operation: () async throws -> Void) async {
do {
try await operation()
} catch {
alert = Alert(error)
}
}
public func executeOnBackground(_ operation: @escaping @Sendable () async throws -> Void) async {
do {
try await operation()
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
}
@@ -35,7 +35,7 @@ public struct NavigationButtonsView: View {
#if os(tvOS)
private var tvOSBody: some View {
viewBuilder {
Group {
if showConnectionsButton {
Button {
onConnectionsTap()
@@ -1,7 +1,7 @@
import SwiftUI
public func NavigationStackCompat(@ViewBuilder content: () -> some View) -> some View {
viewBuilder {
Group {
if #available(iOS 17.0, macOS 13.0, tvOS 17.0, *) {
// view not updating in iOS 16, but why?
NavigationStack {
@@ -4,7 +4,7 @@
import SwiftUI
public func RequestReviewButton(label: @escaping () -> some View) -> some View {
viewBuilder {
Group {
if #available(iOS 16.0, macOS 13.0, visionOS 1.0, *) {
RequestReviewButton0(label: label)
} else {
@@ -1,6 +0,0 @@
import Foundation
import SwiftUI
public func viewBuilder(@ViewBuilder _ builder: () -> some View) -> some View {
builder()
}
@@ -2,22 +2,12 @@ import SwiftUI
public extension View {
func onChangeCompat(of value: some Equatable, _ action: @escaping () -> Void) -> some View {
// if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
// return onChange(of: value, action)
// } else {
onChange(of: value) { _ in
action()
}
// }
}
func onChangeCompat<V>(of value: V, _ action: @escaping (_ newValue: V) -> Void) -> some View where V: Equatable {
// if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
// return onChange(of: value) { _, newValue in
// action(newValue)
// }
// } else {
onChange(of: value, perform: action)
// }
}
}
@@ -3,11 +3,9 @@ import Library
import SwiftUI
@MainActor
public class ConnectionListViewModel: ObservableObject {
@Published public var isLoading = true
public class ConnectionListViewModel: BaseViewModel {
@Published public var connections: [Connection] = []
@Published public var searchText = ""
@Published public var alert: Alert?
@Published public var connectionStateFilter: ConnectionStateFilter {
didSet {
saveStateFilterTask?.cancel()
@@ -30,9 +28,11 @@ public class ConnectionListViewModel: ObservableObject {
private var saveStateFilterTask: Task<Void, Never>?
private var saveSortTask: Task<Void, Never>?
public init() {
public override init() {
connectionStateFilter = .active
connectionSort = .byDate
super.init()
isLoading = true
}
public func connect() {
@@ -7,7 +7,7 @@ import SwiftUI
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@StateObject private var coordinator = DashboardCoordinator()
@StateObject private var coordinator = DashboardViewModel()
@State private var cardConfigurationVersion = 0
#if os(iOS) || os(tvOS)
@State private var showCardManagement = false
@@ -25,7 +25,7 @@ public struct ExtensionStatusView: View {
}
public var body0: some View {
viewBuilder {
Group {
VStack {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) {
if ApplicationLibrary.inPreview {
@@ -9,7 +9,7 @@ public struct StartStopButton: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if ApplicationLibrary.inPreview {
Button {} label: {
#if os(tvOS)
@@ -52,7 +52,7 @@ public extension DashboardPage {
@MainActor
func contentView(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>, _ cardConfigurationVersion: Int) -> some View {
viewBuilder {
Group {
switch self {
case .overview:
OverviewView(profileList, selectedProfileID, systemProxyAvailable, systemProxyEnabled, cardConfigurationVersion: cardConfigurationVersion)
@@ -9,7 +9,7 @@ public struct DashboardView: View {
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@EnvironmentObject private var environments: ExtensionEnvironments
@StateObject private var coordinator = DashboardCoordinator()
@StateObject private var coordinator = DashboardViewModel()
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
#if os(macOS)
@@ -5,11 +5,9 @@ import NetworkExtension
import SwiftUI
@MainActor
public final class DashboardCoordinator: ObservableObject {
@Published public var isLoading = true
public final class DashboardViewModel: BaseViewModel {
@Published public var profileList: [ProfilePreview] = []
@Published public var selectedProfileID: Int64 = 0
@Published public var alert: Alert?
@Published public var selection = DashboardPage.overview
@Published public var systemProxyAvailable = false
@Published public var systemProxyEnabled = false
@@ -22,7 +20,10 @@ public final class DashboardCoordinator: ObservableObject {
public var onEmptyProfilesChange: ((Bool) -> Void)?
private var openURL: ((URL) -> Void)?
public init() {}
public override init() {
super.init()
isLoading = true
}
public func setOpenURL(_ openURL: @escaping (URL) -> Void) {
self.openURL = openURL
@@ -7,7 +7,7 @@ import SwiftUI
public struct OverviewView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@StateObject private var coordinator = OverviewCoordinator()
@StateObject private var coordinator = OverviewViewModel()
@StateObject private var configuration = DashboardCardConfiguration()
@Binding private var profileList: [ProfilePreview]
@@ -4,12 +4,9 @@ import Library
import SwiftUI
@MainActor
public final class OverviewCoordinator: ObservableObject {
@Published public var alert: Alert?
public final class OverviewViewModel: BaseViewModel {
@Published public var reasserting = false
public init() {}
public func switchProfile(_ profileID: Int64, profile: ExtensionProfile, environments: ExtensionEnvironments) async {
await SharedPreferences.selectedProfileID.set(profileID)
environments.selectedProfileUpdate.send()
@@ -3,14 +3,15 @@ import Library
import SwiftUI
@MainActor
public class GroupListViewModel: ObservableObject {
@Published public var isLoading = true
public class GroupListViewModel: BaseViewModel {
@Published public var groups: [OutboundGroup] = []
@Published public var alert: Alert?
private var pendingSelections: [String: String] = [:]
public init() {}
public override init() {
super.init()
isLoading = true
}
public func connect() {
if ApplicationLibrary.inPreview {
@@ -64,7 +64,7 @@ public extension NavigationPage {
@MainActor
var contentView: some View {
viewBuilder {
Group {
switch self {
case .dashboard:
DashboardView()
@@ -22,7 +22,7 @@
@Environment(\.profileEditor) private var profileEditor
public var body: some View {
viewBuilder {
Group {
if viewModel.isLoading {
ProgressView().onAppear {
Task {
@@ -104,7 +104,7 @@
@ViewBuilder
private var defaultEditorView: some View {
viewBuilder {
Group {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
@@ -4,17 +4,17 @@
import SwiftUI
@MainActor
public final class EditProfileContentViewModel: ObservableObject {
@Published public var isLoading = true
public final class EditProfileContentViewModel: BaseViewModel {
@Published public var profile: Profile?
@Published public var profileContent = ""
@Published public var isChanged = false
@Published public var alert: Alert?
private let profileID: Int64?
public init(profileID: Int64?) {
self.profileID = profileID
super.init()
isLoading = true
}
public func markAsChanged() {
@@ -3,15 +3,11 @@ import Library
import SwiftUI
@MainActor
public final class EditProfileViewModel: ObservableObject {
@Published public var isLoading = false
public final class EditProfileViewModel: BaseViewModel {
@Published public var isChanged = false
@Published public var alert: Alert?
@Published public var shareLinkPresented = false
@Published public var shareLinkText: String?
public init() {}
public func markAsChanged() {
isChanged = true
}
@@ -7,17 +7,13 @@
import SwiftUI
@MainActor
public final class ImportProfileViewModel: ObservableObject {
@Published public var isLoading = false
public final class ImportProfileViewModel: BaseViewModel {
@Published public var selected = false
@Published public var alert: Alert?
@Published public var connection: NWConnection?
@Published public var socket: NWSocket?
@Published public var profiles: [LibboxProfilePreview]?
@Published public var isImporting = false
public init() {}
public func reset() {
if let connection {
connection.stateUpdateHandler = nil
@@ -57,7 +57,7 @@ public struct NewProfileView: View {
#if os(tvOS)
.disabled(true)
#endif
viewBuilder {
Group {
if viewModel.fileImport {
HStack {
Text("File Path")
@@ -4,7 +4,7 @@ import Library
import SwiftUI
@MainActor
public final class NewProfileViewModel: ObservableObject {
public final class NewProfileViewModel: BaseViewModel {
@Published public var isSaving = false
@Published public var profileName = ""
#if !os(tvOS)
@@ -18,9 +18,9 @@ public final class NewProfileViewModel: ObservableObject {
@Published public var autoUpdate = true
@Published public var autoUpdateInterval: Int32 = 60
@Published public var pickerPresented = false
@Published public var alert: Alert?
public init(importRequest: NewProfileView.ImportRequest? = nil) {
super.init()
if let importRequest {
profileName = importRequest.name
profileType = .remote
@@ -67,7 +67,7 @@ public struct ProfileView: View {
} else {
List {
ForEach(viewModel.profileList, id: \.id) { profile in
viewBuilder {
Group {
#if os(iOS) || os(tvOS)
if viewModel.editMode.isEditing == true {
Text(profile.name)
@@ -182,7 +182,7 @@ public struct ProfileView: View {
}
private var draggableBody: some View {
viewBuilder {
Group {
#if !os(macOS)
FormNavigationLink {
EditProfileView().environmentObject(profile.origin)
@@ -4,12 +4,10 @@ import Library
import SwiftUI
@MainActor
public class ProfileViewModel: ObservableObject {
public class ProfileViewModel: BaseViewModel {
@Published public var importRemoteProfileRequest: NewProfileView.ImportRequest?
@Published public var importRemoteProfilePresented = false
@Published public var isLoading = true
@Published public var isUpdating = false
@Published public var alert: Alert?
@Published public var profileList: [ProfilePreview] = []
#if os(iOS) || os(tvOS)
@@ -18,7 +16,10 @@ public class ProfileViewModel: ObservableObject {
private weak var environments: ExtensionEnvironments?
public init() {}
public override init() {
super.init()
isLoading = true
}
public func setEnvironments(_ environments: ExtensionEnvironments) {
self.environments = environments
@@ -13,7 +13,7 @@ public struct CoreView: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task {
@@ -17,7 +17,7 @@ public struct AppView: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task {
@@ -8,7 +8,7 @@ public struct OnDemandRulesView: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task.detached {
@@ -14,7 +14,7 @@ struct PacketTunnelView: View {
init() {}
var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task.detached {
@@ -9,7 +9,7 @@ public struct ProfileOverrideView: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task.detached {
@@ -12,7 +12,7 @@ public struct ServiceLogView: View {
public init() {}
public var body: some View {
viewBuilder {
Group {
if viewModel.isLoading {
ProgressView().onAppear {
Task {
@@ -57,7 +57,7 @@ public struct SettingView: View {
@MainActor
var contentView: some View {
viewBuilder {
Group {
switch self {
#if os(macOS)
case .app:
+1 -1
View File
@@ -129,7 +129,7 @@ public struct MenuView: View {
}
var body: some View {
viewBuilder {
Group {
if isLoading {
ProgressView().onAppear {
Task {
+55 -86
View File
@@ -37,46 +37,28 @@ struct MainView: View {
@ViewBuilder
private var tabViewContent: some View {
if shouldShowBottomAccessory {
TabView(selection: $selection) {
ForEach(NavigationPage.allCases, id: \.self) { page in
NavigationStackCompat {
page.contentView
.navigationTitle(page.title)
baseTabView
.tabViewBottomAccessory {
HStack(spacing: 12) {
if let profile = environments.extensionProfile {
StatusText(profile: profile)
.frame(maxWidth: .infinity, alignment: .leading)
}
NavigationButtonsView(
showGroupsButton: buttonState.showGroupsButton,
showConnectionsButton: buttonState.showConnectionsButton,
groupsCount: buttonState.groupsCount,
connectionsCount: buttonState.connectionsCount,
onGroupsTap: { showGroups = true },
onConnectionsTap: { showConnections = true }
)
Divider()
StartStopButton()
}
.tag(page)
.tabItem { page.label }
.padding(.horizontal)
}
}
.tabViewBottomAccessory {
HStack(spacing: 12) {
if let profile = environments.extensionProfile {
StatusText(profile: profile)
.frame(maxWidth: .infinity, alignment: .leading)
}
NavigationButtonsView(
showGroupsButton: buttonState.showGroupsButton,
showConnectionsButton: buttonState.showConnectionsButton,
groupsCount: buttonState.groupsCount,
connectionsCount: buttonState.connectionsCount,
onGroupsTap: { showGroups = true },
onConnectionsTap: { showConnections = true }
)
Divider()
StartStopButton()
}
.padding(.horizontal)
}
} else {
TabView(selection: $selection) {
ForEach(NavigationPage.allCases, id: \.self) { page in
NavigationStackCompat {
page.contentView
.navigationTitle(page.title)
}
.tag(page)
.tabItem { page.label }
}
}
baseTabView
}
}
@@ -88,25 +70,27 @@ struct MainView: View {
}
}
@ViewBuilder
private var baseTabView: some View {
TabView(selection: $selection) {
ForEach(NavigationPage.allCases, id: \.self) { page in
NavigationStackCompat {
page.contentView
.navigationTitle(page.title)
}
.tag(page)
.tabItem { page.label }
}
}
}
private var mainBody: some View {
viewBuilder {
Group {
if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
tabViewContent
.onAppear {
environments.postReload()
updateButtonVisibility()
}
.alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
environments.postReload()
}
}
.onChangeCompat(of: selection) { newValue in
if newValue == .logs {
environments.connect()
}
}
.onReceive(environments.commandClient.$groups) { _ in
updateButtonVisibility()
}
@@ -125,12 +109,6 @@ struct MainView: View {
.onReceive(environments.$emptyProfiles) { _ in
updateButtonVisibility()
}
.environment(\.selection, $selection)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL)
.sheet(isPresented: $showGroups) {
GroupsSheetContent()
}
@@ -138,38 +116,29 @@ struct MainView: View {
ConnectionsSheetContent()
}
} else {
TabView(selection: $selection) {
ForEach(NavigationPage.allCases, id: \.self) { page in
NavigationStackCompat {
page.contentView
.navigationTitle(page.title)
}
.tag(page)
.tabItem { page.label }
}
}
.onAppear {
environments.postReload()
}
.alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
environments.postReload()
}
}
.onChangeCompat(of: selection) { newValue in
if newValue == .logs {
environments.connect()
}
}
.environment(\.selection, $selection)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL)
baseTabView
}
}
.onAppear {
environments.postReload()
}
.alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
environments.postReload()
}
}
.onChangeCompat(of: selection) { newValue in
if newValue == .logs {
environments.connect()
}
}
.environment(\.selection, $selection)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL)
}
private func updateButtonVisibility() {