Listing and closing Windows in a MacOS SwiftUI App

Listing and closing Windows in a MacOS SwiftUI App

It is possible that theres a more SwiftUI-centric way to do this. If theres not yet, I certainly hope Apple adds some better window management stuff for the Mac side of things — right now, everything seems like a bit of a hack.

Heres what I came up with:


@main
struct MultiWindowApp: App {
  @State var gvm = GlobalViewModel()
  var body: some Scene {
    WindowGroup {
      MainView()
      .environmentObject(gvm)
    }
    WindowGroup(Secondary) {
      SecondaryView(bgColor: .blue)
      .environmentObject(gvm)
    }
    .handlesExternalEvents(matching: Set(arrayLiteral: *))
  }
}


struct MainView: View {
  @Environment(.openURL) var openURL
  @EnvironmentObject var vm : GlobalViewModel

  var body: some View {
    VStack {
      Text(MainView)
        List {
            ForEach(Array(vm.windows), id: .windowNumber) { window in
                HStack {
                    Text(Window: (window.windowNumber))
                    Button(Red) {
                        vm.setColor(.red, forWindowNumber: window.windowNumber)
                    }
                    Button(Close) {
                        window.close()
                    }
                }
                
            }
        }
      Button(Open Secondary) {
        if let url = URL(string: OpenNewWindowApp://bla) {
          openURL(url)
        }
      }
    }
    .padding()
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

struct SecondaryView: View {
  var bgColor : Color
  @EnvironmentObject var vm : GlobalViewModel

    @State private var windowNumber = -1
    
  var body: some View {
    VStack{
        HostingWindowFinder { window in
            if let window = window {
                vm.addWindow(window: window)
                self.windowNumber = window.windowNumber
            }
        }
      Spacer()
      Text(Viewer)
      Text(ViewModel: (vm.name))
      Button(Set VM){
        vm.name = Tom
      }
      Spacer()
    }
    .background(vm.backgroundColors[windowNumber] ?? bgColor)
    .frame(minWidth: 300, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center )
  }
}

class GlobalViewModel : NSObject, ObservableObject {
    @Published var name = Frank
    @Published var windows = Set<NSWindow>()
    @Published var backgroundColors : [Int:Color] = [:]
    
    func addWindow(window: NSWindow) {
        window.delegate = self
        windows.insert(window)
    }
    
    func setColor(_ color: Color, forWindowNumber windowNumber: Int) {
        backgroundColors[windowNumber] = color
    }
}

extension GlobalViewModel : NSWindowDelegate {
    func windowWillClose(_ notification: Notification) {
        if let window = notification.object as? NSWindow {
            windows = windows.filter { $0.windowNumber != window.windowNumber }
        }
    }
}

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

Im using a trick from https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/ to get a reference to the NSWindow. That gets stored in the view model in a set. Later, to access things like closing the windows, etc. I reference the windows by windowNumber.

When a window appears, it adds itself to the view models window list. Then, when the view model gets a windowWillClose call as the delegate, it removes it from the list.

Setting the background color is done via the backgroundColors property on the view model. If theres not one set, it uses the passed-in background color property. There are tons of different ways you could choose to architect this bit.

Listing and closing Windows in a MacOS SwiftUI App

Leave a Reply

Your email address will not be published.