<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/"
>

<channel>
	<title>ScreenSaverView &#8211; Wade Tregaskis</title>
	<atom:link href="https://wadetregaskis.com/tags/screensaverview/feed/" rel="self" type="application/rss+xml" />
	<link>https://wadetregaskis.com</link>
	<description></description>
	<lastBuildDate>Thu, 11 Dec 2025 18:47:41 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://wadetregaskis.com/wp-content/uploads/2016/03/Stitch-512x512-1-256x256.png</url>
	<title>ScreenSaverView &#8211; Wade Tregaskis</title>
	<link>https://wadetregaskis.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">226351702</site>	<item>
		<title>How to make a macOS screen saver</title>
		<link>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/</link>
					<comments>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/#comments</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Sat, 06 Dec 2025 22:46:22 +0000</pubDate>
				<category><![CDATA[Coding]]></category>
		<category><![CDATA[Howto]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Bugs!]]></category>
		<category><![CDATA[com.apple.screensaver.willstop]]></category>
		<category><![CDATA[SaveHollywood]]></category>
		<category><![CDATA[Screen saver]]></category>
		<category><![CDATA[ScreenSaverView]]></category>
		<category><![CDATA[Snafu]]></category>
		<category><![CDATA[Swift]]></category>
		<guid isPermaLink="false">https://wadetregaskis.com/?p=8580</guid>

					<description><![CDATA[First, make sure you really want to. macOS&#8217;s screen saver system is absurdly buggy and broken. It&#8217;s frustrating to work with and very difficult to make work right. If you&#8217;re determined, read on. Screen savers are basically just applications. Same bundle format and structure, just with a &#8220;saver&#8221; extension instead of &#8220;app&#8221;. Setting up the&#8230; <a class="read-more-link" href="https://wadetregaskis.com/how-to-make-a-macos-screen-saver/" data-wpel-link="internal">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p>First, make sure you <em>really</em> want to.  macOS&#8217;s screen saver system is absurdly buggy and broken.  It&#8217;s frustrating to work with and very difficult to make work right.</p>



<p>If you&#8217;re determined, read on.</p>



<p>Screen savers are basically just applications.  Same bundle format and structure, just with a &#8220;saver&#8221; extension instead of &#8220;app&#8221;.</p>



<h2 class="wp-block-heading">Setting up the Xcode project</h2>



<p>In Xcode, create a new project using the <code>Screen Saver</code> template.</p>



<p>Delete the Objective-C code &amp; header files it creates by default (unless, I suppose, you want to write your screen saver in Objective-C &#8211; woo, retro! 😆).</p>



<p>You need to import the ScreenSaver module (framework), subclass ScreenSaverView, and implement a couple of method overrides.  Here&#8217;s the basic skeleton:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>import ScreenSaver

class MyScreenSaver: ScreenSaverView {
    override init?(frame: NSRect, isPreview: Bool) {
        super.init(frame: frame, isPreview: isPreview)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    private func setup() {
        // TODO
    }

    override func startAnimation() {
        super.startAnimation()

        // TODO
    }

    override func animateOneFrame() {  // Optional.
        // TODO
    }

    override func stopAnimation() { //  Only for the live preview in System Settings.
        // TODO

        super.stopAnimation()
    }

    override var hasConfigureSheet: Bool {
        true
    }

    private var configureSheetController: ConfigureSheetController?

    override var configureSheet: NSWindow? {
        configureSheetController = ConfigureSheetController(windowNibName: "ConfigureSheet")
        return configureSheetController?.window
    }
}

class ConfigureSheetController: NSWindowController {
    override var windowNibName: NSNib.Name? {
        return "ConfigureSheet"
    }

    override func windowDidLoad() {
        super.windowDidLoad()

        // TODO
    }

    @IBAction func okButtonClicked(_ sender: NSButton) {
        // TODO

        window!.sheetParent!.endSheet(window!, returnCode: .OK)
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #AF00DB">import</span><span style="color: #000000"> </span><span style="color: #267F99">ScreenSaver</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">MyScreenSaver</span><span style="color: #000000">: ScreenSaverView {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">init?</span><span style="color: #000000">(</span><span style="color: #795E26">frame</span><span style="color: #000000">: NSRect, </span><span style="color: #795E26">isPreview</span><span style="color: #000000">: </span><span style="color: #267F99">Bool</span><span style="color: #000000">) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">frame</span><span style="color: #000000">: frame, </span><span style="color: #795E26">isPreview</span><span style="color: #000000">: isPreview)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">setup</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">required</span><span style="color: #000000"> </span><span style="color: #0000FF">init?</span><span style="color: #000000">(</span><span style="color: #795E26">coder</span><span style="color: #000000">: NSCoder) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #0000FF">init</span><span style="color: #000000">(</span><span style="color: #795E26">coder</span><span style="color: #000000">: coder)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #795E26">setup</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">startAnimation</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">animateOneFrame</span><span style="color: #000000">() {  </span><span style="color: #008000">// Optional.</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">() { </span><span style="color: #008000">//  Only for the live preview in System Settings.</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> hasConfigureSheet: </span><span style="color: #267F99">Bool</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">true</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> configureSheetController: ConfigureSheetController?</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> configureSheet: NSWindow? {</span></span>
<span class="line"><span style="color: #000000">        configureSheetController = </span><span style="color: #795E26">ConfigureSheetController</span><span style="color: #000000">(</span><span style="color: #795E26">windowNibName</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;ConfigureSheet&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> configureSheetController?.</span><span style="color: #001080">window</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">class</span><span style="color: #000000"> </span><span style="color: #267F99">ConfigureSheetController</span><span style="color: #000000">: NSWindowController {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> windowNibName: NSNib.Name? {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">return</span><span style="color: #000000"> </span><span style="color: #A31515">&quot;ConfigureSheet&quot;</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">windowDidLoad</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">super</span><span style="color: #000000">.</span><span style="color: #795E26">windowDidLoad</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">@IBAction</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">okButtonClicked</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">sender</span><span style="color: #000000">: NSButton) {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// TODO</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        window!.</span><span style="color: #001080">sheetParent</span><span style="color: #000000">!.</span><span style="color: #795E26">endSheet</span><span style="color: #000000">(window!, </span><span style="color: #795E26">returnCode</span><span style="color: #000000">: .</span><span style="color: #001080">OK</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<h2 class="wp-block-heading">Providing a preferences sheet</h2>



<p>If you don&#8217;t have any options to configure, you can of course change <code>hasConfigureSheet</code> to return <code>false</code>.  Otherwise, you&#8217;ll need to create a &#8220;ConfigureSheet&#8221; xib file with a window in it containing your screen saver&#8217;s settings.  You can use UserDefaults to save your settings (if you wish), same as any other app.  And you&#8217;ll need to add an &#8220;Okay&#8221; or &#8220;Save&#8221; or similar button to dismiss the sheet.</p>



<h2 class="wp-block-heading">Getting ready to render</h2>



<p>The key methods to implement are <code>setup</code>, with any initial configuration you wish to do (e.g. allocate image or video views, load assets, set up the view hierarchy, etc).</p>



<p><code>ScreenSaverView</code> is an <code>NSView</code> subclass with a flat black background by default, onto which you can add subviews.  Typically for a screen saver you have a very simple view hierarchy &#8211; often just a single view or CoreAnimation layer that you&#8217;re rendering to &#8211; but you can load a xib and insert elements from it into the view if you like.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>☝️ It&#8217;s wise to <em>not</em> render anything in <code>setup</code>, which includes accidentally &#8211; you might need to set views to hidden, layers to zero opacity, etc.  This is basically because there can be an arbitrarily long gap between <code>setup</code> and <code>startAnimation</code> calls, and it&#8217;s often weird to render something initially, potentially not animate for a noticeable length of time, and <em>then</em> start actually working properly.</p>



<p>Alternatively, you might insert a placeholder image or text, e.g. &#8220;Loading…&#8221;, if you really want.  But in my opinion it&#8217;s more graceful to just let the initial black screen stand for a moment.</p>
</div></div>



<h2 class="wp-block-heading">Rendering</h2>



<p><code>startAnimation</code> is where you should actually start displaying things.  e.g. if you&#8217;re using an <code>AVPlayer</code>, this is where you actually start it playing (after making it visible, e.g. setting its opacity to 1).</p>



<p><code>animateOneFrame</code> is optional, and is only called if you set the <code>animationTimeInterval</code> property (on <code>self</code>) to a finite, non-zero value in <code>setup</code> (in which case it&#8217;ll be called at intervals <em>at least</em> that long &#8211; it might not be called as often as you desire if previous calls overrun or there&#8217;s other bottlenecks in the screen saver framework).  It&#8217;s essentially just a minor convenience vs having to explicitly set up an <code>NSTimer</code>.</p>



<p>Given how buggy Apple&#8217;s screen saver framework is, I suggest <em>not</em> relying on <code>animateOneFrame</code> if you can at all avoid it.  Even if that means setting up your own timer.  That way when they likely break that too in some future macOS release, your screen saver won&#8217;t necessarily break as well.</p>



<h3 class="wp-block-heading">Bonus topic: fading in</h3>



<p>Unless your screen saver inherently appears gently (e.g. starts rendering with a flat black view and only slowly adds to it), it&#8217;s nice to add a fade-in.  You can do that using CoreAnimation on the view&#8217;s layer:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>override func startAnimation() {
    // Other code…
    
    if let layer {
        layer.opacity = 0.0  // Should already be zero from `setup`, but just to be sure.

        let fadeAnimation = CABasicAnimation(keyPath: "opacity")
        fadeAnimation.fromValue = 0.0
        fadeAnimation.toValue = 1.0
        fadeAnimation.duration = 5  // Seconds.

        // Essential settings to keep the final state
        fadeAnimation.fillMode = .forwards
        fadeAnimation.isRemovedOnCompletion = false

        layer.add(fadeAnimation, forKey: "fadeAnimation")
    }
    
    // Other code…
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> layer {</span></span>
<span class="line"><span style="color: #000000">        layer.</span><span style="color: #001080">opacity</span><span style="color: #000000"> = </span><span style="color: #098658">0.0</span><span style="color: #000000">  </span><span style="color: #008000">// Should already be zero from `setup`, but just to be sure.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #0000FF">let</span><span style="color: #000000"> fadeAnimation = </span><span style="color: #795E26">CABasicAnimation</span><span style="color: #000000">(</span><span style="color: #795E26">keyPath</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;opacity&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">fromValue</span><span style="color: #000000"> = </span><span style="color: #098658">0.0</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">toValue</span><span style="color: #000000"> = </span><span style="color: #098658">1.0</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">duration</span><span style="color: #000000"> = </span><span style="color: #098658">5</span><span style="color: #000000">  </span><span style="color: #008000">// Seconds.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #008000">// Essential settings to keep the final state</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">fillMode</span><span style="color: #000000"> = .</span><span style="color: #001080">forwards</span></span>
<span class="line"><span style="color: #000000">        fadeAnimation.</span><span style="color: #001080">isRemovedOnCompletion</span><span style="color: #000000"> = </span><span style="color: #0000FF">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        layer.</span><span style="color: #795E26">add</span><span style="color: #000000">(fadeAnimation, </span><span style="color: #795E26">forKey</span><span style="color: #000000">: </span><span style="color: #A31515">&quot;fadeAnimation&quot;</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Note that you cannot implement a fade-out when the screen saver exits, because macOS hides your screen saver immediately.  Plus, the user might not want a fade-out as they may be in a rush to do something on their computer.</p>



<h3 class="wp-block-heading">Previewing</h3>



<p>You can determine if you&#8217;re running for real or only the preview via the <code>isPreview</code> property (on <code>self</code>).  Many screen savers don&#8217;t care, but particularly if you save any persistent state, you might want to avoid doing that during preview.  For example, in a screen saver which plays a looping video and resumes where it last left off, you probably don&#8217;t want the preview to quietly advance the video.</p>



<h2 class="wp-block-heading">Stopping</h2>



<p><code>stopAnimation</code> is <em>only</em> used for the live preview thumbnail shown in the Screen Saver System Settings pane.  It is <em>never</em> called in normal operation of the screen saver (contrary to what Apple&#8217;s documentation says &#8211; Apple broke that in macOS Sonoma and later).</p>



<p>And that leads to the first path off the official track.  When the screen saver is dismissed by the user, <em>nothing</em> in Apple&#8217;s framework code does anything.  Your view continues to exist, <code>animateOneFrame</code> continues getting called, etc.  Your screen saver just runs in the background, its output not visible, but wasting CPU cycles and RAM.  Worse, if you have sound, that keeps playing.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>🙏 A big thanks to <a href="https://stackoverflow.com/users/4103152/cwizou" data-wpel-link="external" target="_blank" rel="external noopener">cwizou</a> via <a href="https://stackoverflow.com/questions/66861833/audio-keeps-playing-after-screensaver-ends" data-wpel-link="external" target="_blank" rel="external noopener">StackOverflow</a> for documenting <a href="https://stackoverflow.com/a/67161635" data-wpel-link="external" target="_blank" rel="external noopener">the solution</a>, which I&#8217;ve summarised below.</p>
</div></div>



<p>To get around that, you need to register for the <code>com.apple.screensaver.willstop</code> notification, in <code>setup</code>, like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>  private func setup() {
    DistributedNotificationCenter.default.addObserver(self,
                                                      selector: #selector(willStop(_:)),
                                                      name: Notification.Name("com.apple.screensaver.willstop"),
                                                      object: nil)
}
    
@objc func willStop(_ notification: Notification) {
    stopAnimation()
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #000000">  </span><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    DistributedNotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">addObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">selector</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #795E26">willStop</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">name</span><span style="color: #000000">: Notification.</span><span style="color: #795E26">Name</span><span style="color: #000000">(</span><span style="color: #A31515">&quot;com.apple.screensaver.willstop&quot;</span><span style="color: #000000">),</span></span>
<span class="line"><span style="color: #000000">                                                      </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">willStop</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">notification</span><span style="color: #000000">: Notification) {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>Note that you still need <code>stopAnimation</code> specifically, because in the live preview in System Settings you won&#8217;t receive that <code>com.apple.screensaver.willstop</code> notification (from the system&#8217;s point of view, the screen saver <em>isn&#8217;t</em> running &#8211; it&#8217;s merely previewing).</p>



<h2 class="wp-block-heading">Handling resumption</h2>



<p>Here&#8217;s the second big bug in Apple&#8217;s screen saver framework &#8211; every time the screen saver starts, your <code>ScreenSaverView</code> subclass is created again. But the old one doesn&#8217;t go anywhere. So now you have <em>two</em> copies running simultaneously, which is at the very least wasteful, and can easily lead to gnarly bugs and weird behaviour (e.g. if both are playing sound, or both modify persistent state).</p>



<p>There are essentially two ways to handle this:</p>



<ol class="wp-block-list">
<li>Kill your own process every time you stop animating.</li>



<li>Manually kill or lame-duck older views when a new one is initialised.</li>
</ol>



<p>Note that you <em>cannot</em> simply check at <code>MyScreenSaver</code> initialisation time if an instance already exists and if so fail initialisation (as is prescribed by <a href="https://zsmb.co/building-a-macos-screen-saver-in-kotlin/#macos-sonoma" data-wpel-link="external" target="_blank" rel="external noopener">this</a> otherwise excellent write-up of this problem), because if you don&#8217;t correctly initialise you&#8217;ll sometimes end up with <em>nothing</em> rendering or running (the screen saver framework appears to not gracefully handle initialisation failures).</p>



<p>Killing your own process can work but has some perils:</p>



<ul class="wp-block-list">
<li>If you kill your process in <code>stopAnimation</code> the screen will flash black momentarily before actually exiting screen saver mode, which is visually annoying.</li>



<li>If the screen saver is restarted rapidly after being interrupted, sometimes you&#8217;ll end up with nothing but a black screen (with no screen saver running).  There&#8217;s evidently some race condition in Apple&#8217;s screen saver system between screen saver processes exiting and being [re]launched.</li>
</ul>



<p>So I recommend not taking that approach.  Instead, you can lame-duck the old view instances.  They&#8217;ll stick around, which is a little wasteful of RAM, but as long as they&#8217;re not rendering or otherwise doing anything, they&#8217;re benign.</p>



<p>There are various ways to implement that, but one of the simpler ones is simply a notification between instances:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>static let NewInstanceNotification = "com.myapp.MyScreenSaver.NewInstance";

var lameDuck = false

private func setup() {
    // Initial setup…
    
    NotificationCenter.default.post(name: MyScreenSaver.NewInstanceNotification, object: self)

    NotificationCenter.default.addObserver(self,
                                           selector: #selector(neuter(_:)),
                                           name: MyScreenSaver.NewInstanceNotification,
                                           object: nil)

    // Further setup…
}

@objc func neuter(_ notification: Notification) {
    lameDuck = true

    stopAnimation()

    self.removeFromSuperview()

    // TODO: any additional cleanup you can, e.g. release image &amp; video files, throw out transient models and state, etc.

    NotificationCenter.default.removeObserver(self)
    DistributedNotificationCenter.default().removeObserver(self)
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">static</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> NewInstanceNotification = </span><span style="color: #A31515">&quot;com.myapp.MyScreenSaver.NewInstance&quot;</span><span style="color: #000000">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">var</span><span style="color: #000000"> lameDuck = </span><span style="color: #0000FF">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">private</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">setup</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Initial setup…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">post</span><span style="color: #000000">(</span><span style="color: #795E26">name</span><span style="color: #000000">: MyScreenSaver.</span><span style="color: #001080">NewInstanceNotification</span><span style="color: #000000">, </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">addObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">selector</span><span style="color: #000000">: </span><span style="color: #795E26">#selector</span><span style="color: #000000">(</span><span style="color: #795E26">neuter</span><span style="color: #000000">(_:)),</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">name</span><span style="color: #000000">: MyScreenSaver.</span><span style="color: #001080">NewInstanceNotification</span><span style="color: #000000">,</span></span>
<span class="line"><span style="color: #000000">                                           </span><span style="color: #795E26">object</span><span style="color: #000000">: </span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Further setup…</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">@objc</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">neuter</span><span style="color: #000000">(</span><span style="color: #795E26">_</span><span style="color: #000000"> </span><span style="color: #001080">notification</span><span style="color: #000000">: Notification) {</span></span>
<span class="line"><span style="color: #000000">    lameDuck = </span><span style="color: #0000FF">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #0000FF">self</span><span style="color: #000000">.</span><span style="color: #795E26">removeFromSuperview</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// TODO: any additional cleanup you can, e.g. release image &amp; video files, throw out transient models and state, etc.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">    NotificationCenter.</span><span style="color: #001080">default</span><span style="color: #000000">.</span><span style="color: #795E26">removeObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">    DistributedNotificationCenter.</span><span style="color: #795E26">default</span><span style="color: #000000">().</span><span style="color: #795E26">removeObserver</span><span style="color: #000000">(</span><span style="color: #0000FF">self</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>You should check <code>lameDuck</code> at the start of methods like <code>startAnimation</code> or <code>animateOneFrame</code> and exit immediately if it&#8217;s set to <code>true</code>.  Unfortunately, Apple&#8217;s screen saver framework will still call those methods on old instances.</p>



<h2 class="wp-block-heading">Exiting</h2>



<p>Unfortunately Apple&#8217;s screen saver system will never terminate your screen saver process.  Worse, even if you do <em>nothing</em> yourself, Apple&#8217;s screen saver framework code will run in an infinite loop, wasting [a small amount of] CPU time.  So it&#8217;s not great to leave your screen saver process running indefinitely.</p>



<p>Thus, I implement an idle timeout in my screen savers, to have them exit if they&#8217;re not active for a while.  This can be done like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro padding-disabled" data-code-block-pro-font-family="" style="font-size:.875rem;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#000000;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@MainActor var idleTimeoutWorkItem: DispatchWorkItem? = nil

override func startAnimation() {
    // Other code…
    
    DispatchQueue.main.async {
        if let idleTimeoutWorkItem {
            idleTimeoutWorkItem.cancel()
        }

        idleTimeoutWorkItem = nil
    }
    
    // Other code…
}

override func stopAnimation() {
    // Other code…
    
    if !lameDuck {
        DispatchQueue.main.async {
            idleTimeoutWorkItem?.cancel()

            let workItem = DispatchWorkItem(block: {
                NSApplication.shared.terminate(nil)
            })
            
            idleTimeoutWorkItem = workItem
            
            DispatchQueue.main.asyncAfter(wallDeadline: .now() + 65, execute: workItem)
        }
    }
    
    // Other code…
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki light-plus" style="background-color: #FFFFFF" tabindex="0"><code><span class="line"><span style="color: #0000FF">@MainActor</span><span style="color: #000000"> </span><span style="color: #0000FF">var</span><span style="color: #000000"> idleTimeoutWorkItem: DispatchWorkItem? = </span><span style="color: #0000FF">nil</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">startAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #001080">async</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">        </span><span style="color: #AF00DB">if</span><span style="color: #000000"> </span><span style="color: #0000FF">let</span><span style="color: #000000"> idleTimeoutWorkItem {</span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem.</span><span style="color: #795E26">cancel</span><span style="color: #000000">()</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">        idleTimeoutWorkItem = </span><span style="color: #0000FF">nil</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #0000FF">override</span><span style="color: #000000"> </span><span style="color: #0000FF">func</span><span style="color: #000000"> </span><span style="color: #795E26">stopAnimation</span><span style="color: #000000">() {</span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #AF00DB">if</span><span style="color: #000000"> !lameDuck {</span></span>
<span class="line"><span style="color: #000000">        DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #001080">async</span><span style="color: #000000"> {</span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem?.</span><span style="color: #795E26">cancel</span><span style="color: #000000">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #000000">            </span><span style="color: #0000FF">let</span><span style="color: #000000"> workItem = </span><span style="color: #795E26">DispatchWorkItem</span><span style="color: #000000">(</span><span style="color: #795E26">block</span><span style="color: #000000">: {</span></span>
<span class="line"><span style="color: #000000">                NSApplication.</span><span style="color: #001080">shared</span><span style="color: #000000">.</span><span style="color: #795E26">terminate</span><span style="color: #000000">(</span><span style="color: #0000FF">nil</span><span style="color: #000000">)</span></span>
<span class="line"><span style="color: #000000">            })</span></span>
<span class="line"><span style="color: #000000">            </span></span>
<span class="line"><span style="color: #000000">            idleTimeoutWorkItem = workItem</span></span>
<span class="line"><span style="color: #000000">            </span></span>
<span class="line"><span style="color: #000000">            DispatchQueue.</span><span style="color: #001080">main</span><span style="color: #000000">.</span><span style="color: #795E26">asyncAfter</span><span style="color: #000000">(</span><span style="color: #795E26">wallDeadline</span><span style="color: #000000">: .</span><span style="color: #795E26">now</span><span style="color: #000000">() + </span><span style="color: #098658">65</span><span style="color: #000000">, </span><span style="color: #795E26">execute</span><span style="color: #000000">: workItem)</span></span>
<span class="line"><span style="color: #000000">        }</span></span>
<span class="line"><span style="color: #000000">    }</span></span>
<span class="line"><span style="color: #000000">    </span></span>
<span class="line"><span style="color: #000000">    </span><span style="color: #008000">// Other code…</span></span>
<span class="line"><span style="color: #000000">}</span></span></code></pre></div>



<p>I chose the 65 second timeout somewhat arbitrarily.  I figure there&#8217;s a reasonable chance a user will unlock their screen to do something quick, then engage the screen saver again &#8211; all in the less than a minute &#8211; and the cost of idling in the background for an extra minute is small, compared to the cost of relaunching the whole app and reinitialising your renderer.</p>



<p>I added five extra seconds to reduce the probability of aligning with some one-minute timer (e.g. a spurious wake with the screen saver set to start automatically after one minute of no user activity).</p>



<p>You can adjust it however you like.</p>



<h2 class="wp-block-heading">Testing your screen saver</h2>



<p>Double-clicking the built product (your &#8220;.saver&#8221; app) will prompt the user to install it, replacing an old version if necessary.  So that works, though I find it faster to just manually copy the &#8220;.saver&#8221; app to <code>~/Library/Screen Savers</code>.  Just make sure to kill the existing <code>legacyScreenSaver</code> process, if necessary.</p>



<p>You can test it in System Settings, in the Screen Saver pane.  That&#8217;s the only place you can test the live preview part.</p>



<p>But otherwise, I found it easiest to just set one of the screen hot corners to start the screen saver, and use that immediately after copying the new &#8220;.saver&#8221; file into place.</p>



<p>Just be aware that the first time any new copy of the screen saver runs, macOS runs a verification on the bundle, which can take a while if your screen saver is non-trivial in size (e.g. if you bundle large image or video resources).  You&#8217;ll get a black screen with nothing happening, after invoking the screen saver, while that verification is running.</p>



<h2 class="wp-block-heading">Distributing your screen saver</h2>



<p>You don&#8217;t <em>have</em> to sign your screen saver, necessarily, but users will get some annoying error dialogs trying to run it, and will have to fiddle with things in System Settings &#8211; or, if they&#8217;re on a corporate Mac, they might not be able to run it at all.  So it&#8217;s preferable to just sign it.</p>



<p>Xcode doesn&#8217;t support signing screen savers like it does for plain app targets and the like.  So you have to do it manually via the command line, on the built product (your &#8220;.saver&#8221; app).  Thankfully it&#8217;s just two commands (once you have the appropriate stuff set up in your developer account &#8211; note that you will need a paid Apple Developer account, at $99/year).</p>



<p>Follow the instructions <a href="https://www.gabrieluribe.me/blog/how-to-distribute-a-screensaver-on-macos-2022" data-wpel-link="external" target="_blank" rel="external noopener">here</a>, but note that they&#8217;re missing the final step:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>xcrun stapler staple -v MyScreenSaver.saver</p>
</blockquote>



<p>Note that you run it against the screen saver itself, <em>not</em> the zip file.  The zip file&#8217;s just a hack to get Apple&#8217;s notary tool to accept the submission.  It&#8217;s the screen saver bundle itself that&#8217;s actually notarised and signed.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p>⚠️ <code>notarytool</code> uploads your screen saver to Apple.  Be sure it doesn&#8217;t contain anything you&#8217;re not happy being potentially public (Apple will <em>presumably</em> try to keep your uploads private to Apple, and <em>might</em> not intentionally store them forever, but I wouldn&#8217;t bet my life on their confidentiality).</p>
</div></div>



<h2 class="wp-block-heading">Conclusion</h2>



<p>That&#8217;s &#8220;it&#8221; in a superficial sense &#8211; if you&#8217;ve followed all this so far, you have a <em>roughly</em> working screen saver.</p>



<p>But there are a lot more bugs and nuances thereof that may afflict you, depending on what you&#8217;re doing in your screen saver.  So good luck. 😣</p>



<h2 class="wp-block-heading">Addendum: SaveHollywood</h2>



<p>I found that looking at existing open source screen savers was <em>partially</em> helpful, but also sometimes misleading.  e.g. for my most recent screen saver I basically just play a video in a loop (which <em>should</em> be embarrassingly trivial but took two weeks to get working properly, thanks to the aforementioned bugs in Apple&#8217;s frameworks, among many others).  In that case I looked at <a href="https://github.com/packagesdev/savehollywood" data-wpel-link="external" target="_blank" rel="external noopener">SaveHollywood</a>, a similar screen saver, for aid and ideas.</p>



<p>Unfortunately, SaveHollywood is abandoned and doesn&#8217;t work on recent versions of macOS.  The way it does some things is archaic and either not the best way or not functional at all.</p>



<p>Nonetheless, it did help with some of the higher-level aspects, above the screen saver machinery itself, like how to use <code>AVPlayer</code> in a screen saver.</p>



<p>So, <em>do</em> check out similar, existing screen savers (and of course just use them directly if they suit your needs!) but beware of obsolete or otherwise incorrect code.</p>



<h2 class="wp-block-heading">Addendum:  What this says about Apple</h2>



<p>What really troubles me about the screen saver system is what it says about Apple&#8217;s approach to the Mac, software, and their users.  Which is sadly just the same thing we&#8217;ve been seeing for years now.</p>



<p>Screen savers used to work fine.  There was an API established <em>long</em> ago that was lightweight, straight-forward, and effective.  All Apple had to do was <em>not break it</em>.</p>



<p>And <em>how</em> they broke it is troubling.  Perhaps it was prompted by some otherwise unrelated but well-meaning refactor &#8211; pulling screen saver code out into a separate, sandboxed process, perhaps, for improved system security.  Fine.  But if it&#8217;s worth changing it&#8217;s worth changing <em>properly</em>.  It&#8217;s very clear that whomever did the changes either (a) didn&#8217;t care that they broke things badly, or (b) didn&#8217;t care to check.</p>



<p>It&#8217;s that recurring theme of <em>not caring</em> that&#8217;s most disappointing in today&#8217;s Apple.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://wadetregaskis.com/how-to-make-a-macos-screen-saver/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			<media:content url="https://wadetregaskis.com/wp-content/uploads/2025/12/screen-saver-system-settings-panel-with-my-screen-saver-selected.webp" medium="image" />
<post-id xmlns="com-wordpress:feed-additions:1">8580</post-id>	</item>
	</channel>
</rss>
