{"slug": "i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac", "title": "I Recreated iPhone's Apple Intelligence Edge Glow Effect on Mac", "summary": "A developer created EdgeGlow, a free open-source macOS menu bar app that recreates the iPhone's Apple Intelligence edge glow effect on Mac. The app uses a timer-based animation system to avoid state loss when windows are hidden, and employs a 20-segment gradient with a 4-layer glow stack to mimic the iridescent effect.", "body_md": "iPhone's Apple Intelligence shows a beautiful iridescent glow around the screen edges when Siri is thinking. I wanted that same effect on my Mac when Claude Code is working.\n\nSo I built **EdgeGlow** — a free, open-source macOS menu bar app that recreates iPhone's edge glow effect on Mac.\n\nIn this post, I'll walk through how I did it and the technical challenges I faced.\n\n```\nAI Agent triggers hook → curl http://127.0.0.1:9876/start → Screen glows\nAI Agent finishes     → curl http://127.0.0.1:9876/stop  → Glow fades out\n```\n\nThat's it. Dead simple.\n\nThe interesting part is the animation. Let me walk you through it.\n\nI needed a smooth marquee effect around the screen edges — essentially a dashed line that flows continuously.\n\nMy first approach used `CABasicAnimation`\n\non `lineDashPhase`\n\n:\n\n``` js\nlet anim = CABasicAnimation(keyPath: \"lineDashPhase\")\nanim.fromValue = 0\nanim.toValue = perimeter\nanim.duration = 5.0\nanim.repeatCount = .infinity\nshape.add(anim, forKey: \"flow\")\n```\n\nThis worked... until the window was hidden and shown again.\n\nCore Animation would lose the animation state, and the marquee would:\n\n`CABasicAnimation`\n\nrelies on Core Animation's animation system, which is tied to the layer's presentation state. When the window is hidden (`orderOut`\n\n) or the layer is removed from the hierarchy, the animation state is lost.\n\nI tried several workarounds:\n\nNone of them were bulletproof.\n\nI switched to a `Timer`\n\nat 60fps that directly updates `lineDashPhase`\n\n:\n\n``` js\nprivate var flowTimer: Timer?\nprivate var dashPhase: CGFloat = 0\nprivate var lastTickTime: CFTimeInterval = 0\n\nprivate func startFlow() {\n    stopFlow()\n    lastTickTime = CACurrentMediaTime()\n\n    flowTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in\n        self?.tickFlow()\n    }\n    RunLoop.main.add(flowTimer!, forMode: .common)\n}\n\nprivate func tickFlow() {\n    let now = CACurrentMediaTime()\n    let dt = CGFloat(now - lastTickTime)\n    lastTickTime = now\n\n    // Clamp dt to prevent huge jumps (e.g., returning from background)\n    let clampedDt = min(dt, 0.1)\n\n    let perimeter = currentPerimeter()\n    guard perimeter > 0, settings.animationDuration > 0 else { return }\n\n    let speed = perimeter / CGFloat(settings.animationDuration)\n    dashPhase += speed * clampedDt * (settings.clockwise ? 1 : -1)\n\n    ringLayer.sublayers?.forEach { layer in\n        (layer as? CAShapeLayer)?.lineDashPhase = dashPhase\n    }\n}\n```\n\nThis is **bulletproof** — no dependency on Core Animation's animation system, no state loss on window visibility changes.\n\nThe performance is also great: updating a single `CGFloat`\n\nproperty at 60fps uses ~0% CPU.\n\nTo recreate iPhone's Apple Intelligence edge glow, I needed two things:\n\n**1. 20-segment gradient coloring**: Split the screen edge into 20 segments, each with a different hue (purple → blue → cyan → pink → orange → gold). Gaussian blur at segment boundaries creates smooth transitions:\n\n``` js\nfor i in 0..<20 {\n    let hue = (baseHue + Double(i) * 0.05).truncatingRemainder(dividingBy: 1.0)\n    // Each segment has a different color, forming a gradient\n}\n```\n\n**2. 4-layer glow stack**: Stack 4 `CAShapeLayer`\n\ninstances with different blur levels to create a realistic neon glow effect:\n\n``` js\nlet configs: [(widthMul: CGFloat, alphaMul: CGFloat, blur: Double)] = [\n    (baseWidth * 1.5, 0.15, 12.0),  // Layer 1: Wide line, high blur, low alpha → outer glow\n    (baseWidth * 0.8, 0.30, 8.0),   // Layer 2: Medium line, medium blur, medium alpha → mid glow\n    (baseWidth * 0.3, 0.70, 2.0),   // Layer 3: Thin line, low blur, high alpha → core line\n    (baseWidth * 0.1, 0.95, 0.0),   // Layer 4: Thinnest line, no blur, full alpha → bright center\n]\njs\nfor (lineWidth, alpha, blur) in configs {\n    let shape = CAShapeLayer()\n    shape.frame = ringLayer.bounds\n    shape.path = path\n    shape.fillColor = nil\n    shape.strokeColor = color.withAlphaComponent(alpha).cgColor\n    shape.lineWidth = lineWidth\n    shape.lineCap = .round\n    shape.lineDashPattern = [NSNumber(value: Double(dashLen)),\n                             NSNumber(value: Double(gapLen))]\n\n    if blur > 0 {\n        let bf = CIFilter(name: \"CIGaussianBlur\")!\n        bf.setValue(blur, forKey: \"inputRadius\")\n        shape.filters = [bf]\n    }\n\n    ringLayer.addSublayer(shape)\n}\n```\n\nThe 4 layers stack on top of each other, creating a realistic neon light tube effect:\n\n```\nLayer 4: ██ (bright center, no blur)\nLayer 3: ████ (core line, blur 2)\nLayer 2: ██████ (mid glow, blur 8)\nLayer 1: ████████ (outer glow, blur 12)\n```\n\nThe outer layers create the soft glow halo, while the inner layers create the bright core. Together, they look like a real neon light.\n\nIf you have multiple Claude Code terminals running, one calling `/stop`\n\nwould kill the glow even though others are still active.\n\n``` js\nclass ControlServer {\n    private var activeCount = 0\n\n    private func processRequest(_ raw: String, conn: NWConnection) {\n        switch path {\n        case \"/start\":\n            activeCount += 1\n            onStart?()\n            resetSafetyTimer()\n\n        case \"/stop\":\n            activeCount = max(0, activeCount - 1)\n            if activeCount == 0 { onStop?() }\n\n        case \"/pulse\":\n            activeCount = max(0, activeCount - 1)\n            if activeCount == 0 { onPulse?() }\n        }\n    }\n}\n```\n\nNow `/start`\n\nincrements the count, `/stop`\n\ndecrements it. The glow only hides when the count reaches 0.\n\nWhat if an agent crashes without sending `/stop`\n\n? The reference count would stay > 0 forever.\n\nSolution: a 120-second safety timeout:\n\n``` js\nprivate func resetSafetyTimer() {\n    safetyTimer?.cancel()\n    let work = DispatchWorkItem { [weak self] in\n        guard let self = self else { return }\n        if self.activeCount > 0 {\n            print(\"⏰ 60s no activity, resetting to 0\")\n            self.activeCount = 0\n            self.onStop?()\n        }\n    }\n    safetyTimer = work\n    DispatchQueue.main.asyncAfter(deadline: .now() + 60.0, execute: work)\n}\n```\n\nEvery `/start`\n\nresets the timer. If no `/start`\n\nis received for 60 seconds, the count resets to 0.\n\nEdgeGlow runs an HTTP server on `127.0.0.1:9876`\n\n. I had to be careful about security:\n\n``` js\nlet param = NWParameters.tcp\nparam.acceptLocalOnly = true  // Only accept connections from localhost\n```\n\nThis prevents external network access.\n\n```\nguard method == \"GET\" || method == \"OPTIONS\" else {\n    sendResponse(405, \"Method Not Allowed\", conn: conn)\n    return\n}\n```\n\nRejects POST/PUT/DELETE to prevent CSRF attacks.\n\n```\n// No Access-Control-Allow-Origin header\n// Web JavaScript cannot invoke endpoints\n```\n\nThis prevents web pages from calling the API via JavaScript.\n\nHow to handle multiple displays with different sizes?\n\nCalculate the union of all screen frames:\n\n``` php\nfunc totalScreenFrame() -> NSRect {\n    var frame = NSRect.zero\n    for screen in NSScreen.screens {\n        frame = NSUnionRect(frame, screen.frame)\n    }\n    return frame\n}\n```\n\nListen to screen parameter changes:\n\n```\nNotificationCenter.default.addObserver(\n    forName: NSApplication.didChangeScreenParametersNotification,\n    object: nil,\n    queue: .main\n) { [weak self] _ in\n    // Debounce 500ms to avoid rapid rebuilds\n    self?.screenChangeWorkItem?.cancel()\n    let work = DispatchWorkItem { [weak self] in\n        self?.handleScreenChange()\n    }\n    self?.screenChangeWorkItem = work\n    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: work)\n}\n```\n\nWhen displays change, rebuild the layers with the new frame size.\n\nHow to make settings reactive and persist them across launches?\n\nUse `ObservableObject`\n\n+ `UserDefaults`\n\n+ Combine:\n\n``` js\nclass AppSettings: ObservableObject {\n    @Published var speed: Int {\n        didSet {\n            UserDefaults.standard.set(speed, forKey: \"speed\")\n            notifyChange()\n        }\n    }\n\n    static let shared = AppSettings()\n\n    private init() {\n        self.speed = UserDefaults.standard.integer(forKey: \"speed\")\n        if speed == 0 { speed = 5 } // default\n    }\n}\n```\n\nThen in GlowWindow, observe changes:\n\n```\nsettings.$speed.sink { [weak self] _ in\n    self?.rebuildLayers()\n}.store(in: &cancellables)\n```\n\nWhen the user changes the speed slider, the layers rebuild automatically.\n\n| Metric | Value | Notes |\n|---|---|---|\n| CPU | ~0% | Timer 60fps only updates one CGFloat |\n| Memory | ~50MB | 4 CAShapeLayer + CIFilter |\n| Disk | 892KB | Universal Binary (arm64 + x86_64) |\n| Network | 0 | localhost only, no external requests |\n\nThe key insight: updating a property at 60fps is cheap. It's the GPU-accelerated CAShapeLayer rendering that does the heavy lifting.\n\nIt's my first macOS app. Built with pure Swift + SwiftUI, no third-party dependencies.\n\nWould love to hear your feedback!", "url": "https://wpnews.pro/news/i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac", "canonical_source": "https://dev.to/vector4wang/i-recreated-iphones-apple-intelligence-edge-glow-effect-on-mac-57f5", "published_at": "2026-06-18 16:29:16+00:00", "updated_at": "2026-06-18 16:59:36.598214+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "ai-products"], "entities": ["Apple", "EdgeGlow", "Claude Code", "Core Animation", "CAShapeLayer", "CABasicAnimation"], "alternates": {"html": "https://wpnews.pro/news/i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac", "markdown": "https://wpnews.pro/news/i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac.md", "text": "https://wpnews.pro/news/i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac.txt", "jsonld": "https://wpnews.pro/news/i-recreated-iphone-s-apple-intelligence-edge-glow-effect-on-mac.jsonld"}}