ScreenSaver caching during development on Sonoma

Hi all,

I've been playing around with making a screen saver using ScreenSaver.framework and having issues with (what appears to be) macOS caching the previous version of the screensaver until I reboot.

I have written a screen saver that works, however if I make some changes to the code, re-archive and reinstall the newly compiled screen saver still presents the old screen saver. I've tried incrementing the build number to no avail.

Does anyone have a solution to this?

My workflow :-

  • Uninstall and delete all instances of my screen saver.
  • Reboot
  • Open Xcode, compile, re-archive my screen saver.
  • Double-click the archived screen-saver, click to install for this user.
  • Test screensaver -- success!

Now,

  • Delete the screensaver by right-click on screensaver in System Settings, "Delete <MyScreensaver>"
  • Make changes to my code, rearchive.
  • Double-click the archived screen-saver, click to install for this user.
  • Test screensaver -- fail! Still shows the old version of the screensaver.

Reboot machine

  • Test screensaver -- success! Screensaver now shows the changes I made.

Many thanks all.

Accepted Reply

I’ve seen similar problems to this. I encourage you to file a bug requesting improvements to the developer experience. Please post your bug number, just for the record.

It’s been a while, so my recollection is a bit hazy, but one thing I noticed is that the real screen saver is much more ‘sticky’ than the preview. My strategy for clearing this was:

  1. Select a different screen saver.

  2. Click its Preview button.

  3. Escape that.

  4. Control click on my screen saver and choose Delete.

  5. Quit and relaunch System Settings.

If that didn’t fix things, I used Terminal to find and kill the host process in which my real screen saver was loading. I worked that out because my screen saver uses the system log for debugging, and those log entries are all tagged with info about the host process. See Your Friend the System Log for more info about the system log.

Having said that, I didn’t need to do this very often because I did the bulk of my development in a ‘debug host’ app. Pasted in below is a snippet that does that. It depends on two bits of external state, self.isPreview and self.isAnimating, which are themselves controlled by two checkboxes. I’ll leave it up to you to re-create that stuff (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

private var screenSaverView: ScreenSaverView?

@IBAction
private func loadAction(_ sender: Any) {
    // There is no error handling here, and that’s deliberate.  We simply
    // crash if something goes wrong, allowing you to immediately
    // investigate in the debugger.
    
    precondition(self.screenSaverView == nil)
    
    // You can only click Load once!
    
    self.isPreviewCheckbox.isEnabled = false
    self.loadButton.isEnabled = false
    self.window.styleMask = self.window.styleMask.subtracting(.resizable)
        
    // Find the screen saver on disk next to the app.
    
    let appDir = Bundle.main.bundleURL
        .deletingLastPathComponent()
    let screenSavers = try! FileManager.default.contentsOfDirectory(at: appDir, includingPropertiesForKeys: nil)
        .filter { $0.pathExtension == "saver" }
    guard let screenSaver = screenSavers.first else {
        fatalError("No screen saver.")
    }
    guard screenSavers.count == 1 else {
        fatalError("Too many screen savers.")
    }
    
    // Load the screen saver’s principal class.
    
    guard let b = Bundle(url: screenSaver) else {
        fatalError("Failed to create bundle.")
    }
    guard let principalClass = b.principalClass else {
        fatalError("Bundle has no principal class.")
    }
    guard let screenSaverClass = principalClass as? ScreenSaverView.Type else {
        fatalError("Bundle’s principal class isn’t a screen saver.")
    }

    // Instantiate the view and add it as a subview of our placeholder.
    
    var frame = placeholderView.bounds
    frame.origin = .zero
    guard let view = screenSaverClass.init(frame: frame, isPreview: self.isPreview) else {
        fatalError("Fail to instantiate screen saver view.")
    }
    self.screenSaverView = view
    self.placeholderView.addSubview(view)
    // We don’t need to set up any autolayout constraints because we’ve
    // disabled window resize.  Partly we did that to simplify the code but
    // it also kinda makes sense in that it doesn’t make sense for a screen
    // saver to be resized (I’m not even sure if that’s even possible.
    
    // Start the animation if required.
    
    if self.isAnimating {
        view.startAnimation()
    }
}

Replies

I’ve seen similar problems to this. I encourage you to file a bug requesting improvements to the developer experience. Please post your bug number, just for the record.

It’s been a while, so my recollection is a bit hazy, but one thing I noticed is that the real screen saver is much more ‘sticky’ than the preview. My strategy for clearing this was:

  1. Select a different screen saver.

  2. Click its Preview button.

  3. Escape that.

  4. Control click on my screen saver and choose Delete.

  5. Quit and relaunch System Settings.

If that didn’t fix things, I used Terminal to find and kill the host process in which my real screen saver was loading. I worked that out because my screen saver uses the system log for debugging, and those log entries are all tagged with info about the host process. See Your Friend the System Log for more info about the system log.

Having said that, I didn’t need to do this very often because I did the bulk of my development in a ‘debug host’ app. Pasted in below is a snippet that does that. It depends on two bits of external state, self.isPreview and self.isAnimating, which are themselves controlled by two checkboxes. I’ll leave it up to you to re-create that stuff (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

private var screenSaverView: ScreenSaverView?

@IBAction
private func loadAction(_ sender: Any) {
    // There is no error handling here, and that’s deliberate.  We simply
    // crash if something goes wrong, allowing you to immediately
    // investigate in the debugger.
    
    precondition(self.screenSaverView == nil)
    
    // You can only click Load once!
    
    self.isPreviewCheckbox.isEnabled = false
    self.loadButton.isEnabled = false
    self.window.styleMask = self.window.styleMask.subtracting(.resizable)
        
    // Find the screen saver on disk next to the app.
    
    let appDir = Bundle.main.bundleURL
        .deletingLastPathComponent()
    let screenSavers = try! FileManager.default.contentsOfDirectory(at: appDir, includingPropertiesForKeys: nil)
        .filter { $0.pathExtension == "saver" }
    guard let screenSaver = screenSavers.first else {
        fatalError("No screen saver.")
    }
    guard screenSavers.count == 1 else {
        fatalError("Too many screen savers.")
    }
    
    // Load the screen saver’s principal class.
    
    guard let b = Bundle(url: screenSaver) else {
        fatalError("Failed to create bundle.")
    }
    guard let principalClass = b.principalClass else {
        fatalError("Bundle has no principal class.")
    }
    guard let screenSaverClass = principalClass as? ScreenSaverView.Type else {
        fatalError("Bundle’s principal class isn’t a screen saver.")
    }

    // Instantiate the view and add it as a subview of our placeholder.
    
    var frame = placeholderView.bounds
    frame.origin = .zero
    guard let view = screenSaverClass.init(frame: frame, isPreview: self.isPreview) else {
        fatalError("Fail to instantiate screen saver view.")
    }
    self.screenSaverView = view
    self.placeholderView.addSubview(view)
    // We don’t need to set up any autolayout constraints because we’ve
    // disabled window resize.  Partly we did that to simplify the code but
    // it also kinda makes sense in that it doesn’t make sense for a screen
    // saver to be resized (I’m not even sure if that’s even possible.
    
    // Start the animation if required.
    
    if self.isAnimating {
        view.startAnimation()
    }
}