Context
NB: The question does NOT pertain to iOS
I have a Mac app that shows an NSPopover. The content of that popover is an NSHostingView that displays a simple SwiftUI view:
struct PopoverView: View
{
@State private var buttonWidthScale: CGFloat = 1.0
var body: some View
{
Button {
...
} label: {
RoundedRectangle(cornerRadius: 6.0)
.fill(.blue)
.scaleEffect(CGSize(width: buttonWidthScale, height: 1))
.animation(.easeInOut(duration: 2.5).repeatForever(), value: buttonWidthScale)
.onAppear {
buttonWidthScale = 0.96
}
}
}
}
The goal is to have a blue rectangle that very subtly "pulses" its width. The above works just fine to do that.
The Problem
I assumed (quite stupidly) that SwiftUI is smart enough to suspend the animation when the popover closes and the view is no longer on screen. That is not the case. Once the view appears for the first time, the app will now consume 5-6% CPU forever. The app correctly uses 0% CPU before this NSPopover appears for the first time and the animation kicks off.
What I Need
The SwiftUI
.onAppear()and.onDisappear()methods are poorly named. They should really be called.onInsertion()and.onRemoval(), because they are only called when the view is added/removed from the hierarchy. (The "on appear" and "on disappear" names have historical meaning fromNSViewControllerand Apple should never have recycled those names for a different intent.) As such, I cannot use these methods to start/stop the animation..onAppear()is ever called only once and.onDisappear()is never called at all.This animation should run continuously whenever the view is ON-SCREEN and then stop when the view disappears. So I need a replacement for
.onAppear()and.onDisappear()that.....actually do what they imply they do!My current approach is very hacky. From the
NSViewControllerthat holds theNSHostingView, I do this:
extension PopoverController: NSPopoverDelegate
{
func popoverWillShow(_ notification: Notification)
{
hostingView.rootView.popoverDidAppear()
}
func popoverDidClose(_ notification: Notification)
{
hostingView.rootView.popoverDidDisappear()
}
}
Where popoverDidAppear() and popoverDidDisappear() are two functions I've added to the PopoverView that replace the animation completely, as appropriate. (You can get rid of a .repeatForever() animation by replacing it with a new animation that is finite.)
But...this CANNOT be the right way, can it? There MUST be a canonical SwiftUI solution here that I just don't know about? Surely the future of Apple UI frameworks cannot need AppKit's help just to know when a view is shown and not shown?