在Mac应用中使用SwiftUI对窗口大小进行动画处理

时间:2019-12-18 04:26:28

标签: macos swiftui swiftui-list swiftui-navigationlink

我正在将SwiftUI用于Mac应用程序,其中主窗口包含侧边栏视图和详细信息视图。选择侧边栏中的项目时,它将更改在详细视图中显示的视图。详细视图中显示的视图的大小不同,这会导致窗口的大小在显示时发生变化。但是,当窗口更改大小时,在更改大小的过程中看起来非常“刺眼”。有没有办法平滑改变窗口大小的动画?

在详细视图中显示的视图:

// OneView.swift

struct OneView: View {
    var body: some View {
        VStack {
            Text("One View").font(.headline).padding()
            Text("Some stuff here.")
        }
        .frame(width: 400, height: 300)
    }
}

// TwoView.swift

struct TwoView: View {
    var body: some View {
        VStack {
            Text("Two View").font(.headline).padding()
            Text("More stuff is here.")
        }
        .frame(width: 400, height: 500)    }
}

// ThreeView.swift

struct ThreeView: View {
    var body: some View {
        VStack {
            Text("Three View").font(.headline).padding()
            Text("Another view goes here.")
        }
        .frame(width: 500, height: 200)
    }
}

主要内容视图,详细信息视图和侧边栏视图:

// ContentView.swift

struct SidebarView: View {

    @State private var selected = Set<String>()

    private let items = ["One View", "Two View", "Three View"]

    var body: some View {
        List(items, id: \.self, selection: $selected) { item in
            NavigationLink(destination: DetailView(selection: item)) {
                Text("\(item)")
            }
        }
        .listStyle(SidebarListStyle())
    }
}

struct DetailView: View {

    var selection: String

    var body: some View {
        switch selection {
        case "One View":
            return AnyView(OneView())
        case "Two View":
            return AnyView(TwoView())
        case "Three View":
            return AnyView(ThreeView())
        default:
            return AnyView(
                Text("Some \(selection) view here").frame(maxWidth: .infinity, maxHeight: .infinity)
            )
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            SidebarView()
            DetailView(selection: "One View")
        }
    }
}

应用程序委托的内容是:

// AppDelegate.swift

@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 = NSWindow(
            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)
    }    
}

可以在https://youtu.be/LcF7U6GW_RI上获得窗口更改大小的屏幕记录

3 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:0)

这里是可行方法的演示。我是从另一种角度来看的,因为您需要重新设计解决方案才能采用它。

演示

SwiftUI animate window frame

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

答案 2 :(得分:0)

以下内容无法解决您的问题,但可能(需要做一些额外的工作)会导致您找到解决方法。

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

您需要弄清的问题是,对于诸如您的复杂视图,将反复调用setContentSize方法,从而导致动画无法正常工作。

下面的示例很好用,但这是因为我们正在处理一个非常简单的视图:

enter image description here

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