Apps feel native when they use the phone: where you are, what is nearby, and a reminder at the right moment. These features share two things. They touch private data or attention, so Apple requires a permission and an honest reason. And they depend on the system, which decides when your code runs. This lesson covers location, maps and notifications, with the permission rules first.
Permissions: the rules for all private features
- Every private feature needs a usage description in the app's Info settings, a short sentence that says *why*, such as *Used to show expenses near you*. Without it, the app is stopped at the moment you ask. Reviewers also read these sentences.
- Ask at the moment the feature is needed, not at launch. A person who sees *Allow location?* right after opening the app, with no context, often says no.
- Always handle no. A good app still works, with a clear way to change the setting later.
- Ask for the least you need. *While Using the App* is enough for most apps.
Core Location
import CoreLocation
import Observation
@Observable
final class LocationModel: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var status: CLAuthorizationStatus
var location: CLLocation?
override init() {
status = manager.authorizationStatus
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
}
func requestAccess() {
manager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
status = manager.authorizationStatus
if status == .authorizedWhenInUse || status == .authorizedAlways {
manager.requestLocation() // one fix, uses less battery
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = locations.last
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location failed:", error)
}
}
Add the key NSLocationWhenInUseUsageDescription to your target's Info settings. The delegate methods tell you when the person answers and when a position arrives. Choose the lowest accuracy that does the job, and ask for one position with requestLocation() when you only need one, because GPS drains the battery. Newer iOS versions also offer an async way to read location updates (CLLocationUpdate.liveUpdates()), which is worth learning once you know this delegate version.
MapKit in SwiftUI
From iOS 17, SwiftUI has a Map view you fill with *markers* and *annotations*, written like any other SwiftUI content.
import MapKit
import SwiftUI
struct PlacesMap: View {
private let cafe = CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278)
var body: some View {
Map(initialPosition: .region(
MKCoordinateRegion(center: cafe,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)))
) {
Marker("Coffee shop", systemImage: "cup.and.saucer.fill", coordinate: cafe)
UserAnnotation()
}
.mapStyle(.standard)
.mapControls { MapUserLocationButton(); MapCompass() }
}
}
UserAnnotation shows the blue dot once you have permission. To find places, use MKLocalSearch, which searches Apple's map data.
func searchNearby(_ text: String, around center: CLLocationCoordinate2D) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = text
request.region = MKCoordinateRegion(center: center,
latitudinalMeters: 2000, longitudinalMeters: 2000)
let response = try await MKLocalSearch(request: request).start()
return response.mapItems
}
Local notifications
A local notification is scheduled by your app on the device, with no server. It suits reminders, such as *log today's expenses at 8 pm*. First ask for permission, then schedule a request with a trigger.
import UserNotifications
func scheduleDailyReminder() async throws {
let center = UNUserNotificationCenter.current()
let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge])
guard granted else { return }
let content = UNMutableNotificationContent()
content.title = "Log your expenses"
content.body = "Add today's spending while you remember it."
content.sound = .default
let trigger = UNCalendarNotificationTrigger(
dateMatching: DateComponents(hour: 20, minute: 0), repeats: true)
let request = UNNotificationRequest(identifier: "daily-reminder", content: content, trigger: trigger)
try await center.add(request)
}
- Using the same
identifieragain replaces the old request, so a person does not get two reminders. - By default, a notification does not show while your app is open. If you want it to, set a
UNUserNotificationCenterDelegateand return the options you want fromwillPresent. - Give people a way to turn reminders off inside the app, in addition to the system settings.
Push notifications
A push notification is sent from your server, through Apple's service, APNs. The path is: your app asks the system to register, the system gives the app a device token, the app sends that token to your server, and the server asks APNs to deliver a message to that token. For the server to talk to APNs you create an authentication key (a .p8 file) in your Apple developer account. You add the Push Notifications capability to the app.
import UIKit
import SwiftUI
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
print("Send this token to your server:", token)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Push registration failed:", error)
}
}
@main
struct ExpenseTrackerApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene { WindowGroup { ContentView() } }
}
// after the person has allowed notifications:
UIApplication.shared.registerForRemoteNotifications()
Background work
iOS stops an app soon after the person leaves it, to save battery. For work that must happen anyway, such as refreshing data, you do not run code yourself. You ask the system, and it decides when. BGTaskScheduler lets you register a refresh task, and the system runs it at a time it picks, which may be late, or not at all if the battery is low. A push with content-available can also wake the app briefly. Always design the app so that it works correctly when no background work ran, and treat background work as a bonus.
Common mistakes
- Asking for permission on first launch with no context.
- No handling for *denied*. The feature silently does nothing.
- Keeping GPS on all the time.
- Putting server secrets in the app, or expecting background tasks to run at an exact time.
Your task
- Add the location usage description, ask for permission from a button, and show the coordinates.
- Show a
Mapwith one marker and the user's location, and search for *coffee* nearby. - Schedule the daily reminder from a toggle in a settings screen, and cancel it when the toggle is off.
- Write down the steps of a push notification from your app to a phone, in order, in your own words.
How to check it worked
- Denying permission shows a helpful message and the app keeps working.
- The reminder arrives at the chosen time, and only one is scheduled.
- You can explain why the APNs key must stay on the server.