iOS Push Notifications Integration
Implementation Overview
Implementing push notifications in your iOS app requires just 3 simple steps:
- Enable push notification capability in your Xcode project
- Request permission and get the device token from Apple
- Send this device token to our backend
That's it! Our backend handles all the complex APNs configuration and sending push notifications to your users.
Step 1: Enable Push Notification Capability
- In Xcode, select your project
- Select your app target
- Go to "Signing & Capabilities"
- Click "+ Capability" and add "Push Notifications"
Note: Make sure your bundle ID matches what you've provided to our team for APNs configuration.
Step 2: Request Permission and Get Device Token
Add this code to your AppDelegate:
import UIKit
import UserNotifications
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Request notification permissions
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
guard granted else {
print("Notification permission denied")
return
}
DispatchQueue.main.async {
// This will trigger didRegisterForRemoteNotificationsWithDeviceToken
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}
// This method will be called with your device token
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// Convert binary token to string format
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Device Token: \(token)")
// Send the token to our backend
sendDeviceTokenToBackend(token)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register for remote notifications: \(error.localizedDescription)")
}
}Step 3: Send Device Token to Our Backend
Add this method to your AppDelegate to send the token:
func sendDeviceTokenToBackend(_ token: String) {
// Get the user ID from your authentication system
guard let userId = UserDefaults.standard.string(forKey: "userId") else {
print("No userId found, can't register device token")
return
}
// Prepare API request to our backend
let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/device-token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Create request body with the token
let body: [String: Any] = ["deviceToken": token]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
// Send the request
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Error registering device token: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response")
return
}
if httpResponse.statusCode == 200 {
print("✅ Device token registered successfully with backend")
} else {
print("❌ Failed to register device token: HTTP \(httpResponse.statusCode)")
}
}.resume()
}API Endpoint Details
URL: https://api.wecompleteapp.com/api/users/user/{userId}/device-token
Method: POST
Headers: Content-Type: application/json
Body: { "deviceToken": "your-device-token-string" }
Response: { "success": true, "message": "Device token registered successfully" }
Step 4: Handle Incoming Notifications
Add this method to handle notifications when your app is in the background:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("Received notification: \(userInfo)")
// Process notification data
if let aps = userInfo["aps"] as? [String: Any] {
// Process standard Apple notification fields
}
if let data = userInfo["data"] as? [String: Any] {
// Process our custom data fields
if let notificationId = data["notificationId"] as? String {
// You can use the notificationId to fetch more details
print("Notification ID: \(notificationId)")
}
if let type = data["type"] as? String {
// Handle different notification types
switch type {
case "message":
// Navigate to message screen
break
case "reminder":
// Navigate to reminder screen
break
default:
break
}
}
}
completionHandler(.newData)
}For a better user experience, implement the UNUserNotificationCenterDelegate:
// In your AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ... existing code ...
UNUserNotificationCenter.current().delegate = self
return true
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
// Called when a notification is received while app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// You can choose how to display the notification
completionHandler([.banner, .sound, .badge])
}
// Called when user taps on a notification
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
print("User tapped on notification: \(userInfo)")
// Process the notification and navigate to relevant screen
if let data = userInfo["data"] as? [String: Any],
let type = data["type"] as? String {
// Handle navigation based on notification type
}
completionHandler()
}
}Testing Push Notifications
To test if your push notification setup works correctly:
- Run your app on a physical device (not the simulator)
- Make sure you grant notification permissions when prompted
- Verify the device token appears in your console logs
- Confirm the "Device token registered successfully" message appears
- Use our test endpoint to send a test notification
Test API Endpoint
You can trigger a test notification using this endpoint:
URL: https://api.wecompleteapp.com/api/users/user/{userId}/send-test-notification
Example: https://api.wecompleteapp.com/api/users/user/nh74hejga9ynysqbtkpsfn6nsn7exnbm/send-test-notification
Method: POST
Headers: Content-Type: application/json
Body:
{
"title": "Your custom title",
"message": "Your custom message",
"category": "Your notification category",
"url": "wecomplete://memories/mem_123456"
}All parameters are optional. The url parameter can be used for deep linking to specific screens in your app.
Using Push Notifications Across Your App
To send push notifications to users from your app, use the device token registration endpoint. Our backend will handle sending notifications to your users at the appropriate times.
Device Token Registration
URL: https://api.wecompleteapp.com/api/users/user/{userId}/device-token
Method: POST
Headers: Content-Type: application/json
Body: { "deviceToken": "your-device-token-string" }
Response: { "success": true, "message": "Device token registered successfully" }
Troubleshooting
Not receiving the device token
- Check that push capability is enabled in your app
- Verify you're testing on a physical device, not the simulator
- Make sure your provisioning profile includes push entitlements
- User must grant notification permissions
- Check for any errors in the console log
Token registration fails
- Make sure your userId is correctly stored in UserDefaults
- Check your network connection
- Verify the API URL is correct
- Check the HTTP response code and message
- Try testing with our API endpoints using Postman to verify server-side functionality
Notifications not appearing
- Verify the device token was successfully registered with our backend
- Check that notification permissions are granted in iOS Settings
- Make sure your app is built with the correct bundle ID
- Try sending a test notification to confirm the setup
Real-World Example: Habit Reminder Notifications
Let's walk through a complete example of implementing habit reminder notifications in your app:
Scenario
Users can set reminders for daily habits they want to track. Your app will send push notifications at scheduled times, and when a user taps on the notification, they'll be taken directly to the habit completion screen.
1. Creating the Habit Reminder
// HabitReminderView.swift
import SwiftUI
struct HabitReminderView: View {
@State private var habitTitle = ""
@State private var reminderTime = Date()
@State private var showSuccess = false
var body: some View {
Form {
Section(header: Text("Habit Details")) {
TextField("Habit title", text: $habitTitle)
DatePicker("Reminder time", selection: $reminderTime, displayedComponents: .hourAndMinute)
}
Button("Save Reminder") {
createHabitReminder()
}
}
.navigationTitle("New Habit Reminder")
.alert("Reminder Set", isPresented: $showSuccess) {
Button("OK") {}
} message: {
Text("You'll receive a notification for '(habitTitle)' daily at (formatTime(reminderTime))")
}
}
func createHabitReminder() {
guard !habitTitle.isEmpty else { return }
// 1. Save habit locally
let habit = Habit(id: UUID().uuidString, title: habitTitle, reminderTime: reminderTime)
saveHabit(habit)
// 2. Register with our backend
registerHabitWithBackend(habit)
showSuccess = true
}
func registerHabitWithBackend(_ habit: Habit) {
guard let userId = UserDefaults.standard.string(forKey: "userId") else { return }
let url = URL(string: "https://api.wecompleteapp.com/api/habits/(userId)/create")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"habitId": habit.id,
"title": habit.title,
"reminderTime": ISO8601DateFormatter().string(from: habit.reminderTime)
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
// Handle response
}.resume()
}
func formatTime(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
struct Habit {
let id: String
let title: String
let reminderTime: Date
}2. Handling the Push Notification
When our backend sends a habit reminder, you'll receive a notification with custom data:
// Example payload received from server
{
"aps": {
"alert": {
"title": "Habit Reminder",
"body": "Time to complete your habit: Morning Meditation"
},
"sound": "default",
"badge": 1
},
"data": {
"type": "habit_reminder",
"habitId": "a1b2c3d4-e5f6-7890-abcd-1234567890ab"
}
}3. Processing Notification Taps
Update your UNUserNotificationCenterDelegate implementation to handle habit reminders:
// In your AppDelegate.swift
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if let data = userInfo["data"] as? [String: Any],
let type = data["type"] as? String {
switch type {
case "habit_reminder":
if let habitId = data["habitId"] as? String {
// Open habit completion screen
navigateToHabitCompletion(habitId: habitId)
}
break
// Handle other notification types
default:
break
}
}
completionHandler()
}
func navigateToHabitCompletion(habitId: String) {
// Get the root view controller
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else { return }
// Create and present the habit completion screen
// This implementation will depend on your app's navigation structure
// For UIKit apps:
let habitVC = HabitCompletionViewController(habitId: habitId)
let navController = UINavigationController(rootViewController: habitVC)
rootViewController.present(navController, animated: true)
// For SwiftUI apps, you would set an app state variable that changes the navigation
}4. Testing the Complete Flow
To test your implementation:
- Create a habit reminder in your app
- Verify the habit is sent to your backend (check network logs)
- Use our API to trigger a test notification with the habit payload
// Example cURL request to test the notification
curl -X POST \
https://api.wecompleteapp.com/api/users/user/{"{userId}"}/send-test-notification \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
"userId": "user-123",
"title": "Habit Reminder",
"body": "Time to complete your habit: Morning Meditation",
"data": {
"type": "habit_reminder",
"habitId": "a1b2c3d4-e5f6-7890-abcd-1234567890ab"
}
}'Pro Tips
- Schedule notifications at user-friendly times (not too early, not too late)
- Add deep-linking capabilities to take users directly to relevant screens
- Implement notification categories and actions for quick responses without opening the app
- Track notification engagement metrics to improve your reminder strategy
Implementing Deep Linking with Push Notifications
Deep linking allows your notifications to navigate users directly to specific screens in your app. Here's how to implement it:
1. Configure URL Scheme in Xcode
- Open your Xcode project
- Select your app target
- Go to "Info" tab
- Expand "URL Types"
- Click "+" to add a new URL type
- Set "Identifier" to your bundle ID
- Set "URL Schemes" to your custom scheme (e.g., "wecomplete")
2. Include Deep Link in Notification Payload
When sending notifications, include the deep link URL in the payload:
// Example notification with deep link
let notificationData = [
"userId": "user_to_notify",
"title": "New Memory Added",
"body": "Your partner added a memory from your vacation",
"data": [
"type": "NEW_MEMORY",
"memoryId": "mem_123456",
"deepLink": "wecomplete://memories/mem_123456"
]
]
// Send using our notification API
let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/send-test-notification")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("YOUR_API_KEY", forHTTPHeaderField: "Authorization")
request.httpBody = try? JSONSerialization.data(withJSONObject: notificationData)
URLSession.shared.dataTask(with: request) { data, response, error in
// Handle response
}.resume()3. Handle Deep Links When Notification is Tapped
Update your notification handling code to extract and process the deep link:
// In AppDelegate.swift
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
// Extract deep link if present
if let data = userInfo["data"] as? [String: Any],
let deepLink = data["deepLink"] as? String,
let url = URL(string: deepLink) {
// Process the deep link URL
handleDeepLink(url)
}
completionHandler()
}
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return }
// Example: wecomplete://memories/mem_123456
if components.host == "memories" {
let pathComponents = components.path.components(separatedBy: "/")
if pathComponents.count > 1 {
let memoryId = pathComponents[1]
navigateToMemory(memoryId: memoryId)
}
}
// Handle other deep link patterns
}4. Common Deep Link URL Patterns
| Feature | Deep Link URL Pattern | Example |
|---|---|---|
| Open Memory | wecomplete://memories/:id | wecomplete://memories/mem_123456 |
| View Profile | wecomplete://profile/:id | wecomplete://profile/user_789 |
| Open Chat | wecomplete://chat/:id | wecomplete://chat/chat_456 |
| View Habit | wecomplete://habits/:id | wecomplete://habits/habit_567 |
Best Practices for Deep Linking
- Follow a consistent URL pattern across your app
- Handle cases where the target content doesn't exist
- Consider the user's login state - redirect to login if needed
- For more robust linking, consider implementing Universal Links (iOS) which use your domain
- Test deep links thoroughly on different app states (freshly launched, in background, etc.)
Using Our Notification APIs
Our backend already handles all the complexity of sending push notifications. Here's how to use our existing APIs to send notifications after specific actions.
Example: Notify Partner About New Memory
When a user creates a new memory, you can easily send a notification to their partner:
- First, create the memory using our Memory API
- Then, call our notification API to alert the partner
// Step 1: Create a memory
let userId = "user_abc123" // The current user's ID
let memoryData = [
"action": "create",
"title": "Our First Date",
"description": "Remember that amazing restaurant?",
"date": "2023-11-15T18:30:00",
"location": "Italian Restaurant",
"memoryType": "date"
]
let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/memories")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: memoryData)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("Error creating memory: \(error?.localizedDescription ?? "Unknown error")")
return
}
// Parse the response to get the new memory ID
if let memory = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let memoryId = memory["_id"] as? String {
// Step 2: Send notification to partner
notifyPartnerAboutMemory(memoryId: memoryId, memoryTitle: memoryData["title"] as! String)
}
}.resume()
// Function to notify partner
func notifyPartnerAboutMemory(memoryId: String, memoryTitle: String) {
// Simply call our notification API
let notificationData = [
"partnerId": "partner_xyz789", // Partner's user ID
"notificationType": "NEW_MEMORY",
"title": "New Memory Created",
"body": "Your partner just created a new memory: \(memoryTitle)",
"data": [
"memoryId": memoryId,
"type": "memory_notification"
]
]
let url = URL(string: "https://api.wecompleteapp.com/api/users/user/\(userId)/send-test-notification")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("YOUR_API_KEY", forHTTPHeaderField: "Authorization")
request.httpBody = try? JSONSerialization.data(withJSONObject: notificationData)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Failed to send notification: \(error.localizedDescription)")
} else {
print("Notification sent successfully")
}
}.resume()
}Key Notification Endpoints
Send Notification API
URL: https://api.wecompleteapp.com/api/users/user/{userId}/send-test-notification
Method: POST
Headers:
- Content-Type: application/json
Body:
{
"title": "Notification Title",
"body": "Notification message body",
"category": "notification_category"
}Send Test Notification
To quickly test if notifications are working:
URL: https://api.wecompleteapp.com/api/users/user/{userId}/send-test-notification
Method: POST
Headers: Content-Type: application/json
Body:
{
"title": "Your custom title",
"message": "Your custom message",
"category": "Your notification category",
"url": "wecomplete://memories/mem_123456"
}All parameters are optional. The url parameter can be used for deep linking to specific screens in your app.
Get User Notifications
To retrieve a user's notification history:
URL: https://api.wecompleteapp.com/api/users/user/{userId}/notifications
Method: GET
Query Parameters:
limit- Maximum number of notifications to return (default: 50)offset- Number of notifications to skip (for pagination, default: 0)category- Filter notifications by category (optional)
Example Request:
// Fetch the 20 most recent notifications let url = URL(string: "https://api.wecompleteapp.com/api/users/user/(userId)/notifications?limit=20")! // Fetch only "reminder" category notifications let url = URL(string: "https://api.wecompleteapp.com/api/users/user/(userId)/notifications?category=reminder")! // Pagination example (second page of results) let url = URL(string: "https://api.wecompleteapp.com/api/users/user/(userId)/notifications?limit=20&offset=20")!
Response:
{
"success": true,
"notifications": [
{
"_id": "notification_id_1",
"userId": "user_id",
"title": "Habit Reminder",
"message": "Time to complete your daily meditation",
"category": "reminder",
"sentAt": "2023-11-15T18:30:00Z",
"isRead": false,
"successCount": 1,
"totalDevices": 2
},
{
"_id": "notification_id_2",
"userId": "user_id",
"title": "New Memory",
"message": "Your partner added a new memory",
"category": "memory",
"sentAt": "2023-11-14T12:45:00Z",
"isRead": true,
"successCount": 2,
"totalDevices": 2
}
// ... more notifications
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 2
}
}Use this endpoint to display a notification history in your app or to check if a user has unread notifications.
Example: Displaying Notification History
Here's how to fetch and display a user's notification history in your app:
// NotificationsViewController.swift
import UIKit
class NotificationsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var notifications: [[String: Any]] = []
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
// Load notifications when view appears
fetchNotifications()
}
func fetchNotifications() {
guard !isLoading else { return }
isLoading = true
guard let userId = UserDefaults.standard.string(forKey: "userId") else {
print("No userId found")
isLoading = false
return
}
let url = URL(string: "https://api.wecompleteapp.com/api/users/user/(userId)/notifications?limit=50")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self else { return }
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
print("Error fetching notifications: (error.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let success = json["success"] as? Bool,
success,
let notificationsData = json["notifications"] as? [[String: Any]] {
self.notifications = notificationsData
self.tableView.reloadData()
print("Loaded (notificationsData.count) notifications")
} else {
print("Invalid response format")
}
} catch {
print("Error parsing JSON: (error.localizedDescription)")
}
}
}.resume()
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return notifications.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationCell", for: indexPath)
let notification = notifications[indexPath.row]
// Configure cell
cell.textLabel?.text = notification["title"] as? String
cell.detailTextLabel?.text = notification["message"] as? String
// Show unread indicator if needed
if let isRead = notification["isRead"] as? Bool, !isRead {
cell.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
} else {
cell.backgroundColor = .white
}
// Show category badge
if let category = notification["category"] as? String {
let badgeLabel = UILabel()
badgeLabel.text = category
badgeLabel.textColor = .white
badgeLabel.backgroundColor = categoryColor(for: category)
badgeLabel.font = UIFont.systemFont(ofSize: 12)
badgeLabel.textAlignment = .center
badgeLabel.layer.cornerRadius = 8
badgeLabel.clipsToBounds = true
badgeLabel.frame = CGRect(x: 0, y: 0, width: 80, height: 16)
cell.accessoryView = badgeLabel
}
return cell
}
// Helper to get color for category
func categoryColor(for category: String) -> UIColor {
switch category.lowercased() {
case "reminder": return .systemBlue
case "memory": return .systemPurple
case "message": return .systemGreen
default: return .systemGray
}
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Mark as read when tapped
if let notificationId = notifications[indexPath.row]["_id"] as? String {
markNotificationAsRead(notificationId)
}
// Handle notification tap (e.g., navigate to relevant screen)
if let data = notifications[indexPath.row]["data"] as? [String: Any],
let type = data["type"] as? String {
handleNotificationTap(type: type, data: data)
}
}
func markNotificationAsRead(_ notificationId: String) {
// Implementation to mark notification as read
// This would typically call your backend API
}
func handleNotificationTap(type: String, data: [String: Any]) {
// Navigate based on notification type
switch type {
case "memory_notification":
if let memoryId = data["memoryId"] as? String {
// Navigate to memory details
let memoryVC = MemoryDetailViewController(memoryId: memoryId)
navigationController?.pushViewController(memoryVC, animated: true)
}
case "reminder":
// Navigate to reminder screen
break
default:
break
}
}
}Common Notification Scenarios
| Scenario | When to Send | Notification Type |
|---|---|---|
| New Memory | After creating a memory | NEW_MEMORY |
| Comment on Memory | When a user comments | NEW_COMMENT |
| Mood Updated | When user updates mood | MOOD_UPDATE |
| Event Reminder | 24h before saved date | EVENT_REMINDER |
Need Help?
If you're experiencing issues implementing push notifications, contact our developer support at support@wecompleteapp.com.