Saturday, 27 July 2019

Initialise iOS App Programmatically Along With Unit Testing

The default Xcode iOS template create a main storyboard for the app. Here we will look into initialising an app programmatically in Swift without using storyboards. For that first we need to create a main.swift file with code as shown in the snippet. We will also add option for running unit tests. Since unit tests do not require UI, we will bootstrap the app without the UI code for faster test runs using an AppDelegateMock class.
// main.swift
import Foundation
import UIKit

let isRunningTests = NSClassFromString("XCTestCase") != nil
let appDelegateClass = isRunningTests ? NSStringFromClass(AppDelegateMock.self) : NSStringFromClass(AppDelegate.self)
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, appDelegateClass)
Here we instantiate the right app delegate class and pass it to the UIApplicationMain as an argument. Remove the storyboard given in Main Interface option under app target's General section. The AppDelegate.swift snippet is as follows.
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    private var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        initUI()
    }

    func initUI() {
        window = UI.mainWindow()
        UI.initMainWindow(window!, navVCRoot: MainViewController.self)
    }
}
class MainViewController: UIViewController {
}
class UI {
    /// Returns a main window for the screen.
    static func mainWindow() -> UIWindow {
        return UIWindow(frame: UIScreen.main.bounds)
    }

    /// Initialise main window with a navigation view controller.
    static func initMainWindow(_ window: UIWindow, navVCRoot: UIViewController.Type) {
        let vc = navVCRoot.init()
        window.rootViewController = UI.navigationController(with: vc)
        window.makeKeyAndVisible()
    }

    /// Initialise a navigation controller with a root view controller.
    static func navigationController(with rootVC: UIViewController) -> UINavigationController {
        return UINavigationController(rootViewController: rootVC)
    }
}
In AppDelegate class we remove the @UIApplicationMain annotation and in the didFinishLaunchingWithOptions delegate method, we initialises the UI. Here, the app uses NavigationViewController as the root view controller. So we will create a UIWindow and then sets the rootViewController to a navigation view controller. We then create a navigation view controller with a view controller MainViewController as its root. UI related code can be organized into a UI class.

For the mock app delegate class, create a AppDelegateMock.swift under the Tests folder with target membership to both the main app target and the unit test target.
import Foundation
@testable import AppTargetName

class AppDelegateMock: NSObject {
    private let log = Logger()
    override init() {
        super.init()
    }
}
With this, the app now launches without a storyboard and the unit tests runs quicker as we do not initialise any UI view controllers, all done programatically.

No comments:

Post a comment