# Phỏng vấn iOS Push Notifications 2026: APNs, token và troubleshooting > Hướng dẫn toàn diện chuẩn bị phỏng vấn iOS về Push Notifications, APNs, quản lý token và troubleshooting. Câu hỏi thường gặp kèm câu trả lời chi tiết. - Published: 2026-02-28 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, push-notifications, apns, swift, interview - Reading time: 12 min --- Push Notifications vẫn là chủ đề trọng yếu trong các buổi phỏng vấn iOS. Hiểu cách APNs hoạt động, quản lý device token và xử lý sự cố thường gặp thể hiện hiểu biết sâu về hệ sinh thái Apple. Hướng dẫn này tập hợp những câu hỏi phỏng vấn phổ biến nhất. > **Điểm cốt lõi của buổi phỏng vấn** > > Người phỏng vấn tìm kiếm ứng viên hiểu trọn vòng đời: từ đăng ký thiết bị đến gửi notification, bao gồm xử lý lỗi đúng cách ở mỗi bước. ## Kiến trúc APNs: nền tảng của thông báo iOS Apple Push Notification service (APNs) là dịch vụ tập trung phụ trách phân phối push notification đến các thiết bị Apple. Hiểu kiến trúc của nó là cần thiết để trả lời tốt các câu hỏi phỏng vấn. ### APNs hoạt động như thế nào? Luồng giao tiếp gồm ba thành phần chính: ứng dụng iOS, APNs và máy chủ backend. Quy trình đầy đủ như sau: ```swift // AppDelegate.swift import UIKit import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Request notification authorization UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .badge, .sound] ) { granted, error in guard granted else { return } // Register with APNs DispatchQueue.main.async { application.registerForRemoteNotifications() } } return true } } ``` Việc đăng ký APNs diễn ra theo hai bước: xin quyền người dùng, sau đó gọi `registerForRemoteNotifications()`. ### Quản lý device token Device token là định danh duy nhất do APNs tạo ra để nhắm tới một thiết bị cụ thể. Token có thể thay đổi và cần được gửi lên backend mỗi khi ứng dụng khởi động. ```swift // AppDelegate.swift extension AppDelegate { // Callback when APNs provides the token func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { // Convert token to hexadecimal string let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() print("Device Token: \(tokenString)") // Send to backend server sendTokenToServer(tokenString) } // Callback on registration failure func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { print("Failed to register: \(error.localizedDescription)") } private func sendTokenToServer(_ token: String) { // Implement HTTP request to backend } } ``` Token được trả về dưới dạng `Data` và phải được chuyển thành chuỗi hex trước khi gửi tới máy chủ. ## Câu hỏi phỏng vấn thường gặp về APNs Các buổi phỏng vấn iOS thường kết hợp câu hỏi lý thuyết và thực hành về APNs. Dưới đây là những câu phổ biến nhất kèm câu trả lời chi tiết. ### Câu hỏi 1: Khác biệt giữa APNs sandbox và production là gì? > **Môi trường APNs** > > APNs có hai môi trường tách biệt với endpoint khác nhau. Token sinh ra ở môi trường này không hoạt động ở môi trường kia. | Môi trường | Endpoint | Sử dụng | |------------|----------|---------| | Sandbox | api.sandbox.push.apple.com | Debug, TestFlight | | Production | api.push.apple.com | App Store | ### Câu hỏi 2: Xử lý token hết hạn như thế nào? Device token có thể đổi vì nhiều lý do: khôi phục hệ thống, cài lại trên thiết bị mới hoặc APNs làm mới định kỳ. Máy chủ phải xử lý đúng các phản hồi lỗi từ APNs. ```swift // NotificationService.swift enum APNsError: Int { case badDeviceToken = 400 case unregistered = 410 case payloadTooLarge = 413 case tooManyRequests = 429 case internalServerError = 500 var shouldRemoveToken: Bool { // Remove token only if device is no longer registered return self == .unregistered || self == .badDeviceToken } } struct APNsResponse { let statusCode: Int let deviceToken: String func handleError() { guard let error = APNsError(rawValue: statusCode) else { return } if error.shouldRemoveToken { // Remove token from database TokenRepository.shared.remove(deviceToken) } } } ``` Xử lý lỗi APNs phía máy chủ rất quan trọng để giữ database token sạch và tránh các yêu cầu không cần thiết. ### Câu hỏi 3: Triển khai silent notification ra sao? Silent notification cho phép đánh thức ứng dụng ở chế độ nền để thực hiện tác vụ mà không hiển thị bất kỳ cảnh báo nào cho người dùng. ```swift // AppDelegate.swift func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { // Check if this is a silent notification guard let aps = userInfo["aps"] as? [String: Any], aps["content-available"] as? Int == 1 else { completionHandler(.noData) return } // Perform background task performBackgroundTask { result in switch result { case .success: completionHandler(.newData) case .failure: completionHandler(.failed) } } } ``` Payload JSON của silent notification phải chứa `"content-available": 1` trong đối tượng `aps`. ## Notification Service Extension: tuỳ biến nâng cao Notification Service Extension cho phép sửa nội dung notification trước khi hiển thị. Tính năng này thường được hỏi trong phỏng vấn. ### Tạo Service Extension ```swift // NotificationService.swift (in Extension target) import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { self.contentHandler = contentHandler bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent guard let bestAttemptContent = bestAttemptContent else { contentHandler(request.content) return } // Modify content if let imageURLString = bestAttemptContent.userInfo["image-url"] as? String, let imageURL = URL(string: imageURLString) { downloadImage(from: imageURL) { attachment in if let attachment = attachment { bestAttemptContent.attachments = [attachment] } contentHandler(bestAttemptContent) } } else { contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called when time limit (30 seconds) is exceeded if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } private func downloadImage( from url: URL, completion: @escaping (UNNotificationAttachment?) -> Void ) { URLSession.shared.downloadTask(with: url) { localURL, _, error in guard let localURL = localURL, error == nil else { completion(nil) return } let tempDirectory = FileManager.default.temporaryDirectory let fileName = url.lastPathComponent let destinationURL = tempDirectory.appendingPathComponent(fileName) try? FileManager.default.moveItem(at: localURL, to: destinationURL) let attachment = try? UNNotificationAttachment( identifier: fileName, url: destinationURL, options: nil ) completion(attachment) }.resume() } } ``` Extension có 30 giây để chỉnh sửa nội dung. Phương thức `serviceExtensionTimeWillExpire()` được gọi khi vượt giới hạn này. ## Troubleshooting Push Notifications Gỡ lỗi push notification là chủ đề lặp lại trong phỏng vấn. Ứng viên cần nắm các công cụ và kỹ thuật chẩn đoán. ### Kiểm tra trạng thái đăng ký ```swift // DebugHelper.swift struct PushNotificationDebugger { static func checkNotificationStatus() async { let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() print("=== Push Notification Status ===") print("Authorization: \(settings.authorizationStatus.description)") print("Alert: \(settings.alertSetting.description)") print("Badge: \(settings.badgeSetting.description)") print("Sound: \(settings.soundSetting.description)") print("Notification Center: \(settings.notificationCenterSetting.description)") } } extension UNAuthorizationStatus { var description: String { switch self { case .notDetermined: return "Not Determined" case .denied: return "Denied" case .authorized: return "Authorized" case .provisional: return "Provisional" case .ephemeral: return "Ephemeral" @unknown default: return "Unknown" } } } ``` Hàm debug này giúp nhanh chóng kiểm tra trạng thái cấp quyền của notification. ### Lỗi thường gặp và cách xử lý > **Các lỗi phổ biến cần biết** > > Người phỏng vấn hay hỏi về các lỗi thường gặp và giải pháp. Dưới đây là những lỗi quan trọng nhất cần ghi nhớ. | Lỗi | Nguyên nhân | Giải pháp | |-----|-------------|-----------| | Token không hợp lệ | Sai môi trường (sandbox/prod) | Kiểm tra provisioning profile | | Notification không nhận được | Bật chế độ tiết kiệm pin | Thử lại với pin đầy | | Extension không được gọi | Payload thiếu `mutable-content` | Thêm `"mutable-content": 1` | | Background fetch thất bại | Người dùng đã tắt ứng dụng | Thông báo cho người dùng về giới hạn | ### Kiểm thử APNs trực tiếp Để kiểm thử notification trong quá trình phát triển, công cụ `curl` cho phép gửi yêu cầu trực tiếp đến APNs: ```swift // TestPayload.swift struct APNsTestPayload { static let silentNotification = """ { "aps": { "content-available": 1 }, "custom-data": "test" } """ static let richNotification = """ { "aps": { "alert": { "title": "New message", "body": "Message content" }, "mutable-content": 1, "sound": "default" }, "image-url": "https://example.com/image.jpg" } """ } ``` Những payload thử nghiệm này hữu ích để kiểm chứng hành vi của ứng dụng với các loại notification khác nhau. ## Thực hành tốt nhất khi vận hành Các câu hỏi về thực hành tốt nhất giúp đánh giá kinh nghiệm của ứng viên với ứng dụng production. ### Xử lý lỗi mạng ```swift // PushTokenManager.swift actor PushTokenManager { private var pendingToken: String? private var retryCount = 0 private let maxRetries = 3 func registerToken(_ token: String) async { pendingToken = token await sendTokenWithRetry() } private func sendTokenWithRetry() async { guard let token = pendingToken else { return } do { try await APIClient.shared.registerPushToken(token) pendingToken = nil retryCount = 0 } catch { retryCount += 1 if retryCount < maxRetries { // Retry with exponential backoff let delay = UInt64(pow(2.0, Double(retryCount))) * 1_000_000_000 try? await Task.sleep(nanoseconds: delay) await sendTokenWithRetry() } } } } ``` Dùng `actor` đảm bảo an toàn luồng khi quản lý token trong môi trường đồng thời. ### Lưu trữ token cục bộ ```swift // TokenStorage.swift struct TokenStorage { private static let tokenKey = "com.app.pushToken" static func save(_ token: String) { UserDefaults.standard.set(token, forKey: tokenKey) } static func retrieve() -> String? { UserDefaults.standard.string(forKey: tokenKey) } static func hasTokenChanged(_ newToken: String) -> Bool { guard let savedToken = retrieve() else { return true } return savedToken != newToken } } ``` Lưu token cục bộ giúp tránh các cuộc gọi mạng không cần thiết khi token chưa thay đổi. ## Kết luận Thông thạo iOS Push Notifications cho thấy hiểu biết sâu về hệ sinh thái Apple và các tương tác client-server. Những điểm cốt lõi cần ghi nhớ khi phỏng vấn: ✅ Kiến trúc APNs: hiểu trọn luồng từ đăng ký đến gửi đi ✅ Device token: vòng đời và xử lý khi token thay đổi ✅ Notification Service Extension: tuỳ biến nội dung trong giới hạn 30 giây ✅ Troubleshooting: nắm các lỗi thường gặp và cách xử lý ✅ Silent notifications: `content-available` cho background fetch ✅ Thực hành tốt nhất: cơ chế retry, lưu trữ cục bộ, xử lý lỗi --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/ios-push-notifications-apns-interview