Співбесіда iOS Push Notifications 2026: APNs, токени та troubleshooting
Повний посібник для підготовки до співбесід iOS з тем Push Notifications, APNs, керування токенами та troubleshooting. Поширені запитання з докладними відповідями.

Push Notifications залишаються критичною темою на співбесідах з iOS. Розуміння роботи APNs, керування device-токенами та усунення поширених проблем демонструє глибокі знання екосистеми Apple. Цей посібник охоплює найчастіші запитання співбесід.
Інтерв'юери шукають кандидатів, які розуміють повний життєвий цикл: від реєстрації пристрою до доставки сповіщення, включно з належною обробкою помилок на кожному етапі.
Архітектура APNs: основа iOS-сповіщень
Apple Push Notification service (APNs) — це централізований сервіс, який керує доставкою push-сповіщень на пристрої Apple. Знання його архітектури необхідне для впевнених відповідей на співбесіді.
Як працює APNs?
Комунікаційний потік охоплює трьох основних учасників: iOS-додаток, APNs та backend-сервер. Ось повний процес:
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
}
}Реєстрація в APNs відбувається у два кроки: отримати дозвіл користувача, потім викликати registerForRemoteNotifications().
Керування device-токеном
Device token — це унікальний ідентифікатор, згенерований APNs для адресації конкретного пристрою. Цей токен може змінюватись і має надсилатися на backend під час кожного запуску додатка.
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
}
}Токен надходить як Data і перед відправкою на сервер має бути перетворений на шістнадцятковий рядок.
Поширені запитання співбесід щодо APNs
Співбесіди з iOS зазвичай поєднують теоретичні та практичні запитання щодо APNs. Найпопулярніші з них з докладними відповідями нижче.
Запитання 1: У чому різниця між APNs sandbox і production?
APNs має два окремі середовища з різними endpoint'ами. Токени, згенеровані в одному середовищі, не працюють в іншому.
| Середовище | Endpoint | Використання | |------------|----------|--------------| | Sandbox | api.sandbox.push.apple.com | Debug, TestFlight | | Production | api.push.apple.com | App Store |
Запитання 2: Як обробляти закінчення терміну дії токена?
Device-токени можуть змінюватися з кількох причин: відновлення системи, встановлення на новий пристрій або періодичне оновлення з боку APNs. Сервер має правильно опрацьовувати помилки APNs.
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)
}
}
}Обробка помилок APNs на стороні сервера є критичною для підтримання чистої бази токенів та уникнення зайвих запитів.
Запитання 3: Як реалізувати silent notifications?
Silent notifications дозволяють розбудити додаток у фоні для виконання задач без показу користувачу будь-якого сповіщення.
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)
}
}
}JSON-payload silent notification повинен містити "content-available": 1 всередині об'єкта aps.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Notification Service Extension: розширене налаштування
Notification Service Extensions дозволяють модифікувати вміст сповіщення перед показом. Ця функція часто згадується на співбесідах.
Створення Service Extension
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 має 30 секунд на модифікацію вмісту. Якщо цей ліміт перевищено, викликається метод serviceExtensionTimeWillExpire().
Troubleshooting Push Notifications
Дебагінг push-сповіщень — повторювана тема на співбесідах. Кандидати мають знати інструменти та техніки діагностики.
Перевірка статусу реєстрації
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"
}
}
}Ця debug-функція дозволяє швидко перевірити статус авторизації сповіщень.
Поширені помилки та рішення
Інтерв'юери часто запитують про поширені помилки та шляхи їхнього усунення. Нижче наведено найважливіші для запам'ятовування.
| Помилка | Причина | Рішення |
|---------|---------|---------|
| Недійсний токен | Неправильне середовище (sandbox/prod) | Перевірити provisioning profile |
| Сповіщення не отримано | Активний режим енергозбереження | Тестувати із зарядженою батареєю |
| Extension не викликано | Payload без mutable-content | Додати "mutable-content": 1 |
| Background fetch впав | Додаток закрито користувачем | Повідомити користувачу про обмеження |
Пряме тестування APNs
Для тестування сповіщень під час розробки інструмент curl дозволяє надсилати запити безпосередньо до APNs:
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"
}
"""
}Ці тестові payload'и корисні для перевірки поведінки додатка з різними типами сповіщень.
Найкращі практики у production
Питання про найкращі практики допомагають оцінити досвід кандидата з production-додатками.
Обробка мережевих помилок
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()
}
}
}
}Використання actor забезпечує безпеку потоків при керуванні токенами в конкурентних середовищах.
Локальне збереження токена
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
}
}Локальне збереження токена усуває зайві мережеві виклики, коли токен не змінився.
Висновок
Володіння iOS Push Notifications свідчить про глибоке розуміння екосистеми Apple та клієнт-серверної взаємодії. Ключові моменти, які варто пам'ятати на співбесідах:
✅ Архітектура APNs: розуміти повний шлях від реєстрації до доставки
✅ Device-токени: життєвий цикл та керування змінами токена
✅ Notification Service Extension: налаштування вмісту з обмеженням 30 секунд
✅ Troubleshooting: знати поширені помилки та їхні розв'язання
✅ Silent notifications: content-available для background fetch
✅ Найкращі практики: логіка повтору, локальне збереження, обробка помилок
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Співбесіда StoreKit 2: Управління Підписками та Валідація Чеків
Опануйте питання співбесіди iOS щодо StoreKit 2, управління підписками, валідації чеків та реалізації покупок у застосунку з практичними прикладами коду на Swift.

Swift Testing Framework Співбесіда 2026: Макроси #expect та #require проти XCTest
Опанування нового Swift Testing Framework для iOS-співбесід: макроси #expect і #require, міграція з XCTest, складні шаблони та поширені помилки.

Топ 25 запитань зі Swift на співбесідах для iOS-розробників
Підготовка до технічних співбесід з iOS: 25 найпоширеніших запитань зі Swift — optionals, closures, ARC, протоколи, async/await та просунуті патерни з прикладами коду.