Monday, July 29, 2019

Linting Swift Code with SwiftLint

When working within a team, it is generally a good practice to follow a common coding convention and style guide. SwiftLint is a nice tool that helps to enforce style rules and best practices on code structure which makes all code in a project to have a familiar style. This helps to easily grasp the app logic rather than trying to work out the code differences. It's really easy to get started with adding SwiftLint to an existing project. Install the library using brew.
λ brew install swiftlint
Add the following script in the run phase under Xcode Build Phases section
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
This will enable the default SwiftLint configuration to be used to lint the code base. We can have custom rules defined under .swiftlint.yml file placed inside the project root. A sample config file is given below.
excluded:
- Carthage
- Pods
- SwiftLint/Common/3rdPartyLib

disabled_rules:
- trailing_whitespace
- identifier_name
- type_body_length
- large_tuple

opt_in_rules:
- unneeded_parentheses_in_closure_argument

force_cast: warning

line_length:
warning: 160

file_length:
warning: 1000

function_body_length:
warning: 100
error: 200

allowed_symbols: "_"

reporter: "xcode"
The project is very active and contributions are welcomed. I did fix a bug and raised a pull request which got merged in release v0.32.0.

Saturday, July 27, 2019

Display Xcode Build Time

To show Xcode build time, from the Terminal, run the below command.
λ defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES
After which, running Xcode builds will display the time in the info bar. To get the full build time, clean the project and build again.


To get the dynamic linked framework startup time, add DYLD_PRINT_STATISTICS environment variable with value as 1. Environment variables can be added from the Run section by editing the target's schema from Xcode.



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 an 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.

A Programmatic UI Pattern

Creating UI programmatically gives better control over various UI components and very much helps in writing modular, reusable UI components. Also this makes it easier when working as a team were multiple developers can work on same UI components, resolve code conflicts quickly as opposed to fixing storyboard XML file conflicts, which are harder to understand. There are some repeating patterns one will encounter when working with view and view controllers. We will make this modular as shown in the sample code.
class ViewController: UIViewController {
private lazy var submitBtn: UIButton = {
let b = UIButton(type: .system)
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Submit", for: .normal)
return b
}()

deinit {
NotificationCenter.default.removeObserver(self)
}

override func loadView() {
super.loadView()
initUI()
initConstraints()
}

override func viewDidLayoutSubviews() {
initStyle()
}

override func viewDidLoad() {
super.viewDidLoad()
initDelegates()
initEvents()
initNotifications()
initData()
}

func initUI() {
self.view = UI.view() // set the view for the view controller
self.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.backgroundColor = UIColor.white // set the default view color to white
// add sub views...
}
}
class UI {
static func view() -> UIView {
let screenSize = UIScreen.main.bounds
let v = UIView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height))
v.backgroundColor = UIColor.white
return v
}
}
Here in loadView() method, we initialise the UI using the initUI() method, where we write any UI related code to display the view controller. We can use a static class UI.swift for common UI related methods, which can be used from any view controller. We can then build the UI by adding subviews, with independent components as lazy variables, which can be accessed from other parts of the code for getting values, adding events to it and such. We can further customise this by moving all UI related code to a separate view class if the view controller has a lot of functionality.

Once the UI is set up, next we need to add constraints. Constraints for each UI component will be added separately using the NSLayoutConstraint.activate([]) method. Make sure that the UI components have translatesAutoresizingMaskIntoConstraints set to false.
NSLayoutConstraint.activate([
submitBtn.topAnchor.constraint(equalTo: self.formView.bottomAnchor, constant: 8),
submitBtn.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16),
submitBtn.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16),
submitBtn.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -8)
])
Here, the difference between programmatic constraint and storyboard is that the bottom and trailing units need to be in negative where as the same constraint when added in Interface Builder, we use positive values. In the viewDidLayoutSubviews() method, we call initStyles() to add any style to UI elements, at which point we will have the UI initialised properly with constraints and adding properties like border, color etc will work because the UI component's frame is not of CGRect.zero. Once the constraints are setup, we will set the delegates of any UI components and objects. Then we add target-action for UI elements in initEvents() method. If the view controller needs to listen to certain notifications, we add that in the initNotifications() method. Next we call initData() where we populate any model variables which is required for displaying, like in case of table view as well as making calls to service layer to fetch or update data.