Why might my iOS app be stuck on splash screen for Apple reviewers but not for me?

I'm building a new iOS app and upon submission to the App Store for review, it was rejected, with the reviewer saying:

We discovered one or more bugs in your app when reviewed on iPad running iOS 12.3 on Wi-Fi.

Specifically, the app became stuck on the splash screen. No other content loaded.

However, I personally could not reproduce this issue.

I've tried clean installs on all the simulators with Xcode, as well as a physical iPhone XS Max on iOS 12.3, and a physical iPad Pro 11in. running iOS 12.3, and the app has always been able to move past the splash/launch screen with no problems, and quickly. All of my installs are done through Xcode, but I did try installs through TestFlight as well and again could not reproduce the issue.

Edit: Should mention that I also tried with no network connection and the app loads successfully.

Code

I only use storyboard for the launch screen, which just contains a single centered image of the app logo. Other than that, all of my UI is done programmatically. In AppDelegate didFinishLaunchingWithOptions I install a root viewcontroller and the first thing it does is check for Firebase authentication by Auth.auth().addStateDidChangeListener. If there is no user, then I switch to a login view controller immediately.

Since anyone using the app for the first time is not logged in, I can't seem to understand why the app would hang on the splash screen unless somehow the Firebase auth listener does not make any progress.

(Edited to include) Actual code below.

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    // Firebase
    FirebaseApp.configure()

    // Set custom root view controller
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = RootViewController()
    window?.makeKeyAndVisible()

    return true
}
class RootViewController: UIViewController {

    private var current: UIViewController

    init() {
        self.current = SplashViewController()
        super.init(nibName: nil, bundle: nil)
    }   

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        addChild(current)
        current.view.frame = view.bounds
        view.addSubview(current.view)
        current.didMove(toParent: self)
    }
    ...
}
class SplashViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.white

        let image = UIImageView(image: UIImage(named: "1024.png")!)
        image.contentMode = .scaleAspectFit
        image.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(image)
        image.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        image.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        image.widthAnchor.constraint(equalToConstant: 128).isActive = true
        image.heightAnchor.constraint(equalToConstant: 128).isActive = true

        checkAuthenticationAndChangeScreens()
    }

    private func checkAuthenticationAndChangeScreens() {
        Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                UserDefaults.standard.set(user.uid, forKey: "userEmail")
                PushNotificationManager().registerForPushNotifications {
                    PartnerManager.shared.fetchPartnerEmail() {
                        AppDelegate.shared.rootViewController.switchToMainScreen()
                    }
                }
            } else {
                AppDelegate.shared.rootViewController.switchToLogOut()
                UIApplication.shared.unregisterForRemoteNotifications()
            }
        }
    }
}
// Class PushNotificationManager
func registerForPushNotifications(completion: @escaping () -> Void) {
    UNUserNotificationCenter.current().delegate = self
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
        granted, error in
        guard granted else { return }
        DispatchQueue.main.async {
            Messaging.messaging().delegate = self //Firebase Messaging
            UIApplication.shared.registerForRemoteNotifications()
            self.updateFirestorePushTokenIfNeeded() // Makes a single write to Firestore
            completion()
        }
    }
}
// Class PartnerManager
func fetchPartnerEmail(completion: @escaping () -> Void) {
    let userUID = UserDefaults.standard.string(forKey: "userEmail")
    db.collection("users").document(userUID).getDocument() {
        (document, error) in
        if let partnerEmail = document!.get("partnerEmail") as? String {
            UserDefaults.standard.set(partnerEmail, forKey: "partner")
        } 
        completion()
    }
}

Something weird

One last thing that has me puzzled is that while my submission to the App Store was rejected citing the app being stuck on the splash screen (along with a screenshot of the splash screen), my submission of the build for Beta testing was rejected for a different reason - that I did not provide sign in details as I should have, but here they provided a screen shot of my sign in screen. But this would mean that the app progressed beyond the splash screen for the Beta testing review.

This leads me to think that perhaps the issue is with some kind of testing environment at Apple.

Is something different in the Apple review testing environment? How might I better attempt to reproduce this stuck-splash-screen issue? And why might the Beta testing reviewer seem to be able to load the app properly while the App Store reviewer cannot?

1 answer

  • answered 2019-05-15 03:31 staticVoidMan

    That may be because your UI update is not being done on main thread.

    Try:

    DispatchQueue.main.async {
        //Update rootViewController now
    }
    

    I had once done a similar mistake. Its a random issue as UI update is not guaranteed as its not explicitly on main thread.