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

  1. "Si të hyni në NSWindow nga @main App duke përdorur vetëm SwiftUI?"
  2. "Si të hyni në dritaren e vet brenda pamjes SwiftUI?"
  3. https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/
  4. http://www.gfrigerio.com/build-a-macos-app-with-swiftui/
  5. https://troz.net/post/2021/swiftui_mac_menus/
  6. https://onmyway133.com/posts/how-to-manage-windowgroup-in-swiftui-for-macos/
  7. https://stackoverflow.com/questions/68242439/how-to-implement-multi-window-with-menu-commands-in-swiftui-for-macos