Instantly share code, notes, and snippets.
Created
January 23, 2025 04:20
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save randomor/266759a6ec48b61cbbace9f4df570cea to your computer and use it in GitHub Desktop.
Animating Menubar Icon
This file contains hidden or 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 AppKit | |
@main | |
struct AnimatedSVGMenuBarApp: App { | |
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate | |
var body: some Scene { | |
WindowGroup { | |
ControlPanelView() | |
.environmentObject(appDelegate) // Pass the AppDelegate to the view | |
} | |
.windowStyle(HiddenTitleBarWindowStyle()) | |
} | |
} | |
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { | |
@Published var statusItem: NSStatusItem? // Make it optional and observable | |
@Published var rotationAngle: CGFloat = .pi / 4 // Start at 45 degrees (X shape) | |
private var timer: Timer? | |
private var targetAngle: CGFloat = .pi / 4 | |
func applicationDidFinishLaunching(_ notification: Notification) { | |
// Create the status bar item | |
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) | |
statusItem = item | |
updateIcon() | |
} | |
func updateIcon() { | |
guard let button = statusItem?.button else { return } | |
button.image = renderRotatedSVG(angle: rotationAngle) | |
} | |
func renderRotatedSVG(angle: CGFloat) -> NSImage? { | |
let size = NSSize(width: 20, height: 20) // Icon size | |
let image = NSImage(size: size) | |
image.lockFocus() | |
guard let context = NSGraphicsContext.current?.cgContext else { return nil } | |
// Clear the background | |
context.clear(CGRect(origin: .zero, size: size)) | |
// Apply rotation | |
context.translateBy(x: size.width / 2, y: size.height / 2) | |
context.rotate(by: angle) | |
context.translateBy(x: -size.width / 2, y: -size.height / 2) | |
// Draw the cross | |
context.setStrokeColor(NSColor.white.cgColor) | |
context.setLineWidth(1.4) // Thin line | |
let rect1 = CGRect(x: 2.5, y: 7.5, width: 15, height: 5) // Adjusted width and height | |
let rect2 = CGRect(x: 7.5, y: 2.5, width: 5, height: 15) | |
let path1 = CGPath(roundedRect: rect1, cornerWidth: 2, cornerHeight: 2, transform: nil) | |
let path2 = CGPath(roundedRect: rect2, cornerWidth: 2, cornerHeight: 2, transform: nil) | |
context.addPath(path1) | |
context.addPath(path2) | |
context.strokePath() | |
image.unlockFocus() | |
return image | |
} | |
func startRotationAnimation() { | |
guard timer == nil else { return } // Prevent multiple timers | |
// Update target angle | |
targetAngle = normalizeAngle(targetAngle + .pi / 2) | |
timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] _ in | |
guard let self = self else { return } | |
// Normalize current angle | |
self.rotationAngle = self.normalizeAngle(self.rotationAngle) | |
if abs(self.rotationAngle - self.targetAngle) < 0.01 { | |
self.rotationAngle = self.targetAngle | |
self.updateIcon() | |
self.timer?.invalidate() | |
self.timer = nil | |
} else { | |
let step: CGFloat = .pi / 60 // Smooth incremental rotation | |
if self.shouldRotateClockwise(from: self.rotationAngle, to: self.targetAngle) { | |
self.rotationAngle += step | |
} else { | |
self.rotationAngle -= step | |
} | |
self.updateIcon() | |
} | |
} | |
} | |
private func normalizeAngle(_ angle: CGFloat) -> CGFloat { | |
var normalized = angle.truncatingRemainder(dividingBy: 2 * .pi) | |
if normalized < 0 { | |
normalized += 2 * .pi | |
} | |
return normalized | |
} | |
private func shouldRotateClockwise(from current: CGFloat, to target: CGFloat) -> Bool { | |
let delta = normalizeAngle(target - current) | |
return delta <= .pi // Rotate clockwise if delta is less than or equal to 180 degrees | |
} | |
} | |
struct ControlPanelView: View { | |
@EnvironmentObject var appDelegate: AppDelegate // Access the AppDelegate object | |
var body: some View { | |
VStack(spacing: 16) { | |
Button("Rotate 90°") { | |
appDelegate.startRotationAnimation() | |
} | |
.padding() | |
.buttonStyle(DefaultButtonStyle()) | |
Button("Quit") { | |
NSApplication.shared.terminate(nil) | |
} | |
.padding() | |
.buttonStyle(DefaultButtonStyle()) | |
} | |
.frame(width: 200, height: 100) | |
.background(Color(NSColor.windowBackgroundColor)) | |
.cornerRadius(12) | |
.padding() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment