Created
June 13, 2022 18:08
-
-
Save disc0infern0/b2fe0342a203d155ee9a1ef6daeea16c to your computer and use it in GitHub Desktop.
Screen Saver in SwiftUI - simple colour rotation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
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