Arhn - архитектура программирования

SwitfUI: доступ к ViewModel конкретной сцены в macOS

В этом простом примере приложения у меня есть следующие требования:

  1. иметь несколько окон, каждое из которых имеет собственное ViewModel
  2. переключение Toggle в одном окне не должно не обновлять другое окно
  3. Я также хочу иметь возможность переключаться через меню

введите описание изображения здесь

В настоящий момент первые два пункта не даны, но последний пункт работает. Я уже знаю, что, когда я перемещаю единственный источник истины ViewModel в 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 нужно изменить.

28.01.2021

  • Может попробовать узнать, что за старый способ добавления пунктов в Меню. Затем добавляем их в ToggleView. 29.01.2021
  • Честная оценка. Это означало бы, что мне пришлось бы использовать мост, чтобы использовать AppDelegate и создать экземпляр моего меню в applicationDidFinishLaunching(_:), я полагаю, что также означало бы, что чистый жизненный цикл SwiftUI не будет работать для этого прямо сейчас ... 29.01.2021
  • Изучив это дальше, я все еще не могу понять, как получить дескриптор текущего активного окна и его модели просмотра. Я пришел к выводу, что это то, что мне нужно, чтобы различать целевую модель представления в моей кнопке Toggle Some State ... 29.01.2021
  • Я согласен. Посмотрите на мой ответ ниже. Вместо ViewModel посмотрите на View как на DetailView, где переключатель является частью Model, который можно использовать где угодно. vs только нацелен на одно представление. Источник истины должен находиться в одном месте, где вы можете его получить / получить. 29.01.2021

Ответы:


1

Короче говоря, см. Код ниже. Проект называется 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)")
            })
        }
    }
}
29.01.2021
  • Спасибо за ваш вклад, я ценю это. Проведя (еще) 2 часа, я все еще не могу разобраться в этом. Хотя приведенный выше пример кода - это действительно очень простой пример, показывающий рассматриваемую проблему, мое настоящее приложение, которое мне пришлось бы использовать, это приложение, которое вызывает api с настраиваемым запросом, который может быть разным для каждого окна. Поэтому, естественно, этот запрос может быть разным для каждого окна. Я также хочу иметь возможность перезагружать через меню и Cmd + R, для чего мне нужно активное окно. Прямо сейчас, кажется, не существует простого способа добиться этого, так что я думаю, что пока уступаю. :-( 30.01.2021
  • Смотрите новый код. Думаю, это сработает. Это попало прямо в проект, над которым я работаю. 31.01.2021
  • Это так близко, и, возможно, на данный момент подойдет два отдельных окна. Что меня беспокоит, так это то, что это не динамично. Было бы идеально, если бы _1 _ ›_ 2_ просто создал экземпляр нового ViewModel с новым UUID для этого конкретного нового окна / сцены. Я просто не могу понять, где нужно создать этот UUID. Придется еще немного поиграть, чтобы, возможно, найти способ ... Но это намного ближе, чем когда-либо прежде, и я ценю ваши усилия. 31.01.2021
  • Я буду думать, но не думаю, что это возможно. Поскольку строка меню не кажется динамической, я нашел несколько ссылок, в которых упоминалось, что menuBuilder запускается в самом начале. Чтобы иметь возможность воздействовать на тумблер, он должен быть там с самого начала. Я не особо в это разбирался. Но DocumentGroup создает документы на init, что позволяет им иметь строку меню для определенных документов. Кажется, это то, что вы пытаетесь сделать, аналогично тому, что вы пытаетесь сделать. 31.01.2021
  • Что ж, единственное, что мне нужно, это дескриптор текущего активного окна и его ViewModel на уровне Button, я думаю. Возможно, следующим шагом станет изучение DocumentGroup ... 01.02.2021
  • Новые материалы

    Коллекции публикаций по глубокому обучению
    Последние пару месяцев я создавал коллекции последних академических публикаций по различным подполям глубокого обучения в моем блоге https://amundtveit.com - эта публикация дает обзор 25..

    Представляем: Pepita
    Фреймворк JavaScript с открытым исходным кодом Я знаю, что недостатка в фреймворках JavaScript нет. Но я просто не мог остановиться. Я хотел написать что-то сам, со своими собственными..

    Советы по коду Laravel #2
    1-) Найти // You can specify the columns you need // in when you use the find method on a model User::find(‘id’, [‘email’,’name’]); // You can increment or decrement // a field in..

    Работа с временными рядами спутниковых изображений, часть 3 (аналитика данных)
    Анализ временных рядов спутниковых изображений для данных наблюдений за большой Землей (arXiv) Автор: Рольф Симоэс , Жильберто Камара , Жильберто Кейрос , Фелипе Соуза , Педро Р. Андраде ,..

    3 способа решить квадратное уравнение (3-й мой любимый) -
    1. Методом факторизации — 2. Используя квадратичную формулу — 3. Заполнив квадрат — Давайте поймем это, решив это простое уравнение: Мы пытаемся сделать LHS,..

    Создание VR-миров с A-Frame
    Виртуальная реальность (и дополненная реальность) стали главными модными терминами в образовательных технологиях. С недорогими VR-гарнитурами, такими как Google Cardboard , и использованием..

    Демистификация рекурсии
    КОДЕКС Демистификация рекурсии Упрощенная концепция ошеломляющей О чем весь этот шум? Рекурсия, кажется, единственная тема, от которой у каждого начинающего студента-информатика..