I have a NSHostingView
containing SwiftUI view that can change its size. NSHostingView
does enlarge or shrink to accomodate the content, but these changes are never animated. The inner SwiftUI size changes do animate, but NSHostingView
always jumps directly to the final size. How to make it animate its size changes?
I was trying to make a custom NSHostingView
that does animate. However this is my first time animating on MacOS, so I don't know if I am on the right track. I tried the following subclass of NSHostingView
, and it does not work. It recognizes changes in size correctly, but these changes are still not animated.
class AnimatableHostingView<Content>: NSHostingView<Content> where Content: View {
var oldSize: NSSize = .zero
public required init(rootView: Content) {
super.init(rootView: rootView)
self.needsLayout = true
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.systemTeal.cgColor //just to see the view better
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layout() {
super.layout()
if (self.oldSize != self.frame.size) {
let newsize = self.frame.size
self.animator().frame.size = self.oldSize
self.oldSize = newsize
NSAnimationContext.runAnimationGroup() {context in
context.duration = 2
context.allowsImplicitAnimation = true
self.animator().frame.size = newsize
}
}
}
}
At last I was able to get it working. Not generally, but well enough for my case. So ...
NSHostingView
determines is size using autolayout constraints it automatically creates. You can explore them using Debug > View debugging > Capture view hierarchy.
You can control how these constraints are created using hostingView.sizingOptions
(hostingView is NSHostingView
). If contained SwiftUI changes its size, NSHostingView
changes constants of its constraints, but DOES NOT animate their change. And you have no way how to make it animated.
However if you set hostingView.sizingOptions = []
, hostingView does not create any width or height constraints at all. Which means that you can then create similar constraints by yourself and have them under your control. If you store these constraints in your NSViewController and pass reference to this NSViewController into your SwiftUI View, you can set constants of these constraints from within SwiftUI and animate them as you wish. Or, better, it is NSViewController that changes and animates them, and from SwiftUI you only call its functions.
To show some code, I ended by adding this to my main SwiftUIView:
MyMainView()
.background {
GeometryReader { geometry in
Color.clear
.onAppear {
self.controller.SetHostingSize(geometry.size, animated: false)
}
.onChange(of: geometry.size, perform: {val in
self.controller.SetHostingSize(geometry.size, animated: true)
})
}
}
self.controller is NSViewController passed to my SwiftUI. This is my (simplified) implementation of its SetHostingSize()
public func SetHostingSize(_ size: NSSize, animated: Bool = true) {
if (!animated) {
self.widthConstr.constant = size.width //my stored width constraint
self.heightConstr.constant = size.height //my stored height constraint
return
}
NSAnimationContext.runAnimationGroup { context in
context.allowsImplicitAnimation = false
context.duration = 0.5
context.timingFunction = .init(name: .linear)
self.widthConstr.animator().constant = size.width
self.heightConstr.animator().constant = size.height
}
}
It needs more more work to align constraint animation with animations inside SwiftUI, and also maybe to make it easier to use. But this is the core and you can work with that.