0
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    hideActivity()
    let products = response.products
    if products.count > 0{
        let controller = HAPurchaseViewController.instantiate() // crashes here
        controller.categories = dataManager.paidCategories()
        controller.products = products
        self.navigationController?.pushViewController(controller, animated: true)
    }
    else{
        DispatchQueue.main.async { [weak self] in
            let alertController = UIAlertController(title: "Oops!", message: "Unable to load or products not found, please try again later", preferredStyle: .alert)
            let okAction = UIAlertAction(title: "Ok", style: .default)
            alertController.addAction(okAction)
            self?.present(alertController, animated: true)
        }
    }
}

This extension works everywhere else except when I am using it to load my In App Purchase View Controller.

import Foundation
import UIKit

protocol Storyboarded {
    static func instantiate() -> Self
}


extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        let id = String(describing: self)
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        if #available(iOS 13.0, *) {
            return storyboard.instantiateViewController(identifier: id) as! Self // App crash here when loading storyboard
        } else {
            return storyboard.instantiateViewController(withIdentifier: id) as! Self
        }
    }
    static func value() -> Self {
        return UIViewController() as! Self
    }
}


================================================================= 
Main Thread Checker: UI API called on a background thread: -[UIViewController initWithCoder:] > PID: 13525, TID: 4347723, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 0
Backtrace:

After the advice from SO, I updated the code

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        DispatchQueue.main.async {
            let id = String(describing: self)
            let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
            if #available(iOS 13.0, *) {
                return storyboard.instantiateViewController(identifier: id) as! Self
            } else {
                return storyboard.instantiateViewController(withIdentifier: id) as! Self
            }
        }
    }
    static func value() -> Self {
        return UIViewController() as! Self
    }
}

enter image description here

software is fun
  • 7,286
  • 18
  • 71
  • 129
  • 3
    `UI API called on a background thread`, that's quite explicit. Where is called exactly `let controller = HAPurchaseViewController.instantiate()` Put it in a `DispatchQueue.main.async {}`. – Larme Jan 25 '21 at 21:01
  • 1
    Does this answer your question? [Main Thread Checker: UI API called on a background thread: -\[UIApplication applicationState\]](https://stackoverflow.com/questions/44767778/main-thread-checker-ui-api-called-on-a-background-thread-uiapplication-appli) – Larme Jan 25 '21 at 21:02
  • Is there a storyboard with that identifier or is that class programmatically defined? If so, you definitely should be having issues. – impression7vx Jan 25 '21 at 21:02
  • 1
    The problem is that you are calling `Storyboarded.instantiate()` incorrectly — namely, from a background thread. But you did not show that code, so it is impossible to say more. Meanwhile, as you've been told, the error is totally self-explanatory and no question should arise. – matt Jan 25 '21 at 21:04
  • @matt Thanks for the advice. How can I tell if it's a background thread? when am I supposed to call it? I added the offending code inside DispatchQueue.main.async but then I started getting new error messages and it was complaining about 'Consecutive declarations on a line must be separated by ';'' – software is fun Jan 26 '21 at 00:16
  • I tried that too. DispatchQueue.main.async returns -> Void. My function is expecting a return of ->Self not sure what to do here.||| Error in Xcode "Cannot convert return expression of type '()' to return type 'Self'" – software is fun Jan 26 '21 at 00:34
  • 1
    @matt updated the question with the code and posted new image of error – software is fun Jan 26 '21 at 00:36
  • I think the real question you need to think about is why `instantiate` is being called on a background thread in the first place — as I said earlier. You _still_ have not shown that code. You did show the line `let controller = HAPurchaseViewController.instantiate()` but not in any kind of context. That context, wherever it is, is where we need to make sure are on the main thread. – matt Jan 26 '21 at 00:38
  • I hope I have enough code in the question to provide Bette context. I don't mind posting the entire class but I also don't want to go overboard – software is fun Jan 26 '21 at 00:47

1 Answers1

1

The problem is here:

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    // ... this is a background thread!
}

So you cannot do anything at this point that involves the interface, without getting on the main thread. You are in fact already doing that in the second part:

hideActivity()
let products = response.products
if products.count > 0{
    let controller = HAPurchaseViewController.instantiate() 
    controller.categories = dataManager.paidCategories()
    controller.products = products
    self.navigationController?.pushViewController(controller, animated: true)
}
else{
    // LOOK! You are already doing it here
    DispatchQueue.main.async { [weak self] in
        let alertController = UIAlertController(title: "Oops!", message: "Unable to load or products not found, please try again later", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "Ok", style: .default)
        alertController.addAction(okAction)
        self?.present(alertController, animated: true)
    }
}

Well? So do the same thing in the first part!

hideActivity()
let products = response.products
if products.count > 0{
    DispatchQueue.main.async { [weak self] in
        let controller = HAPurchaseViewController.instantiate() 
        controller.categories = dataManager.paidCategories()
        controller.products = products
        self?.navigationController?.pushViewController(controller, animated: true)
    }
} else {
    DispatchQueue.main.async { [weak self] in
        let alertController = UIAlertController(title: "Oops!", message: "Unable to load or products not found, please try again later", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "Ok", style: .default)
        alertController.addAction(okAction)
        self?.present(alertController, animated: true)
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141