首页 > 解决方案 > SwitfUI:在 macOS 上访问特定场景的 ViewModel

问题描述

在这个简单的示例应用程序中,我有以下要求:

  1. 有多个窗口,每个窗口都有自己的ViewModel
  2. Toggle在一个窗口中切换不应更新另一个窗口的
  3. 我也希望能够通过菜单切换

在此处输入图像描述

就像现在一样,前两点没有给出,但最后一点有效。我已经知道,当我将ViewModel's 的单一事实来源移至ContentView前两点的作品时,但随后我将无法在WindowGroup注入命令的级别访问。

import SwiftUI

@main
struct ViewModelAndCommandsApp: App {
    var body: some Scene {
        ContentScene()
    }
}

class ViewModel: ObservableObject {
    @Published var toggleState = true
}

struct ContentScene: Scene {
    @StateObject private var vm = ViewModel()// injecting here fulfills the last point only…
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(vm)
                .frame(width: 200, height: 200)
        }
        .commands {
            ContentCommands(vm: vm)
        }
    }
}

struct ContentCommands: Commands {
    @ObservedObject var vm: ViewModel
    
    var body: some Commands {
        CommandGroup(before: .toolbar) {
            Button("Toggle Some State") {
                vm.toggleState.toggle()
            }
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel//injecting here will result in window independant ViewModels, but make them unavailable in `ContactScene` and `ContentCommands`…
    
    var body: some View {
        Toggle(isOn: $vm.toggleState, label: {
            Text("Some State")
        })
    }
}

我怎样才能满足这些要求——是否有 SwiftUI 解决方案或者我必须实现一个SceneDelegate(这仍然是解决方案吗?)?

编辑:

更具体地说:我想知道如何为每个单独的场景实例化一个 ViewModel,并且能够从菜单栏中知道要更改哪个 ViewModel。

标签: macosmvvmswiftui

解决方案


长话短说,看下面的代码。项目名为WindowSample这需要在URL注册中匹配你的应用名称。

import SwiftUI

@main
struct WindowSampleApp: App {
    var body: some Scene {
        ContentScene()
    }
}
//This can be done several different ways. You just
//need somewhere to store multiple copies of the VM
class AStoragePlace {
    private static var viewModels: [ViewModel] = []
    
    static func getAViewModel(id: String?) -> ViewModel? {
        var result: ViewModel? = nil
        if id != nil{
            result = viewModels.filter({$0.id == id}).first
            
            if result == nil{
                let newVm = ViewModel(id: id!)
                viewModels.append(newVm)
                result = newVm
            }
        }
        return result
    }
}

struct ContentCommands: Commands {
    @ObservedObject var vm: ViewModel
    
    var body: some Commands {
        CommandGroup(before: .toolbar) {
            Button("Toggle Some State \(vm.id)") {
                vm.testMenu()
            }
        }
    }
}
class ViewModel: ObservableObject, Identifiable {
    let id: String
    @Published var toggleState = true
    
    init(id: String) {
        self.id = id
    }
    func testMenu() {
        toggleState.toggle()
    }
}
struct ContentScene: Scene {
    var body: some Scene {
        //Trying to init from 1 windowGroup only makes a copy not a new scene
        WindowGroup("1") {
            ToggleView(vm: AStoragePlace.getAViewModel(id: "1")!)
                .frame(width: 200, height: 200)
        }
        .commands {
            ContentCommands(vm: AStoragePlace.getAViewModel(id: "1")!)
        }.handlesExternalEvents(matching: Set(arrayLiteral: "1"))
        //To open this go to File>New>New 2 Window
        WindowGroup("2") {
            ToggleView(vm: AStoragePlace.getAViewModel(id: "2")!)
                .frame(width: 200, height: 200)
        }
        .commands {
            ContentCommands(vm: AStoragePlace.getAViewModel(id: "2")!)
        }.handlesExternalEvents(matching: Set(arrayLiteral: "2"))
        
        
    }
}
struct ToggleView: View {
    @Environment(\.openURL) var openURL
    @ObservedObject var vm: ViewModel
    var body: some View {
        VStack{
            //Makes copies of the window/scene
            Button("new-window-of type \(vm.id)", action: {
                //appname needs to be a registered url in info.plist
                //Info Property List>Url types>url scheme>item 0 == appname
                //Info Property List>Url types>url identifier == appname
                if let url = URL(string: "WindowSample://\(vm.id)") {
                    openURL(url)
                }
            })
            
            //Toggle the state
            Toggle(isOn: $vm.toggleState, label: {
                Text("Some State \(vm.id)")
            })
        }
    }
}

推荐阅读