惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
Help Net Security
Help Net Security
Security Archives - TechRepublic
Security Archives - TechRepublic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
W
WeLiveSecurity
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
aimingoo的专栏
aimingoo的专栏
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
Project Zero
Project Zero
T
Tailwind CSS Blog
V
Vulnerabilities – Threatpost
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
Last Week in AI
Last Week in AI
腾讯CDC
Schneier on Security
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
量子位
L
Lohrmann on Cybersecurity
J
Java Code Geeks
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements
IT之家
IT之家
博客园_首页
罗磊的独立博客
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Fortinet All Blogs
博客园 - Franky
P
Palo Alto Networks Blog
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Martin Fowler
Martin Fowler
N
News and Events Feed by Topic
C
Cybersecurity and Infrastructure Security Agency CISA
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
TaoSecurity Blog
TaoSecurity Blog
H
Heimdal Security Blog
G
GRAHAM CLULEY
Cloudbric
Cloudbric

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Swift Protocol Magic II: Designing a Reusable Location Tracking System
Salah Nahed · 2026-05-16 · via DEV Community

Location Tracking System

  • How to stop rewriting CLLocationManager boilerplate in every screen — and design something your future self will actually thank you for.*

If you've shipped more than a couple of iOS apps, you've written this code. Probably more than once. Maybe more than ten times.

locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
// ...somewhere else...
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    // do the thing
}

Enter fullscreen mode Exit fullscreen mode

And every time, you end up with the same scattered mess: a CLLocationManager instance hanging off a view controller, delegate methods sprinkled in between unrelated UI code, an if ladder for authorization status, and some half-built UI for telling the user "hey, you actually need to enable location for this to work."

It works. But it doesn't scale across an app. The second screen that needs location duplicates 80% of the first one. The third screen tweaks just enough to make extraction painful. By the fifth screen, you're copy-pasting and praying.

I hit this exact wall on WinchCore, the shared layer behind a production app I'm building. So I sat down and asked the question I should have asked years ago:

What if a view controller could become "location-aware" just by conforming to a protocol — and the framework handled the rest?

This article walks through the design I landed on. It's four small files, but they compose into something I keep reaching for in screen after screen. Let's get into it.


The wishlist

Before any code, here's what I wanted out of this:

  1. Any view controller should be able to opt into location tracking by conforming to a protocol — no inheritance, no base class.
  2. Authorization handling (the denied/restricted screen, the "please enable location" UI) should be centralized, not re-implemented per screen.
  3. The restriction UI should be configurable — different screens want different titles, subtitles, and behaviors. Some screens must have location (a map). Others can tolerate the user dismissing the prompt (a profile screen showing nearby suggestions).
  4. Testable. The whole point of touching this code was to make it possible to swap CLLocationManager for a fake in tests.
  5. No surprises. When the screen appears, location updates start. When the user denies permission, the right UI shows up. That's it.

Four protocols. One concrete class. Let's walk through them in the order they make sense to read.


Piece 1: LocationManager — the abstraction

public protocol LocationManager: CLLocationManagerDelegate {
    func startTrackingLocation()
    func startUpdatingLocation()
    var currentLocation: CLLocation? { get }
}

Enter fullscreen mode Exit fullscreen mode

That's it. The whole protocol.

The trick here is that it inherits from CLLocationManagerDelegate. That way any concrete implementation already has the delegate surface it needs, and from the call site you never have to think about CLLocationManager directly. You think about "tracking" and "updating" — verbs that match what your screen actually wants to do.

startTrackingLocation() is the "I just appeared, start the whole flow" entry point — authorization, delegate setup, all of it.

startUpdatingLocation() is the "I already have permission, just give me a fresh fix" version — useful when the user taps a refresh button, for example.

currentLocation is a snapshot accessor for the cases where you just want the last known coordinate without subscribing to anything.

This is the seam where testing gets easy. In a unit test, you don't construct a CLLocationManager. You construct a mock that conforms to LocationManager, return whatever location you want, and assert on what the screen does with it. No simulator GPS, no Info.plist dance, no flakiness.


Piece 2: TrackableLocationViewController — making any VC location-aware

public protocol TrackableLocationViewController: UIViewController {
    func handleLocationUpdate(lat: Double, lng: Double) async
    var locationManager: LocationManager { get }
}

Enter fullscreen mode Exit fullscreen mode

This is the protocol your view controller conforms to. It says two things:

  • "I own a LocationManager."
  • "When a location update arrives, here's what I want to do with it."

The fact that handleLocationUpdate is async is intentional. Most of the time, when a location comes in, you're not just updating a label — you're firing off a network call to fetch nearby data, hitting a geocoder, or syncing with a backend. Making the contract async from the start means the manager can await the screen's work without us bolting on Combine pipelines or completion handler hell later.

And here's where protocol extensions earn their keep:

public extension TrackableLocationViewController {
    func startTrackingLocation() {
        locationManager.startTrackingLocation()
    }

    func startUpdatingLocation() {
        locationManager.startUpdatingLocation()
    }

    var currentLocation: CLLocation? {
        locationManager.currentLocation
    }
}

Enter fullscreen mode Exit fullscreen mode

Free methods. The view controller didn't write any of this — it just gets it by conforming. Now any conforming screen can call self.startTrackingLocation() in viewDidLoad() and the system takes over.

The usage looks like this:

class MapViewController: NibViewController, TrackableLocationViewController {

    private lazy var appLocationManager: AppLocationManager = {
        AppLocationManager(locationManager: CLLocationManager(), viewController: self)
    }()

    var locationManager: LocationManager { appLocationManager }

    func handleLocationUpdate(lat: Double, lng: Double) async {
        mapView.setCenter(CLLocationCoordinate2D(latitude: lat, longitude: lng), animated: true)
        await fetchNearbyPlaces(lat: lat, lng: lng)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        startTrackingLocation()
    }
}

Enter fullscreen mode Exit fullscreen mode

Three lines of contract. The rest is your screen's actual job.


Piece 3: LocationRestrictionConfigurable — the "you blocked us, now what?" protocol

This is the piece I'm most proud of, because it's the one that usually gets hacked together five different ways across an app.

When the user denies location, you need to show something. A screen, an alert, a banner — whatever your designer decided. And that something is almost never identical across screens. The map screen probably wants a full-screen blocker that says "Enable location to see places near you." The profile screen might want a softer, dismissible message. Different titles, different subtitles, different blocking behavior.

So instead of hardcoding any of that into the location manager, I made it configurable through another protocol:

public protocol LocationRestrictionConfigurable: TrackableLocationViewController {
    var titleText: String? { get }
    var subtitle: String? { get }
    var shouldBlock: Bool { get }
}

Enter fullscreen mode Exit fullscreen mode

Three properties. Each screen decides what it wants. And — critically — the protocol extends TrackableLocationViewController, so anywhere we need both behaviors we can compose them with the & operator and Swift's type system enforces it.

But here's the part I love: default values via protocol extension.

public extension LocationRestrictionConfigurable {
    var titleText: String? { nil }
    var subtitle: String? { nil }
    var shouldBlock: Bool { false }
}

Enter fullscreen mode Exit fullscreen mode

If a screen doesn't care, it conforms and ignores. If a screen does care, it overrides just the properties it needs. No required boilerplate. No // TODO: configure this landmines.

The shouldBlock flag is the small detail that captures a real product question: can the user leave this screen without enabling location? For a map showing nearby drivers, the answer is no — the screen would be useless. For a search screen with optional location filtering, the answer is yes. The flag pushes that decision back where it belongs: with the screen itself, not with some central authorization handler making assumptions.


Piece 4: AppLocationManager — the concrete glue

This is where it all comes together. The full file is on the longer side, so let's read it in chunks.

The init

@MainActor
public class AppLocationManager: NSObject, LocationManager {

    private let locationManager: CLLocationManager
    private let viewController: TrackableLocationViewController & LocationRestrictionConfigurable

    public init(locationManager: CLLocationManager,
                viewController: TrackableLocationViewController & LocationRestrictionConfigurable) {
        self.locationManager = locationManager
        self.viewController = viewController
    }
}

Enter fullscreen mode Exit fullscreen mode

Two things to notice.

First, @MainActor. Location callbacks touch the UI — they trigger navigation, update maps, etc. By marking the whole class @MainActor, every callback is guaranteed to run on the main thread. You don't need to wrap every callback in DispatchQueue.main.async just to be safe.

Second, the composed type in the initializer:

viewController: TrackableLocationViewController & LocationRestrictionConfigurable

Enter fullscreen mode Exit fullscreen mode

This is the Swift type system doing real work for us. You literally cannot construct an AppLocationManager with a view controller that hasn't said "yes, I will track location" and "yes, I will configure my restriction UI." If you try, the compiler stops you. Bug prevented before it exists.

The deliberate delay

public func startTrackingLocation() {
    print("startTrackingLocation called from \(type(of: viewController))")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
        guard let self = self else { return }
        self.locationManager.delegate = self
        let status = self.locationManager.authorizationStatus
        self.handleLocationAuthorization(status: status)
    }
}

Enter fullscreen mode Exit fullscreen mode

That one-second delay isn't laziness — it's intentional. When a screen appears, you usually want the skeleton/loading state to render first, then the permission prompt or the restriction UI to come up. If the system permission alert fires the instant viewDidLoad runs, you get a jarring experience: blank screen → modal alert → screen finally renders. Adding a small delay lets the UI breathe.

This is the kind of decision you don't see in tutorials, but you absolutely feel in production.

The authorization switch — and the nested router fallback

private func handleLocationAuthorization(status: CLAuthorizationStatus) {
    switch status {
    case .notDetermined:
        locationManager.requestWhenInUseAuthorization()
    case .restricted, .denied:
        if let router = viewController.nestedRouter {
            router.navigate(to: .locationRestricted(
                nested: true,
                title: viewController.titleText,
                subtitle: viewController.subtitle,
                shouldBlock: viewController.shouldBlock
            ))
        } else {
            viewController.router?.navigate(to: .locationRestricted(
                nested: false,
                title: viewController.titleText,
                subtitle: viewController.subtitle,
                shouldBlock: viewController.shouldBlock
            ))
        }
    case .authorizedAlways, .authorizedWhenInUse:
        locationManager.startUpdatingLocation()
    @unknown default:
        fatalError("Unknown authorization status.")
    }
}

Enter fullscreen mode Exit fullscreen mode

The notDetermined and authorized cases are mechanical. The interesting case is .restricted, .denied.

When location is denied, the manager asks the view controller: do you have a nested router? If you're a child controller inside a tab bar or a container view controller, you probably want the restriction screen to present inside your container, not over the entire app. The nestedRouter is the seam for that. If there's no nested router, we fall back to the screen's main router and present it normally.

This is a small thing, but it's the kind of detail that turns a "decent abstraction" into "an abstraction I actually want to use in every screen." Real apps have nested navigation. The framework should know that.

The delegate methods

extension AppLocationManager {
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        if location.coordinate.latitude == 0 || location.coordinate.longitude == 0 { return }

        Task {
            await viewController.handleLocationUpdate(lat: location.coordinate.latitude,
                                                      lng: location.coordinate.longitude)
        }
        manager.stopUpdatingLocation()
    }

    public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        handleLocationAuthorization(status: status)
    }

    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("AppLocationManager locationManager error: \(error)")
    }
}

Enter fullscreen mode Exit fullscreen mode

Three things to point out:

  1. The (0, 0) guard. On rare occasions, CLLocationManager will hand you a coordinate of (0, 0) — which is technically a real place in the Atlantic Ocean off the coast of Africa, but practically always means the GPS hasn't acquired a real fix yet. Filtering those out is a small piece of defensive code that saves you from rendering a map of the open sea.

  2. Task { await ... } bridges the worlds. The delegate callback is synchronous, but handleLocationUpdate is async. Wrapping it in a Task lets the screen's async work run without blocking the delegate.

  3. stopUpdatingLocation() after the first fix. This particular flow is one-shot — we want a single accurate location, not a continuous stream. If you needed continuous tracking (turn-by-turn navigation, fitness tracking), you'd skip this line. But for "where is the user right now so I can fetch nearby data," one fix is usually enough, and it's much better for battery.


Why this design pays off

Let me zoom out, because the pieces individually are small, but the system they form is the point.

Adding location tracking to a new screen takes about five lines. Conform to the two protocols, provide a LocationManager, implement handleLocationUpdate, call startTrackingLocation() in viewDidLoad. Done. Every other concern — authorization, restriction UI, blocking behavior, threading — is handled.

Testing is trivial. Mock LocationManager, feed it whatever location you want, assert on what handleLocationUpdate does. No real GPS, no real CLLocationManager, no flakiness.

Customization is opt-in. Want a different restriction title for this screen? Override titleText. Want this screen to block until location is enabled? Set shouldBlock to true. Don't care? Conform and move on with your day.

The compiler enforces the contract. You can't construct an AppLocationManager with a view controller that's missing either protocol. The bug where someone forgets to handle restriction UI literally cannot exist.

The main thread is guaranteed. @MainActor removes a whole category of "why is my UI updating weirdly" bugs that come from delegate callbacks not being on the main thread.


What I'd add next

Honest reflection — this design isn't done. Things I'd evolve in a future version:

  • Continuous tracking mode. Right now it's one-shot. A TrackingMode enum (oneShot, continuous, significantChanges) would make it more flexible.
  • Last-known-location caching. For screens that just want a rough location to fetch initial data, returning the last cached fix immediately would feel snappier.
  • Background location — a whole separate beast that deserves its own protocol layer.
  • Replace print with a proper logger — the current print statements were debugging aids that lingered. A real app should route these through os.Logger or whatever your logging story is.

But none of these change the core shape. They extend it.


Takeaways

If you take one thing from this, let it be this: the magic isn't the code, it's the seams.

The seams are where you draw the lines between what a view controller knows and what the framework handles. Protocols are the tool for drawing those lines cleanly. Default implementations are how you keep them ergonomic. Composed types (A & B) are how you let the compiler enforce them.

When you find yourself copy-pasting the same setup code into yet another view controller, that's the signal. There's a protocol hiding in there. Pull it out, give it defaults, compose it with what's already there.

That's all "Protocol Magic" really is — recognizing those moments and acting on them.


If you missed part one — Swift Protocol Magic: Building a Beautiful, Reusable Option Selection System — it applies the same thinking to bottom sheets and option pickers. And if you're curious about navigation patterns, AppRouter-UIKit is an iOS navigation library I open-sourced that uses similar protocol-oriented ideas.


👋 A quick note: I'm currently open to new opportunities. After 10+ years building iOS apps (and Flutter more recently), I'm looking to join an amazing team — ideally an international, on-site role — where I can keep building things like this. If your team is hiring, or you just want to talk iOS architecture, please reach out on any channel below.

📩 Connect with me on LinkedIn
Follow my tech journey
🐦 Twitter/X: @salahamassi
📸 Instagram: @salahamassi
🐙 GitHub: salahamassi

Let's build something elegant together. ❤️