Produkti final
Këtu është produkti përfundimtar i asaj që flet ky postim: zbatoni një aplikacion me shumë dritare në macOS ku secila dritare ka gjendjen e vet ndërsa gjendja është e aksesueshme nga menyja e sistemit.
Rasti i përdorimit
Ne kemi implementuar Aplikacionin SwiftUI me shumë dritare duke përdorur StateObject. Kjo i mundësoi aplikacionit të hapte shumë dritare në të njëjtën kohë, ku çdo dritare mban gjendjen e vet.
Pyetje (aka problemi)
Nëse ka disa dritare të hapura të të njëjtit aplikacion, si të aksesoni programatikisht dritaren e zgjedhur aktualisht?
Për shembull, aplikacioni ka Open File
artikull të menusë së sistemit që supozohet të aktivizojë përzgjedhësin e skedarit. Pasi përdoruesi të zgjedhë një skedar, supozohet të hapë skedarin e zgjedhur në dritaren e aplikacionit të fokusuar (aktivizo) aktualisht.
Le të shohim situatën. Ekziston kodi i mëposhtëm i aplikacionit që përpiqet të zbatojë aplikacionin me shumë dritare.
import SwiftUI
@main
struct multi_window_menuApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
MenuCommands()
}
}
}
struct ContentView: View {
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
TextField("", text: $viewModel.inputText)
.disabled(true)
.padding()
}
}
public class ViewModel: ObservableObject {
@Published var inputText: String = ""
}
Por MenuCommands
nuk di se si të hyjë në ContentView
të zgjedhur aktualisht.
import Foundation
import SwiftUI
import Combine
struct MenuCommands: Commands {
var body: some Commands {
CommandGroup(after: CommandGroupPlacement.newItem, addition: {
Divider()
Button(action: {
let dialog = NSOpenPanel();
dialog.title = "Choose a file";
dialog.showsResizeIndicator = true;
dialog.showsHiddenFiles = false;
dialog.allowsMultipleSelection = false;
dialog.canChooseDirectories = false;
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
let result = dialog.url
if (result != nil) {
let path: String = result!.path
do {
let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8)
// HOW TO SOLVE THIS?
// how to get access to the currently active view
// model to update the inputText variable?
// viewModel.inputText = string
}
catch {
print("Error \(error)")
}
}
} else {
return
}
}, label: {
Text("Open File")
})
.keyboardShortcut("O", modifiers: .command)
})
}
}
Zgjidhje
Vini re se çdo shembull
NSWindow
ka një atribut me njëwindowNumber
unik. Kjo na ndihmon të identifikojmë çdo dritare të hapur.
Ideja është të krijohet një model i përbashkët i pamjes "globale" që mban gjurmët e të gjitha dritareve të hapura dhe modelet e pamjes së tij. Kur një dritare bëhet një dritare aktive (një dritare kyçe), modeli i pamjes globale e shikon modelin e pamjes nga windowNumber
dhe e vendos atë si activeViewModel
.
Më pas, modeli i pamjes globale i kalohet çdo pamjeje që duhet të ketë akses në modelin aktual të pamjes aktive.
import SwiftUI
class GlobalViewModel : NSObject, ObservableObject {
// all currently opened windows
@Published var windows = Set<NSWindow>()
// all view models that belong to currently opened windows
@Published var viewModels : [Int:ViewModel] = [:]
// currently active aka selected aka key window
@Published var activeWindow: NSWindow?
// currently active view model for the active window
@Published var activeViewModel: ViewModel?
func addWindow(window: NSWindow) {
window.delegate = self
windows.insert(window)
}
// associates a window number with a view model
func addViewModel(_ viewModel: ViewModel, forWindowNumber windowNumber: Int) {
viewModels[windowNumber] = viewModel
}
}
Kodi tjetër i detyrueshëm do të reagojë në ndryshimin e gjendjes së dritares. Kodi do të kujdeset vetëm për dy gjendje:
- kur është gati të bëhet një dritare aktive, kështu që ne mund të vendosim dritaren aktualisht aktive dhe modelin e pamjes
- kur është duke u mbyllur, kështu që ne mund të bëjmë pastrimin
import SwiftUI
extension GlobalViewModel: NSWindowDelegate { func windowWillClose(_ notification: Notification) { if let window = notification.object as? NSWindow { windows.remove(window) viewModels.removeValue(forKey: window.windowNumber) } } func windowDidBecomeKey(_ notification: Notification) { if let window = notification.object as? NSWindow { activeWindow = window activeViewModel = viewModels[window.windowNumber] } } }
Pjesa e fundit e enigmës është të kuptoni se si të hyni në dritare nga pamja e përmbajtjes që është aktive. HostingWindowFinder
ofron një mënyrë për të kërkuar dritaren që lidhet me pamjen aktuale.
import SwiftUI
struct HostingWindowFinder: NSViewRepresentable { var callback: (NSWindow?) -> () func makeNSView(context: Self.Context) -> NSView { let view = NSView() DispatchQueue.main.async { [weak view] in self.callback(view?.window) } return view } func updateNSView(_ nsView: NSView, context: Context) {} }
Këtu është pamja që regjistron modelin e dritares dhe pamjes në modelin e pamjes globale.
import SwiftUI
struct ContentView: View { @EnvironmentObject var globalViewModel : GlobalViewModel @StateObject var viewModel: ViewModel = ViewModel() var body: some View { HostingWindowFinder { window in if let window = window { self.globalViewModel.addWindow(window: window) self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber) } } TextField("", text: $viewModel.inputText) .disabled(true) .padding() } }
Pastaj duhet të krijojmë një shembull të GlobalViewModel
në nivelin e aplikacionit dhe t'ia kalojmë atë komandave të pamjeve dhe menusë.
import SwiftUI
@main struct multi_window_menuApp: App { @State var globalViewModel = GlobalViewModel() var body: some Scene { WindowGroup { ContentView() .environmentObject(self.globalViewModel) } .commands { MenuCommands(globalViewModel: self.globalViewModel) } Settings { VStack { Text("My Settingsview") } } } }
Më në fund, ja se si MenuCommands
do t'i qaset modelit të pamjes aktualisht aktive për të përditësuar gjendjen që i përket pamjes së zgjedhur (dritares).
import Foundation import SwiftUI import Combine
struct MenuCommands: Commands { var globalViewModel: GlobalViewModel var body: some Commands { CommandGroup(after: CommandGroupPlacement.newItem, addition: { Divider() Button(action: { let dialog = NSOpenPanel(); dialog.title = "Choose a file"; dialog.showsResizeIndicator = true; dialog.showsHiddenFiles = false; dialog.allowsMultipleSelection = false; dialog.canChooseDirectories = false; if (dialog.runModal() == NSApplication.ModalResponse.OK) { let result = dialog.url if (result != nil) { let path: String = result!.path do { let string = try String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8) print("Active Window", self.globalViewModel.activeWindow?.windowNumber) self.globalViewModel.activeViewModel?.inputText = string } catch { print("Error \(error)") } } } else { return } }, label: { Text("Open File") }) .keyboardShortcut("O", modifiers: [.command]) }) } }
Kodi
Gjithçka që u përmend më lart zbatohet dhe ekzekutohet nën këtë projekt Github:
shënim
Ende nuk jam i sigurt se kjo është mënyra e duhur për ta bërë këtë, por tani (pas ditësh kërkimi), e shoh si të vetmen mënyrë për të implementuar aplikacionin me shumë dritare në SwiftUI për macOS.
Referencat
- "Si të hyni në NSWindow nga @main App duke përdorur vetëm SwiftUI?"
- "Si të hyni në dritaren e vet brenda pamjes SwiftUI?"
- https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/
- http://www.gfrigerio.com/build-a-macos-app-with-swiftui/
- https://troz.net/post/2021/swiftui_mac_menus/
- https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/
- https://stackoverflow.com/questions/68242439/how-to-implement-multi-window-with-menu-commands-in-swiftui-for-macos