Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
389 views
in Technique[技术] by (71.8m points)

mvvm - SwitfUI: access the specific scene's ViewModel on macOS

In this simple example app, I have the following requirements:

  1. have multiple windows, each having it's own ViewModel
  2. toggling the Toggle in one window should not update the other window's
  3. I want to also be able to toggle via menu

enter image description here

As it is right now, the first two points are not given, the last point works though. I do already know that when I move the ViewModel's single source of truth to the ContentView works for the first two points, but then I wouldn't have access at the WindowGroup level, where I inject the commands.

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")
        })
    }
}

How can I fulfill theses requirements–is there a SwiftUI solution to this or will I have to implement a SceneDelegate (is this the solution anyway?)?

Edit:

To be more specific: I'd like to know how I can go about instantiating a ViewModel for each individual scene and also be able to know from the menu bar which ViewModel is meant to be changed.

question from:https://stackoverflow.com/questions/65935505/switfui-access-the-specific-scenes-viewmodel-on-macos

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Long story short, see the code below. The project is called WindowSample this needs to match your app name in the URL registration.

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)")
            })
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...