Last active
May 18, 2022 17:42
-
-
Save mattgallagher/eaa5d3242d83360a52c45c9706479e34 to your computer and use it in GitHub Desktop.
Animated circle views in SwiftUI and AppKit/CoreAnimation
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
// | |
// AppDelegate.swift | |
// SwiftUITestApp | |
// | |
// Created by Matt Gallagher on 4/6/24. | |
// Copyright © 2019 Matt Gallagher. All rights reserved. | |
// | |
import Cocoa | |
import SwiftUI | |
import Combine | |
@NSApplicationMain | |
class AppDelegate: NSObject, NSApplicationDelegate { | |
var window: NSWindow! | |
var window2: NSWindow! | |
let source = DispatchSource.makeTimerSource(queue: DispatchQueue.main) | |
func applicationDidFinishLaunching(_ aNotification: Notification) { | |
window = NSWindow( | |
contentRect: NSRect(x: 0, y: 200, width: 600, height: 600), | |
styleMask: [.titled, .closable, .miniaturizable, .resizable], | |
backing: .buffered, defer: false) | |
let passthroughBindable = PassthroughBindable<UInt64>() | |
window.contentView = NSHostingView(rootView: ContentView(step: passthroughBindable)) | |
window2 = NSWindow( | |
contentRect: NSRect(x: 600, y: 200, width: 600, height: 600), | |
styleMask: [.titled, .closable, .miniaturizable, .resizable], | |
backing: .buffered, defer: false) | |
window2.contentView = CircleContainer() | |
window2.contentView!.wantsLayer = true | |
var state: UInt64 = 0 | |
source.setEventHandler { [window, passthroughBindable, window2] in | |
if window?.isVisible == true { | |
state += 1 | |
passthroughBindable.didChange.send(state) | |
} | |
(window2?.contentView as? CircleContainer)?.needsLayout = true | |
} | |
source.schedule(deadline: DispatchTime.now(), repeating: .milliseconds(3500)) | |
source.resume() | |
window.makeKeyAndOrderFront(nil) | |
window2.makeKeyAndOrderFront(nil) | |
} | |
func applicationWillTerminate(_ aNotification: Notification) { | |
// Insert code here to tear down your application | |
} | |
} | |
let circleCount = 200 | |
let animationDuration: Double = 5 | |
func randomRect(in bounds: CGRect) -> CGRect { | |
let l = CGFloat.random(in: (0.1 * bounds.size.width)...(0.7 * bounds.size.width)) | |
return CGRect( | |
x: 0.5 * (bounds.width - l) + CGFloat.random(in: (-0.25 * bounds.size.width)...(0.25 * bounds.size.width)), | |
y: 0.5 * (bounds.height - l) + CGFloat.random(in: (-0.25 * bounds.size.height)...(0.25 * bounds.size.height)), | |
width: l, | |
height: l | |
) | |
} | |
let appKitColors: [NSColor] = [.systemRed, .systemBlue, .systemGreen, .systemPink, .systemGray, .black, .systemPurple, .systemOrange, .systemYellow] | |
let colors: [Color] = [.red, .blue, .green, .pink, .gray, .black, .purple, .orange, .yellow] | |
class CircleContainer: NSView { | |
var circles: [CAShapeLayer] = [] | |
override func layout() { | |
if circles.isEmpty { | |
for i in 0..<circleCount { | |
let c = CAShapeLayer() | |
c.lineWidth = 1 | |
c.fillColor = nil | |
c.strokeColor = appKitColors[i % appKitColors.count].cgColor | |
circles.append(c) | |
c.frame = bounds | |
c.path = CGPath(ellipseIn: randomRect(in: bounds), transform: nil) | |
self.layer?.addSublayer(c) | |
} | |
} else { | |
for c in circles { | |
let path = CGPath(ellipseIn: randomRect(in: bounds), transform: nil) | |
let anim = CABasicAnimation(keyPath: "path") | |
anim.fromValue = c.presentation()?.path | |
anim.toValue = path | |
anim.duration = animationDuration | |
c.add(anim, forKey: "path") | |
} | |
} | |
} | |
} | |
class PassthroughBindable<Output>: BindableObject { | |
let didChange = PassthroughSubject<Output, Never>() | |
} | |
struct ContentView: View { | |
@ObjectBinding var step: PassthroughBindable<UInt64> | |
var body: some View { | |
GeometryReader { proxy in | |
ZStack(alignment: .topLeading) { | |
Spacer() | |
ForEach(0..<circleCount) { index -> AnyView in | |
_ = self.step | |
let rect = randomRect(in: CGRect(origin: .zero, size: proxy.size)) | |
return AnyView(StrokedShape(shape: Circle(), style: StrokeStyle()) | |
.foregroundColor(colors[index % colors.count]) | |
.frame(width: rect.width, height: rect.height) | |
.offset(x: rect.origin.x, y: rect.origin.y) | |
.animation(.basic(duration: animationDuration)) | |
) | |
} | |
}.drawingGroup() | |
} | |
} | |
} | |
#if DEBUG | |
struct ContentView_Previews : PreviewProvider { | |
static var previews: some View { | |
ContentView(step: PassthroughBindable<UInt64>()) | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment