@IBOutlets going nil when simulating Background App Refresh

Hello, I have an application which displays the APOD (Astronomy Photo Of The Day), using NASA's API (this includes displaying the actual picture, description, author and title). I am learning how to implement Background App Refresh using the BackgroundTasks framework and simulating it using Apple's code for simulating Background App Refresh

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]

My Background App Refresh Task, updates the UI from the main View Controller (including the outlets), by fetching this information from the APOD API by NASA. However, when I simulate the Background App Refresh Task, the app crashes with the following message:

2023-03-01 16:43:08.394054-0600 SpacePhoto[9069:858984] SpacePhoto/ViewController.swift:73: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

This message is pointed out in the following code line:

 descriptionLabelOutlet.text = photoInfo.description

From what I have read, when my Background App Refresh Task is executed, the @IBOutlets are deallocated from memory and the View Controller references for such outlets are not being read, thus leading to the application crash.

How can I fix this? Is there a way to update the UI and my @IBOutlets' information using Background App Refresh?

Thank you.

Answered by diegosiep in 746548022

Btw, here is more code to clarify the context under which I am trying to implement Background App Refresh;

Here is the code from my View Controller:

import UIKit

@MainActor
class ViewController: UIViewController, UIImagePickerControllerDelegate {
    
    static var shared = ViewController()
    
    var spacePhotoInfoTask: Task<Void, Never>? = nil
    deinit { spacePhotoInfoTask?.cancel()}
    
    // Access network request
    let photoInfoController = PhotoInfoController()
    
   ...
    
    @IBOutlet var descriptionLabelOutlet: UILabel!

    ...


// Send network request and assign the resulting String to the descriptionLabelOutlet       
        func updateInterface() {
            spacePhotoInfoTask?.cancel()
            spacePhotoInfoTask = Task {
                do {
                    let photoInfo = try await photoInfoController.fetchPhotoInfo()
                    self.descriptionLabelOutlet.text = photoInfo.description
                
            } catch {
                print("Could not update extra Label with \(error)")
            }
        }
    }
    
   ...
}

Here is the code corresponding to the AppDelegate:

import UIKit
import BackgroundTasks

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "Space", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        
        return true
    }
    
    func applicationDidEnterBackground(_ application: UIApplication) {
        scheduleAppRefresh()
        print("Scheduled")
        
    }
    
    
    func scheduleAppRefresh() {
        
        let request = BGAppRefreshTaskRequest(identifier: "Space")
        
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        
        do {
            try BGTaskScheduler.shared.submit(request)
            
        } catch {
            print("Could not schedule app refresh: \(error)")
        }
        
    }
    
    
    func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleAppRefresh()
        
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1

// The performApplicationUpdates method, performs the UI update (method declaration included below)

            let operations = Operations.performApplicationUpdates()
            let lastOpertation = operations.last!
        
        
        task.expirationHandler = {
            queue.cancelAllOperations()
        }
        
        lastOpertation.completionBlock = {
            task.setTaskCompleted(success: !lastOpertation.isCancelled)
        }
        
        queue.addOperations(operations, waitUntilFinished: false)
        
        print("Handled App Refresh")
        
    }
    
}

And finally, here is the declaration for the performApplicationUpdates method, which calls the updateInterface() method from the main View Controller:

import Foundation
import UIKit



@available(iOS 16.0, *)
struct Operations {
    static func performApplicationUpdates() -> [Operation] {
        let refreshHomeViewController = RefreshViewControllerInterface()
        return [refreshHomeViewController]
        
    }
}


@available(iOS 16.0, *)
class RefreshViewControllerInterface: Operation {
    
    override func main() {
        DispatchQueue.main.async {
            ViewController.shared.updateInterface()
        }
    }
}
Accepted Answer

Btw, here is more code to clarify the context under which I am trying to implement Background App Refresh;

Here is the code from my View Controller:

import UIKit

@MainActor
class ViewController: UIViewController, UIImagePickerControllerDelegate {
    
    static var shared = ViewController()
    
    var spacePhotoInfoTask: Task<Void, Never>? = nil
    deinit { spacePhotoInfoTask?.cancel()}
    
    // Access network request
    let photoInfoController = PhotoInfoController()
    
   ...
    
    @IBOutlet var descriptionLabelOutlet: UILabel!

    ...


// Send network request and assign the resulting String to the descriptionLabelOutlet       
        func updateInterface() {
            spacePhotoInfoTask?.cancel()
            spacePhotoInfoTask = Task {
                do {
                    let photoInfo = try await photoInfoController.fetchPhotoInfo()
                    self.descriptionLabelOutlet.text = photoInfo.description
                
            } catch {
                print("Could not update extra Label with \(error)")
            }
        }
    }
    
   ...
}

Here is the code corresponding to the AppDelegate:

import UIKit
import BackgroundTasks

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "Space", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        
        return true
    }
    
    func applicationDidEnterBackground(_ application: UIApplication) {
        scheduleAppRefresh()
        print("Scheduled")
        
    }
    
    
    func scheduleAppRefresh() {
        
        let request = BGAppRefreshTaskRequest(identifier: "Space")
        
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        
        do {
            try BGTaskScheduler.shared.submit(request)
            
        } catch {
            print("Could not schedule app refresh: \(error)")
        }
        
    }
    
    
    func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleAppRefresh()
        
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1

// The performApplicationUpdates method, performs the UI update (method declaration included below)

            let operations = Operations.performApplicationUpdates()
            let lastOpertation = operations.last!
        
        
        task.expirationHandler = {
            queue.cancelAllOperations()
        }
        
        lastOpertation.completionBlock = {
            task.setTaskCompleted(success: !lastOpertation.isCancelled)
        }
        
        queue.addOperations(operations, waitUntilFinished: false)
        
        print("Handled App Refresh")
        
    }
    
}

And finally, here is the declaration for the performApplicationUpdates method, which calls the updateInterface() method from the main View Controller:

import Foundation
import UIKit



@available(iOS 16.0, *)
struct Operations {
    static func performApplicationUpdates() -> [Operation] {
        let refreshHomeViewController = RefreshViewControllerInterface()
        return [refreshHomeViewController]
        
    }
}


@available(iOS 16.0, *)
class RefreshViewControllerInterface: Operation {
    
    override func main() {
        DispatchQueue.main.async {
            ViewController.shared.updateInterface()
        }
    }
}
&#64;IBOutlets going nil when simulating Background App Refresh
 
 
Q