Skip to content

Instantly share code, notes, and snippets.

@mattgallagher
Last active May 18, 2022 17:42
Show Gist options
  • Save mattgallagher/eaa5d3242d83360a52c45c9706479e34 to your computer and use it in GitHub Desktop.
Save mattgallagher/eaa5d3242d83360a52c45c9706479e34 to your computer and use it in GitHub Desktop.
Animated circle views in SwiftUI and AppKit/CoreAnimation
//
// 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