Изменение размера окна на основе NavigationView в приложении SwiftUI для macOS
Я использую SwiftUI для приложения Mac, в котором главное окно содержит NavigationView. Этот 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()
}
}
А вот как выглядит пример приложения при изменении размера детального представления:
2 ответа
Вот демонстрация возможного подхода, который работает. Я сделал это с одной другой точки зрения, потому что вам нужно будет переделать свое решение, чтобы принять его.
Демо
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)
}
...
Ответ Аспери у меня работает, но анимация не работает на 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: {
})
Также следует отметить, что
ResizingView
и
window
не нужно инициализировать внутри 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)
}
}
Следующее не решит вашу проблему, но может (с некоторой дополнительной работой) привести вас к решению.
Мне нечего было исследовать дальше, но можно перезаписать метод setContentSize в NSWindow (конечно, путем создания подклассов). Таким образом вы можете переопределить поведение по умолчанию, которое устанавливает рамку окна без анимации.
Проблема, которую вам придется решить, заключается в том, что для сложных представлений, таких как ваше, метод 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)
}
}