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
@@ -1,22 +1,19 @@
import Libbox
import Library
import SwiftUI
public struct GroupListView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var isLoading = true
@StateObject private var commandClient = CommandClient(.groups)
@State private var groups: [OutboundGroup] = []
@StateObject private var viewModel = GroupListViewModel()
public init() {}
public var body: some View {
VStack {
if isLoading {
if viewModel.isLoading {
Text("Loading...")
} else if !groups.isEmpty {
} else if !viewModel.groups.isEmpty {
ScrollView {
VStack {
ForEach(groups, id: \.hashValue) { it in
ForEach(viewModel.groups, id: \.hashValue) { it in
GroupView(it)
}
}.padding()
@@ -26,56 +23,17 @@ public struct GroupListView: View {
}
}
.onAppear {
connect()
viewModel.connect()
}
.onDisappear {
commandClient.disconnect()
viewModel.disconnect()
}
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
commandClient.connect()
viewModel.connect()
} else {
commandClient.disconnect()
viewModel.disconnect()
}
}
.onReceive(commandClient.$groups, perform: { groups in
if let groups {
setGroups(groups)
}
})
}
private func connect() {
if ApplicationLibrary.inPreview {
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
]),
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
(0 ..< 234).map { index in
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
}),
]
isLoading = false
} else {
commandClient.connect()
}
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]()
for goGroup in goGroups {
var items = [OutboundGroupItem]()
let itemIterator = goGroup.getItems()!
while itemIterator.hasNext() {
let goItem = itemIterator.next()!
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
}
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
}
self.groups = groups
isLoading = false
}
}
@@ -0,0 +1,60 @@
import Combine
import Libbox
import Library
import SwiftUI
@MainActor
public class GroupListViewModel: ObservableObject {
@Published public var isLoading = true
@Published public var groups: [OutboundGroup] = []
private let commandClient = CommandClient(.groups)
private var cancellables = Set<AnyCancellable>()
public init() {
commandClient.$groups
.compactMap { $0 }
.sink { [weak self] goGroups in
self?.setGroups(goGroups)
}
.store(in: &cancellables)
}
public func connect() {
if ApplicationLibrary.inPreview {
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
]),
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
(0 ..< 234).map { index in
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
}),
]
isLoading = false
} else {
commandClient.connect()
}
}
public func disconnect() {
commandClient.disconnect()
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]()
for goGroup in goGroups {
var items = [OutboundGroupItem]()
let itemIterator = goGroup.getItems()!
while itemIterator.hasNext() {
let goItem = itemIterator.next()!
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
}
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
}
self.groups = groups
isLoading = false
}
}
+16 -43
View File
@@ -1,36 +1,31 @@
import Libbox
import Library
import SwiftUI
@MainActor
public struct GroupView: View {
@State private var group: OutboundGroup
@StateObject private var viewModel: GroupViewModel
@State private var geometryWidth: CGFloat = 300
@State private var alert: Alert?
public init(_ group: OutboundGroup) {
_group = State(initialValue: group)
_viewModel = StateObject(wrappedValue: GroupViewModel(group: group))
}
private var title: some View {
HStack {
Text(group.tag)
Text(viewModel.group.tag)
.font(.headline)
Text(group.displayType)
Text(viewModel.group.displayType)
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(group.items.count)")
Text("\(viewModel.group.items.count)")
.font(.subheadline)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.gray.opacity(0.5))
.cornerRadius(4)
Button {
group.isExpand = !group.isExpand
Task {
await setGroupExpand()
}
viewModel.toggleExpand()
} label: {
if group.isExpand {
if viewModel.group.isExpand {
Image(systemName: "arrow.down.to.line")
} else {
Image(systemName: "arrow.up.to.line")
@@ -40,9 +35,7 @@ public struct GroupView: View {
.buttonStyle(.plain)
#endif
Button {
Task {
await doURLTest()
}
viewModel.performURLTest()
} label: {
Image(systemName: "bolt.fill")
}
@@ -50,19 +43,19 @@ public struct GroupView: View {
.buttonStyle(.plain)
#endif
}
.alertBinding($alert)
.alertBinding($viewModel.alert)
.padding([.top, .bottom], 8)
.animation(.easeInOut, value: group.isExpand)
.animation(.easeInOut, value: viewModel.group.isExpand)
}
public var body: some View {
Section {
if group.isExpand {
if viewModel.group.isExpand {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: explandColumnCount()))
{
ForEach(group.items, id: \.tag) { it in
GroupItemView($group, it)
ForEach(viewModel.group.items, id: \.tag) { it in
GroupItemView($viewModel.group, it)
}
}
} else {
@@ -73,7 +66,7 @@ public struct GroupView: View {
ZStack {
Rectangle()
.fill(it.delayColor)
if it.tag == group.selected {
if it.tag == viewModel.group.selected {
Rectangle()
.fill(Color.white)
#if !os(tvOS)
@@ -120,9 +113,9 @@ public struct GroupView: View {
count = Int(Int(geometryWidth) / 20)
#endif
if count == 0 {
return [group.items]
return [viewModel.group.items]
} else {
return group.items.chunked(
return viewModel.group.items.chunked(
into: count
)
}
@@ -138,26 +131,6 @@ public struct GroupView: View {
return standardCount < 1 ? 1 : standardCount
#endif
}
private nonisolated func doURLTest() async {
do {
try await LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func setGroupExpand() async {
do {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(group.tag, isExpand: group.isExpand)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
}
private extension Array {
@@ -0,0 +1,46 @@
import Libbox
import Library
import SwiftUI
@MainActor
public class GroupViewModel: ObservableObject {
@Published public var group: OutboundGroup
@Published public var alert: Alert?
public init(group: OutboundGroup) {
self.group = group
}
public func toggleExpand() {
group.isExpand = !group.isExpand
Task {
await setGroupExpand()
}
}
public func performURLTest() {
Task {
await doURLTest()
}
}
private nonisolated func doURLTest() async {
do {
try await LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func setGroupExpand() async {
do {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(group.tag, isExpand: group.isExpand)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
}