【问题标题】:Change window size based on NavigationView in a SwiftUI macOS app在 SwiftUI macOS 应用程序中基于 NavigationView 更改窗口大小
【发布时间】:2020-04-10 14:49:42
【问题描述】:

我将 SwiftUI 用于主窗口包含 NavigationView 的 Mac 应用程序。此 NavigationView 包含一个侧边栏列表。选择侧栏中的项目时,它会更改详细视图中显示的视图。详细视图中显示的视图大小不同,这会导致窗口大小在显示时发生变化。但是,当详细视图更改大小时,窗口不会更改大小以适应新的详细视图。

如何根据 NavigationView 的大小来改变窗口大小?

我的应用程序示例代码如下:

import SwiftUI

struct View200: View {
    var body: some View {
        Text("200").font(.title)
            .frame(width: 200, height: 400)
            .background(Color(.systemRed))
    }
}

struct View500: View {
    var body: some View {
        Text("500").font(.title)
            .frame(width: 500, height: 300)
            .background(Color(.systemBlue))
    }
}

struct ViewOther: View {
    let item: Int
    var body: some View {
        Text("\(item)").font(.title)
            .frame(width: 300, height: 200)
            .background(Color(.systemGreen))
    }
}

struct DetailView: View {

    let item: Int

    var body: some View {
        switch item {
        case 2:
            return AnyView(View200())
        case 5:
            return AnyView(View500())
        default:
            return AnyView(ViewOther(item: item))
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
             List {
                 ForEach(1...10, id: \.self) { index in
                     NavigationLink(destination: DetailView(item: index)) {
                         Text("Link \(index)")
                     }
                 }
             }
             .listStyle(SidebarListStyle())
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

下面是示例应用在详细视图更改大小时的样子:

【问题讨论】:

    标签: macos swiftui swiftui-list swiftui-navigationlink


    【解决方案1】:

    这里是可行的方法的演示。我是根据一种不同的观点来做的,因为你需要重新设计你的解决方案才能采用它。

    演示

    1) 需要窗口动画调整大小的视图

    struct ResizingView: View {
        public static let needsNewSize = Notification.Name("needsNewSize")
    
        @State var resizing = false
        var body: some View {
            VStack {
                Button(action: {
                    self.resizing.toggle()
                    NotificationCenter.default.post(name: Self.needsNewSize, object: 
                        CGSize(width: self.resizing ? 800 : 400, height: self.resizing ? 350 : 200))
                }, label: { Text("Resize") } )
            }
            .frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
        }
    }
    

    2) 窗口的所有者(在本例中为 AppDelegate

    import Cocoa
    import SwiftUI
    import Combine
    
    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        var window: NSWindow!
        var subscribers = Set<AnyCancellable>()
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
    
            let contentView = ResizingView()
    
            window = NSWindow(
                contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), // just default
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered, defer: false)
            window.center()
            window.setFrameAutosaveName("Main Window")
            window.contentView = NSHostingView(rootView: contentView)
            window.makeKeyAndOrderFront(nil)
    
            NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
                .sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
                    if let size = notificaiton.object as? CGSize {
                        var frame = self.window.frame
                        let old = self.window.contentRect(forFrameRect: frame).size
    
                        let dX = size.width - old.width
                        let dY = size.height - old.height
    
                        frame.origin.y -= dY // origin in flipped coordinates
                        frame.size.width += dX
                        frame.size.height += dY
                        self.window.setFrame(frame, display: true, animate: true)
                    }
                }
                .store(in: &subscribers)
        }
        ...
    

    【讨论】:

    • 在您的示例中,您使用按钮发布通知。但在我的示例中,我使用了一个列表,其中每个项目都是一个更改视图的 NavigationLink。所以我不确定如何在我的特定示例中使用您的方法。注意 - 我更新了我的问题,添加了一个 YouTube 视频链接,该视频显示了我的示例应用的屏幕录像。
    • NavigationLink 可以通过编程方式激活,见here by tag, here by isActive
    • 如果我在 NavigationLink 中使用 tagselection 参数,示例中的侧边栏将停止工作。
    • 我尝试的另一种方法是从 DetailView 的 switch 语句中发布通知。但这给出了一个警告:“NSHostingView 在渲染其 SwiftUI 内容时被重新布局。这是不支持的,当前的布局过程将被跳过。”
    • 我更新了我的示例代码,但我仍然不知道如何将您的方法与 ListNavigationLink 一起使用。
    【解决方案2】:

    Asperi 的回答对我有用,但动画不适用于 Big Sur 11.0.1、Xcode 12.2。值得庆幸的是,如果你将动画包装在 NSAnimationContext 中,动画就可以工作:

    NSAnimationContext.runAnimationGroup({ context in
        context.timingFunction = CAMediaTimingFunction(name: .easeIn)
        window!.animator().setFrame(frame, display: true, animate: true)
    }, completionHandler: {
    })
    

    还应该提到ResizingViewwindow 不必在AppDelegate 内部初始化;你可以继续使用 SwiftUI 的 App 结构体:

    @main
    struct MyApp: App {
    
        @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var body: some Scene {
            WindowGroup {
                ResizingView()
            }
        }
    }
    
    class AppDelegate: NSObject, NSApplicationDelegate {
    
        var window: NSWindow?
        var subscribers = Set<AnyCancellable>()
    
        func applicationDidBecomeActive(_ notification: Notification) {
            self.window = NSApp.mainWindow
        }
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
            setupResizeNotification()
        }
    
        private func setupResizeNotification() {
            NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
                .sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
                    if let size = notificaiton.object as? CGSize, self.window != nil {
                        var frame = self.window!.frame
                        let old = self.window!.contentRect(forFrameRect: frame).size
                        let dX = size.width - old.width
                        let dY = size.height - old.height
                        frame.origin.y -= dY // origin in flipped coordinates
                        frame.size.width += dX
                        frame.size.height += dY
                        NSAnimationContext.runAnimationGroup({ context in
                            context.timingFunction = CAMediaTimingFunction(name: .easeIn)
                            window!.animator().setFrame(frame, display: true, animate: true)
                        }, completionHandler: {
                        })
                    }
                }
                .store(in: &subscribers)
        }
    }
    

    【讨论】:

      【解决方案3】:

      以下内容不会解决您的问题,但可能(通过一些额外的工作)引导您找到解决方案。

      我没有太多要进一步研究的东西,但是可以覆盖 NSWindow 中的 setContentSize 方法(当然是通过子类化)。这样您就可以覆盖默认行为,即设置没有动画的窗口框架。

      您必须弄清楚的问题是,对于像您这样的复杂视图,setContentSize 方法被重复调用,导致动画无法正常工作。

      以下示例可以正常工作,但那是因为我们正在处理一个非常简单的视图:

      @NSApplicationMain
      class AppDelegate: NSObject, NSApplicationDelegate {
      
          var window: NSWindow!
      
      
          func applicationDidFinishLaunching(_ aNotification: Notification) {
              // Create the SwiftUI view that provides the window contents.
              let contentView = ContentView()
      
              // Create the window and set the content view. 
              window = AnimatableWindow(
                  contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
                  styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                  backing: .buffered, defer: false)
              window.center()
              window.setFrameAutosaveName("Main Window")
              window.contentView = NSHostingView(rootView: contentView)
              window.makeKeyAndOrderFront(nil)
          }
      
          func applicationWillTerminate(_ aNotification: Notification) {
              // Insert code here to tear down your application
          }
      }
      
      
      class AnimatableWindow: NSWindow {
          var lastContentSize: CGSize = .zero
      
          override func setContentSize(_ size: NSSize) {
      
              if lastContentSize == size { return } // prevent multiple calls with the same size
      
              lastContentSize = size
      
              self.animator().setFrame(NSRect(origin: self.frame.origin, size: size), display: true, animate: true)
          }
      
      }
      
      struct ContentView: View {
          @State private var flag = false
      
          var body: some View {
              VStack {
                  Button("Change") {
                      self.flag.toggle()
                  }
              }.frame(width: self.flag ? 100 : 300 , height: 200)
          }
      }
      

      【讨论】:

      • 正如您在回答中提到的那样,这种方法不适用于我的示例。显示 ThreeView 时侧边栏消失。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-14
      • 1970-01-01
      • 1970-01-01
      • 2020-08-01
      • 1970-01-01
      • 2021-08-11
      • 1970-01-01
      相关资源
      最近更新 更多