How to detect if Local Network permissions are granted in iOS 14 - swift5

How to detect if the user has granted the local network permission in the app for iOS 14? I have to show an error screen if the user has denied permission and redirects to os settings to grant permission.
Has Apple provided any way to find out just like location permission?

I wrote up this class that can be used if you're not on iOS 14.2.
This class will prompt user for permission to access local network (first time).
Verify existing permission state if already denied/granted.
Just remember this instance has to be kept alive so if you are using this in a function call within another class you need to keep the instance alive outside of the scope of the calling function.
import UIKit
import Network
class LocalNetworkPermissionChecker {
private var host: String
private var port: UInt16
private var checkPermissionStatus: DispatchWorkItem?
private lazy var detectDeclineTimer: Timer? = Timer.scheduledTimer(
withTimeInterval: .zero,
repeats: false,
block: { [weak self] _ in
guard let checkPermissionStatus = self?.checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now(), execute: checkPermissionStatus)
})
init(host: String, port: UInt16, granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
self.host = host
self.port = port
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInBackground),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInForeground),
name: UIApplication.didBecomeActiveNotification,
object: nil)
actionRequestNetworkPermissions(granted: granted, failure: failure)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// Creating a network connection prompts the user for permission to access the local network. We do not have the need to actually send anything over the connection.
/// - Note: The user will only be prompted once for permission to access the local network. The first time they do this the app will be placed in the background while
/// the user is being prompted. We check for this to occur. If it does we invalidate our timer and allow the user to make a selection. When the app returns to the foreground
/// verify what they selected. If this is not the first time they are on this screen, the timer will not be invalidated and we will check the dispatchWorkItem block to see what
/// their selection was previously.
/// - Parameters:
/// - granted: Informs application that user has provided us with local network permission.
/// - failure: Something went awry.
private func actionRequestNetworkPermissions(granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
guard let port = NWEndpoint.Port(rawValue: port) else { return }
let connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: .udp)
connection.start(queue: .main)
checkPermissionStatus = DispatchWorkItem(block: { [weak self] in
if connection.state == .ready {
self?.detectDeclineTimer?.invalidate()
granted()
} else {
failure(nil)
}
})
detectDeclineTimer?.fireDate = Date() + 1
}
/// Permission prompt will throw the application in to the background and invalidate the timer.
#objc private func applicationIsInBackground() {
detectDeclineTimer?.invalidate()
}
/// - Important: DispatchWorkItem must be called after 1sec otherwise we are calling before the user state is updated.
#objc private func applicationIsInForeground() {
guard let checkPermissionStatus = checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: checkPermissionStatus)
}
}
//Can be used like this:
LocalNetworkPermissionChecker(host: "255.255.255.255", port: 4567, granted: {
//Perform some action here...
},
failure: { error in
if let error = error {
print("Failed with error: \(error.localizedDescription)")
}
})

Related

How to do Apple Watch Background App Refresh?

I've consulted many variations of background app refresh for Apple Watch so that I can update the complications for my app. However, the process seems very much hit or miss and most of the time it doesn't run at all after some time.
Here is the code I currently have:
BackgroundService.swift
Responsibility: Schedule background refresh and handle download processing and update complications.
import Foundation
import WatchKit
final class BackgroundService: NSObject, URLSessionDownloadDelegate {
var isStarted = false
private let requestFactory: RequestFactory
private let logManager: LogManager
private let complicationService: ComplicationService
private let notificationService: NotificationService
private var pendingBackgroundTask: WKURLSessionRefreshBackgroundTask?
private var backgroundSession: URLSession?
init(requestFactory: RequestFactory,
logManager: LogManager,
complicationService: ComplicationService,
notificationService: NotificationService
) {
self.requestFactory = requestFactory
self.logManager = logManager
self.complicationService = complicationService
self.notificationService = notificationService
super.init()
NotificationCenter.default.addObserver(self,
selector: #selector(handleInitialSchedule(_:)),
name: Notification.Name("ScheduleBackgroundTasks"),
object: nil
)
}
func updateContent() {
self.logManager.debugMessage("In BackgroundService updateContent")
let complicationsUpdateRequest = self.requestFactory.makeComplicationsUpdateRequest()
let config = URLSessionConfiguration.background(withIdentifier: "app.wakawatch.background-refresh")
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
self.backgroundSession = URLSession(configuration: config,
delegate: self,
delegateQueue: nil)
let backgroundTask = self.backgroundSession?.downloadTask(with: complicationsUpdateRequest)
backgroundTask?.resume()
self.isStarted = true
self.logManager.debugMessage("backgroundTask started")
}
func handleDownload(_ backgroundTask: WKURLSessionRefreshBackgroundTask) {
self.logManager.debugMessage("Handling finished download")
self.pendingBackgroundTask = backgroundTask
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
processFile(file: location)
self.logManager.debugMessage("Marking pending background tasks as completed.")
if self.pendingBackgroundTask != nil {
self.pendingBackgroundTask?.setTaskCompletedWithSnapshot(false)
self.backgroundSession?.invalidateAndCancel()
self.pendingBackgroundTask = nil
self.backgroundSession = nil
self.logManager.debugMessage("Pending background task cleared")
}
self.schedule()
}
func processFile(file: URL) {
guard let data = try? Data(contentsOf: file) else {
self.logManager.errorMessage("file could not be read as data")
return
}
guard let backgroundUpdateResponse = try? JSONDecoder().decode(BackgroundUpdateResponse.self, from: data) else {
self.logManager.errorMessage("Unable to decode response to Swift object")
return
}
let defaults = UserDefaults.standard
defaults.set(backgroundUpdateResponse.totalTimeCodedInSeconds,
forKey: DefaultsKeys.complicationCurrentTimeCoded)
self.complicationService.updateTimelines()
self.notificationService.isPermissionGranted(onGrantedHandler: {
self.notificationService.notifyGoalsAchieved(newGoals: backgroundUpdateResponse.goals)
})
self.logManager.debugMessage("Complication updated")
}
func schedule() {
let time = self.isStarted ? 15 * 60 : 60
let nextInterval = TimeInterval(time)
let preferredDate = Date.now.addingTimeInterval(nextInterval)
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: preferredDate,
userInfo: nil) { error in
if error != nil {
self.logManager.reportError(error!)
return
}
self.logManager.debugMessage("Scheduled for \(preferredDate)")
}
}
#objc func handleInitialSchedule(_ notification: NSNotification) {
if !self.isStarted {
self.schedule()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
The flow for the above file's usage is that it will be used by the ExtensionDelegate to schedule background refresh. The first time, it'll schedule a refresh for 1 minute out and then every 15 minutes after that.
Here is the ExtensionDelegate:
import Foundation
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
private var backgroundService: BackgroundService?
private var logManager: LogManager?
override init() {
super.init()
self.backgroundService = DependencyInjection.shared.container.resolve(BackgroundService.self)!
self.logManager = DependencyInjection.shared.container.resolve(LogManager.self)!
}
func isAuthorized() -> Bool {
let defaults = UserDefaults.standard
return defaults.bool(forKey: DefaultsKeys.authorized)
}
func applicationDidFinishLaunching() {
self.logManager?.debugMessage("In applicationDidFinishLaunching")
if isAuthorized() && !(self.backgroundService?.isStarted ?? false) {
self.backgroundService?.schedule()
}
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
self.logManager?.debugMessage("In handle backgroundTasks")
if !isAuthorized() {
return
}
for task in backgroundTasks {
self.logManager?.debugMessage("Processing task: \(task.debugDescription)")
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
self.backgroundService?.updateContent()
backgroundTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
self.backgroundService?.handleDownload(urlSessionTask)
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
When the app is launched from background, it'll try to schedule for the first time in applicationDidFinishLaunching.
From my understanding, WKExtension.shared().scheduleBackgroundRefresh will get called in schedule, then after the preferred time WatchOS will call handle with WKApplicationRefreshBackgroundTask task. Then I will use that to schedule a background URL session task and immediately start it as seen in the updateContent method of BackgroundService. After some time, WatchOS will then call ExtensionDelegate's handle method with WKURLSessionRefreshBackgroundTask and I'll handle that using the handleDownload task. In there, I process the response, update the complications, clear the task, and finally schedule a new background refresh.
I've found it works great if I'm actively working on the app or interacting with it in general. But let's say I go to sleep then the next day the complication will not have updated at all.
Ideally, I'd like for it to function as well as the Weather app complication WatchOS has. I don't interact with the complication, but it reliably updates.
Is the above process correct or are there any samples of correct implementations?
Some of the posts I've consulted:
https://wjwickham.com/posts/refreshing-data-in-the-background-on-watchOS/
https://developer.apple.com/documentation/watchkit/background_execution/using_background_tasks
https://spin.atomicobject.com/2021/01/26/complications-basic-functionality/

AWS Cognito API `AWSMobileClient.default().addUserStateListener` only fires when user authentication state changes

I am using xcode 11 with swift 4. I am following the docs for aws cognito here https://aws-amplify.github.io/docs/ios/authentication and in AppDelegate.swift, I have:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// initialize AWS amplify
let apiPlugin = AWSAPIPlugin(modelRegistration: AmplifyModels())
do {
try Amplify.add(plugin: apiPlugin)
try Amplify.configure()
} catch {
// Navigate to page showing bad network connection
print("Failed to configure Amplify \(error)")
}
AWSMobileClient.default().addUserStateListener(self) { (userState, info) in
switch (userState) {
case .signedOut:
// user clicked signout button and signedout
print("AppDelegate.application state: user signed out")
case .signedIn:
print("AppDelegate.application state: user signed in: \(userState)")
case .signedOutUserPoolsTokenInvalid:
print("need to login again.")
default:
print("unsupported")
}
}
return true
}
The AWSMobileClient.default().addUserStateListener only fires when the user is logging in or signing up for the first time. if the user is already authenticated. then addUserStateListener does not fire. This is an issue because I need to get the user's account token to access his/her information. Am I using the wrong Cognito API? Or is addUserStateListener used in the wrong function.
If I use the other Cognito API
AWSMobileClient.default().initialize { (userState, error) ... }
It fires each time, but I cannot access the user token from this api.
You can access the user token by using the AWSAppSync api and this method in you app delegate either by extending appDelegate or using your own custom class as I did.
class CognitoPoolProvider : AWSCognitoUserPoolsAuthProviderAsync {
func getLatestAuthToken(_ callback: #escaping (String?, Error?) -> Void) {
AWSMobileClient.default().getTokens { (token, error) in
if let error = error {
callback(nil,error)
}
callback(token?.accessToken?.tokenString, error)
}
}
}
So, in my appDelegate I did something like this
var appSyncClientBridge : AWSAppSyncClient?
/// Sets the configuration settings for application's AWSAppSync Client.
func appSyncSetup()throws -> AWSAppSyncClientConfiguration{
let cache = try AWSAppSyncCacheConfiguration()
let serviceConfig = try AWSAppSyncServiceConfig()
let appSyncConfiguration = try AWSAppSyncClientConfiguration(appSyncServiceConfig: serviceConfig,
userPoolsAuthProvider: CognitoPoolProvider(),
cacheConfiguration: cache)
return appSyncConfiguration
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
let configFile = try appSyncSetup()
appSyncClientBridge = try AWSAppSyncClient(appSyncConfig: configFile)
}
catch(let error){
print(error.localizedDescription)
}
return true
}
UPDATE:
If the user is already logged in. You can create a method like this one.
func getTokens(){
let getPool = CognitoPoolProvider()
func getToken(completion: #escaping (Result<String,Error>)->Void){
getPool.getLatestAuthToken { (token, error) in
if let error = error {
completion(.failure(error))
}
if let token = token {
completion(.success(token))
}
}
}
getToken { (result) in
print(("This is the token — \(String(describing: try? result.get() as String))"))
}
}
If you place this in your viewDidLoad you'll be able to access the user token. In this example it's just printed out, but I think from here you'll be able to use it for what you need.

AWS Mobile Hub states user is not signed-in after custom UI User Pool sign-in/authentication

I'm currently using AWS Mobile Hub for an iOS application that utilizes Cognito and Cloud Logic.
I decided to replace the default AuthUIViewController because I didn't like how it looked. I used this sample project to help me implement sign up through User Pools: https://github.com/awslabs/aws-sdk-ios-samples/tree/master/CognitoYourUserPools-Sample/Swift .
Here is my implementation:
Starting in my AppDelegate, I set the UserPool I want to sign into to a commonly accessible constant variable. One idea I have to why AWSMobileClient doesn't think my user is signed in is because it defines its own service configuration/pool, but I'm not sure:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
AWSDDLog.sharedInstance.logLevel = .verbose
// setup service configuration
let serviceConfiguration = AWSServiceConfiguration(region: Constants.AWS.CognitoIdentityUserPoolRegion, credentialsProvider: nil)
// create pool configuration
let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: Constants.AWS.CognitoIdentityUserPoolAppClientId,
clientSecret: Constants.AWS.CognitoIdentityUserPoolAppClientSecret,
poolId: Constants.AWS.CognitoIdentityUserPoolId)
// initialize user pool client
AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: poolConfiguration, forKey: AWSCognitoUserPoolsSignInProviderKey)
// fetch the user pool client we initialized in above step
Constants.AWS.pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
return AWSMobileClient.sharedInstance().interceptApplication(
application, didFinishLaunchingWithOptions:
launchOptions)
}
After the AppDelegate is finished, the application goes to its root view controller named InitialViewController. Here, I allow the user to click a facebook sign in or regular (user pool) sign in.
class InitialViewController:UIViewController {
#objc func regLogin() {
//Set a shared constants variable "user" to the current user
if (Constants.AWS.user == nil) {
Constants.AWS.user = Constants.AWS.pool?.currentUser()
}
Constants.AWS.pool?.delegate = self
//This function calls the delegate function startPasswordAuthentication() in the extension below to initiate login
Constants.AWS.user?.getDetails().continueOnSuccessWith { (task) -> AnyObject? in
DispatchQueue.main.async(execute: {
//called after details for user are successfully retrieved after login
print(AWSSignInManager.sharedInstance().isLoggedIn)// false
print(AWSSignInManager.init().isLoggedIn)// false
print(AWSCognitoUserPoolsSignInProvider.init().isLoggedIn())// false
print(Constants.AWS.user?.isSignedIn) // true
AppDelegate.del().signIn()
})
return nil
}
}
}
extension InitialViewController: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
self.present(loginVC, animated: true, completion: nil)
return self.loginVC
}
}
As you can see, the functions do their job and the user is successfully logged in according to (Constants.AWS.user?.isSignedIn) as well as the fact that I am successfully able to retrieve the user details. However, when I ask AWSSignInManager or the UserPoolsSignInProvider whether my user is logged in, it returns false. This is a problem because without AWSMobileHub seeing my user as logged in, I cannot access my Cloud Logic functions etc.
Can someone please help me shed light on how I can notify MobileHub and the sign in manager that my user is logged into the user pool so that my application can work right?
Thank You!
Jonathan's answer is a good starting point for this, but it requires to include AWSAuthUI to your project.
A better solution is to implement directly the functions in AWSUserPoolsUIOperations.m. In particular, the function triggered when pressing the sign in button, should look like this:
#IBAction func signInPressed(_ sender: AnyObject) {
if (self.usernameTextField.text != nil && self.passwordTextField.text != nil) {
self.userName = self.usernameTextField.text!
self.password = self.passwordTextField.text!
AWSCognitoUserPoolsSignInProvider.sharedInstance().setInteractiveAuthDelegate(self)
AWSSignInManager.sharedInstance().login(
signInProviderKey: AWSCognitoUserPoolsSignInProvider.sharedInstance().identityProviderName,
completionHandler: { (provider: Any?, error: Error?) in
print(AWSSignInManager.sharedInstance().isLoggedIn)
})
} else {
let alertController = UIAlertController(title: "Missing information",
message: "Please enter a valid user name and password",
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
}
}
Then include the following functions as an extension of your SignIn view controller:
public func handleUserPoolSignInFlowStart() {
let authDetails = AWSCognitoIdentityPasswordAuthenticationDetails(username: self.userName!, password: self.password!)
self.passwordAuthenticationCompletion?.set(result: authDetails)
}
public func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
return self
}
public func getDetails(_ authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>) {
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
}
public func didCompleteStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as NSError? {
let alertController = UIAlertController(title: error.userInfo["__type"] as? String,
message: error.userInfo["message"] as? String,
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
} else {
self.usernameTextField.text = nil
self.dismiss(animated: true, completion: nil)
}
}
}
After digging more through the AWS code, I found my answer in the pod AWSAuthUI in AWSSignInViewController.m (The view controller used in the default authentication process for mobile hub/cognito).
The code is:
- (void)handleUserPoolSignIn {
Class awsUserPoolsUIOperations = NSClassFromString(USERPOOLS_UI_OPERATIONS);
AWSUserPoolsUIOperations *userPoolsOperations = [[awsUserPoolsUIOperations alloc] initWithAuthUIConfiguration:self.config];
[userPoolsOperations loginWithUserName:[self.tableDelegate getValueForCell:self.userNameRow forTableView:self.tableView]
password:[self.tableDelegate getValueForCell:self.passwordRow forTableView:self.tableView]
navigationController:self.navigationController
completionHandler:self.completionHandler];
}
and getting to just the parts that matter... in Swift!!
userPoolsOperations.login(withUserName: "foo", password: "bar", navigationController: self.navigationController!, completionHandler: { (provider: Any?, error: Error?) in
print(AWSSignInManager.sharedInstance().isLoggedIn) // true
print(AWSSignInManager.init().isLoggedIn) // false
print(AWSCognitoUserPoolsSignInProvider.init().isLoggedIn()) // false
print(Constants.AWS.user?.isSignedIn) // nil
})
}
lesson learned: Reading through AWS's code is helpful even though it sucks

0Auth2 Stuck on Permission Page

Currently having an issue with 0auth2 permissions. It keeps on getting stuck to this page: Stuck101. It doesn't crash, just remains completely dormant.
The debug console shows this:
[Debug] OAuth2: Initialization finished
[Debug] OAuth2: Looking for items in keychain
[Debug] OAuth2: Starting authorization
[Debug] OAuth2: No access token, checking if a refresh token is available
[Debug] OAuth2: Error refreshing token: I don't have a refresh token, not
trying to refresh
[Debug] OAuth2: Opening authorize URL embedded: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?state=7A3CEE60&response_type=code&scope=openid+profile+offline_access+User.Read+Mail.Read+Calendars.Read&redirect_uri=KTracker2%3A%2F%2Foauth2%2Fcallback&client_id=584bb6db-5b71-4d7c-9015-fab8a9dfae4c
2018-05-09 04:49:47.198828+0100 KTracker2[8281:180974] [App] if we're in the real pre-commit handler we can't actually add any new fences due to CA restriction
My App delegate file houses this code:
// AppDelegate.swift
// KTracker2
//
// Created by CaudyMac on 07/05/2018.
// Copyright © 2018 Kallum Caudwell. All rights reserved.
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
if url.scheme == "KTracker2" {
let service = OutlookService.shared()
service.handleOAuthCallback(url: url)
return true
}
else {
return false
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
Service Code is here:
// OutlookService.swift
// KTracker2
// Created by CaudyMac on 09/05/2018.
// Copyright © 2018 Kallum Caudwell. All rights reserved.
import Foundation
import p2_OAuth2
import SwiftyJSON
class OutlookService {
// Configure the OAuth2 framework for Azure
private static let oauth2Settings = [
"client_id" : "584bb6db-5b71-4d7c-9015-fab8a9dfae4c",
"authorize_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"token_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"scope": "openid profile offline_access User.Read Mail.Read Calendars.Read",
"redirect_uris": ["KTracker2://oauth2/callback"],
"verbose": true,
] as OAuth2JSON
private static var sharedService: OutlookService = {
let service = OutlookService()
return service
}()
private let oauth2: OAuth2CodeGrant
private init() {
oauth2 = OAuth2CodeGrant(settings: OutlookService.oauth2Settings)
oauth2.authConfig.authorizeEmbedded = true
//oauth2.authConfig.ui.useSafariView = false
userEmail = ""
}
class func shared() -> OutlookService {
return sharedService
}
var isLoggedIn: Bool {
get {
return oauth2.hasUnexpiredAccessToken() || oauth2.refreshToken != nil
}
}
func handleOAuthCallback(url: URL) -> Void {
oauth2.handleRedirectURL(url)
}
func login(from: AnyObject, callback: #escaping (String?) -> Void) -> Void {
oauth2.authorizeEmbedded(from: from) {
result, error in
if let unwrappedError = error {
callback(unwrappedError.description)
} else {
if let unwrappedResult = result, let token = unwrappedResult["access_token"] as? String {
// Print the access token to debug log
NSLog("Access token: \(token)")
callback(nil)
}
}
}
}
func logout() -> Void {
oauth2.forgetTokens()
}
func makeApiCall(api: String, params: [String: String]? = nil, callback: #escaping (JSON?) -> Void) -> Void {
// Build the request URL
var urlBuilder = URLComponents(string: "https://graph.microsoft.com")!
urlBuilder.path = api
if let unwrappedParams = params {
// Add query parameters to URL
urlBuilder.queryItems = [URLQueryItem]()
for (paramName, paramValue) in unwrappedParams {
urlBuilder.queryItems?.append(
URLQueryItem(name: paramName, value: paramValue))
}
}
let apiUrl = urlBuilder.url!
NSLog("Making request to \(apiUrl)")
var req = oauth2.request(forURL: apiUrl)
req.addValue("application/json", forHTTPHeaderField: "Accept")
let loader = OAuth2DataLoader(oauth2: oauth2)
// Uncomment this line to get verbose request/response info in
// Xcode output window
//loader.logger = OAuth2DebugLogger(.trace)
loader.perform(request: req) {
response in
do {
let dict = try response.responseJSON()
DispatchQueue.main.async {
let result = JSON(dict)
callback(result)
}
}
catch let error {
DispatchQueue.main.async {
let result = JSON(error)
callback(result)
}
}
}
}
private var userEmail: String
func getUserEmail(callback: #escaping (String?) -> Void) -> Void {
// If we don't have the user's email, get it from
// the API
if (userEmail.isEmpty) {
makeApiCall(api: "/v1.0/me") {
result in
if let unwrappedResult = result {
let email = unwrappedResult["mail"].stringValue
self.userEmail = email
callback(email)
} else {
callback(nil)
}
}
} else {
callback(userEmail)
}
}
func getInboxMessages(callback: #escaping (JSON?) -> Void) -> Void {
let apiParams = [
"$select": "subject,receivedDateTime,from",
"$orderby": "receivedDateTime DESC",
"$top": "10"
]
makeApiCall(api: "/v1.0/me/mailfolders/inbox/messages", params: apiParams) {
result in
callback(result)
}
}
func getEvents(callback: #escaping (JSON?) -> Void) -> Void {
let apiParams = [
"$select": "subject,start,end",
"$orderby": "start/dateTime ASC",
"$top": "10"
]
makeApiCall(api: "/v1.0/me/events", params: apiParams) {
result in
callback(result)
}
}
func getContacts(callback: #escaping (JSON?) -> Void) -> Void {
let apiParams = [
"$select": "givenName,surname,emailAddresses",
"$orderby": "givenName ASC",
"$top": "10"
]
makeApiCall(api: "/v1.0/me/contacts", params: apiParams) {
result in
callback(result)
}
}
}
I also have KTracker2 added in the plist files
Any ideas to why my app lies dormant would be great. Thanks a bunch.

URLSession downloadTask behavior when running in the background?

I have an app that needs to download a file which may be rather large (perhaps as large as 20 MB). I've been reading up on URLSession downloadTasks and how they work when the app goes to the background or is terminated by iOS. I'd like for the download to continue and from what I've read, that's possible. I found a blog post here that discusses this topic in some detail.
Based on what I've read, I first created a download manager class that looks like this:
class DownloadManager : NSObject, URLSessionDownloadDelegate, URLSessionTaskDelegate {
static var shared = DownloadManager()
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let sessionId = session.configuration.identifier {
log.info("Download task finished for session ID: \(sessionId), task ID: \(downloadTask.taskIdentifier); file was downloaded to \(location)")
do {
// just for testing purposes
try FileManager.default.removeItem(at: location)
print("Deleted downloaded file from \(location)")
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
I also add this method in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
DownloadManager.shared.backgroundSessionCompletionHandler = completionHandler
// if the app gets terminated, I need to reconstruct the URLSessionConfiguration and the URLSession in order to "re-connect" to the previous URLSession instance and process the completed download tasks
// for now, I'm just putting the app in the background (not terminating it) so I've commented out the lines below
//let config = URLSessionConfiguration.background(withIdentifier: identifier)
//let session = URLSession(configuration: config, delegate: DownloadManager.shared, delegateQueue: OperationQueue.main)
// since my app hasn't been terminated, my existing URLSession should still be around and doesn't need to be re-created
let session = DownloadManager.shared.session
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// downloadTasks = [URLSessionDownloadTask]
print("There are \(downloadTasks.count) download tasks associated with this session.")
for downloadTask in downloadTasks {
print("downloadTask.taskIdentifier = \(downloadTask.taskIdentifier)")
}
}
}
Finally, I start my test download like this:
let session = DownloadManager.shared.session
// this is a 100MB PDF file that I'm using for testing
let testUrl = URL(string: "https://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf")!
let task = session.downloadTask(with: testUrl)
// I think I'll ultimately need to persist the session ID, task ID and a file path for use in the delegate methods once the download has completed
task.resume()
When I run this code and start my download, I see the delegate methods being called but I also see a message that says:
A background URLSession with identifier com.example.testapp.background already exists!
I think this is happening because of the following call in application:handleEventsForBackgroundURLSession:completionHandler:
let session = DownloadManager.shared.session
The getter for the session property in my DownloadManager class (which I took directly from the blog post cited previously) is always trying to create a new URLSession using the background configuration. As I understand it, if my app had been terminated, then this would be the appropriate behavior to "reconnect" to the original URLSession. But since may app is not being terminated but rather just going to the background, when the call to application:handleEventsForBackgroundURLSession:completionHandler: happens, I should be referencing the existing instance of URLSession. At least I think that's what the problem is. Can anyone clarify this behavior for me? Thanks!
Your problem is that you are creating a new session every time you reference the session variable:
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
Instead, keep the session as an instance variable, and just get it:
class DownloadManager:NSObject {
static var shared = DownloadManager()
var delegate = DownloadManagerSessionDelegate()
var session:URLSession
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
override init() {
session = URLSession(configuration: config, delegate: delegate, delegateQueue: OperationQueue())
super.init()
}
}
class DownloadManagerSessionDelegate: NSObject, URLSessionDelegate {
// implement here
}
When I do this in a playground, it shows that repeated calls give the same session, and no error:
The session doesn't live in-process, it's part of the OS. You're incrementing reference count every time you access your session variable as written, which causes the error.