Skip to content

Instantly share code, notes, and snippets.

@disc0infern0
Created June 13, 2022 18:08
Show Gist options
  • Save disc0infern0/b2fe0342a203d155ee9a1ef6daeea16c to your computer and use it in GitHub Desktop.
Save disc0infern0/b2fe0342a203d155ee9a1ef6daeea16c to your computer and use it in GitHub Desktop.
Screen Saver in SwiftUI - simple colour rotation
//
import SwiftUI
import ScreenSaver
struct Defaults {
/// Use bundleID for the Module name. It is found in the project properties, of target, General Tab.
let storage = ScreenSaverDefaults(forModuleWithName: "com.ajc.SSaver")!
let refreshRateKey = "refreshRate"
var refreshRate: Double { storage.double(forKey: refreshRateKey) }
func saveRefreshRate(value: Double) -> Bool {
storage.set(value, forKey: refreshRateKey)
return refreshRate == value ? true : false
}
func setDefault(refreshRate: Double) -> Bool {
guard self.refreshRate == 0 else { return true }
storage.register(defaults: [refreshRateKey: refreshRate])
return refreshRate == self.refreshRate ? true : false
}
static var shared = Defaults()
}
class SSaver: ScreenSaverView {
var viewModel: ViewModel /// An observable object for sharing with SwiftUI
var preferencesSheet: NSWindow /// Maintain reference to the screensaver Preferences sheet
required init?(coder: NSCoder) { fatalError("Coder not implemented") }
override init?(frame: NSRect, isPreview: Bool) {
preferencesSheet = NSWindow()
viewModel = ViewModel(defaults: Defaults.shared )
super.init(frame: frame, isPreview: isPreview)
setup()
}
/// Update screensaver state, and request redraw when needed
override func animateOneFrame() {
viewModel.animationTick()
needsDisplay = true
}
/// Called just after screen is drawn
// override func startAnimation() { super.startAnimation() }
/// Stops animation - also called when showing Preferences
// override func stopAnimation() { super.stopAnimation() }
override var hasConfigureSheet: Bool { true }
override var configureSheet: NSWindow? { return preferencesSheet }
}
extension SSaver {
func setup() {
animationTimeInterval = 1/30.0
// Previews don't animate by default, so kickstart it
if isPreview {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: startAnimation)
}
// Attach Preferences SwiftUI view to a NSWindow (preferencesSheet)
let preferencesView = NSHostingView(rootView:
Preferences(viewModel: viewModel) { // pass a function to SwiftUI to allow it to close the Preferences sheet
self.window!.endSheet(self.preferencesSheet) } )
preferencesView.frame = bounds
preferencesSheet.contentView = preferencesView
let saverView = NSHostingView(rootView: Saver(viewModel: viewModel))
saverView.frame = bounds
/// setting to window?.contentView doesnt work at all. If set in startAnimation, the view appears, but does not animate
addSubview(saverView)
}
}
// SwiftUI from here on out :-
@MainActor
class ViewModel: ObservableObject {
@Published var defaults: Defaults
@Published var viewColors: RotatingColors
var lastUpdate = Date.now
func animationTick() {
if Date.now > lastUpdate + defaults.refreshRate {
lastUpdate = Date.now
viewColors.next()
}
}
init(defaults: Defaults) {
self.defaults = defaults
viewColors = RotatingColors([.red, .orange, .yellow, .green, .blue, .indigo, .purple] )
_ = defaults.setDefault(refreshRate: 4.0)
}
}
struct RotatingColors { // Initialise with array of colors, and the getColor function will rotate through them every "refresh" seconds.
private let colours: Array<Color>
private var index: Array<Color>.Index
private var nextIndex: Array<Color>.Index = 0
init(_ colours: [Color]) {
self.colours = colours.count == 0 ? [.black] : colours
index = self.colours.startIndex
nextIndex = getNextIndex()
}
var color: Color { colours[index] }
var nextColor: Color { colours[nextIndex]}
private func getNextIndex() -> Array<Color>.Index {
let n = colours.index(after: index)
return n == colours.endIndex ? colours.startIndex : n
}
mutating func next() {
let i = nextIndex;
nextIndex = getNextIndex()
index = i
}
}
struct Saver: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
(color: viewModel.viewColors.nextColor, location: location) ])
ZStack(alignment: .bottomLeading) {
viewModel.viewColors.color
.animation(.linear(duration: viewModel.defaults.refreshRate), value: viewModel.viewColors.color)
Text("\(Int(viewModel.defaults.refreshRate))")
.foregroundColor(viewModel.viewColors.nextColor)
}
}
}
struct Preferences: View {
@ObservedObject var viewModel: ViewModel
var close: () -> Void
@State private var refreshRate = -2.0
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Refresh colour rate: \(Int(refreshRate))").padding()
Slider(value: $refreshRate, in: 1...20) }
.frame(width: 300)
Divider().frame(width: 300)
Spacer()
HStack {
Button("Cancel") { close() }
Button("OK") { saveDefault(); close() }.buttonStyle(.borderedProminent) }
}
.padding()
.onAppear { loadSavedDefault() }
.frame(minWidth: 300, idealWidth: 400, maxWidth: .infinity, minHeight: 200, idealHeight: 210, maxHeight: .infinity)
.fixedSize()
}
func loadSavedDefault() {
self.refreshRate = viewModel.defaults.refreshRate
}
func saveDefault() {
let value = Double(Int(self.refreshRate))
_ = viewModel.defaults.saveRefreshRate(value: value)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment