Swift and SwiftUI tutorials for Swift Developers

Push vs In App vs Local Notifications in iOS and SwiftUI

In the mobile development ecosystem, user retention is the “Holy Grail.” An application might be technically perfect, but if the user never opens it, it serves no purpose. This is where communication strategy comes into play.

Many novice developers (and some experienced ones) confuse terms or use the wrong tool for the job. When should I use a local notification? What infrastructure do I need for Push? How do I design an In-App notification that isn’t intrusive?

In this in-depth tutorial, we will break down the architectural differences, use cases, and technical implementation using Swift and SwiftUI for all three types of notifications: Local, Push, and In-App.


1. Local Notifications

Concept and Architecture

Local notifications are scheduled, configured, and triggered entirely from the device itself. They do not require an external server or an internet connection to function once they have been scheduled.

They are ideal for time-based reminders (alarms, timers) or location-based triggers (geofencing). From the operating system’s perspective, they are managed via the UserNotifications framework.

Key Features

  • Origin: The user’s device.
  • Dependency: No backend or internet required.
  • Cost: Free and immediate.
  • Privacy: Requires explicit user permission.

Implementation in SwiftUI

To implement local notifications, we need to interact with the UNUserNotificationCenter.

Step 1: Request Permissions

First and foremost, we must ask for permission. It is best practice to do this at a contextual moment, but for this example, we will do it when a view initiates.

import UserNotifications
import SwiftUI

class NotificationManager: ObservableObject {
    static let shared = NotificationManager()
    
    func requestPermission() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
            if success {
                print("Permission granted")
            } else if let error = error {
                print("Error: \(error.localizedDescription)")
            }
        }
    }
}

Step 2: Schedule the Notification

A local notification has three parts: the Content (title, body, sound), the Trigger, and the Request.

func scheduleNotification(title: String, body: String, timeInterval: TimeInterval) {
        // 1. Content
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = UNNotificationSound.default
        
        // 2. Trigger (E.g., 5 seconds from now)
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
        
        // 3. Request (Unique identifier is crucial for canceling or updating)
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        
        // 4. Add to notification center
        UNUserNotificationCenter.current().add(request)
    }

Step 3: UI Integration

struct LocalNotificationView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Task Manager")
                .font(.title)
            
            Button("Request Permissions") {
                NotificationManager.shared.requestPermission()
            }
            
            Button("Remind me in 5 seconds") {
                NotificationManager.shared.scheduleNotification(
                    title: "Time to code!",
                    body: "Don't forget to review your pull requests.",
                    timeInterval: 5
                )
            }
        }
    }
}

Important Note on Lifecycle: If the app is in the foreground, by default, the notification is not visually shown (even though the event triggers). To show it even when the app is open, you must implement the UNUserNotificationCenterDelegate.


2. Push Notifications (Remote Notifications)

Concept and Architecture

Push notifications are messages sent from a remote server to the user’s device. They are the most powerful tool for “re-engagement,” allowing you to reach the user with new information (breaking news, a new like, a chat message).

The Workflow (APNs)

Unlike local notifications, the architecture here is complex:

  1. Your App requests a token from the device.
  2. The device requests the token from APNs (Apple Push Notification service).
  3. Your App sends that token to Your Server (Backend).
  4. When an event occurs, your Backend sends the message + the token to APNs.
  5. APNs delivers the message to the device.

Prerequisites

  • Apple Developer Account (Paid).
  • Configure Push Certificates or Authentication Keys (.p8) in the developer portal.
  • “Push Notifications” Capability enabled in Xcode.

Implementation in SwiftUI (App Lifecycle)

SwiftUI manages the app lifecycle, but for Push, we need access to methods that traditionally lived in the AppDelegate. We will use an adaptor.

Step 1: Configure the AppDelegate

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Configure notification delegate
        UNUserNotificationCenter.current().delegate = self
        return true
    }
    
    // Success registering
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        print("Device Token APNs: \(token)")
        // HERE YOU WOULD SEND THIS TOKEN TO YOUR BACKEND SERVER
    }
    
    // Failure registering
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for remote notifications: \(error)")
    }
}

// Extension to handle foreground notifications
extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // Show banner even if app is open
        completionHandler([.banner, .sound, .badge])
    }
}

Step 2: Connect to the App Entry Point

@main
struct MyNotificationApp: App {
    // Inject the AppDelegate
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    // Initiate registration process
                    UIApplication.shared.registerForRemoteNotifications()
                }
        }
    }
}

Visual Difference

To the end user, a Push Notification and a Local Notification look exactly the same on the lock screen or as a banner. The difference is purely architectural (the origin of the data).


3. In-App Notifications

Concept and Architecture

This is where confusion often arises. An In-App notification is not a system notification. It does not appear in the iOS Notification Center or on the lock screen.

They are custom views (pop-ups, banners, modals, tooltips) that appear only when the user is actively using the application.

Use Cases

  • Confirming an action (e.g., “Item saved to favorites”).
  • Validation errors (e.g., “No internet connection”).
  • Onboarding (e.g., “Tap here to create your first project”).
  • Gamification (e.g., “You earned a badge!”).

Implementation in SwiftUI

Unlike the previous types that use Apple frameworks (UserNotifications), here we have total control over the design using pure SwiftUI code.

We will create a “Toast” (a small banner that appears and disappears).

Step 1: Create the Toast View

struct InAppNotificationView: View {
    let title: String
    let type: NotificationType
    
    enum NotificationType {
        case success, error, info
        
        var color: Color {
            switch self {
            case .success: return Color.green
            case .error: return Color.red
            case .info: return Color.blue
            }
        }
        
        var icon: String {
            switch self {
            case .success: return "checkmark.circle.fill"
            case .error: return "xmark.octagon.fill"
            case .info: return "info.circle.fill"
            }
        }
    }
    
    var body: some View {
        HStack {
            Image(systemName: type.icon)
            Text(title)
                .font(.subheadline)
                .fontWeight(.medium)
        }
        .padding()
        .background(type.color.opacity(0.9))
        .foregroundColor(.white)
        .cornerRadius(10)
        .shadow(radius: 5)
        .padding(.top, 50) // Space for Dynamic Island or Notch
        .transition(.move(edge: .top).combined(with: .opacity))
    }
}

Step 2: Create a ViewModifier

To make this reusable on any screen, we will use a ViewModifier.

struct InAppNotificationModifier: ViewModifier {
    @Binding var isPresented: Bool
    let title: String
    let type: InAppNotificationView.NotificationType
    
    func body(content: Content) -> some View {
        ZStack(alignment: .top) {
            content
            
            if isPresented {
                InAppNotificationView(title: title, type: type)
                    .onAppear {
                        // Auto-hide after 3 seconds
                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                            withAnimation {
                                isPresented = false
                            }
                        }
                    }
                    .zIndex(1) // Ensure it is on top of everything
            }
        }
    }
}

// Extension for easy usage
extension View {
    func inAppNotification(isPresented: Binding<Bool>, title: String, type: InAppNotificationView.NotificationType) -> some View {
        self.modifier(InAppNotificationModifier(isPresented: isPresented, title: title, type: type))
    }
}

Step 3: Usage in the Main View

struct ContentView: View {
    @State private var showSuccess = false
    
    var body: some View {
        VStack {
            Button("Save Changes") {
                withAnimation {
                    showSuccess = true
                }
            }
            .buttonStyle(.borderedProminent)
        }
        // Inject our in-app notification
        .inAppNotification(isPresented: $showSuccess, title: "Saved successfully", type: .success)
    }
}

4. Summary Comparison Table

To clarify when to use which, observe the following table:

FeatureLocal NotificationPush NotificationIn-App Notification
OriginThe device itself (Client-side)Remote Server (Server-side)The device itself (UI)
ConnectivityInternet not requiredRequires Internet and APNsInternet not required
PermissionsRequires user permissionRequires user permissionNo permission required
VisibilityOutside and inside the appOutside and inside the appOnly inside the app
PurposeReminders, Alarms, GeofencingNews, Chat, Marketing, AlertsFeedback, Tutorials, Errors
FrameworkUserNotificationsUserNotifications + APNsSwiftUI (ViewOverlay)

5. Best Practices and Strategy

The Golden Rule of Context

The most common mistake is abusing Push Notifications. If the user receives a push for something that provides no immediate value, they will revoke permissions.

  • Use Push only for critical and time-sensitive information that happens outside the user’s control (e.g., “Your order has shipped”).
  • Use Local for things the user configured (e.g., “Your 8:00 AM alarm”).

Permission Management

Never ask for permission to send notifications (Push or Local) immediately upon opening the app for the first time.

  1. Let the user navigate.
  2. Wait for them to perform a related action (e.g., after the user presses “Follow” on a soccer team).
  3. Show an In-App Notification explaining why they need to enable notifications (“Turn on notifications to know when your team scores”).
  4. If they accept the In-App prompt, then trigger the system requestAuthorization request.

In-App Design

In-App notifications should feel like part of the application. Do not use standard iOS alerts (UIAlertController) for everything, as they interrupt the flow. Use smooth animations and elements that do not block the full screen (like the Toast we programmed above) to notify of successes or mild warnings.


Conclusion

Mastering notifications in iOS means understanding the balance between technical infrastructure and User Experience (UX).

  1. Use Push Notifications when the server has something new to say.
  2. Use Local Notifications when time or location dictates the alert.
  3. Use In-App Notifications to communicate with the user while they are already paying attention to you.

With SwiftUI, implementing In-App interfaces is easier and more powerful than ever, while integration with UserNotifications remains robust to ensure your messages arrive, whether from the device or the cloud.

You now have the code and theory to build a complete communication system in your next iOS application.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to build a document-based app in SwiftUI

Next Article

How to install GitHub Copilot in Xcode for Swift and SwiftUI

Related Posts