0% found this document useful (0 votes)
10 views90 pages

Practical Tips For Junior IOS Devs Master

The document is a preface and introduction to a book written by Aryaman Sharda, an experienced iOS developer, sharing practical tips and insights from his career. It covers various topics related to iPhone app development, including coding best practices, testing, and functional programming. The book aims to help new developers succeed in their roles and will be periodically updated at no additional cost.

Uploaded by

Hans
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views90 pages

Practical Tips For Junior IOS Devs Master

The document is a preface and introduction to a book written by Aryaman Sharda, an experienced iOS developer, sharing practical tips and insights from his career. It covers various topics related to iPhone app development, including coding best practices, testing, and functional programming. The book aims to help new developers succeed in their roles and will be periodically updated at no additional cost.

Uploaded by

Hans
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 90

Preface

Thanks for purchasing this book.

A little bit about myself and then we'll jump into the code.

My name is Aryaman Sharda and I've been making iPhone apps since 2015. My career
has included working for a variety of companies, from a few lesser-known startups in
San Francisco to well-known companies like Turo, Scoop Technologies, and Porsche.

Ever since I wrote my first line of Objective-C code, I started maintaining a notebook
where I'd write down anything meaningful I learnt. Regardless of whether it was a
particularly challenging bug, a novel code snippet, or noteworthy insight from a more
senior engineer - it would go in the notebook.

Now, 5 years later, it's closing in on 200 pages and is full of practical tips derived from
actual day to day experience working as an iOS developer. It now serves as the
foundation for this book.

My hope is that the following tips and practical suggestions will help you succeed in
your new role.

The book will be updated on a periodic basis for no additional cost. It presents topics
ranging from optimal build settings and design patterns to Swift best practices and
conventions. There's no particular order and most tips are standalone. Feel free to
jump around.

The ideas expressed here are my own and you may not agree with them - that's fine
too. I'm only here to share my experience.

Let's get started.

Feel free to connect on LinkedIn , Twitter, or on my blog!

Apologies for any unconventional code formatting. It's my attempt to keep the
code readable within the constraints of the Markdown editor used to write this
book.
Table of Contents
Preface
Table of Contents
#1: Embrace Generics
#2: Use switch Pattern Matching
#3: There's Always More To Be Tested
PR Checklist
#4: Leverage Functional Programming
#5: Abstraction Between 3rd Party Dependencies
#6: Know When To Use PDF > PNG
#7: Increase Readability With Extensions
Implementing Protocols
Extending Class Functionality
Creating Optional Protocol Functions
Extensions & Initializers
#8 Dependency Injection & Creating Testable View Controllers
#9: Periodically Clean Up Your Imports & Code
#10: Conflicting Constraints + UIStackViews & Cells
#11: Build An Actual Logging System
#12: Structuring Your Endpoints
#13: You Probably Don't Need That Dependency
Network Layer (e.g. Alamofire)
Image View + URL (e.g. Kingfisher / SDWebImage)
#14: Start With A Stylesheet
#15: Comments Need To Go Beyond "What"
#16: DateFormatter & Locales
#17: Use Xcode's Documentation Features
#18: Keychain > UserDefaults
#19: Follow Delegate Conventions
#20: Avoiding Hardcoded Strings
#21: Working With Access Levels
#22: Computed Properties
#23: Project & App Optimization Tips
Opaque Views
Unused Resources
Don't print() In Release
Image View
Structs > Classes
Build Settings
Instruments
Modular Architecture
#24: Static vs. Dynamic Libraries
Static Libraries
Dynamic Libraries
#25: Careful with default
#26: Establish A Pull Request (PR) Template
#27: Moving Navigation Out Of The UIViewController
#28: @discardableResult
#29: Knowing When To Use Structs vs. Classes
#30: Child View Controllers
#31: Increase Readability With typealias
#32: Use Singletons Sparingly
#33: Utilizing Code Snippets
#34: Learn Some Basic DevOps (Fastlane, CircleCI, BitRise)
#35: Making the Happy Path Clear
#36: Remove Unnecessary self References
Conclusion
#1: Embrace Generics

Creating classes and functions with generics is a great way to write general purpose
code. In simple terms, generics allow you to write the code once, but apply it to
a variety of different data types.

At the very start of my programming career, I avoided generics as much as possible.


They always seemed like overkill for the scale of projects I was working on and their
syntax seemed confusing.

In reality, though the syntax appears unusual at first, after some practice it becomes a
lot more friendly.

Generics can greatly reduce the total amount of code you need to write when used
correctly.

In the following example, we are saying we are going to pass something into
exists() that is Equatable . We don't know what yet, but we promise it will
implement the Equatable protocol when the time comes. Additionally, the second
parameter will be an array of items that are also Equatable and we're simply looking
for a Bool in response.

Since these items all implement the Equatable protocol, we can use == to
verify equality.

Using this method, we can pass an input array of any Equatable type, and easily
check if it contains the target element - item . We can use custom objects, structs,
primitive data types, or anything else that implements Equatable .

// Here's our generic exists() function


func exists<T: Equatable>(item:T, elements:[T]) -> Bool {
for element in elements {
if item == element {
return true
}
}

return false
}

// The main takeaway here is that we've written the code once, but
// we can apply it over a wide variety of data types.

// Output: true
exists(item: "1", elements: ["1", "2", "3", "4"]))

// Output: false
exists(item: -1, elements: [1, 2, 3, 4])
// Output: true
exists(item: CGPoint(x: 0, y: 0), elements: [CGPoint(x: 0, y: 0),
CGPoint(x: 0, y: 1),
CGPoint(x: 0, y: 2)])

Here's an advanced iOS example:

extension UIView {
// In this code, we're saying that `T` is eventually going to
// be a `UIView`. It could be a `UIButton`, `UIImageView`, or
// an ordinary `UIView`. Doesn't matter.
//
// Now, we can load any `UIView` or subclass from a `.nib`.
class func fromNib<T: UIView>() -> T {
return Bundle(for: T.self).loadNibNamed(
String(describing: T.self), owner: nil, options: nil
)![0] as! T
}
}

// Usage: let view = RestaurantView.fromNib()


// Usage: let cell = PictureCell.fromNib()
// Usage: let photoGallery = GalleryView.fromNib()
#2: Use switch Pattern Matching

Your application may often make decisions using the combined state of multiple
properties.

Consider the function below, which shows a message based on the user's role and
payment status.

func determineErrorMessage() -> String {


if (user.isAdmin && user.hasActiveMembership) {
return "You're all set"
} else if (user.isAdmin && !user.hasActiveMembership) {
return "You'll need to activate your membership."
} else if (!user.isAdmin && !user.hasActiveMembership) {
return "You'll need to update your account."
} else if (!user.isAdmin && user.hasActiveMembership) {
return "You don't have the right permissions."
}
}

Imagine how unruly the above code would be if we had to consider more than just
two properties (i.e. user.isAdmin , user.hasActiveMembership ,
user.isEmailVerified , etc).

This could be written much more elegantly by using computed properties and
switch pattern matching.

var determineErrorMessage: String {


switch (user.isAdmin, user.hasActiveMembership) {
case (true, true):
return "You're all set"
case (true, false):
return "You'll need to activate your membership."
case (false, false):
return "You'll need to update your account."
case (false, true):
return "You don't have the right permissions."
}
}

This approach is especially useful when working with optionals, multiple elements in a
tuple, or different input types.

switch (didChangeFromOriginalStatus, status) {


case (false, .notSpecified):
return .checkInDisabled
case (false, .notGoing):
return .hidden
case (false, .going):
return .viewPass
case (true, .notSpecified):
Log.error("Invalid state")
return .hidden
case (true, .notGoing):
return .checkInEnabled
case (true, .going):
return .checkInEnabled
}
#3: There's Always More To Be Tested

As a young professional programmer, the most common feedback I received was to


"slow down". Every young developer, myself included, wants to be a “rockstar” – faster
and more productive than their coworkers. More often than not, this meant I would
create pull requests (PRs) without doing nearly enough testing.

Letting obvious mistakes slip through the cracks will have a more lasting
impression on your colleagues than taking a few extra minutes to check your
work.

I struggled with this habit for a long time. I was always impatient to move on to the
next problem.

Eventually, I started making a PR checklist for myself to prevent that tendency.

I'd encourage you to start your own checklist and include cases that you routinely
forget to check.

PR Checklist

Am I duplicating existing code?


Did I test for memory leaks?
Could any of my new code be replaced by native functions or existing helper
functions?
Did I chose appropriate names for my classes, enums, structs, methods, and
variables?
Did I clear out any commented code?
Did I remove all TODO , @hack , and placeholder code?
Did I remove all of the print statements I was using? Am I doing any
unnecessary logging?
Am I handling and logging all errors correctly?
Does linting pass?
Are there tests? Should there be?
Did I test different locales and languages? Did the currency, time, and date
formatting work as expected?
Did I test night mode support or remember to disable it?
Did I test multiple screen sizes and orientations?
Did I test on the lowest iOS version we support?
Did I receive design and product approval?
Did I test what happens if the user declines / limits permission access (location,
camera roll, contacts, etc)?
Did I test for accessibility compatibility?
Did I test with Double Length Pseudolanguage to ensure a dynamic layout?
Is everything behind a feature flag that should be?
Did I document everything that I needed to?
Did I test a poor or offline WiFi connection?
#4: Leverage Functional Programming

Functional programming is one of the best ways to write readable safe code.

Let's say you wanted to create a function to double all of the numbers in an array.

You might write something like this:

var input = [1,2,3,4,5]

for i in 0..<input.count {
input[i] = input[i] * 2
}

// `input` now equals [2, 4, 6, 8, 10]

However, we've immediately run into a problem. What happens if I wanted to access
the original values in input ?

You can no longer retrieve those values.

Here we used imperative programming - which is a paradigm in which you execute


statements that change the state of a program. In this case, by changing the input
array's values.

Functional programming, instead, tries to prevent making any changes to the existing
state of the application or introduce any side effects.

You'll be able to find a more rigorous mathematical definition of functional


programming elsewhere, but intuitively the goal is:

Avoid mutability wherever possible.


Use functions as the building blocks for functionality (in other words, combining
functions together).
Using pure functions where possible (a pure function is a function that will
always produce the same result for the same input regardless of when and
where it is being called).

Simply put, in imperative programming, changing the variable's state and


introducing side effects is permissible, in functional programming it is not.

Let's take a look at some examples in Swift.

You'll notice that in all of the following examples, the input variable's values are
never changed allowing us to avoid mutability. Instead, they return an entirely new
value rather than modifying the inputted one.
.map {}

// map: Applies a function to every element in the input and returns


// a new output array.
let input = [1, 2, 3, 4, 5, 6]

// We'll take every number in the input, double it, and return it in
a new
// array.
//
// Notice that the input array is never modified.
//
// Output: [2, 4, 6, 8, 10, 12]
// $0 refers to the current element in the input array `map` is
operating on.
let mapOutput = input.map { $0 * 2 }

.compactMap {}

// compactMap: Works the same way as map does, but it removes nil
values.
let input = ["1", "2", "3", "4.04", "aryamansharda"]

// We'll try and convert each String in `input` into a Double.


//
// Notice that the input array is never modified and the values that
resolve
// to nil are skipped.
//
// compactMap is often used to convert one type to another.
// Output: [1.0, 2.0, 3.0, 4.04]
let compactMapOutput = input.compactMap { Double($0) }

.flatMap {}

// flatMap: Use this method to receive a single-level collection from


an input
// that may have some nesting.
let input = [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

// This will flatten our array of arrays structure into just a single
array.
// Output: [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
let flatMapOutput = input.flatMap { $0 }
.reduce {}

// reduce: Allows you to produce a single value from the elements in


// a sequence.
let input = [1, 2, 3, 4]

// This will add up all of the numbers in the sequence.


// `sum` will be 10
let sum = input.reduce(0, { x, y in
x + y
})

// You can also write this more simply as:


let sum = input.reduce(0, +)
// `sum` will be 10

.filter {}

// filter: Returns an array containing, in order, the elements of the


// sequence that satisfy the given constraint(s).
let input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// This will only return the even numbers.


// Output: [2, 4, 6, 8, 10]
let filterOutput = input.filter { $0 % 2 == 0}

.forEach {}

// forEach: Calls the given closure on each element in the sequence


in the
// same order as a for-loop.
let input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// This will print out the numbers in `input` just like a traditional
for-loop.
input.forEach {
print($0)
}

When you use forEach it is guaranteed to go through all items in sequence order,
but map is free to process items in any order.
.sorted {}

// sorted: Returns the elements of the sequence, sorted using the


given
// condition as the comparison between elements.
let input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// This will create an array with the numbers sorted in descending


order.
// Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
let sortedInput = input.sorted(by: { $0 > $1 })

These functions can be applied to objects of any type - even custom ones. In addition,
combining these calls can deliver impressive functionality for a minimal amount of
code:

// We'll create an array of Cars and specify some basic properties.


struct Car {
let name: String
let horsepower: Int
let price: Int
}

var cars = [Car?]()


cars.append(Car(name: "Porsche 718", horsepower: 300, price: 60500))
cars.append(Car(name: "Porsche 911", horsepower: 379, price: 101200))
cars.append(nil)
cars.append(Car(name: "Porsche Taycan", horsepower: 402, price:
79900))
cars.append(Car(name: "Porsche Panamera", horsepower: 325, price:
87200))
cars.append(nil)
cars.append(nil)
cars.append(Car(name: "Porsche Macan", horsepower: 248, price:
52100))
cars.append(Car(name: "Porsche Cayenne", horsepower: 335, price:
67500))

// Let's return valid cars (not nil) that have a horsepower greater
than 300
// and are sorted by descending price.
cars.compactMap { $0 }.filter { $0.horsepower > 300}
.sorted { $0.price > $1.price }.forEach {
print($0)
}

// Output
Car(name: "Porsche 911", horsepower: 379, price: 101200)
Car(name: "Porsche Panamera", horsepower: 325, price: 87200)
Car(name: "Porsche Taycan", horsepower: 402, price: 79900)
Car(name: "Porsche Cayenne", horsepower: 335, price: 67500)

Functional programming in iOS is becoming more and more mainstream and is critical
to the SwiftUI and Combine ecosystems. Levering these language features and new
paradigm correctly allows you to write optimized, thread-safe, easily testable, and
readable code.
#5: Abstraction Between 3rd Party Dependencies

One of the most useful patterns I encountered as a junior developer was the concept
of layering abstractions between your codebase and external dependencies.

Let's say we want to implement analytics. We have a variety of options - Firebase,


Mixpanel, etc. Our code should be applicable to any of these analytics providers and
should be able to handle replacing the provider efficiently. The only constant in
software development is change and you never know when product
requirements might trigger a change in dependencies / tooling.

We'll use Firebase for the following example.

A simple implementation would involve calling the Firebase SDK wherever needed:

import FirebaseAnalytics
Analytics.logEvent("analytic_event_name", parameters: [:])

Analytics.logEvent() is part of FirebaseAnalytics .

With this approach, we would now have hundreds of references to


FirebaseAnalytics and Analytics.logEvent() in our codebase, which would
make removing it a challenge in the future.

A better implementation would be one that does not even expose the provider
to the codebase.

All analytic events have the same structure; a name and optional metadata. So, let's
start by defining this requirement in a protocol.

This is general enough to accommodate any analytics service.

protocol AnalyticsEngine: class {


func sendAnalyticsEvent(named name: String, metadata: [String :
Any]?)
}

Now, let's complete the implementation for a class that uses this protocol.
import FirebaseAnalytics

class FirebaseAnalyticsEngine: AnalyticsEngine {


func sendAnalyticsEvent(named name: String, metadata: [String :
Any]?) {
Analytics.logEvent(name, parameters: metadata)
}
}

This will be the only time we import FirebaseAnalytics . We could have just as
easily created a MixpanelAnalyticsEngine or any other variant instead.

We can now use AnalyticsManager to send events out without revealing the
provider.

This is not intended to be production ready code, but rather to demonstrate the
separation between the FirebaseSDK and the rest of the codebase.

class AnalyticsManager {
private var engine: AnalyticsEngine?
static var shared = AnalyticsManager()

private init() {}

func configure(engine: AnalyticsEngine) {


self.engine = engine
}

func track(eventName: String, metadata: [String: Any]?) {


guard let engine = engine else {
print("Analytics engine not provided.")
return
}

engine.sendAnalyticsEvent(named: eventName, metadata:


metadata)
}
}

// Somewhere else in the code - AppDelegate, perhaps.


AnalyticsManager.shared.configure(engine: FirebaseAnalyticsEngine())

This class can now be freely used throughout the code:

AnalyticsManager.shared.track(eventName:
"user_clicked_forgot_password",
metadata: ["userID": "aryamansharda"])
Whenever we need to replace the provider, we can simply create a new class that
implements the AnalyticsEngine protocol and pass it in to the
AnalyticsManager 's shared instance:

AnalyticsManager.shared.configure(engine: MixpanelAnalyticsEngine())

With this approach, our codebase is not aware which providers we are using and
swapping them out becomes a straightforward task.
#6: Know When To Use PDF > PNG

Back in Xcode 6, Apple introduced a feature that allowed you to add .pdf files
instead of just .png or .jpg files to your Xcode asset catalogs. This enabled you to
support all image sizes without manually exporting @1x , @2x , and @3x
resolutions. At build time, Xcode would generate the appropriate resolutions.

With Xcode 9+, this functionality is even more robust. However, there are two edge
cases to consider.

First, make sure that Xcode preserves vector data for your .pdf assets.
Otherwise, these images will not be treated as vectors at runtime.

Additionally, in some circumstances you may prefer a .png over a .pdf . In most
cases, this is done when you want to minimize your application's size.

A complex illustration, like the one below, may result in a .pdf file larger in
size than several smaller .png ones combined. In addition, if you export images
from an application such as Sketch or Figma, the default output size may exceed what
the phone will ever be able to display. So, before including an asset in your project,
make sure you check its file size. You could be increasing the executable size without
any reason and thus degrading your app's performance.
Generally, I stick to .pdf files unless the .pdf is exceeding a few hundred KB in size
(which can be the case for complex illustrations). In that case, I prefer to use multiple
.png files instead. I usually use .png for illustrations and .pdf for any type of
graphic, symbol, or icon asset. However, the primary deciding factor here should be
the file size - we want to keep the app as small as possible.
#7: Increase Readability With Extensions

Extensions are incredibly useful in Swift and allow you to easily add new functionality
to a class, struct, enumeration, or protocol without access to the source code. We can
also use them to easily group related sections of code together thereby improving the
code's readability. Though this doesn't do much to prevent the Massive View
Controller issue, leveraging extensions correctly can make an already large
class a bit more manageable.

For example, we could use extensions and pragma marks to restructure a


UIViewController like this:

final class MapViewController: UIViewController {

// MARK: - View Lifecycle


override func viewDidLoad() {}
override func viewWillAppear() {}

// MARK: - UI
private func showFiltersView() {}
private func updateUserCurrentLocation() {}
}

// MARK: - Business Logic


extension MapViewController {
func fetchRestaurantLocations()
func fetchRestaurantMetadata(with identifier: String)
}

// MARK: - Events
extension MapViewController {
@IBAction func didTapRecenterButton( _ sender: UIButton)
}

// MARK: - Table View Data Source


extension MapViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) ->
UITableViewCell {}
}

Now, a new programmer seeing this class for the first time would be able to easily
understand the structure of the code and all of the responsibilities of the
MapViewController .
Implementing Protocols

Extensions should also be used when implementing a protocol.

This can be accomplished within the same source code file (see above) by using
MARK:// to easily delineate different sections of the code.

import UIKit

class NewsFeedViewController: UIViewController {


}

// MARK: - Table View


fileprivate extension ViewController: UITableViewDataSource {
}

fileprivate extension ViewController: UITableViewDelegate {


}

Extending Class Functionality

Using extensions, you can easily add functionality to existing classes which you can
then leverage in any future project. It has been useful for me to create a private
repository of Swift extensions which I can then integrate into any new project through
Cocoapods.

Here are a few examples:

extension UIView {
// Provides a type-safe way of loading a view from a .nib
// Usage: let restaurantView = RestaurantView.fromNib()
class func fromNib<T: UIView>() -> T {
return Bundle(for: T.self).loadNibNamed(
String(describing: T.self), owner: nil, options: nil
)![0] as! T
}
}

extension UIView {
// Helper function for defining auto-layout constraints
func pinEdges(to other: UIView) {
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo:
other.leadingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive =
true
bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive
= true
trailingAnchor.constraint(equalTo:
other.trailingAnchor).isActive =
true
}
}

My personal repository has over a hundred such commonly used extensions. Starting
your own collection will save you countless hours in the long run.

Creating Optional Protocol Functions

If you are defining a protocol and you want to make certain functions optional,
you can use a protocol extension to provide default implementations.

Then, a future class or struct that implements this protocol may optionally override
the default implementation.

Here, the CameraPickerVC informs the UIViewController that presented it that a


new photo has been taken by the user.

class CameraPickerVC: UIViewController {}

protocol CameraPickerVCDelegate: AnyObject {


func cameraPickerControllerDidTakePhoto(
_ cameraPickerController: CameraPickerVC, photo: UIImage)
}

extension CameraPickerVCDelegate {
func cameraPickerControllerDidTakePhoto(
_ cameraPickerController: CameraPickerVC, photo: UIImage) {
// Save to disk
}
}

class PrivateAlbumViewController: UIViewController,


CameraPickerVCDelegate {
// Save photo to user's camera roll
}

class PublicAlbumViewController: UIViewController,


CameraPickerVCDelegate {
func cameraPickerControllerDidTakePhoto(
_ cameraPickerController: CameraPickerVC, photo: UIImage) {
// Upload photo to public image gallery
}
}
Since saving the newly captured photo to disk would be the most common use case,
we can implement this capability through an extension on the protocol.

Therefore, future classes that implement this protocol won't have to re-implement the
most common use case. In the event that we would prefer to do something different -
for instance, upload the picture to a server - we have the freedom to do that as well.

By utilizing this approach, you will be able to write less code and re-use common
implementations.

Extensions & Initializers

You can also add new initializers to existing types through extensions. This enables
you to extend other types to accept your own custom types as initialization
parameters or to provide additional initialization options that were not included
as part of the type’s original implementation.

A struct's member-wise initializers are created by Swift automatically. If you want a


custom initializer, you can implement it in an extension. This approach will allow the
default member-wise initializer to co-exist.

struct Rocket {
let weight: Double
let height: Double
let width: Double
}

extension Rocket {
init(dictionary: [String: Double]) {
self.weight = dictionary["weight"]
self.height = dictionary["height"]
self.width = dictionary["width"]
}
}

// Usage:
// This is created by default in Swift.
Rocket(weight: 1000, height: 2000, width: 3000)

// This is our custom initializer that co-exists with the member-wise


one above.
Rocket(dictionary: ["weight": 1000, "height": 2000, "width": 3000])
#8 Dependency Injection & Creating Testable View
Controllers

Modern iOS development relies heavily on a software pattern called dependency


injection. When I first attempted to understand this pattern, it was difficult to find
relatable examples.

In practice, the concept is pretty intuitive. Consider a UIViewController that relies


on classes for managing network requests, data persistence, and the user's location.

Dependency injection is simply a method through which an object receives the


objects it depends on. In the following example, you'll see that ViewController
relies on NetworkManager , DataManager , and LocationManager objects.

Instead of initializing them directly within the ViewController :

final class ViewController: UIViewController {


let networkManager = NetworkManager()
let dataManager = DataManager()
let locationManager = LocationMananger()

override func viewDidLoad() {


super.viewDidLoad()
loadData()
}

func loadData() {
let response = networkManager.makeNetworkCall()
dataManager.save(response: response)
updateUI(response: response)
}
}

Dependency injection tells us to do something like this instead:

final class ViewController: UIViewController {


var networkManager: NetworkManager
var dataManager: DataManager
var locationManager: LocationMananger

override func viewDidLoad() {


super.viewDidLoad()
loadData()
}

func injectDependencies(networkManager: NetworkManager,


dataManager: DataManager,
locationManager: LocationManager) {
self.networkManager = networkManager
self.dataManager = dataManager
self.locationManager = locationManager
}

func loadData() {
let response = networkManager.makeNetworkCall()
dataManager.save(response: response)
updateUI(response: response)
}
}

// Usage:
viewController.injectDependencies(networkManager: NetworkManager(),
dataManager: DataManager(),
locationManager: LocationManager())

It's up to you how you inject these dependencies. You can create a designated
function, public getters / setters for the dependencies, pass them through the
initializer, etc.

Because of this approach, our solution is much more flexible:

let viewController = ViewController()

// For a basic example, we can inject the dependencies like this.


viewController.injectDependencies(networkManager: NetworkManager(),
dataManager: DataManager(),
locationManager: LocationManager())

// Assume these classes are implemented elsewhere.


class NetworkManager {}
class DataManager {}
protocol LocationManager {}

class SlowNetworkManager: NetworkManager {}


class OfflineNetworkManager: NetworkManager {}
class RemoteDataManager: DataManager {}
class LocalDataManager: DataManager {}
class RealTimeLocationManager: LocationManager {}
class FakeLocationManagerForTesting: LocationManager {}

// Here's where dependency injection comes in handy.


// The ViewController does not know if it is relying on a concrete
type,
// a subclass, or some other entity that implements the correct
protocol.
// We could just as easily initialize the ViewController with
alternate
// classes.
// Alternative:
viewController.injectDependencies(networkManager:
SlowNetworkManager(),
dataManager: RemoteDataManager(),
locationManager:
RealTimeLocationManager())

// Alternative:
viewController.injectDependencies(networkManager:
OfflineNetworkManager(),
dataManager: LocalDataManager(),
locationManager:
FakeLocationManagerForTesting())

With this simple change, we have greatly extended the view controller's reusability
and made it far more testable and extensible.

If we kept the initial approach - let networkManager = NetworkManager() - our


ViewController would be extremely coupled with NetworkManager() and we
wouldn't be able to reuse any of the logic. Now, we have many more options as we
can substitute in SlowNetworkManager , OfflineNetworkManager ,
NetworkManager , etc. and reuse the other logic in ViewController without issue.

Consider some other use cases of dependency injection:

If you're writing user authentication code and you've created an


AuthenticationManager , you might create a FakeAuthenticationManager
for local testing.
You can easily implement an A/B test by using this pattern to inject into a class
AlgorithmControl and AlgorithmExperiment .
This is a good way to create some temporary responses for testing while the
backend endpoint is being developed. For example, creating a StubPricingAPI
while PricingAPI is in development.

Dependency injection allows the same code to be compatible with any of the concrete
types or subclasses referenced above.

This approach is particularly useful when it comes to writing tests.

Tests need to be deterministic in order to run reliably - they should produce the same
output for the same input. As a result, we would prefer to avoid network calls in our
tests as they are often unreliable.

Dependency injection allows us to write deterministic tests like this:


class SlowNetworkManager: NetworkManager {
func makeNetworkCall() -> [String: Any] {
// Makes an HTTP request
}
}

class FakeSlowNetworkManager: NetworkManager {


func makeNetworkCall() -> [String: Any] {
sleep(5)
return ["username": "aryamansharda", "location": "San Francisco"]
}
}

/************************************************/
// Now, in our testing target:

func testSlowNetworkCall() {
let viewController = ViewController()
viewController.injectDependencies(
networkManager: FakeSlowNetworkManager(), ....))

// Now, we can test just the behavior of the ViewController


instead of the
// ViewController and all of its dependencies.

// We can validate that `loadData()` and `updateUI()` work


correctly now
// independent of any network conditions.
XCTAssertEqual(viewController.usernameLabel.text,
"aryamansharda")
XCTAssertFalse(viewController.loadingIndicator.isVisible)
}

By simply passing in the dependencies rather than initializing them directly, the
ViewController class becomes more modular and testable.
#9: Periodically Clean Up Your Imports & Code

When classes undergo extensive refactoring, unused imports are often present.
Unfortunately, Xcode doesn't flag this as an issue.

The use of unnecessary imports interferes with the next developer's ability to
understand the intent behind the code. Keeping the imports and dependencies in line
with the purpose of the class makes understanding and debugging the code much
simpler. Additionally, unnecessary imports can increase the size of your application
and the time it takes to compile.

This is also true for any deprecated code and resources in your application. Even if the
code is commented out, there's no reason to keep it in the project - that's what
version control is for.

There have been instances where we've kept a 3rd party dependency in a project far
longer than we needed to simply because we saw extraneous import statements -
the library itself was never being used.
#10: Conflicting Constraints + UIStackViews & Cells

iOS projects inevitably have conflicting constraints.

Probably at least one of the constraints in the following list is


one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't
expect;
(2) find the code that added the unwanted constraint or
constraints and fix it.
...
...
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000125f0ff0 UIView:0x7fd4c9265820.height == 1
(active)>

In most cases, if the view works, a ticket is created to resolve the conflicting constraint,
and then never revisited. These can cause unpredictable errors in your app if they're
not caught as part of the code review process.

I was working on an issue a couple of weeks ago in which a small view on a cell
should've been hidden, but it wasn't responding to changes to the isHidden
property. Moreover, it was showing in an incorrect position and only on some of the
cells in the UITableView .

Initially, I thought I had made a logic error and was accidentally toggling the
isHidden property somewhere. As it turns out, it was due to conflicting constraints.
A particular UIStackView was unable to honor all the constraints and upon trying to
recover, it broke a constraint that resulted in this odd behavior.

The point here is that failing to resolve conflicts is just kicking the problem
down the road. They'll eventually manifest as a bug and often in a way you
wouldn't suspect. Moreover, resolving these conflicts becomes even more important
when dealing with UIStackViews and reusable cells since their use tends to
exacerbate the problem.

In the CPU Profiler , you'll see small spikes in CPU usage when the system is
trying to resolve these constraints at runtime. If you encounter conflicting
constraints in a UITableViewCell or in a UICollectionViewCell then you are
incurring this performance hit every time the cell is reused.

Just because the view appears to be 'okay' despite some conflicting constraints, does
not mean it will continue to "work" on future iOS updates or devices.
You'll also find that by giving proper titles to each view in your .storyboard / .xib
you'll be able to debug conflicting constraints more clearly. And finally, when dealing
with UIStackViews , a solid understanding of Content Hugging Priorities and
Content Compression Resistance Priorities will be your go-to tools in resolving
conflicting constraints.
#11: Build An Actual Logging System

Most junior developers, myself included, rely heavily on print and NSLog
statements to aid them in their debugging. While this approach has its place, you
will greatly benefit from taking the time to create a proper logging utility.

A lightweight logging utility does not have to be complicated. Here is a simple


implementation you can use in your next project.

You'll notice that it could easily include support for:

Disabling logging in production.


Increasing your verbosity by adding a simple flag.
Logs can be saved to a file or uploaded immediately.
More granular logging is available than NSLog or print provides.

import Foundation

enum Log {
enum LogLevel {
case info, warning, error
fileprivate var prefix: String {
switch self {
case .info: return "INFO"
case .warning: return "WARN ⚠ "
case .error: return "ALERT ❌ "
}
}
}

struct Context {
let file: String
let function: String
let line: Int
var description: String {
return "\((file as NSString).lastPathComponent):\(line) \
(function)"
}
}

static func info(_ str: StaticString, shouldLogContext: Bool =


true,
file: String = #file, function: String =
#function,
line: Int = #line) {
let context = Context(file: file, function: function, line:
line)
Log.handleLog(level: .info, str: str.description,
error: nil, shouldLogContext: shouldLogContext,
context: context)
}

static func warning(_ str: StaticString, file: String = #file,


function: String = #function, line: Int =
#line) {
let context = Context(file: file, function: function, line:
line)
Log.handleLog(level: .warning, str: str.description,
error: nil, shouldLogContext: true, context:
context)
}

static func warning(_ str: StaticString, _ error: Error,


file: String = #file, function: String =
#function,
line: Int = #line) {
let context = Context(file: file, function: function, line:
line)
Log.handleLog(level: .warning, str: str.description,
error: error, shouldLogContext: true, context:
context)
}

static func error(_ str: StaticString, file: String = #file,


function: String = #function, line: Int =
#line) {
let context = Context(file: file, function: function, line:
line)
Log.handleLog(level: .error, str: str.description,
error: nil, shouldLogContext: true, context:
context)
}

static func error(_ str: StaticString, _ error: Error,


file: String = #file, function: String =
#function,
line: Int = #line) {
let context = Context(file: file, function: function, line:
line)
Log.handleLog(level: .error, str: str.description,
error: error, shouldLogContext: true, context:
context)
}

fileprivate static func handleLog(level: LogLevel, str: String,


error: Error?,
shouldLogContext: Bool,
context: Context) {
let logComponents = ["[\(level.prefix)]", str]
var fullString = logComponents.joined(separator: " ")
if shouldLogContext {
fullString += " ➜ \(context.description)"
}
print(fullString)
}
}

// Usage:
Log.error("Failed to identify user location.", error)
#12: Structuring Your Endpoints

Managing a growing codebase requires that all API calls are organized properly.
There should be no need for you to make multiple calls to URLSession across
your codebase. Instead, you should rely on some type of networking layer to
take care of these HTTP requests.

We will take a look at a sample networking layer shortly. But first, let’s take a look at an
option for organizing our endpoints. The following approach is scalable and can be
applied to your own private API or to any external API your app relies on.

import Foundation

enum HTTPMethod: String {


case delete = "DELETE"
case get = "GET"
case patch = "PATCH"
case post = "POST"
case put = "PUT"
}

enum HTTPScheme: String {


case http
case https
}

/// The API protocol allows us to separate the task of constructing a


URL,
/// its parameters, and HTTP method from the act of executing the URL
request
/// and parsing the response.
protocol API {
/// .http or .https
var scheme: HTTPScheme { get }

// Example: "maps.googleapis.com"
var baseURL: String { get }

// "/maps/api/place/nearbysearch/"
var path: String { get }

// [URLQueryItem(name: "api_key", value: API_KEY)]


var parameters: [URLQueryItem] { get }

// "GET"
var method: HTTPMethod { get }
}
For example, if we wanted to implement the Google Places API , we could easily do
so:

enum GooglePlacesAPI: API {


case getNearbyPlaces(searchText: String?,
latitude: Double, longitude: Double)

var scheme: HTTPScheme {


switch self {
case .getNearbyPlaces:
return .https
}
}

var baseURL: String {


switch self {
case .getNearbyPlaces:
return "maps.googleapis.com"
}
}

var path: String {


switch self {
case .getNearbyPlaces:
return "/maps/api/place/nearbysearch/json"
}
}

var parameters: [URLQueryItem] {


switch self {
case .getNearbyPlaces(let query, let latitude, let
longitude):
var params = [
URLQueryItem(name: "key", value:
GooglePlacesAPI.key),
URLQueryItem(name: "language",
value: Locale.current.languageCode),
URLQueryItem(name: "type", value: "restaurant"),
URLQueryItem(name: "radius", value: "6500"),
URLQueryItem(name: "location",
value: "\(latitude),\(longitude)")
]

if let query = query {


params.append(URLQueryItem(name: "keyword", value:
query))
}
return params
}
}
var method: HTTPMethod {
switch self {
case .getNearbyPlaces:
return .get
}
}
}

In the next section, you'll see this in action.


#13: You Probably Don't Need That Dependency

As a new developer, I had a bad habit of introducing too many external dependencies.
Adding something to my Podfile was so easy and it'd save me hours of work - it
seemed like a no brainer.

However, as you transition to more professional work, 3rd party dependencies can
dramatically raise the risk and maintenance costs for your codebase.
Occasionally, a new version of iOS would break one of our dependencies, and we'd
have to patiently wait for the developer to provide a fix before we could continue
releasing our application.

Often, we would rely on large libraries only to use a small portion of their capabilities
(usually frameworks like Alamofire and Kingfisher).

Be pragmatic about whether or not the risk associated with the dependency is
worthwhile. Bringing in a new dependency might solve your current problem,
but will increase your application's size, upkeep costs, potentially block
releases, be difficult to de-integrate, and will likely be a black box of
functionality.

Network Layer (e.g. Alamofire)

It is often easy to create a version of a library's functionality that focuses on just the
functionality you need. The network layer here can easily be extended to handle most
common scenarios without the use of external dependencies.

import Foundation

final class NetworkManager {


/// Builds the relevant URL components from the values specified
/// in the API.
fileprivate class func buildURL(endpoint: API) -> URLComponents {
var components = URLComponents()
components.scheme = endpoint.scheme.rawValue
components.host = endpoint.baseURL
components.path = endpoint.path
components.queryItems = endpoint.parameters
return components
}

/// Executes the web call and will decode the JSON response into
/// the Codable object provided.
/// - Parameters:
/// - endpoint: the endpoint to make the HTTP request against
/// - completion: the JSON response converted to the provided
Codable
/// object, if successful, or failure otherwise
class func request<T: Decodable>(endpoint: API,
completion: @escaping (Result<T,
Error>)
-> Void) {
let components = buildURL(endpoint: endpoint)
guard let url = components.url else {
Log.error("URL creation error")
return
}

var urlRequest = URLRequest(url: url)


urlRequest.httpMethod = endpoint.method.rawValue

let session = URLSession(configuration: .default)


let dataTask = session.dataTask(with: urlRequest) {
data, response, error in
if let error = error {
completion(.failure(error))
Log.error("Unknown Error", error)
return
}

guard response != nil, let data = data else {


return
}

if let responseObject = try? JSONDecoder().decode(T.self,


from:
data) {
completion(.success(responseObject))
} else {
let error = NSError(domain: "com.AryamanSharda",
code: 200,
userInfo: [
NSLocalizedDescriptionKey:
"Failed"
])
completion(.failure(error))
Log.error("Decode Error", error)
}
}

dataTask.resume()
}
}
Image View + URL (e.g. Kingfisher / SDWebImage)

The advantage of creating your own solution is that it keeps you in full control of your
code rather than you having to depend on whatever magic happens within the
external dependency's implementation. Here's an example of a lightweight
UIImageView that can fetch remote images asynchronously with just a few lines of
code:

import Foundation
import UIKit

let imageCache = NSCache<NSString, UIImage>()

extension UIImageView {
/// Loads an image from a URL and saves it into an image cache,
returns
/// the image if already available in the cache.
/// - Parameter urlString: String representation of the URL to
load the
/// image from
/// - Parameter placeholder: An optional placeholder to show
while the
/// image is being fetched
/// - Returns: A reference to the data task in order to pause,
cancel,
/// resume, etc.
@discardableResult
func loadImageFromURL(urlString: String,
placeholder: UIImage? = nil) ->
URLSessionDataTask? {
self.image = nil

let key = NSString(string: urlString)


if let cachedImage = imageCache.object(forKey: key) {
self.image = cachedImage
return nil
}

guard let url = URL(string: urlString) else {


return nil
}

if let placeholder = placeholder {


self.image = placeholder
}

let task = URLSession.shared.dataTask(with: url) { data, _, _


in
DispatchQueue.main.async {
if let data = data, let downloadedImage =
UIImage(data: data) {
imageCache.setObject(downloadedImage,
forKey: NSString(string:
urlString))
self.image = downloadedImage
}
}
}

task.resume()
return task
}
}

There is always a delicate balance between reinventing the wheel, managing


your current bandwidth as a developer, and introducing unnecessary risk.

I want to emphasize that it is important to consider alternative approaches before


defaulting to a new dependency as the solution. It's far easier to introduce a
dependency than to remove one.
#14: Start With A Stylesheet

One of my first freelance experiences involved working with an individual who would
continuously alter the look and feel of the iOS application. As a result, I spent so much
time in the initial days of that project changing font and color attributes in each
.xib and .storyboard manually.

Don't make the same mistake - you're better off creating a "stylesheet" from the start
than trying to implement one later on.

These days, any new project I start begins like this. You can include as many properties
and sections as you require.

Here's a basic example:

import Foundation
import UIKit

enum Theme {
enum Constants {
static var standardMargin: CGFloat {
16
}
}

enum Colors {
static var companyDarkBlue: UIColor {
UIColor(named: "CompanyDarkBlue")!
}
}

enum Font {
static func regular(size: CGFloat) -> UIFont {
getCustomFont(withName: "CompanyCustomFont", size: size)
}

static func bold(size: CGFloat) -> UIFont {


getCustomFont(withName: "CompanyCustomFont-Bold", size:
size)
}

private static func getCustomFont(withName name: String,


size: CGFloat) -> UIFont {
if let font = UIFont(name: name, size: size) {
return font
}

return UIFont.systemFont(ofSize: size)


}
}
}

// Usage: Theme.Colors.companyDarkBlue
// Usage: Font.regular(size: 18.0)

One of the advantages of this approach is that you can define styling properties in one
location and they will propagate throughout the rest of the application.

Consider, for example, changing the look of the application to reflect changes in your
company's branding or changes to support night mode. Just by changing a few
properties in the code above, you would be able to overhaul the entire
appearance of your iOS app. Otherwise, you might have to go into each
.storyboard , .xib , and source-code file and update these properties
manually.

Furthermore, in the past, I have used a similar setup in white-labeled apps to


allow the backend to specify the "look and feel" of the application so it can be
styled differently depending on the customer. In other words, the backend
would specify the values in the Theme class above.

Next, you could then create subclasses of UIKit components and custom views you
intend to reuse in your application: such as UnderlineLabel , HeaderLabel ,
StandardTableViewFooterView . This will reduce the amount of repeated code in
your codebase and offer a single source of truth on all UI related topics. This greatly
speeds up development as you can define this class once and use it anywhere in this
project or easily carry it over into a new one.

With .storyboards , you'll often see clashes between what the .storyboard shows
and what the code specifies. It's common for a developer to specify a custom
UIView or custom style on the .storyboard only to override some properties later
in the code (i.e. cornerRadius , clipsToBounds , alpha ).

Therefore debugging becomes complex, as the programmer must now check multiple
locations to figure out the expected behavior. Instead, my suggestion would be to lay
out and specify the elements on the .storyboard / .xib , but any of the other
customizations (fonts, text colors, attributed text) should be specified in code. The
code - not the .storyboard - should be the single source of truth. Additionally, this
approach allows you to still retain features like "Search & Replace" and visibility in
Xcode's refactoring tool which wouldn't be possible if you were using the
.storyboard .
#15: Comments Need To Go Beyond "What"

When writing comments, explaining why the code is written a certain way is
arguably more important than explaining what it does. Programmers will
understand what a given piece of code does, but they might find it harder to infer the
context in which it was written and its possible side effects.

Here's some examples of good comments; they clearly indicate why the code was
written a certain way instead of what it does:

// TODO: When iOS 12 and lower support is dropped replace this


// method with withTintColor(color, renderingMode:)

// Declaring as private to avoid accidentally being instantiated from


// another instance.

// We're disabling the "Next" button as this is an invalid state and


the
// user has no available options.

For some bad examples, you'll see they don't provide any additional information or
insight the code doesn't already convey:

// Hide error label

// - Parameter urlString: String representation of the URL to load


the
// image from

// Presents the next view in the user flow

In addition, comments should:

Mention alternate approaches that you considered and explain why they were
not used.
Be used to provide a brief summary for a longer piece of code.
Be used to address questions or concerns the code is incapable of answering on
its own.
Clearly communicate any possible side effects that execution of the code might
cause.
Be tailored to other developers who may not have had the same experiences or
knowledge as you and may be less familiar with the code.
#16: DateFormatter & Locales

When an API expects dates and times in a specific format, it's important to
include the locale information in your DateFormatter .

Let's say we're using the following date format: "yyyy-MM-dd'T'HH:mm" .

We'd expect "April 6th, 2021 10:59 pm" to be formatted like this:

However, if you were to use the following code as is, there is an edge case that isn't
handled.

let timeDateFormatter = DateFormatter()


timeDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm"

If a user's region typically uses 12 hour time and they have specified in their iOS
settings to use 24 hour time or vice-versa, iOS will add an "AM" / "PM" suffix to
the date.

For example, France typically uses 24 hour time. If a French user has manually
enabled 12 hour time instead, the code above would have "AM" or "PM" tacked on the
end.

The simple fix here is to include locale information in your DateFormatter setup.
This will help standardize the formatting of your dates across various regions and
locales.

let timeDateFormatter = DateFormatter()


timeDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm"
timeDateFormatter.locale = Locale(identifier: "en_US_POSIX")
#17: Use Xcode's Documentation Features

As you are probably aware, you can easily create comments using the // or
/*...*/ .

However, there is another option in Xcode that is perhaps less well known - /// . In
general, this is meant to provide more context around a section of code - either the
inputs / outputs, the side effects, or the return values.

With this syntax, you can easily view the documentation for a method or class, without
having to use the Xcode shortcut to jump to its definition. This has saved me a lot of
time and enabled me to create more useable libraries and frameworks for other
developers.

This comment syntax can be applied to methods, classes, structs, etc.

Here's a straightforward example:

func showAlert(alertText: String, alertMessage: String) {


let alert = UIAlertController(title: alertText,
message: alertMessage,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default,
handler: nil))
self.present(alert, animated: true, completion: nil)
}

We're not doing anything special here. It's just a helper function that lets you easily
create and present a UIAlertController . If we put our cursor on the line where we
have declared the showAlert method, and then hit Command + Option + / , Xcode
will generate this:

/// Description
/// - Parameters:
/// - alertText: alertText description
/// - alertMessage: alertMessage description
func showAlert(alertText: String, alertMessage: String) {
let alert = UIAlertController(title: alertText,
message: alertMessage,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L10n.alertConfirmation,
style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}

Then, once it's filled out:


/// Presents a UIAlertController with a single button on any
UIViewController or subclass.
/// - Parameters:
/// - alertText: The title for the UIAlertController
/// - alertMessage: The message body for the UIAlertController
func showAlert(alertText: String, alertMessage: String) {
let alert = UIAlertController(title: alertText,
message: alertMessage,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L10n.alertConfirmation,
style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}

Any comment with /// will be surfaced in multiple locations across the Xcode
IDE. Your team would lose out on a major convenience if you were to opt out of
using this documentation style.

Now, pressing Option + Left Mouse Click on the call to showAlert() results in:

Option + Left Mouse Click Xcode Quick Help


#18: Keychain > UserDefaults

When I first started making iOS apps, I would abuse UserDefaults . I would store
large amounts of information there and used it as my primary method of passing data
between views. Obviously, that's not how it was meant to be used. Even more
damning was the fact that I was storing sensitive user data in UserDefaults .

You may also think, as I did, that storing your information there is safe enough. The
possibility that someone would have my phone, know my password, and reverse-
engineer the data seemed unlikely. However, this isn't as difficult as you might expect.

Remember, UserDefaults is not encrypted.

If you follow DEFCON and the jailbreaking community, you'll find plenty of tools and
approaches for retrieving this information. Instead, I should've been using Keychain .

Essentially, the Keychain is a system level mechanism that persists across app
updates and reinstalls for storing and encrypting sensitive data.

If you're planning on storing access tokens, passwords, or other sensitive data,


then you're going to need Keychain . Currently, it is the only encrypted offering
available for the iOS platform; Core Data , .plist , and UserDefaults are not
encrypted.

Instead of directly working with the Keychain API in C, you can use a Swift wrapper
like KeychainAccess.
#19: Follow Delegate Conventions

When you work with delegates, make sure you honor all of the following Apple
recommendations and conventions.

To start with, delegate references should always be weak or unowned . Otherwise,


you may inadvertently create a reference cycle.

Additionally, when creating custom delegate methods, the first argument should be a
reference to the entity that is triggering the delegate function.

Method names for delegate function should also include keywords like should ,
will , has , did to indicate to the programmer if the event will occur in the near
future or already has.

Let's look at some examples from UIKit.

func tableView(_ tableView: UITableView,


numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status:
CLAuthorizationStatus)
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions)

The first parameter is always unnamed and a reference to the entity from which the
delegate function is being called (e.g. tableView , manager , and scene ). With this
approach, naming conflicts can be avoided with other delegate functions that might
have the same function declarations.

Moreover, this allows us to differentiate between which object is responsible for


triggering the delegate function.

Let's consider the following implementation of a LoginViewController :

final class LoginViewController: UIViewController,


UITextFieldDelegate {
@IBOutlet fileprivate(set) var usernameTextField: UITextField!
@IBOutlet fileprivate(set) var passwordTextField: UITextField!

override func viewDidLoad() {


super.viewDidLoad()
usernameTextField.delegate = self
passwordTextField.delegate = self
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// If user hits Return on usernameTextField, take them
// to passwordTextField.
// If user hits Return on passwordTextField, attempt sign in.
return true
}
}

In the absence of this pattern - func textFieldShouldReturn() -> Bool - we


would have no way of knowing whether usernameTextField or the
passwordTextField triggered the call and would not be able to handle each
case separately.
#20: Avoiding Hardcoded Strings

Many applications have hard-coded strings throughout the code:

UITableViewCells and their reuse identifiers


Segue identifiers
Localized text
Titles of analytic events
Image names

It's easy for these hard-coded strings to become out of date or contain typos that
crash the application. I'm sure we've all experienced the crash that would occur in the
following situation:

// App will crash as it should be LoginViewController


self.performSegue(withIdentifier:"LognViewController", sender: self)

We'll look at some extensions and tools developed by the iOS community to
manage this issue and make our code more type-safe.

Let's assume we have a new UITableViewCell called HotelCell . You're probably


used to using it like this:

func tableView(_ tableView: UITableView,


cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier:
“HotelCell”,
for: indexPath) as!
HotelCell
return cell
}

So, what happens now if we change the name of the class? We'd have to go in and
manually update the identifier in any location we reference the HotelCell . In
situations like this, Xcode's refactor tool is far from comprehensive and reliable.

By adding a small extension, we can deal with custom cells and their reuse identifiers
in a more type-safe manner:
extension UITableViewCell {
static var identifier: String {
// Returns a String version of the current class's name.
return String(describing: self)
}
}

Our implementation can now be rewritten to look like this:

func tableView(_ tableView: UITableView,


cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(
withIdentifier: HotelCell.identifier, for: indexPath) as!
HotelCell
return cell
}

The change may seem minor, but it helps us prevent easy avoidable crashes and is
more suited to future refactoring.

It's easy to forget about updating and managing these hardcoded strings, now it's a
non-issue.

Let's take this one step further using tools like SwiftGen (https://github.com/SwiftGen/
SwiftGen).

By using this tool, we're able to add type-safety to our asset catalogs, colors, fonts,
storyboard files, and more.

Here's a simple configuration file (see GitHub for more detailed instructions):

strings:
inputs: Resources/Base.lproj
outputs:
- templateName: structured-swift5
output: Generated/Strings.swift
xcassets:
inputs:
- Resources/Images.xcassets
- Resources/MoreImages.xcassets
- Resources/Colors.xcassets
outputs:
- templateName: swift5
output: Generated/Assets.swift
If you add it to your project's Build Phases , it will run every time you compile. End
result here is that this utility will automatically generate the following Swift files for
you allowing you to access all your resources in a type-safe manner.

For your Colors.xcassets & Assets.xcassets , it generates something like this:

internal enum Asset {


internal enum Assets {
internal static let accentColor = ColorAsset(name: "AccentColor")
internal static let dish = ImageAsset(name: "dish")
...
internal static let markerUnselected = ImageAsset(name:
"markerUnselected")
internal static let mountain = ImageAsset(name: "mountain")
internal static let searchBarIcon = ImageAsset(name:
"searchBarIcon")
internal static let star = ImageAsset(name: "star")
}
internal enum Colors {
internal static let background = ColorAsset(name: "background")
internal static let boldText = ColorAsset(name: "boldText")
...
internal static let subtitleText = ColorAsset(name:
"subtitleText")
internal static let white = ColorAsset(name: "white")
}
}

// Usage: backgroundImageView.image = Asset.Assets.mapCard.image


// Usage: supportingTextLabel.textColor =
Asset.Colors.subtitleText.color

And for your .storyboard files:

internal enum Main: StoryboardType {


internal static let storyboardName = "Main"

internal static let listViewController =


SceneType<Demo.LunchListViewController>(
storyboard: Main.self, identifier: "ListViewController"
)

internal static let mapViewController =


SceneType<Demo.LunchMapViewController>(
storyboard: Main.self, identifier: "MapViewController"
)

internal static let mainViewController =


SceneType<Demo.MainViewController>(
storyboard: Main.self, identifier: "MainViewController"
)
}

// Usage: let mainViewController =


StoryboardScene.Main.mainViewController.instantiate()

Remember, all of this code is automatically generated and updated every time
you compile.

This is just scratching the surface of the possibilities with this tool. I encourage you to
try it out on a small project to see if you like it.

Since this runs at compile time, any changes - especially removals - to the
.xcassets , .storyboard , .strings , etc are raised as compilation errors
instead of runtime errors. Plus, we get auto-completion on our strings, images,
segue identifiers, assets, etc. for free!
#21: Working With Access Levels

Swift provides five access levels we can use with various entities (functions, types,
enums, extensions, classes, etc.) in our code.

Starting from most-restrictive to least-restrictive:

private implies that something can be seen only inside the entity in which it is
defined, or in extensions of that type in the same file.
fileprivate refers to something that is visible to all of the classes and
functions declared in the same source code file (even in extensions), but is
hidden from all other code.
internal is the default access level for Swift classes and properties. It limits
the access level to within the module that defined it. If you were building a
framework, for example, and marked a property internal , code that uses the
framework could not access it because it was not part of the same module.
Instead, you'd have to use public or open .
public enables entities to be used within any source file from their defining
module and also in a source file from another module that imports the defining
module.
open is similar to public with the addition that the entity can now be
subclassed or overridden by something that exists outside of its defining
module.

These access modifiers allow us to easily separate and modularize our codebase.
It is extremely important that our code behaves predictably as we would not
want one part of the code changing state in another unrelated part.

If an unrelated class has the potential to change your object's properties, then
ensuring you have accounted for all of the potential edge cases becomes much
harder. Correctly leveraging access modifiers makes code more testable, reduces the
potential for bugs, and helps prevent "spaghetti" code.

Additionally, consider the "Law of Demeter" - a popular programming rule that states:

Each unit should have only limited knowledge about other units: only units
"closely" related to the current unit.
Each unit should only talk to its friends; don't talk to strangers.
Only talk to your immediate friends.

In practice, this means that:

class BikeStore {
let unicycle = Unicycle()
}

class Unicycle {
let title: String = "Hello, I have one wheel!"
let wheel = Wheel()
}

class Wheel {
let circumference: Double = 20.0
}

let store = BikeStore()

// Since BikeStore knows about Unicycle, this is valid and


// doesn't break the Law of Demeter ✔
print(store.unicycle)

// However, this exposes too much information. We're talking to a


// "friend of a friend", so we are violating the Law of Demeter.
// Instead, `circumference` should be private. ❌
print(store.unicycle.circumference)

// Often times optional chaining in Swift violates the Law of


Demeter.
user?.profile?.photo?.thumbnailSize?.width

// Though it's an extreme example, you can see that there is no


// clear boundaries between the different objects and one part of the
// code can access properties several layers deep.
//
// Optional chaining, when used incorrectly, can tend to encourage
this
// bad behavior.

Knowing the Law of Demeter and utilizing the correct access levels can protect you
from opening up the codebase more than you should.

These are the rules I tend to follow nowadays:

I typically don't specify internal as it's the default; I'd rather be concise.
For @IBOutlets , I generally use fileprivate(set) .
Generally, I'll start of with the most restrictive access level - private - on all
definitions (instance variables, classes, properties, computed variables, etc.) and
re-evaluate on a case-by-case basis.

When changing access levels on any entity, be sure to consider the possible side
effects and any potential other options. Should I really change this access level or is
there another solution? Am I putting myself at risk of having bugs or introducing
"spaghetti" code later on if I make this change?

Lastly, according to an Apple's blog:


Like many other languages, Swift allows a class to override methods
and properties declared in its superclasses. This means that the
program has to determine at runtime which method or property is being
referred to and then perform an indirect call or indirect access.
This technique, called dynamic dispatch, increases language
expressivity at the cost of a constant amount of runtime overhead for
each indirect usage. In performance sensitive code such overhead is
often undesirable.

The article continues on to say that using final , private , and Whole Module
Optimization will help improve the compile time and performance of your
application. Another reason to start leveraging more restrictive access levels.
#22: Computed Properties

Swift's computed properties are one of my favorite features, but it's worth discussing
when they are appropriate to use. Unlike traditional properties that you assign a value
directly to, computed properties in Swift don't have any "storage" ability.

With computed properties, this expression is not possible:

var x = 5

Instead, when computed properties are referenced, they generate a return value, for
example:

private var radius: Double = 0

// `circleArea` doesn't contain a property, it just computes one when


called.
var circleArea: Double {
return Double.pi * radius * radius
}

radius = 2
print(circleArea) // 12.57

This means we need to be careful when we use them and ensure we implement
them in an efficient manner.

Let's say we would like to access a computed property in func tableView(_


tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell . This would mean that whenever a cell will be displayed and this
function is executed, the computed property will be re-generated. It may happen
hundreds of times as the user scrolls through the content in the UITableView . In the
event that generating the computed property is computationally expensive, our app
will surely encounter a performance hit.

When To Use

A computed property should have a simple implementation. A function that works


by taking no arguments and returns a single value without producing any other
side-effects could be re-written as a computed property.

The following example illustrates where using a computed property is appropriate:

struct Context {
let file: String
let function: String
let line: Int
var description: String {
return "\((file as NSString).lastPathComponent):\(line) \
(function)"
}
}

var sizeOfView: CGFloat {


switch self {
case .body, .small, .medium, .large:
return 16
case .xLarge:
return 24
case .tv:
return 36
}
}

You'll notice that the implementations in the closures are simple and do not alter the
state of other properties in the application.

When Not To Use

Here are some examples of situations where you should opt for a stored property or a
traditional function instead:

Making network calls


Long synchronous / slow operations (like reading from a database or file)
Computationally expensive tasks in general
If the computed property will always return the same information, then it
shouldn't exist. It should be a normal variable instead. Use computed properties
only if the value they return is going to change throughout the lifetime of the
program.
The code in the closure can throw an error or an exception.

The following example is a bad use of computed properties. Since the API key within
the .plist won't change, there's no reason to read from disk every time you want to
use the apiKey property. As you can imagine, apiKey might be referenced many
times throughout the lifetime of the application and unnecessarily reading from disk
will only slow the program down.
// Slow operation, incurring a performance cost every time this is
accessed.
static var apiKey: String {
return loadKeyFromPlist(key: "GOOGLE_API_KEY")
}

// This shouldn't be expressed as a computed property as the value


// will never change.
// Instead, it should simply be `var kNumberOfTotalLivesInGame = 3`
var kNumberOfTotalLivesInGame: Int {
3
}

If you find yourself regularly reading the property and the value hasn't changed, then
you should opt for a stored property instead.

Generally speaking, your main concern should be whether the code behind the
computed property:

Is a slow / expensive process


Throws errors or exceptions
Modifies state or has side effects elsewhere in the code

If none of these concerns apply, and the value of the computed property changes
overtime, then it's a reasonable implementation choice.
#23: Project & App Optimization Tips

Here are some general tips you can use to optimize your Swift projects.

Opaque Views

To improve rendering performance it's helpful to limit the amount of transparency in


views. If you have a view without any transparency, make sure to set its isOpaque
property to true . This allows the system to render your views in a more optimal
order.

Unused Resources

Every now and then it's good practice to go through your project and remove unused
assets, classes, functions, colors, and compiler directives. These can be "quick wins"
and often help in reducing app size and app launch time.

Don't print() In Release

Unnecessary calls to print() can really slow down your app.

While they're often useful in development, they aren't as useful in production. Instead,
you could easily use the Log class shown previously and use a compiler directive to
disable unnecessary print() calls when the release build of the app is running.

Additionally, if an attacker is running your iOS app on a jailbroken phone, they may be
able to reverse-engineer sensitive data by looking at the log output of your
application.

Image View

Resizing images at runtime is an expensive process. So, if you are exporting assets
from Figma / Sketch, double-check that the resolution of the exported assets is
proportional to the display size in the app. The application does not need to include
images that are larger than the image view they are intended to appear in. This simple
fix can help decrease app size and improve performance.

Structs > Classes

Due to the absence of inheritance and the "pass by value" nature of structs, Swift is
able to make certain compiler optimizations that are unavailable on "pass by
reference" types.

When dealing with reference types, using private and final allow for additional
compiler optimizations with regards to dynamic dispatch.
Dynamic dispatch is the process of selecting which implementation of a polymorphic
operation to call at run time. When we use the final keyword, we know that no
subclass / alternative implementation exists, so Swift is able to execute the function
call directly without this extra computation step of checking for alternative
implementations.

Build Settings

Now, we'll look at some of the best settings for compilation performance here.

Build Phases

Check your Build Phases and make sure only the absolutely required scripts are
running for your Debug and Release builds, respectively.

Compilation Mode

We have two compilation modes available to us:

Whole Module compilation will rebuild all files in the project regardless of
whether they were modified since the last build.
Incremental only recompiles files that have changed since the previous build.

For a faster development flow, we can choose Incremental for our debug release
and defer Whole Module compilation for our production release.

The suggested configuration would be:

Debug: Incremental
Release: Whole Module

Optimization Level

As the name suggests, this property instructs the compiler about the degree of
optimizations it should make.

We have 3 options here:

No Optimization
Optimization for Speed (refers generally to the build time)
Optimization for Size (refers to the overall size of the machine code
generated)

The suggested configuration would be:

Debug: No Optimization - This allows us to still debug values specified by let


and var which are essential for debugging. Otherwise, they would be optimized
away.

Release: Optimize for Speed - This would improve the performance of the
version of the app we intend to distribute.
Build Options

A .dSYM file is used for re-symbolicating your crash reports (converting memory
addresses of objects and methods into more readable data).

Symbolication allows us to better protect our code from hackers and reverse-
engineers. We don't need such a service during development as we'd have access to
the debugger in Xcode. Instead, the benefits of a .dSYM file are more evident in
production builds.

So, the recommended settings here are:

Debug Information Format: Debug - DWARF

Debug Information Format: Release - DWARF with dSYM file

DWARF (debugging with attributed record formats) is a debugging file format


used by many compilers and debuggers to support source-level debugging.

Instruments

Instruments is an incredibly powerful developer tool that comes with Xcode. I'd
recommend taking the time to play around with it and better understand how you can
use the Time Profiler , Energy Log , Network , and Leaks utilities to better
understand shortcomings in your application.

Modular Architecture
As your codebases becomes larger and larger, you may benefit from chunking
relevant sections of your codebase into separate modules (UI, Data Persistence,
Authentication, etc.). You can use tools like Cocoapods to share them between
projects and easily manage updates.

Modularing your code in this manner allows Xcode to only compile the modules that
have changed - similar to the way Incremental compilation works.
#24: Static vs. Dynamic Libraries

As your application matures and your application size and launch speed start to
suffer, you'll likely find yourself re-evaluating how you integrate libraries into your
project.

Libraries and frameworks can either be linked statically or dynamically.

Static libraries are collections of object files which are essentially the machine code
output after compilation. If you’ve ever seen a file ending in .a , it’s a static library.
These files are copied into the larger executable that eventually runs on your
phone. Imagine a suitcase filled with everything you need for your vacation. A static
library is similar; everything you need in order to run is included in the executable
itself.

Dynamic libraries ( .dylib files) are loaded into memory when needed, instead of
being included in the executable itself. All iOS and macOS system libraries are
actually dynamic. The main advantage here is that any application that relies on these
dynamic libraries will benefit from all future speed improvements and bug fixes in
these libraries without having to create a new release. Additionally, dynamic libraries
are shared between applications, so the system only needs to maintain one copy of
the resource. Since it’s shared and only loaded when needed, invoking code in a
dynamic library is slower than a static one.

Let’s take a detailed look at the advantages and disadvantages:

Static Libraries

Pros

Guaranteed to be present and the correct version at runtime.


The application can run without any external dependencies. You don’t need to
monitor library updates since the object files are part of the executable itself. As
a result, it becomes standalone and can move from platform to platform.
Faster performance compared to calls made into a dynamic library.

Cons

Makes the executable larger as it simply just contains more code.


Your application will become slower to launch as the library needs to be loaded
into memory during app launch.
Any changes in a static library require the application to be compiled and re-
distributed.
You have to integrate the entire library even if you only rely on a small portion of
it.

Dynamic Libraries
Pros

Doesn’t increase app size.


Faster application start time as the library is loaded only when needed.
Only the relevant section of the library for the current execution is loaded, not
the entire library.

Cons

Application may crash if the library updates are not compatible with your
application (i.e. business logic / iOS version).
Application may crash if the dynamic library cannot be loaded / found.
Calls to dynamic library functions are slower than with static libraries.

There’s no one size fits all answer. You’ll have to make a pragmatic decision and weigh
how much you value performant library calls, app size, launch time, etc. and pick the
approach or a hybrid approach that best suits your needs.
#25: Careful with default

All new states or behaviors introduced into an application should be done


intentionally. It's the same reason that even though the compiler knows where to put
the missing ; , it knows better than to make changes to the code without the
programmers approval.

When you use default cases in your code, you introduce new states to your
application that are not explicitly handled. Instead, you should opt to specify all
cases and rely on the compile errors to notify you about states you are not explicitly
handing.

Shield your eyes, some Objective-C is coming.

Imagine Apple changes some internal enum - in this case, the


UIUserInterfaceIdiom enum.

typedef NS_ENUM(NSInteger, UIUserInterfaceIdiom) {


UIUserInterfaceIdiomUnspecified = -1,
UIUserInterfaceIdiomPhone API_AVAILABLE(ios(3.2)), // iPhone &
iPod touch
UIUserInterfaceIdiomPad API_AVAILABLE(ios(3.2)), // iPad style UI
UIUserInterfaceIdiomTV API_AVAILABLE(ios(9.0)), // Apple TV style
UI
UIUserInterfaceIdiomCarPlay API_AVAILABLE(ios(9.0)), // CarPlay
style UI
UIUserInterfaceIdiomMac API_AVAILABLE(ios(14.0)) = 5, // Mac UI
};

switch UIDevice.current.userInterfaceIdiom {
case .phone:
// It's an iPhone
case .pad:
// It's an iPad (or macOS Catalyst)

@unknown default:
// Uh, oh! What could it be?
}

Imagine we're on iOS 13.0 and iOS 14.0 is just released. Had we used a
default case in our switch , we wouldn't have even know about the existence of
UIUserInterfaceIdiomMac and would have lost the opportunity to make a different
product decision on that platform.

Here is the UIDeviceBatteryState enum:


typedef NS_ENUM(NSInteger, UIDeviceBatteryState) {
UIDeviceBatteryStateUnknown,
UIDeviceBatteryStateUnplugged, // on battery, discharging
UIDeviceBatteryStateCharging, // plugged in, less than 100%
UIDeviceBatteryStateFull, // plugged in, at 100%
} API_UNAVAILABLE(tvos); // available in iPhone 3.0

Let's say we're developing a navigation app that utilizes the user's location, but tries to
ensure it's not polling too frequently for the available battery charge.

It's conceivable that Apple may one day provide more granular battery updates. By
using the default case in our switch , we would have missed the opportunity to
recognize this new state. As a result, we would miss the chance to make optimizations
in our application - i.e. changing the user location polling frequency.

Or, one of your 3rd party dependencies may introduce a new case that your
default statement happily accommodates. This approach can easily introduce an
unknowable number of new states into your application - none of which you've
explicitly handled.

Although there are exceptions to every rule, I'd caution you against using default
unless you're absolutely confident you've mitigated the risk.
#26: Establish A Pull Request (PR) Template

A simple PR (pull request) template can save your team a lot of time and it takes very
little effort to set up. It will keep everyone on the same page and helps
standardize your PR structures and code review philosophies. It can also serve
as a useful checklist before you ask your team for a review. If you don't already
have one, it is very useful once in place.

Here's a starting template you can use - feel free to customize it as needed.

If you're using GitHub to manage your repository, simply save the following
template in your project's root folder as pull_request_template.md .

### Description of Changes

<!--
Add overview of changes including technical details here
- Include links to the ticket, design, and relevant documentation
-->

### Screen Shots

<!--
Remove this section if there are no UI changes
-->

### Pull Request Checklist

- [ ] CHANGELOG updated (exceptions: documentation, PRs into feature


branches, and hotfixes for unreleased features)
- [ ] Manually tested (different device sizes if necessary)
- [ ] Voice-over tested
- [ ] Analytics included
- [ ] Unit tested
- [ ] QA tested
- [ ] Merges into a feature branch
- [ ] Hotfix into the release branch

### Note

- Prefix PRs and commits with Jira ticket numbers. (optional)


- Our [standards](https://example.com/master/docs/code-style.md)

#27: Moving Navigation Out Of The UIViewController


UIViewControllers tend to get overburdened with responsibilities that go far
beyond their initial scope. It's not uncommon for UIViewControllers to manage
network calls, navigation, auto layout, handle user input, etc.

In the past few years, Soroush Khanlou has popularized the use of the Coordinator
design pattern in iOS applications, and you'll now find variations of this pattern in
many modern iOS codebases.

This pattern attempts to remove navigation responsibilities from a


UIViewController . Instead, it promotes the idea of having an object whose
sole purpose is to manage what UIViewControllers are presented and when.
Instead of a UIViewController presenting the next view in the flow, the
Coordinator determines what the next view should be and passes the relevant
data along.

The main objective is to create a UIViewController that's unaware of what


UIViewController presented it and what UIViewController will come after it.
Instead, the active UIViewController should inform the presenting class about
different actions and events via a delegate.

By doing so, we can greatly increase the usability of that UIViewController as it can
now be dropped into many different user flows without any changes.

Imagine a social media app like Facebook or Instagram. In these application, you have
multiple paths you can take in the app in order to capture an image.

Rather than creating a new image capture related UIViewController for every flow
a user could take through the app, it would make much more sense to create a single
image capture UIViewController that you then introduce into the needed flows.

Imagine we have two Coordinators :

The OnboardingCoordinator guides the user through a series of initial


UIViewControllers that act as a tutorial on how the application works.
The BookingCoordinator application manages a series of
UIViewControllers that enable the user to book a hotel.

The key takeaway from the following example should be that the
UIViewControllers are standalone entities. They would have no understanding of
what came before them or what would come after them. Navigation logic now
belongs strictly to the Coordinator class.

Here's an example of the Coordinator pattern in action:

// This is all you need to start the Coordinator pattern.


protocol Coordinator {
func start()
}

// These are a few simplified UIViewControllers.


//
// Imagine they show the user some info on the screen and a "Next"
button.
// When the user clicks the "Next" button, it triggers the delegate
function.
protocol FirstOnboardingVCDelegate: AnyObject {
func didClickNextOnFirstOnboardingVC()
}
class FirstOnboardingVC: UIViewController {}

protocol SecondOnboardingVCDelegate: AnyObject {


func didClickNextOnSecondOnboardingVC()
}
class SecondOnboardingVC: UIViewController {}

// This coordinator is now responsible for managing the view


controllers
// defined above.
class UserOnboardingCoordinator: Coordinator {
let navigationController: UINavigationController
let firstVC = FirstOnboardingVC()
let secondVC = SecondOnboardingVC()

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}

// Whenever the start function is called, the first step in this


// flow is to present firstVC.
func start() {
// Show first onboarding page
firstVC.delegate = self
navigationController.present(firstVC, animated: true,
completion: nil)
}

func showSecondOnboardingPage() {
// Present second onboarding page
secondVC.delegate = self
navigationController.present(secondVC, animated: true,
completion: nil)
}

func finishOnboardingPage() {
// Start the next Coordinator
// Maybe the BookingCoordinator to guide the user through all
// the screens to book a hotel, for example.
let bookingCoordinator = BookingCoordinator(
navigationController: self.navigationController
)
bookingCoordinator.start()
}
}

extension UserOnboardingCoordinator: FirstOnboardingVCDelegate {


func didClickNextOnFirstOnboardingVC() {
// Show the next view in the flow
showSecondOnboardingPage()
}
}

extension UserOnboardingCoordinator: SecondOnboardingVCDelegate {


func didClickNextOnSecondOnboardingVC() {
// Show the next view in the flow
finishOnboardingPage()
}
}

// To start the whole process off, you would run the


// following command whenever you want to start this flow.
let coordinator = UserOnboardingCoordinator(
navigationController: UINavigationController()
)
coordinator.start()

If you kept this navigation logic in the UIViewControllers directly, they would
become tightly coupled with that specific user flow.

Now, you could easily re-use the UIViewControllers in this example to create an
ExpeditedOnboardingCoordinator or EditHotelBookingCoordinator instead.
Since all the UIViewControllers know how to do is report on actions / events that
occur, you can string them together in whatever flow you wish.

You can use this pattern in a variety of other situations too:

Forgot Password
Account Registration
Surveys / Questionnaire (sequences of UIViewControllers you want to present to
the user and record results along the way)
Bookings (AirBnB, Turo, Lime, hotels, etc)
Onboarding (tutorial composed of a series of UIViewControllers)

Moreover, Coordinators are well suited to support A/B testing. A series of screens
can be created to match the experiment you're running or any other feature flags you
may have set in place.

Though understanding this pattern can be tricky, almost every company uses some
variation of this pattern. Feel free to choose the variation that appeals most to you.
#28: @discardableResult

@discardableResult is a lesser-known feature of Swift, but when used properly, it


allows you to write more readable code.

Basically, if you have a function that returns a value that you may not always
need, you can mark it as having a @discardableResult . When you use this
keyword the compiler warning will be suppressed and you will no longer be
notified that the function you're using has a return value that's not being used.

Consider the standard removeFirst() function:

@discardableResult public mutating func removeFirst() -> Self.Element

The exact value dropped may not matter to you at all. For example, you might only
want to drop the first element in an array and then continue on with the remaining
elements. Since the function is marked with @discardableResult , you are under no
obligation to do anything with the return value if all you need is the side effect of
calling this function.

The @discardableResult property is commonly used in conjunction with HTTP


request methods. For example, if you do not intend to pause or cancel the request,
there is no need to maintain a reference to the returned URLSessionTask .

import Foundation
import UIKit

let imageCache = NSCache<NSString, UIImage>()

extension UIImageView {
/// Loads an image from a URL and saves it into an image cache,
returns
/// the image if already available in the cache.
/// - Parameter urlString: String representation of the URL to
load the
/// image from
/// - Parameter placeholder: An optional placeholder to show
while the
/// image is being fetched
/// - Returns: A reference to the data task in order to pause,
cancel,
/// resume, etc.
@discardableResult
func loadImageFromURL(urlString: String,
placeholder: UIImage? = nil) ->
URLSessionDataTask? {
self.image = nil
let key = NSString(string: urlString)
if let cachedImage = imageCache.object(forKey: key)) {
self.image = cachedImage
return nil
}

guard let url = URL(string: urlString) else {


return nil
}

if let placeholder = placeholder {


self.image = placeholder
}

let task = URLSession.shared.dataTask(with: url) { data, _, _


in
DispatchQueue.main.async {
if let data = data, let downloadedImage =
UIImage(data: data) {
imageCache.setObject(downloadedImage,
forKey: NSString(string:
urlString))
self.image = downloadedImage
}
}
}

task.resume()
return task
}
}

// You can use either depending on your use case, but now it's not
// required to "save" the return value.

// Usage: thumbnailImageView.loadImageFromURL(urlString: photoURL)


// Usage: let urlDataTask =
thumbnailImageView.loadImageFromURL(urlString: photoURL)

Using this keyword clarifies the intent of your function, its side effects, and prevents
warnings for the programmer that calls it.
#29: Knowing When To Use Structs vs. Classes

When you encounter situations where both a struct and a class are plausible options,
it is important to have some guidelines to help determine which one to use.

Use a struct if:

Your intent is to encapsulate some simple data values.


It is not necessary to inherit properties or behavior from other types.
The process simply involves converting an API response into an immutable
domain model.
You need thread safety (remember structs are "passed by value").
The encapsulated values and the struct itself are meant to be passed by value to
other entities in the application.
Don't use a class when a struct would do.

struct Photo: Decodable {


let height: Int
let photoReference: String
let width: Int

enum CodingKeys: String, CodingKey {


case height
case photoReference = "photo_reference"
case width
}
}

Use a class if:

You need to be able to support an inheritance hierarchy.


The entity in question contains a lot of data, so "passing by value" would be
computationally expensive.
You need Objective-C interoperability.

final class LoginViewController: UIViewController,


UITableViewDelegate {
...
}

When in doubt, Apple recommends starting with a struct and transitioning to a class
later on if needed.
#30: Child View Controllers

Similar to building complex views with UILabels , UITextFields , and UIButtons ,


we can easily build sophisticated UIViewControllers by introducing child view
controllers.

Child view controllers are no different than regular UIViewControllers , except


in that they're added to existing UIViewControllers . They behave the exact
same way - they receive lifecycle events ( viewDidLoad() , viewDidAppear() ,
etc.), can present / dismiss other view controllers, and can have custom logic
too.

Leveraging them lets you easily divide a UIViewController 's functionality into a few
modular pieces. This helps prevent the Massive View Controller problem and
allows you to create more reusable components.

Check out the following UIViewController , which displays a map view and a list of
turn-by-turn directions across the bottom of the screen.

You'll have to forgive my graphic design skills...


You might create a UIViewController consisting of a UICollectionView and a
MKMapView and all the logic to work with both components contained inside:
class MapViewController: UIViewController {
@IBOutlet var mapView: MKMapView!
@IBOutlet var directionsCarouselView: UICollectionView!

func setupMap() {}
func setupDirectionsCarousel() {}
func getUserLocation() {}
func drawRouteLine() {}
func fetchDrivingInformationFromAPI() {}
}

Clearly, this UIViewController is doing too much. It's making network calls,
managing user location, updating the UI, etc.

We can make this a lot cleaner and more re-useable with child view controllers.

// The responsibilities of MapViewController now make much more sense


class MapViewController: UIViewController {
func setupMap() {}
func drawRouteLine() {}
func getUserLocation() {}
}

class DirectionsCarouselViewController: UIViewController {


func setupDirectionsCarousel() {}
}

class TurnByTurnDirectionsViewController: UIViewController {


let mapViewController = MapViewController()
let carouselViewController = DirectionsCarouselViewController()

func setup() {
// Adding 2 child view controllers
addChild(mapViewController, to: self.view)
addChild(carouselViewController, to: self.view)

mapViewController.setupMap()
carouselViewController.setupDirectionsCarousel()
}

func fetchDrivingInformationFromAPI() {
// Now this parent view controller is only responsible for making
// the network call and passing the relevant data down to its
children
// view controllers - the MapViewController and
// DirectionsCarouselViewController - to display.
}
}

extension UIViewController {
// The following code tells iOS that the current view controller
will
// now be managing the view for a new child view controller as
well.
// Then, we add the child view controller's view to our view and
// finally call didMove(toParent:) which informs the child view
// controller that it has a parent.
func addChild(_ child: UIViewController, to view: UIView) {
addChild(child)
view.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.pinEdges(to: view)
child.didMove(toParent: self)
}

func removeChild() {
guard parent != nil else {
return
}

willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}

Now, the responsibility and scope of each view controller is more clear. With separate
UIViewControllers to manage the map and carousel, they can be reused
independently within your application. Or, the
TurnByTurnDirectionsViewController could itself be a child of a more
sophisticated view controller.
#31: Increase Readability With typealias

One simple way to make your code more readable is to use Swift's typealias
keyword. As the name suggest, it provides an alias for an existing type.

Imagine we were trying to represent time on a clock. We could use typealias to


make tuples easier to work with.

typealias ClockTime = (hours: Int, min: Int)


ClockTime(3, 30)

func drawHands(clockTime: ClockTime) {


print(clockTime.hours) // 3
print(clockTime.min) // 30
}

An excellent example is Swift's Codeable protocol which is implemented as a


typealias .

public typealias Codable = Decodable & Encodable

By the way, did you notice that you can combine protocols with & in Swift?

Now, whenever the compiler sees Codable it will replace it with Decodable &
Encodable and continue compilation.

If you wish to make the typealias accessible throughout your codebase, declare it
outside of any class. If not, the typealias will be limited in scope, as any other
variable would be.

// Accessible across the codebase


typealias ClockTime = (hours: Int, min: Int)

class HelloWorld {
// Only available within this class
typealias Greeting = String

func sayGreeting(greeting: Greeting) {}


}

It's easy to overuse typealiases and thereby limit the discoverability of the code,
but as long as you exercise a little restraint, you'll be fine.
#32: Use Singletons Sparingly

Singletons are exceeding popular in iOS. For those unfamiliar with the idea, a
singleton is simply a class that is instantiated once and only once. Or, more formally -
singletons provide a globally accessible shared instance of a class.

You've likely encountered them before:

UIApplication.shared
UserDefaults.standard
OperationQueue.main

No matter where you call these classes in your code, you're always referring to the
same instance.

The use of this pattern is a contentious topic in the software development community
and many developers consider this to be an "anti-pattern" - a design pattern that
should be avoided, not endorsed.

Sometimes singletons are reasonable choices, for example:

Shared state like an app-wide cache or local storage.


A shared URLSession for grouping related network calls.
A shared queue to manage and prioritize different asynchronous operations.

What often happens with singletons is that their sheer convenience ends up creating
code that is untestable, lacks modularity, and isn't stateful.

We'll take a look at exactly how that happens shortly, but let's first understand how to
implement one.

The code is fairly simple. Our main objective is to make sure that there is only one
instance of a class during its lifetime.

Here's a simple implementation in Swift:

class Singleton {

static let shared = Singleton()

private init() {}

// This pattern should look familiar to UIApplication.shared,


// UserDefaults.standard, etc.

// Usage: Singleton.shared
My making a private init , we ensure that no other class is able to create an
instance of Singleton . Additionally, the static reference allows it be globally
accessible.

Imagine you create a singleton to manage the currently logged in user in your app,
let's call it UserManager .

Initially, this seems like a reasonable approach, but UserManager is accessible


throughout your entire application. This means that any part of your codebase has
the potential to access and change properties on the currently logged in user. Kinda
scary, right?

With an approach like this, we've relinquished all control. It'd be exceedingly difficult to
debug and identify what part of the code is causing changes to the properties on the
UserManager singleton - it could be anywhere across the entire codebase. As the
project matures, you'll inevitably lose track of what objects have access to the
UserManager singleton and how they modify its properties.

Remember Swift isn't thread safe by default, so having different areas of your code all
potentially mutating the same property is asking for unpredictable behavior in your
app. Not to mention how untestable something like this would become.

Another issue to keep in mind is that since singletons are globally accessible, they end
up housing logic that exceeds their initial scope. In the same way that a
UIViewController tends to be responsible for more topics than it should be, the
same problem can happen with singletons.

They become a dumping ground for all sorts of properties and helper functions. And,
since they're accessible everywhere it can cause the codebase to quickly devolve into a
big mess of "spaghetti" code with no clear separation of concerns. Furthermore, if you
have to eventually remove one of these singletons it can be quite painful as they tend
to be referenced hundreds of times across an application.

We can still use singletons provided we go about integrating them into the code
correctly. The recommend approach for doing this is dependency injection. Instead of
passing a reference to the full UserManager class to something that relies on it, you
just pass the select properties it needs instead. This will help prevent side-effects and
will make the code more testable.

Let's continue with our UserManager singleton. Previously, we might have had code
like this:

class ForgotPasswordViewController: UIViewController {


func sendPasswordResetEmail() {
sendEmail(UserManager.shared.currentUser.email)
}
}
Refactoring this to use dependency injection might look like this instead:

class ForgotPasswordViewController: UIViewController {


var email: String?

func injectDependencies(email: String) {


self.email = email
}

func sendPasswordResetEmail() {
sendEmail(email)
}
}

In this approach, the ForgotPasswordViewController doesn't have access to the


singleton - it doesn't actually need it. In just needs some email to operate. With this
approach, it's now even more re-useable because it can accept any email given to it,
instead of strictly the UserManager.shared.currentUser.email .

Can you see how it's much more testable now? We can easily inject any email address
for testing purposes instead of having to simulate a logged in user.

So, singletons should only be used if absolutely necessary - the should not be your
default solution. In addition, instead of giving unlimited access to the singleton, you
should carefully consider whether a stricter bound on its properties would be
sufficient (e.g. dependency injection). Typically, this would enhance the testability of
your code and clarify the interface of the class that would be using the singleton's
data.
#33: Utilizing Code Snippets

One of the fastest and easiest ways to speed up your development workflow is
to use Code Snippets in Xcode. Simply press Command + Shift + L or go to View
Menu - Show Library to see the default snippets that come with Xcode.

You can start typing and autocomplete will provide relevant snippets for you to insert
into your code. You can also create your own snippets for your personal use or to
share with your team.

Just select some code, right-click and select Create Code Snippet . If you notice any
common patterns or Swift extensions that you tend to use repeatedly, you can easily
add them here.

Simply save the snippet with a title and description and it'll be ready for immediate
use without restarting Xcode.
My snippets often relate to:

Custom initializers
Setup of testing code
Creating an HTTP request
Loading from a .xib or .storyboard
Date formatting and conversions
User permission requests (camera, notifications, microphone, etc.)

In addition to a private collection for my personal use, I found it helpful to create a


repository of common snippets for the entire team to contribute to.

In Xcode, your custom snippets are stored here:


~/Library/Developer/Xcode/UserData/CodeSnippets . You can install new
snippets by dropping them into this folder.

Furthermore, code snippets can reduce the number of comments in code review
about not adhering to conventions by standardizing the code as a whole.
#34: Learn Some Basic DevOps (Fastlane, CircleCI, BitRise)

When I started my first job out of college, I was the only iOS developer on the team.
Even though I was just a junior developer, I had to take on some DevOps (software
development (Dev) + IT operations (Ops)) responsibilities.

Understanding this tooling is an excellent way to distinguish yourself from other


junior developers. You may be assigned the task of managing the release for a
sprint which would require you to debug the release pipeline if it fails.

At a minimum, I'd recommend learning fastlane and practicing integration with at


least one CI / CD (Continuous Integration / Continuous Deployment) platform.

CI/CD is a development process that aims to increase the speed at which


development teams can ship code. Every time new code is checked into the
codebase, the CI/CD tool will run a series of tests and automated scripts to
ensure the stability and accuracy of the codebase. This is used to ensure that
the master copy of the codebase is deployable at all times and passes all of the
tests. It provides assurances to development teams that new issues and
regressions will be caught early.

fastlane (http://fastlane.tools/)

As a new developer, every time I needed to upload to the App Store, I would trigger
the build from my laptop. Obviously, this isn't sustainable. What if a hotfix needs to be
released and I'm not available to trigger a build?

Or, what happens if my laptop breaks? I'd lose access to my private keys and other
development certificates.

fastlane is an open source platform that enables you to easily create


localized screenshots of your iOS application, submit builds to TestFlight, upload
builds to the App Store, manage code signing, and more all from the command
line.

That's where tools like fastlane come in. It's meant to help iOS teams with every
aspect of their release. The learning curve is steep, but the payoff is worth it.

Essentially, it works on the notion of "lanes", which are sequential groupings of


commands. There could be a build lane for your QA (quality assurance) team, a
production lane for the App Store release, another for TestFlight users and so on.

Here's an example of a typical fastlane configuration. It will create new screenshots


for the App Store, manage code signing, upload, and notify your team via Slack.
lane :release do
# generate new screenshots for the App Store
capture_screenshots
# see code signing guide for more information
sync_code_signing(type: "appstore")
build_app(scheme: "MyApp")
# upload your app to App Store Connect
upload_to_app_store
slack(message: "Successfully uploaded a new App Store build")
end

This is just the tip of the iceberg. I highly recommend practicing setting up fastlane
on one of your personal projects as this tool is widely used across iOS teams.

CI / CD (CircleCI, BitRise, TravisCI)

Perhaps you work for a large company with a dedicated DevOps team, but for those
who are interested in the startup space, this will certainly be something you should
know and might be expected to implement.

I've also found that in the process of setting up these services, I've gained a much
deeper understanding of how multiple targets, code signing, provisioning profiles, and
certificates work. Relying on Xcode to "Automatically manage signing" left a lot of
opportunity for technical understanding on the table.

CI / CD services can also be a little fickle...

Often, new versions of iOS or Xcode can break your pipeline or at the very least
introduce some unintended side effect.

Those failures can be very expensive - both financially and as a time cost to your team.
If the CI/CD pipeline fails, then this can easily result in a backlog of pull requests that
cannot be safely integrated into the codebase and can make releasing a build of your
application more difficult as well.

You'll be miles ahead of your colleagues if you learn how to set up, customize, and
debug CI/CD pipelines.

I have been using these tools everyday since my first freelance client to my first day as
a junior engineer and now as a senior engineer.

Demonstrating this type of competency will not only increase your skill set and
understanding, but will also make you a more valuable member of your team.
#35: Making the Happy Path Clear

By using guard statements, you can write self-documenting code that's easy to read.

When you use them to stop execution early when conditions aren't met, you
demonstrate to other programmers the ideal set of circumstances under which
the code is meant to run.

Take a look at this function, for example. The nested if statements make it hard to
read and it's even harder to figure out what the ideal input values should be.

func generatePrice(account: Account?, itemsPrice: [Double]?) throws -


> Double {
if let account = account {
if let itemsPrice = itemsPrice {
return itemsPrice.reduce(0.0, +) + account.additionalCharge
} else {
throw PricingError.noItems
}
} else {
throw PricingError.noAccount
}
}

Instead, we can rewrite it as:

func generatePrice(account: Account?, itemsPrice: [Double]?) throws -


> Double {
guard let account = account else {
throw PricingError.noAccount
}

guard let itemsPrice = itemsPrice else {


throw PricingError.noItems
}

return itemsPrice.reduce(0.0, +) + account.additionalCharge


}

By replacing a cascading series of if statements with multiple guard commands,


the code becomes much more understandable and concise. Now, it becomes clear to
another programmer that this code is intended to run only when there is a valid
account and itemPrice parameter.
#36: Remove Unnecessary self References

Although we always prefer clarity over brevity, sometimes unnecessary self


references render our code more difficult to read.

Instead, we should only use self when it's required by the compiler. In practice,
this means @escaping closures, initializers, completion blocks, etc.

Unlike Objective-C, Swift does not require us to use self when accessing an object's
properties or methods. So, if the compiler doesn't require it, the code will likely be
more readable without it.

Instead of this:

fileprivate func setup() {


self.layer.cornerRadius = 6
self.layer.borderWidth = 1
self.layer.borderColor = Asset.Colors.lightGray.color.cgColor
self.titleLabel?.font = TextStyle.subtitle.font
self.setTitleColor(Asset.Colors.boldText.color, for: .normal)
self.setTitleColor(Asset.Colors.white.color, for: .selected)
}

Our code can be written more concisely without any loss of clarity or functionality:

fileprivate func setup() {


layer.cornerRadius = 6
layer.borderWidth = 1
layer.borderColor = Asset.Colors.lightGray.color.cgColor
titleLabel?.font = TextStyle.subtitle.font
setTitleColor(Asset.Colors.boldText.color, for: .normal)
setTitleColor(Asset.Colors.white.color, for: .selected)
}
Conclusion

Thanks for making it to the end! Hopefully, you learnt something from this book. I'll
continue to make free updates over the next few months.

In the meantime, feel free to connect on LinkedIn , Twitter, or on my blog!

You might also like