├── .gitignore ├── LICENSE ├── Package.swift ├── README.md └── Sources └── APNSManager ├── AppDelegate.swift └── apnsManager.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chris Coffin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swiftui-apnsManager", 7 | platforms: [.iOS(.v14)], 8 | products: [ 9 | .library( 10 | name: "APNSManager", 11 | targets: ["APNSManager"]), 12 | ], 13 | dependencies: [ 14 | ], 15 | targets: [ 16 | .target( 17 | name: "APNSManager", 18 | dependencies: []), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swiftui-apnsManager 2 | SwiftUI package that allows you to gate access to your app's main view until user has completed Sign In With Apple and granted permission for push notifications. 3 | 4 | Handles requesting user permissions for notifications, registering with APNS server and receiving device token, implements Sign In With Apple, and and uploads user ID + device token to [your remote notification server](https://github.com/magnolialogic/python-apns_server). 5 | 6 | *Requires Xcode 12 / iOS 14* 7 | 8 | ## Usage 9 | 10 | 1. Add to your Xcode 12 / iOS 14 project as a Swift Package Dependency 11 | 2. `import APNSManager` 12 | 3. Add a Configuration Settings File to your project and define the route to its /user REST API (e.g. `https://apns.example.com/v1/user/`) 13 | 4. If you need to manage/track a user's Admin state, add "ADMIN_CHECK" key to Info.plist with value "true" 14 | 5. Start with MyExampleApp.swift below 15 | 16 | **Note:** these steps result in a read-only library, so when you're ready to start customizing this boilerplate example do a `git clone` onto your local disk and then drag the local folder in to your Xcode sidebar. This will move the library from "Swift Package Dependencies" into your app's resources, and you can edit/update the implementation from there. 17 | 18 | #### MyExampleApp.swift 19 | ```swift 20 | import APNSManager 21 | import AuthenticationServices 22 | import os 23 | import SwiftUI 24 | 25 | @main 26 | struct MyExampleApp: App { 27 | @UIApplicationDelegateAdaptor var appDelegate: AppDelegate 28 | @StateObject var apnsManagedSettings = apnsManager.shared 29 | 30 | var body: some Scene { 31 | WindowGroup { 32 | if apnsManagedSettings.notificationPermissionStatus == "Unknown" { 33 | VStack { 34 | Spacer() 35 | ProgressView() 36 | Spacer() 37 | } 38 | } else if apnsManagedSettings.notificationPermissionStatus == "NotDetermined" { 39 | GetStartedView().environmentObject(apnsManagedSettings) 40 | } else if apnsManagedSettings.notificationPermissionStatus == "Denied" { 41 | NotificationsDeniedView().environmentObject(apnsManagedSettings) 42 | } else { 43 | NotificationsAllowedView().environmentObject(apnsManagedSettings) 44 | } 45 | } 46 | } 47 | } 48 | 49 | struct GetStartedView: View { 50 | @EnvironmentObject var apnsManagedSettings: apnsManager 51 | 52 | var body: some View { 53 | VStack { 54 | Spacer() 55 | Button(action: { 56 | apnsManagedSettings.requestNotificationsPermission() 57 | }, label: { 58 | Text("Get Started") 59 | }) 60 | Text("Note: push notification permissions are required!") 61 | .font(.system(size: 10)) 62 | .padding(.top, 10) 63 | Spacer() 64 | }.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in 65 | apnsManagedSettings.checkNotificationAuthorizationStatus() 66 | }) 67 | } 68 | } 69 | 70 | struct NotificationsDeniedView: View { 71 | @EnvironmentObject var apnsManagedSettings: apnsManager 72 | 73 | var body: some View { 74 | if apnsManagedSettings.notificationPermissionStatus == "Denied" { 75 | VStack { 76 | Spacer() 77 | Text("Notifications permissions are required") 78 | Button(action: { 79 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) 80 | }, label: { 81 | Text("Enable in Settings") 82 | .padding(.top, 20) 83 | }) 84 | Spacer() 85 | }.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in 86 | apnsManagedSettings.checkNotificationAuthorizationStatus() 87 | }) 88 | } else { 89 | NotificationsAllowedView() 90 | } 91 | } 92 | } 93 | 94 | struct NotificationsAllowedView: View { 95 | @EnvironmentObject var apnsManagedSettings: apnsManager 96 | 97 | var body: some View { 98 | if apnsManagedSettings.proceedToMainView { 99 | MyExampleMainView() 100 | } else { 101 | Spacer() 102 | SignInWithAppleButton( 103 | .signIn, 104 | onRequest: { request in 105 | request.requestedScopes = [.fullName] 106 | }, 107 | onCompletion: { result in 108 | switch result { 109 | case .success (let authResults): 110 | let authCredential = authResults.credential as! ASAuthorizationAppleIDCredential 111 | let authCredentialUserID = authCredential.user 112 | apnsManagedSettings.userID = authCredentialUserID 113 | let userName = authCredential.fullName?.givenName ?? "" 114 | if !userName.isEmpty { 115 | apnsManagedSettings.userName = userName 116 | } 117 | apnsManagedSettings.updateRemoteNotificationServer() 118 | DispatchQueue.main.async { 119 | apnsManagedSettings.signInWithAppleSuccess = true 120 | } 121 | case.failure (let error): 122 | os_log(.error, "Authorization failed: \(error.localizedDescription)") 123 | DispatchQueue.main.async { 124 | apnsManagedSettings.signInWithAppleSuccess = false 125 | } 126 | } 127 | } 128 | ) 129 | .frame(width: 280, height: 60, alignment: .center) 130 | Spacer() 131 | } 132 | } 133 | } 134 | 135 | extension UserDefaults { 136 | func valueExists(forKey key: String) -> Bool { 137 | return object(forKey: key) != nil 138 | } 139 | } 140 | ``` 141 | 142 | #### Example console output 143 | ``` 144 | MyApp[15355:2946298] apnsManager.shared.notificationPermissionStatus set: NotDetermined 145 | MyApp[15355:2946599] appDelegate: User granted permissions for notifications, registering with APNS 146 | MyApp[15355:2946298] apnsManager.shared.deviceToken set: fcc37fb74f2506277739c1e343c535f131447327105e23ad2a0ce 147 | MyApp[15355:2946298] apnsManager.shared.apnsRegistrationSuccess set: true 148 | MyApp[15355:2946298] apnsManager.shared.notificationPermissionStatus set: Allowed 149 | MyApp[15355:2946298] apnsManager.shared.userID set: 1234567890 150 | MyApp[15355:2946298] apnsManager.shared.userName set: Test User 1 151 | MyApp[15355:2946718] apnsManager.shared.updateRemoteNotificationServer(): HTTP PUT https://apns.example.com/v1/user/1234567890, requestData: ["bundle-id": "com.example.MyApp", "device-token": "fcc37fb74f2506277739c1e343c535f131447327105e23ad2a0ce", "name": "Test User 1"] 152 | MyApp[15355:2946718] apnsManager.shared.updateRemoteNotificationServer(): responseCode: 200 Success 153 | MyApp[15355:2946718] apnsManager.shared.updateRemoteNotificationServer(): responseData: User 1234567890 updated 154 | MyApp[15355:2946298] apnsManager.shared.remoteNotificationServerRegistrationSuccess set: true 155 | MyApp[15355:2946719] apnsManager.shared.checkForAdminFlag(): HTTP GET https://apns.example.com/v1/user/1234567890 156 | MyApp[15355:2946719] apnsManager.shared.checkForAdminFlag(): responseCode: 200 Success 157 | MyApp[15355:2946719] apnsManager.shared.checkForAdminFlag(): responseData: ["name": "Test User 1", "admin": True, "device-tokens": <__NSArrayI 0x2819a7a00>( 158 | cdcadc070ae340a723133db22b9c8a11f05e323c5d60339f45fa9795ec29f130, 159 | fcc37fb74f2506277739c1e343c535f131447327105e23ad2a0ce 160 | ) 161 | , "user-id": 1234567890] 162 | MyApp[15355:2946298] apnsManager.shared.userIsAdmin set: true 163 | MyApp[15355:2946298] appDelegate: didReceiveRemoteNotification: [AnyHashable("aps"): { 164 | "content-available" = 1; 165 | }, AnyHashable("Data"): 75] 166 | MyApp[15355:2946298] apnsManager.shared.handleAPNSContent: Received new data: 75 167 | MyApp[15355:2946298] apnsManager.shared.size set: 75.0 168 | 169 | ``` 170 | -------------------------------------------------------------------------------- /Sources/APNSManager/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // https://github.com/magnolialogic/swiftui-apnsManager 4 | // 5 | // Created by Chris Coffin on 8/5/20. 6 | // 7 | 8 | import os 9 | import SwiftUI 10 | 11 | public class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { 12 | 13 | // willFinishLaunchingWithOptions callback for debugging lifecycle state issues 14 | public func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 15 | return true 16 | } 17 | 18 | // didFinishLaunchingWithOptions callback to claim UNUserNotificationCenterDelegate, request notification permissions, and register with APNS 19 | // Handles push notification via launchOptions if app is not running and user taps on notification 20 | public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 21 | UNUserNotificationCenter.current().delegate = self 22 | apnsManager.shared.checkNotificationAuthorizationStatus() 23 | if apnsManager.shared.notificationPermissionStatus == "Allowed" { 24 | apnsManager.shared.requestNotificationsPermission() 25 | } 26 | return true 27 | } 28 | 29 | // Callback for successful APNS registration 30 | public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 31 | apnsManager.shared.deviceToken = deviceToken.map { String(format: "%02x", $0)}.joined() 32 | apnsManager.shared.apnsRegistrationSuccess = true 33 | } 34 | 35 | // Callback for failed APNS registration 36 | public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { 37 | apnsManager.shared.apnsRegistrationSuccess = false 38 | os_log(.error, "appDelegate: Failed to register with APNS: \(error.localizedDescription)") 39 | } 40 | 41 | // Print current notification settings to debug console 42 | func getNotificationSettings() { 43 | UNUserNotificationCenter.current().getNotificationSettings { settings in 44 | os_log(.debug, "appDelegate: Notification settings: \(settings)") 45 | } 46 | } 47 | 48 | // Callback for handling background notifications 49 | public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 50 | os_log(.debug, "appDelegate: didReceiveRemoteNotification: \(userInfo.debugDescription)") 51 | if let apsPayload = userInfo["aps"] as? [String: AnyObject] { 52 | if apsPayload["content-available"] as? Int == 1 { 53 | // Handle silent notification content 54 | apnsManager.shared.handleAPNSContent(content: userInfo) 55 | } else { 56 | // Got user-facing notification 57 | } 58 | completionHandler(.newData) 59 | } else { 60 | completionHandler(.failed) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/APNSManager/apnsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // apnsManager.swift 3 | // https://github.com/magnolialogic/swiftui-apnsManager 4 | // 5 | // Created by Chris Coffin on 7/24/20. 6 | // 7 | 8 | import Foundation 9 | import os 10 | import SwiftUI 11 | 12 | public class apnsManager: ObservableObject { 13 | 14 | 15 | 16 | // MARK: Initialization 17 | 18 | 19 | 20 | // Private init to prevent clients from creating another instance 21 | private init() {} 22 | 23 | // Create shared singleton 24 | public static let shared: apnsManager = apnsManager() 25 | 26 | // Create shared background DispatchQueue and shared DispatchGroup 27 | let serialQueue = DispatchQueue(label: "apnsManager.shared.staticQueue", qos: .userInteractive, target: .global()) 28 | let dispatchGroup = DispatchGroup() 29 | 30 | // Root URL for python-apns_server API 31 | let apiRoute = Bundle.main.object(forInfoDictionaryKey: "APNS_API_USER") as! String // If this isn't set we should crash, #@$&! it 32 | 33 | // Check for admin flag? 34 | let adminCheck = Bundle.main.object(forInfoDictionaryKey: "ADMIN_CHECK") as? Bool ?? false 35 | 36 | 37 | 38 | // MARK: User info properties 39 | 40 | 41 | 42 | // Push Sign In With Apple user credentials to remote notification server 43 | public var userID = UserDefaults.standard.string(forKey: "userID") ?? "" { 44 | didSet { 45 | os_log(.debug, "apnsManager.shared.userID set: \(self.userID)") 46 | UserDefaults.standard.setValue(userID, forKey: "userID") 47 | } 48 | } 49 | 50 | // Tracks whether user has admin flag set in DB 51 | @Published public var userIsAdmin = false { 52 | didSet { 53 | os_log(.debug, "apnsManager.shared.userIsAdmin set: \(self.userIsAdmin)") 54 | } 55 | } 56 | 57 | // If deviceToken is valid and remoteNotificationServerRegistrationSuccess, update userName on remote notification server if local userName changes 58 | @Published public var userName: String = UserDefaults.standard.string(forKey: "userName") ?? "no name provided" { 59 | didSet { 60 | os_log(.debug, "apnsManager.shared.userName set: \(self.userName)") 61 | UserDefaults.standard.setValue(userName, forKey: "userName") 62 | if self.signInWithAppleSuccess && self.remoteNotificationServerRegistrationSuccess { 63 | os_log(.debug, "apnsManager.shared.userName: updating remote notification server") 64 | updateRemoteNotificationServer() 65 | } 66 | } 67 | } 68 | 69 | // Attempt to update remote notification server when deviceToken is changed 70 | var deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? "" { 71 | didSet { 72 | os_log(.debug, "apnsManager.shared.deviceToken set: \(self.deviceToken)") 73 | UserDefaults.standard.setValue(deviceToken, forKey: "deviceToken") 74 | if self.signInWithAppleSuccess && self.remoteNotificationServerRegistrationSuccess { 75 | os_log(.debug, "apnsManager.shared.deviceToken: updating remote notification server") 76 | updateRemoteNotificationServer() 77 | } 78 | } 79 | } 80 | 81 | 82 | 83 | // MARK: Server interaction tracking 84 | 85 | 86 | 87 | // Tracks whether APNS registration completed successfully 88 | @Published var apnsRegistrationSuccess = false { 89 | didSet { 90 | os_log(.debug, "apnsManager.shared.apnsRegistrationSuccess set: \(self.apnsRegistrationSuccess)") 91 | } 92 | } 93 | 94 | // Tracks whether updateRemoteNotificationServer received HTTP response indicating success 95 | @Published var remoteNotificationServerRegistrationSuccess = UserDefaults.standard.bool(forKey: "remoteNotificationServerRegistrationSuccess") { 96 | didSet { 97 | os_log(.debug, "apnsManager.shared.remoteNotificationServerRegistrationSuccess set: \(self.remoteNotificationServerRegistrationSuccess)") 98 | UserDefaults.standard.setValue(remoteNotificationServerRegistrationSuccess, forKey: "remoteNotificationServerRegistrationSuccess") 99 | if adminCheck { 100 | checkForAdminFlag() 101 | } 102 | } 103 | } 104 | 105 | // Tracks whether we've completed the Sign In With Apple process 106 | @Published public var signInWithAppleSuccess = UserDefaults.standard.bool(forKey: "signInWithAppleSuccess") { 107 | didSet { 108 | os_log(.debug, "apnsManager.shared.signInWithAppleSuccess set: \(self.signInWithAppleSuccess)") 109 | UserDefaults.standard.setValue(signInWithAppleSuccess, forKey: "signInWithAppleSuccess") 110 | } 111 | } 112 | 113 | 114 | 115 | // MARK: Notification permission methods and properties 116 | 117 | 118 | 119 | // Request notification permissions 120 | public func requestNotificationsPermission() { 121 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (allowed, error) in 122 | if allowed { 123 | os_log(.debug, "appDelegate: User granted permissions for notifications, registering with APNS") 124 | DispatchQueue.main.async { 125 | UIApplication.shared.registerForRemoteNotifications() 126 | } 127 | } else if (error != nil) { 128 | os_log(.error, "appDelegate: Error requesting permissions: \(error!.localizedDescription)") 129 | } else { 130 | os_log(.default, "appDelegate: Notifications not allowed!") 131 | DispatchQueue.main.async { 132 | apnsManager.shared.notificationPermissionStatus = "Denied" 133 | } 134 | } 135 | } 136 | } 137 | 138 | // Get current notification authorization status 139 | public func checkNotificationAuthorizationStatus() { 140 | UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in 141 | var status: String 142 | switch settings.authorizationStatus { 143 | case .notDetermined: 144 | status = "NotDetermined" 145 | case .denied: 146 | status = "Denied" 147 | case .authorized, .provisional, .ephemeral: 148 | status = "Allowed" 149 | @unknown default: 150 | fatalError("apnsManager.shared.checkNotificationAuthorizationStatus(): Got unexpected value for getNotificationSettings \(settings.authorizationStatus.rawValue.description)") 151 | } 152 | DispatchQueue.main.async { 153 | self.notificationPermissionStatus = status 154 | } 155 | }) 156 | } 157 | 158 | // Track whether user granted permission for notifications 159 | @Published public var notificationPermissionStatus = "Unknown" { 160 | didSet { 161 | os_log(.debug, "apnsManager.shared.notificationPermissionStatus set: \(self.notificationPermissionStatus)") 162 | if self.notificationPermissionStatus == "Allowed" && !self.apnsRegistrationSuccess { 163 | os_log(.debug, "apnsManager.shared.notificationPermissionStatus: registering with APNS") 164 | UIApplication.shared.registerForRemoteNotifications() 165 | } 166 | } 167 | } 168 | 169 | // Tracks whether user should be gated or can proceed to app's main view 170 | public var proceedToMainView: Bool { 171 | self.remoteNotificationServerRegistrationSuccess && self.signInWithAppleSuccess 172 | } 173 | 174 | 175 | 176 | // MARK: HTTP request methods 177 | 178 | 179 | 180 | // Construct HTTP request to send APNS token to remote notification server 181 | public func updateRemoteNotificationServer() { 182 | // If userName is empty we've signed in with Apple previously, so username should be on remote notification server 183 | if self.userName == "no name provided" { 184 | os_log(.debug, "apnsManager.shared.updateRemoteNotificationServer(): userName not set, fetching from remote notification server") 185 | self.dispatchGroup.enter() 186 | self.getRemoteUserName() { 187 | self.dispatchGroup.leave() 188 | } 189 | } 190 | // Construct request URL + payload 191 | let requestURL = self.apiRoute + self.userID 192 | guard let url = URL(string: requestURL) else { 193 | os_log(.error, "apnsManager.shared.updateRemoteNotificationServer(): Failed to create request URL") 194 | return 195 | } 196 | guard let bundleID = Bundle.main.bundleIdentifier else { 197 | os_log(.error, "apnsManager.shared.updateRemoteNotificationServer(): Failed to read Bundle.main.bundleIdentifier") 198 | return 199 | } 200 | self.dispatchGroup.wait() 201 | let payload: [String : String] = [ 202 | "bundle-id": bundleID, 203 | "device-token": self.deviceToken, 204 | "name": self.userName 205 | ] 206 | 207 | // Construct HTTP request 208 | var request = URLRequest(url: url) 209 | request.httpMethod = "PUT" 210 | request.setValue("application/json", forHTTPHeaderField: "content-type") 211 | request.timeoutInterval = 10 212 | guard let httpBody = try? JSONSerialization.data(withJSONObject: payload, options: []) else { 213 | os_log(.error, "apnsManager.shared.updateRemoteNotificationServer(): httpBody: Failed to serialize payload JSON") 214 | return 215 | } 216 | request.httpBody = httpBody 217 | 218 | // Send HTTP request 219 | let session = URLSession.shared 220 | session.dataTask(with: request) { (data, response, error) in 221 | os_log(.debug, "apnsManager.shared.updateRemoteNotificationServer(): HTTP \(request.httpMethod! as NSObject) \(requestURL), requestData: \(payload)") 222 | // Check whether we like the HTTP response status code and report success or failure 223 | if let response = response as? HTTPURLResponse { 224 | let successResponseCodes = [ 225 | 200: "Success", 226 | 201: "Created", 227 | 409: "AlreadyExists" 228 | ] 229 | os_log(.debug, "apnsManager.shared.updateRemoteNotificationServer(): responseCode: \(response.statusCode) \(successResponseCodes[response.statusCode] as NSObject? ?? "Unknown" as NSObject)") 230 | DispatchQueue.main.async { 231 | self.remoteNotificationServerRegistrationSuccess = successResponseCodes.keys.contains(response.statusCode) 232 | } 233 | } 234 | if let data = data { 235 | do { 236 | let responseData = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) 237 | os_log(.debug, "apnsManager.shared.updateRemoteNotificationServer(): responseData: \(responseData as! NSObject)") 238 | } catch { 239 | os_log(.error, "apnsManager.shared.updateRemoteNotificationServer(): URLSession.dataTask: failed to create request session: \(error as NSObject)") 240 | } 241 | } 242 | if let error = error { 243 | os_log(.error, "apnsManager.shared.updateRemoteNotificationServer(): URLSession.dataTask caught error: \(error as NSObject)") 244 | } 245 | }.resume() 246 | } 247 | 248 | // Fetch admin status for user from remote notification server 249 | public func checkForAdminFlag() { 250 | // Construct request URL + payload 251 | let requestURL = self.apiRoute + self.userID 252 | guard let url = URL(string: requestURL) else { 253 | os_log(.error, "apnsManager.shared.checkForAdminFlag(): Failed to create request URL") 254 | return 255 | } 256 | 257 | // Construct HTTP request 258 | var request = URLRequest(url: url) 259 | request.httpMethod = "GET" 260 | request.setValue("application/json", forHTTPHeaderField: "content-type") 261 | request.timeoutInterval = 10 262 | 263 | // Send HTTP request 264 | let session = URLSession.shared 265 | session.dataTask(with: request) { (data, response, error) in 266 | os_log(.debug, "apnsManager.shared.checkForAdminFlag(): HTTP \(request.httpMethod! as NSObject) \(requestURL)") 267 | // Check whether we like the HTTP response status code and report success or failure 268 | if let response = response as? HTTPURLResponse { 269 | let responseCodes = [ 270 | 200: "Success", 271 | 404: "NotFound" 272 | ] 273 | os_log(.debug, "apnsManager.shared.checkForAdminFlag(): responseCode: \(response.statusCode) \(responseCodes[response.statusCode] as NSObject? ?? "Unknown" as NSObject)") 274 | } 275 | if let data = data { 276 | do { 277 | let responseData = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as! [String : Any] 278 | os_log(.debug, "apnsManager.shared.checkForAdminFlag(): responseData: \(responseData)") 279 | var adminFlag: Bool 280 | if responseData["admin"] as! String == "True" { 281 | adminFlag = true 282 | } else { 283 | adminFlag = false 284 | } 285 | DispatchQueue.main.async { 286 | self.userIsAdmin = adminFlag 287 | } 288 | } catch { 289 | os_log(.error, "apnsManager.shared.checkForAdminFlag(): URLSession.dataTask: failed to create request session: \(error as NSObject)") 290 | } 291 | } 292 | if let error = error { 293 | os_log(.error, "apnsManager.shared.checkForAdminFlag(): URLSession.dataTask caught error: \(error as NSObject)") 294 | } 295 | }.resume() 296 | } 297 | 298 | // Fetch admin status for user from remote notification server 299 | func getRemoteUserName(completionHandler: @escaping () -> Void) { 300 | // Construct request URL + payload 301 | let requestURL = self.apiRoute + self.userID 302 | guard let url = URL(string: requestURL) else { 303 | os_log(.error, "apnsManager.shared.getRemoteUserName():Failed to create request URL") 304 | return 305 | } 306 | 307 | // Construct HTTP request 308 | var request = URLRequest(url: url) 309 | request.httpMethod = "GET" 310 | request.setValue("application/json", forHTTPHeaderField: "content-type") 311 | request.timeoutInterval = 10 312 | 313 | // Send HTTP request 314 | let session = URLSession.shared 315 | session.dataTask(with: request) { (data, response, error) in 316 | os_log(.debug, "apnsManager.shared.getRemoteUserName(): HTTP \(request.httpMethod! as NSObject) \(requestURL)") 317 | // Check whether we like the HTTP response status code and report success or failure 318 | if let response = response as? HTTPURLResponse { 319 | let responseCodes = [ 320 | 200: "Success", 321 | 404: "NotFound" 322 | ] 323 | os_log(.debug, "apnsManager.shared.getRemoteUserName(): responseCode: \(response.statusCode) \(responseCodes[response.statusCode] as NSObject? ?? "Unknown" as NSObject)") 324 | if response.statusCode == 404 { 325 | os_log(.error, "apnsManager.shared.getRemoteUserName(): User not found!") 326 | return 327 | } 328 | } 329 | if let data = data { 330 | do { 331 | let responseData = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as! [String : Any] 332 | os_log(.debug, "apnsManager.shared.getRemoteUserName(): responseData: \(responseData)") 333 | guard let remoteName = responseData["name"] as? String else { 334 | os_log(.error, "apnsManager.shared.getRemoteUserName(): failed to decode response") 335 | return 336 | } 337 | // Revisit this so SwiftUI will stop yelling at me about updating data from background thread 338 | if remoteName != self.userName { 339 | self.serialQueue.async { 340 | self.userName = remoteName 341 | completionHandler() 342 | } 343 | } 344 | } catch { 345 | os_log(.error, "apnsManager.shared.getRemoteUserName(): URLSession.dataTask: failed to create request session: \(error as NSObject)") 346 | } 347 | } 348 | if let error = error { 349 | os_log(.error, "apnsManager.shared.getRemoteUserName(): URLSession.dataTask caught error: \(error as NSObject)") 350 | } 351 | }.resume() 352 | } 353 | 354 | 355 | 356 | // MARK: Data Model methods and properties 357 | 358 | 359 | 360 | func handleAPNSContent(content: [AnyHashable : Any]) { 361 | if let newData = content["Data"] as? CGFloat { 362 | os_log(.info, "apnsManager.shared.handleAPNSContent: Received new data: \(content["Data"] as! NSObject)") 363 | self.size = newData 364 | } else { 365 | os_log(.error, "apnsManager.shared.handleAPNSContent: No Data key in notification dictionary!") 366 | } 367 | } 368 | 369 | @Published public var size: CGFloat = 56.0 { 370 | didSet { 371 | os_log(.debug, "apnsManager.shared.size set: \(self.size)") 372 | } 373 | } 374 | 375 | } 376 | --------------------------------------------------------------------------------