SwiftUI 和 MVVM:使用按钮对列表进行排序

SwiftUI and MVVM: Using Buttons to Sort Lists

我正在构建一个使用按钮对各种列表进行排序的应用程序。

我正在尽最大努力尽可能地遵循 MVVM 设计模式。有几个显示列表的视图,所以我希望能够在多个视图中重用按钮结构,并将它们连接到多个视图模型

我目前使用此设置代码生成的方式,但按下新按钮时列表不会更改。有什么想法吗?

可以从 GitHub 此处下载有问题的示例项目:https://github.com/sans-connaissance/SOQ-BetterButtons

这是视图的代码:

import SwiftUI

struct ContentView: View {
    
    @StateObject private var vm = ContentViewModel()
    
    
    var body: some View {
        
        VStack {
            HStack {
                
                SortButton(name: .arrayOne, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
                SortButton(name: .arrayTwo, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
                SortButton(name: .arrayThree, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
            
            }
            
            List {
                ForEach(vm.contentArray, id: \.self) { content in
                    Text(content.self)
                }
                
            }
        }
        .onAppear {vm.setButtons()}
        .onAppear {vm.getArray()}
        
    }
}

这是按钮的代码

import SwiftUI

struct SortButton: View {
    
    var name: Select
    @Binding var bools: [String : Bool]

    var body: some View {
        Button {
            func show(button: Select) {
                Select.allCases.forEach { button in
                    bools[button.rawValue] = false
                }
                
                bools[button.rawValue] = true
            }
            
        } label: {
            Text(name.rawValue)
        }
        
    }
}

enum Select: String, CaseIterable {
    case arrayOne = "arrayOne"
    case arrayTwo = "arrayTwo"
    case arrayThree = "arrayThree"
}

最后是本示例的 ViewModel。

import Foundation

class ContentViewModel: ObservableObject {
    
    @Published var contentArray = [String]()
    @Published var bools = [String : Bool]()
    
    
    private let arrayOne = ["One", "Two", "Three"]
    private let arrayTwo = ["Four", "Five", "Six"]
    private let arrayThree = ["Seven", "Eight", "Nine"]
    
    
    func setButtons() {
        
        Select.allCases.forEach { button in
            bools[button.rawValue] = false
        }
        
        bools["arrayOne"] = true
    }
    
    
    func getArray() {
        
        if bools["arrayOne"]! {
            contentArray.removeAll()
            contentArray.append(contentsOf: arrayOne)
        }
        
        if bools["arrayTwo"]! {
            contentArray.removeAll()
            contentArray.append(contentsOf: arrayTwo)
        }
        
        if bools["arrayThree"]! {
            contentArray.removeAll()
            contentArray.append(contentsOf: arrayThree)
        }
        
    }
}

Link 到 GitHub 上的示例项目: https://github.com/sans-connaissance/SOQ-BetterButtons

感谢观看!!

我认为您在尝试学习 MVVM 时在代码中走得太远,并且不必要地使事情复杂化。这段代码非常脆弱,不容易更改。我确实让你的代码工作了,我想看一下它。

我清理了你的 enum 并在代码中进行了注释。我还将您的 var bools 更改为 [Select:Bool] 类型。这允许您使用枚举案例本身,而不是原始值。然后编译器可以帮助您,因为它知道什么是有效的 Select 情况,什么不是。当您使用 String 时,它不知道您指的是一个“字符串,它是 Select 案例的原始值。这大大减少了错误。

此外,请避免在您的代码中有太多空白 space。尝试使用 space 分隔逻辑分组。太多的白色 space 会使您的代码难以阅读,因为您最终滚动太多只是为了看到它。

您的代码实际失败的地方是 SortButton 本身。在 Button 中,您告诉 Button 执行的操作是定义您的 func show(button:)。因此,这个函数只在使用任何按钮时被简单地定义,然后就消失了。坦率地说,我不敢相信 Xcode 允许这样做而不引发一些错误。您想要在 var body 之外定义函数并从按钮的操作中调用它。

最后,这不是我可以在你的代码中真正改变的东西,你应该避免使用 ! 来强制解包选项。它在这里工作只是因为你有少量固定数量的常量数组,但随着事情变得越来越复杂,这会变成一个致命的错误。

struct ContentView: View {
    
    @StateObject private var vm = ContentViewModel()

    var body: some View {
        VStack {
            HStack {
                SortButton(name: .arrayOne, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
                SortButton(name: .arrayTwo, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
                SortButton(name: .arrayThree, bools: $vm.bools)
                    .onChange(of: vm.bools){ _ in vm.getArray()}
            }
            
            List {
                ForEach(vm.contentArray, id: \.self) { content in
                    Text(content.self)
                }
            }
        }
        .onAppear {
            vm.setButtons()
            vm.getArray()
        }
    }
}

struct SortButton: View {
    
    var name: Select
    @Binding var bools: [Select : Bool]

    var body: some View {
        Button {
            show(button: name)
        } label: {
            Text(name.rawValue)
        }
    }
    //This is defined outside of the body, and called from the button.
    func show(button: Select) {
        Select.allCases.forEach { button in
            bools[button] = false
        }
        bools[button] = true
    }
}

enum Select: String, CaseIterable {
    // when you declare an enum to be of type String, you get the string version of the name for free
    // you don't need case arrayOne = "arrayOne". Also, once you remove the = ""
    // you can use one case statement to define them all. The only time you need the = "" is when
    // you want to change the default rawValue such as case arrayOne = "Array One"
    case arrayOne, arrayTwo, arrayThree
}

class ContentViewModel: ObservableObject {
    
    @Published var contentArray = [String]()
    @Published var bools = [Select : Bool]()

    private let arrayOne = ["One", "Two", "Three"]
    private let arrayTwo = ["Four", "Five", "Six"]
    private let arrayThree = ["Seven", "Eight", "Nine"]
    
    func setButtons() {
        Select.allCases.forEach { button in
            bools[button] = false
        }
        bools[.arrayOne] = true
    }
    
    
    func getArray() {
        // if you just set contentArray equal to one of the other arrays, you
        // get the same result as the .removeAll and the .append(contentsOf:)
        if bools[.arrayOne]! { contentArray = arrayOne }
        if bools[.arrayTwo]! { contentArray = arrayTwo }
        if bools[.arrayThree]! { contentArray = arrayThree }
    }
}

我还快速 运行 了解了如何进一步压缩和简化您的代码。还有更多工作要做,但这是一种人为练习。使用 MVVM,您希望将模型逻辑与视图分开并将其放置在视图中,但您应该有视图逻辑来显示视图模型的数据。尽管视图模型隐藏了模型逻辑,视图应该能够处理不同的视图模型并一致地显示数据。这就是可重用性的本质。

此外,您会注意到我删除了 ContentView 中单独的 SortButton 调用并使用了 ForEach。这是 DRY 实际应用的一个很好的例子,它可以在您添加 Select 个案例时轻松扩展。

此代码可以改进的一个方面是 SortButton 中的一个更好的机制来获取 'ContentViewModel' 更新。我刚刚传入 ContentViewModel,但这可以进一步简化。

struct ContentView: View {
    @StateObject private var vm = ContentViewModel()

    var body: some View {
        VStack {
            HStack {
                ForEach(Select.allCases, id: \.self) { name in
                    SortButton(name: name, vm: vm)
                }
            }
            List {
                ForEach(vm.contentArray, id: \.self) { content in
                    Text(content.self)
                }
            }
        }
    }
}

struct SortButton: View {
    var name: Select
    let vm: ContentViewModel
    
    var body: some View {
        Button {
            vm.updateContentArray(select: name)
        } label: {
            Text(name.rawValue)
        }
    }
}

enum Select: String, CaseIterable {
    case arrayOne, arrayTwo, arrayThree
}

class ContentViewModel: ObservableObject {
    
    @Published var contentArray: [String]

    private let arrayOne = ["One", "Two", "Three"]
    private let arrayTwo = ["Four", "Five", "Six"]
    private let arrayThree = ["Seven", "Eight", "Nine"]
    
    init() {
        contentArray = arrayOne
    }
    
    func updateContentArray(select: Select) {
        switch select {
        case .arrayOne:
            contentArray = arrayOne
        case .arrayTwo:
            contentArray = arrayTwo
        case .arrayThree:
            contentArray = arrayThree
        }
    }
}