macosswiftuiappkitnshostingview

Can NSHostingView animate its frame changes?


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

 }

Solution

  • At last I was able to get it working. Not generally, but well enough for my case. So ...

    1. NSHostingView determines is size using autolayout constraints it automatically creates. You can explore them using Debug > View debugging > Capture view hierarchy.

    2. 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.

    3. 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.