UnsafePointer in Swift

Issue #130

Code is in Swift 4

Constructing UnsafeMutablePointer

1
2
let byteCount = 32
let result = UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount)

Data to UnsafePointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Data {
func toPointer() -> UnsafePointer<UInt8>? {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: count)
let stream = OutputStream(toBuffer: buffer, capacity: count)

stream.open()
withUnsafeBytes({ (p: UnsafePointer<UInt8>) -> Void in
stream.write(p, maxLength: count)
})

stream.close()

return UnsafePointer<UInt8>(buffer)
}
}

UnsafePointer to Data

1
2
3
4
5
extension UnsafePointer {
func toData() -> Data {
return Data(bytes: UnsafeRawPointer(self), count: 32)
}
}

Dealing with C API

This is how to do keccak hash using C API from https://github.com/ethereum/ethash/blob/master/src/libethash/sha3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
class KeccakHash {
func hash(data: Data) throws -> Data {
guard let dataPointer = data.toPointer() else {
throw InteralError.invalid
}

let byteCount = 32

let result = UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount)
sha3_256(result, byteCount, dataPointer, data.count)
return result.toData()
}
}

Read more

How to prevent UIVisualEffectView crash

Issue #124

We all know that there’s a potential crash with UIVisualEffectView on iOS 11. The fix is to not add sub views directly to UIVisualEffectView, but to its contentView. So we should change

1
effectView.addSubview(button)

to

1
effectView.contentView.addubView(button)

Here we don’t need to perform iOS version check, because effectView.contentView works for any iOS versions.

Potential cases for crashes

Here are some cases you can potentially cause the crashes

Strange namings

Normally we name our UIVisualEffectView as blurView, effectView. But there’s times we name it differently like navigationView, containerView, boxView, … This way we may completely forget that it’s a UIVisualEffectView 🙀

1
2
containerView.addSubview(button)
boxView.insertSubview(label, at: 0)

Custom loadView

Sometimes it’s convenient to have our UIViewController 's view as a whole blur view, so that all things inside have a nice blur effect background

1
2
3
4
5
6
7
8
9
10
11
12
13
class OverlayController: UIViewController {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
override func loadView() {
super.loadView()
self.view = blurView
}

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(button)
}
}

By setting our blurView as view in loadView, we have no idea afterwards that view is actually a UIVisualEffectView 🙀

Inheritance

What happen if we have another UIViewController that inherits from our OverlayController, all it knows about view is UIView, it does not know that it is a disguising UIVisualEffectView 🙀

1
2
3
4
5
6
7
class ClocksController: OverlayController {
override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(timeLabel)
}
}

Superclass type

Sometimes declare our things but with protocol or superclass types. Consumers of our API have no clue to know that it is UIVisualEffectView 🙀

1
let view: UIView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))

Here it appears to us that view is of type UIView

Legacy codebase

Now imagine you ‘ve handled a legacy codebase to deal with. Perform finding and replacing all those things related to UIVisualEffectView is very hard task. Especially since we tend to write less tests for UI

Making it impossible to crash

I like concept like Phantom type to limit interface. Here we’re not using type but a wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final class BlurView: UIView {
private let effectView: UIVisualEffectView

init(style: UIBlurEffectStyle, backgroundColor: UIColor? = nil) {
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: style))
self.effectView.backgroundColor = backgroundColor
super.init(frame: .zero)
insertSubview(effectView, at: 0)
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

override func addSubview(_ view: UIView) {
effectView.contentView.addSubview(view)
}

override func layoutSubviews() {
super.layoutSubviews()

effectView.frame = bounds
}
}

Here we override addSubview to always add views to effectView.contentView. In the init method, we need to call insertSubview instead because of our overriden addSubview

Now BlurView has a blur effect thanks to is underlying UIVisualEffectView, but expose only addSubview because of its UIView interface. This way it is impossible to cause crashes 😎

1
2
let blurView = BlurView(style: .dark)
blurView.addSubview(button(

Hashable and Set in Swift

Issue #122

From Set

You can create a set with any element type that conforms to the Hashable protocol. By default, most types in the standard library are hashable, including strings, numeric and Boolean types, enumeration cases without associated values, and even sets themselves.

From Hashable

The Hashable protocol inherits from the Equatable protocol, so you must also add an equal-to operator (==) function for your custom type.

1
2
3
4
5
6
7
8
public protocol Hashable : Equatable {

/// The hash value.
///
/// Hash values are not guaranteed to be equal across different executions of
/// your program. Do not save hash values to use during a future execution.
public var hashValue: Int { get }
}

Read more

Generic declaration in Swift

Issue #121

These are different

1
class DiffService<T: MKAnnotation & Equatable>
1
class DiffService<T: MKAnnotation, Equatable>

Collection Update

Issue #119

This is about collection update, how to provide correct IndexPath and a simple diffing algorithm

CollectionView

It’s hard to imagine of any apps that don’t use Table View or CollectionView. And by CollectionView, I actually mean UICollectionView 😉 . Most of the time, we show something with response from backend, then potentially update and insert new items as data changed.

collectionview

We can totally call reloadData to reflect the changes, but an animation is better here as it gives user better understanding of what’s going on, and to not surprise them.

This talks about UICollectionView, but UITableView behaves the same

Drag and Drop

Let’s imagine an app where user are allowed to customise their experience by moving items from one collection to another.

You can take a look at the example DragAndDrop which is using the new drag and drop API in iOS 11.

ipad

You must ensure that your data is changed before calling update methods on UICollectionView. And then we call deleteItems and insertItems to reflect data changes. UICollectionView performs a nice animation for you

1
2
3
4
5
6
7
8
9
10
11
12
13
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {

let destinationIndexPath = coordinator.destinationIndexPath
let sourceIndexPath = coordinator.items.last?.dragItem.localObject as! IndexPath

// remove
sourceItems.remove(at: sourceIndexPath.item)
sourceCollectionView.deleteItems(at: [sourceIndexPath])

// insert
destinationItems.insert(draggedItem, at: destinationIndexPath.item)
destinationCollectionView.insertItems(at: [destinationIndexPath])
}

NSInternalInconsistencyException

If you have large number of items with many insertions and deletions from backend response, you need to calculate the correct IndexPath to call, which are not easy thing. Most the time you will get the following crashes

Terminating app due to uncaught exception ‘NSInternalInconsistencyException’,
reason: ‘Invalid update: invalid number of items in section 0.
The number of items contained in an existing section after the update (213)
must be equal to the number of items contained in that section before
the update (154), plus or minus the number of items inserted or
deleted from that section (40 inserted, 0 deleted) and plus
or minus the number of items moved into or out of
that section (0 moved in, 0 moved out).’

In my experience it happened randomly because everyone has different data. Although the message is very descriptive, it may take a while to figure it out.

Game of IndexPath

Let’s refine our knowledge of IndexPath by going through some examples. With a collection of 6 items, we perform some update operations and figure out what IndexPath should be.

1
items = ["a", "b", "c", "d", "e", "f"]

Take a look at my example here CollectionUpdateExample, there are many more examples

index vs offset

Before we go any further, I just want to mention that, by index I actually mean offset from the start. If you take a look at the enumerated function, it suggests the name as offset instead of index

1
2
3
Array(0..<10).enumerated().forEach { (offset, element) in

}

This zero based numbering could shed some light on this matter

Particularly in C, where arrays are closely tied to pointer arithmetic, this makes for a simpler implementation: the subscript refers to an offset from the starting position of an array, so the first element has an offset of zero.

1. Insert 3 items at the end

1
2
3
4
5
6
items.append(contentsOf: ["g", "h", "i"])

// a, b, c, d, e, f, g, h, i

let indexPaths = Array(6...8).map { IndexPath(item: $0, section: 0) }
collectionView.insertItems(at: indexPaths)

2. Delete 3 items at the end

1
2
3
4
5
6
7
8
items.removeLast()
items.removeLast()
items.removeLast()

// a, b, c

let indexPaths = Array(3...5).map { IndexPath(item: $0, section: 0) }
collectionView.deleteItems(at: indexPaths)

3. Update item at index 2

1
2
3
4
5
6
items[2] = "👻"

// a, b, 👻, d, e, f

let indexPath = IndexPath(item: 2, section: 0)
collectionView.reloadItems(at: [indexPath])

4. Move item “c” to the end

1
2
3
4
5
6
7
8
9
items.remove(at: 2)
items.append("c")

// a, b, d, e, f, c

collectionView.moveItem(
at: IndexPath(item: 2, section: 0),
to: IndexPath(item: 5, section :0)
)

5. Delete 3 items at the beginning, then insert 3 items at the end

With multiple different operations, we should use performBatchUpdates

You can use this method in cases where you want to make multiple changes to the collection view in one single animated operation, as opposed to in several separate animations. You might use this method to insert, delete, reload, or move cells or use it to change the layout parameters associated with one or more cells

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
items.removeFirst()
items.removeFirst()
items.removeFirst()

items.append(contentsOf: ["g", "h", "i"])

// d, e, f, g, h, i

collectionView.performBatchUpdates({
let deleteIndexPaths = Array(0...2).map { IndexPath(item: $0, section: 0) }
collectionView.deleteItems(at: deleteIndexPaths)

let insertIndexPaths = Array(3...5).map { IndexPath(item: $0, section: 0) }
collectionView.insertItems(at: insertIndexPaths)
}, completion: nil)

6. Insert 3 items at the end, then delete 3 items beginning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
items.append(contentsOf: ["g", "h", "i"])

items.removeFirst()
items.removeFirst()
items.removeFirst()

// d, e, f, g, h, i

collectionView.performBatchUpdates({
let insertIndexPaths = Array(6...8).map { IndexPath(item: $0, section: 0) }
collectionView.insertItems(at: insertIndexPaths)

let deleteIndexPaths = Array(0...2).map { IndexPath(item: $0, section: 0) }
collectionView.deleteItems(at: deleteIndexPaths)
}, completion: nil)

🙀

If you run the example 6, you will get a crash

1
2
3
4
Terminating app due to uncaught exception 
'NSInternalInconsistencyException',
reason: 'attempt to insert item 6 into section 0,
but there are only 6 items in section 0 after the update'

performBatchUpdates

It is because the way performBatchUpdates works. If you take a look at the documentation

Deletes are processed before inserts in batch operations. This means the indexes for the deletions are processed relative to the indexes of the collection view’s state before the batch operation, and the indexes for the insertions are processed relative to the indexes of the state after all the deletions in the batch operation.

No matter how we call insert or delete, performBatchUpdates always performs deletions first. So we need to call deleteItems and insertItems as if the deletions occur first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
items.append(contentsOf: ["g", "h", "i"])

items.removeFirst()
items.removeFirst()
items.removeFirst()

// d, e, f, g, h, i

collectionView.performBatchUpdates({
let deleteIndexPaths = Array(0...2).map { IndexPath(item: $0, section: 0) }
collectionView.deleteItems(at: deleteIndexPaths)

let insertIndexPaths = Array(3...5).map { IndexPath(item: $0, section: 0) }
collectionView.insertItems(at: insertIndexPaths)
}, completion: nil)

Operations

There are many operations on UICollectionView, and there are operations to update whole section as well. Take a look Ordering of Operations and Index Paths

1
2
3
4
5
insertItems(at indexPaths: [IndexPath])
deleteItems(at indexPaths: [IndexPath])
reloadItems(at indexPaths: [IndexPath])
moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath)
performBatchUpdates(_ updates, completion)
1
2
3
4
insertSections(_ sections: IndexSet)
deleteSections(_ sections: IndexSet)
reloadSections(_ sections: IndexSet)
moveSection(_ section: Int, toSection newSection: Int)

inline

Edit distance

Doing these calculations by hand is quite tedious and error prone. We can build our own abstraction using some algorithms. The naive one is Wagner–Fischer algorithm which uses Dynamic Programming to tell the edit distance between two strings of characters.

Edit distance means the number of steps needed to change from one string to another. String is just a collection of characters, so we can generalise this concept to make it work for any collection of items. Instead of comparing character, we require items to conform to Equatable

“kit” -> “kat”

How can we transform form the word “kit” to “kat”? What kinds of operations do we nede to perform? You may tell “just change the i to a”, but this trivial example helps you understand the algorithm. Let’s get started.

inline

Deletions

If we go from “kit” to an empty string “”, we need 3 deletions

inline

“k” -> “” 👉 1 deletion
“ki” -> “” 👉 2 deletions
“kit” -> “” 👉 3 deletions

Insertions

If we go from an empty string “” to “kat”, we need 3 insertions

inline

“” -> “k” 👉 1 insertion
“” -> “ka” 👉 2 insertions
“” -> “kat” 👉 3 insertions

If equal, take value from the top left

You can think of the algorithm as if we go from source string, to empty string, to destination string. We try to find the minimum steps to update. Going horizontally means insertions, vertically means deletions and diagonally means substitutions

This way we can build our matrix, iterate from row to row, column by column. First, the letter “k” from source collection is the same with letter “k” from destination collection, we simply take value from the top left, which is 0 substituion

inline

If not equal

We continue with the next letter from the destination collection. Here “k” and “a” are not the same. We take minimum value from left, top, top left. Then increase by one

inline

Here we take value from left, which is horizontally, so we increase by 1 insertion

“k” -> “kat” 👉 2 insertions

Continue, they are not the same, so we take value from left horizontally. Here you can see it kind makes sense, as to go from “k” to “kat”, we need 2 insertions, which is to insert letters “a” and “t”

inline

The bottom right value

Continue with the next row, and next row until we got to the bottom right value, which gives you the edit distance. Here 1 substitution means that we need to perform 1 substitution to go from “kit” to “kat”, which is update “i” with “a’

final

You can easily see that we need to update index 1. But how do we know that it is index 1 🤔

Edit steps

In each step, we need to associate the index of item in source and destination collection. You can take a look at my implementation DeepDiff

inline

Complexity

We iterate through the matrix, with m and n are the length of source and destination collection respectively, we can see that the complexity of this algorithm is 0(mn).

Also the performance greatly depends on the size of the collection and how complex the item is. The more complex and how deep you want to perform Equatable can greatly affect your performance.

Improving performance

The section How does it work shows several ways we can improve performance.

Firstly, instead of building a matrix, which costs memory m*n, we can just use temporary arrays as holder.

Secondly, to quickly compare 2 items, we can use Hashable, as 2 identical items will always have the same hash.

More performance

If you want better performance, then algorithms with linear complexity may be interested to you. Take a look at Diff algorithm

Using Playground with CocoaPods

Issue #113

This is a follow up from my post Learning from Open Source: Using Playground on how to actually add a playground to your production project.

The idea is simple: create a framework so that Playground can access the code. This demo an iOS project with CocoaPods. See the demo https://github.com/onmyway133/UsingPlayground

This is also my question to this question https://stackoverflow.com/questions/47589855/how-to-expose-your-project-code-to-a-xcode-playground-when-using-cocoapods/47595120#47595120

1. Add a pod

Create a new project called UsingPlayground. Create a Podfile with a pod Cheers because we want something fun 😄

1
2
3
4
5
6
platform :ios, '9.0'
use_frameworks!

pod 'Cheers'

target 'UsingPlayground'

2. Use the pod in your project

This is very straightforward. Just to make sure the pod work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit
import Cheers

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let cheerView = CheerView()
view.addSubview(cheerView)
cheerView.frame = view.bounds

// Configure
cheerView.config.particle = .confetti

// Start
cheerView.start()
}
}

Build and run the project to enjoy a very fascinating confetti 🎊

3. Add a CocoaTouch framework

In your workspace, select the UsingPlayground project, add new CocoaTouch framework. Let’s call it AppFramework.

framework

Then add source files to this framework. For now, just check file ViewController.swift add add it to the AppFramework target too.

targets

4. Public

Swift types and methods are internal by default. So in order for them to be visible in the Playground, we need to declare them as public.

1
2
3
public class ViewController: UIViewController {
...
}

5. Add pod to AppFramework

In order for AppFramework to use our pods, we need to add those pods into framework target as well. Add target 'AppFramework' to your Podfile

1
2
3
4
5
6
7
8
9

platform :ios, '9.0'

use_frameworks!

pod 'Cheers'

target 'UsingPlayground'
target 'AppFramework'

Now run pod install again. In some rare cases, you need to run pod deintegrate and pod install to start from a clean slate

6. Add a Playground

Add a Playground and drag that to our workspace. Let’s call it MyPlayground

play

6. Enjoy

Now edit our MyPlayground. You can import frameworks from pod and our AppFramework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import UIKit
import Cheers
import AppFramework
import PlaygroundSupport

let cheerView = CheerView()
cheerView.frame = CGRect(x: 0, y: 50, width: 200, height: 400)

// Configure
cheerView.config.particle = .confetti

// Start
cheerView.start()

let myController = ViewController()

PlaygroundPage.current.liveView = myController.view

Remember to toggle Editor Mode so you can see Playground result

enjoy

URL Routing with Compass

Issue #110

Medium version https://medium.com/@onmyway133/url-routing-with-compass-d59c0061e7e2


Apps often have many screens, and UIViewController works well as the basis for a screen, together with presentation and navigation APIs. Things are fine until you get lost in the forest of flows, and code becomes hard to maintain.

One way to avoid this is the central URL routing approach. Think of it as a network router that handles and resolves all routing requests. This way, the code becomes declarative and decoupled, so that the list component does not need to know what it’s presenting. URL routing also makes logging and tracking easy along with ease of handling external requests such as deep linking.

There are various frameworks that perform URL routing. In this tutorial you’ll use Compass for its simplicity. You’ll refactor an existing app, which is a simplified Instagram app named PhotoFeed. When you’ve finished this tutorial, you’ll know how to declare and use routers with Compass and handle deep linking.

Getting Started

Download the starter project and unzip it. Go to the PhotoFeed folder and run pod install to install the particular dependencies for this project. Open PhotoFeed.xcworkspace and run the project. Tap Login to go to the Instagram login page and enter your Instagram credentials, then have a look around the app.

artboard

The main app is made of a UITabBarController that shows the feed, the currently logged-in user profile and a menu. This is a typical Model View Controller project where UIViewController handles Cell delegates and takes responsibility for the navigation. For simplicity, all view controllers inherit from TableController and CollectionController that know how to manage list of a particular model and cell. All models conform to the new Swift 4 Codable protocol.

Registering Your App on Instagram

In order to use the Instagram API, you’ll need to register your app at Instagram Developer. After obtaining your client id, switch back to the project. Go to APIClient.swift and modify your clientId.

artboard 2

Note: The project comes with a default app with limited permissions. The app can’t access following or follower APIs, and you can only see your own posts and comments

Compass 101

The concept of Compass is very simple: you have a set of routes and central place for handling these routes. Think of a route as a navigation request to a specific screen within the app. The idea behind URL routing is borrowed from the modern web server. When user enters a URL into the browser, such as https://flawlessapp.io/category/ios, that request is sent from the browser to the web server. The server parses the URL and returns the requested content, such as HTML or JSON. Most web server frameworks have URL routing support, including ASP.NET, Express.js, and others. For example, here is how you handle a URL route in express.js:

1
2
3
4
app.get('/api/category/:categoryTag', function (req, res) {
const page = getCategoryPageFor(req.params.categoryTag)
res.send(page)
})

Users or apps request a specific URL that express an intent about what should be returned or displayed. But instead of returning web pages, Compass constructs screens in terms of UIViewController and presents them.

Route Patterns

This is how you declare a routing schema in Compass:

1
Navigator.routes = ["profile:{userId}", "post:{postId}", "logout"]

This is simply as array of patterns you register on the Navigator. This is the central place where you define all your routes. Since they are in one place, all your navigations are kept in one place and can easily be understood. Looking at the example above, {userId}, {postId} are placeholders that will be resolved to actual parameters. For example with post:BYOkwgXnwr3, you get userId of BYOkwgXnwr3. Compass also performs pattern matching, in that post:BYOkwgXnwr3 matches post:{postId}, not comment:{postId}, blogpost:{postId}, …This will become to make sense in following sections.

The Navigator

The Navigator is a the central place for routes registration, navigating and handling.

artboard 3

The next step is to trigger a routing request. You can do that via the Navigator. For example, this is how you do in the feed to request opening a specific post:

1
Navigator.navigate(urn: "post:BYOkwgXnwr3")

Compass uses the user-friendly urn, short for Uniform Resource Name to make itwork seamlessly with Deep Linking. This urn matches the routing schema post:{postId}. Compass uses {param} as the special token to identifier the parameter and : as the delimiter. You can change the delimiter to something else by configuring Navigator.delimiter. You have learned how to register routes and navigate in Compass. Next, you will learn how to customize the handling code to your need.

Location

Navigator parses and works with Location under the hood. Given the URN of post:BYOkwgXnwr3, you get a Location where path is the route pattern, and arguments contain the resolved parameters.

1
2
3
4
path = "post:{postId}"
arguments = [
"postId": "BYOkwgXnwr3"
]

To actually perform the navigation, you assign a closure that takes a Location to Navigator.handle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Navigator.handle = { [weak self] location in
guard let `self` = self else {
return
}

let arguments = location.arguments

switch location.path {
case "post:{postId}":
let postController = PostController(postId: postID)
self.rootController.pushViewController(postController, animated: true)
default:
break
}
}

The letself= self dance is to ensure self isn’t released by the time this closure is executed. If it is released, the routing it’s about to perform is likely invalid, and you return without doing anything instead. You should typically do the above in the components that own the root controller, such as AppDelegate as seen above. That’s the basic of Compass. Astute readers may have noticed that it does not scale, as the number of switch statements will grow as the number of routes and endpoints increase in your app. This is where the Routable protocol comes in. Anything conforming to Routable knows how to handle a specific route. Apps may have many modular sections, and each section may have a set of routes. Compass handles these scenario by using a composite Routable named Router that groups them . You can have a router for a pre-login module, a post-login module, premium features module, and so on.

untitled 2 2017-08-30 09-53-58

In the next section, you’ll change PhotoFeed to use Router and Routable.

Router to the Rescue

The first step is to include Compass in your project. Using CocoaPods, this is an easy task. Edit the Podfile with the project and type pod 'Compass', '~> 5.0' just before the end statement. Then open Terminal and execute the following:

1
pod install

The version of Compass used in this tutorial is 5.1.0.

Registering a Router

untitled 2 2017-08-30 10-04-50

To start, you’ll create a simple router to handle all post-login routes. Open AppDelegate.swift, and import Compass at the top of the file:

1
import Compass

Next, add the following router declaration under the var mainController: MainController? declaration:

1
var postLoginRouter = Router()

Then declare a function called setupRouting, you ‘ll do this in an extension to separate the routing setup from the main code in AppDelegate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension AppDelegate {
func setupRouting() {
// [1] Register scheme
Navigator.scheme = "photofeed"

// [2] Configure routes for Router
postLoginRouter.routes = [:]

// [3] Register routes you 'd like to support
Navigator.routes = Array(postLoginRouter.routes.keys)

// [4] Do the handling
Navigator.handle = { [weak self] location in
guard let selectedController = self?.mainController?.selectedViewController else {
return
}

// [5] Choose the current visible controller
let currentController = (selectedController as? UINavigationController)?.topViewController
?? selectedController

// [6] Navigate
self?.postLoginRouter.navigate(to: location, from: currentController)
}
}
}

Here’s what you do in the above method:

  1. Declare a scheme for Compass to work. This is your application URL scheme. This shines when you wish to support deep linking .
  2. Register all the routes in your app. Router accepts a mapping of route and Routable conformers. This is empty for now, but you will add several routes in a moment.
  3. A Navigator can manage multiple routers. In this case, you only register one router.
  4. This is where you supply the handling closure. Navigator uses this to handle a resolved location request.
  5. Screens in one modular section originate from one root or parent view controller. In order to show something from the route, you should try to push or present it from the selected most-visible view controller. In this project, the root is a UITabBarController, so you try to get the top controller from the current selected navigation. The selection of current controller depends on the module and your app use cases, so Compass let you decide it. If you use the side menu drawer, then you can just change the selected view controller.
  6. Finally, since Router is a composite Routable, you dispatch to it the Location.

main storyboard 2017-08-30 10-37-16

Finally, you need to call this newly added function. Add the following line right above window?.makeKeyAndVisible():

1
setupRouting()

Build and run. Nothing seems to work yet! To make things happen, you’ll need to add all the route handlers. You’ll do this in the next section.

Implementing the Route Handlers

First, create a new file and name it Routers.swift. This is where you’ll declare all of your route handlers. At the beginning of the file, add import Compass. Compass declares a simple protocol — Routable — that decides what to do with a given Location request from a Current Controller. If a request can’t be handled, it will throw with RouteError. Its implementation looks like this:

1
2
3
public protocol Routable {
func navigate(to location: Location, from currentController: CurrentController) throws
}

It’s an incredibly simple protocol. Any routes you create only need to implement that single method. Now create your first handler to deal with user info request.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct UserRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
// [1] Examine arguments
guard let userId = location.arguments["userId"] else {
return
}

// [2] Create the controller
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "UserController") as! UserController
controller.userId = userId
currentController.navigationController?.pushViewController(controller, animated: true)
}
}

This is called when you touch the post author on the feed. Here’s what’s happening:

  1. UserRoute deals with user:{userId} urn, so location.arguments["userId"] should contain the correct userId to inject into UserController.
  2. This app uses storyboards to make the UI, so get the correct view controller based on its identifier. Remember tha currentController is the current visible controller in the navigation stack. So you ask for its UINavigationController to push a new view controller.

Right below this router, add one more route for the screen shown when the user wants to see who likes a particular post:

1
2
3
4
5
6
7
8
9
10
11
struct LikesRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
guard let mediaId = location.arguments["mediaId"] else {
return
}

let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LikesController") as! LikesController
controller.mediaId = mediaId
currentController.navigationController?.pushViewController(controller, animated: true)
}
}

The remaining Route

Now it’s your turn to write the other route handlers: CommentsRoute, FollowingRoute, FollowerRoute. See if you can figure it out first, you can find the solution below. Here’s what you should have:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct CommentsRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
guard let mediaId = location.arguments["mediaId"] else {
return
}

let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "CommentsController") as! CommentsController
controller.mediaId = mediaId
currentController.navigationController?.pushViewController(controller, animated: true)
}
}

struct FollowingRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
guard let userId = location.arguments["userId"] else {
return
}

let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FollowingController") as! FollowingController
controller.userId = userId
currentController.navigationController?.pushViewController(controller, animated: true)
}
}

struct FollowerRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
guard let userId = location.arguments["userId"] else {
return
}

let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FollowerController") as! FollowerController
controller.userId = userId
currentController.navigationController?.pushViewController(controller, animated: true)
}
}

The LogoutRoute

There is one more route to add: the one you’ll use for logout. LogoutRoute is quite tricky, as it usually involves changing the current root view controller. Who knows this better than the app delegate? Open AppDelegate.swift and add the following code at the very bottom:

1
2
3
4
5
6
struct LogoutRoute: Routable {
func navigate(to location: Location, from currentController: CurrentController) throws {
APIClient.shared.accessToken = nil
(UIApplication.shared.delegate as! AppDelegate).showLogin()
}
}

Now that you’ve implemented all of the route handlers, you will have to tell Navigator which route is used for which URN. Still in AppDelegate.swift, find postLoginRouter.routes = [:] and replace it with the following:

1
2
3
4
5
6
7
8
postLoginRouter.routes = [
"user:{userId}": UserRoute(),
"comments:{mediaId}": CommentsRoute(),
"likes:{mediaId}": LikesRoute(),
"following:{userId}": FollowingRoute(),
"follower:{userId}": FollowerRoute(),
"logout": LogoutRoute()
]

Build the app and everything should compile. Now all that’s left is to actually all all of the code you’ve written!

Refactoring Time

It’s time to refactor all the code in UIViewController by replacing all the navigation code with your new routing instructions. Start by freeing the FeedController from the unnecessary tasks of navigation. Open FeedController.swift and add the following import to the top of the file:

1
import Compass

Next, look for // MARK: - MediaCellDelegate and replace the three MediaCell delegate methods with the following:

1
2
3
4
5
6
7
8
9
10
11
func mediaCell(_ cell: MediaCell, didViewLikes mediaId: String) {
try? Navigator.navigate(urn: "likes:\(mediaId)")
}

func mediaCell(_ cell: MediaCell, didViewComments mediaId: String) {
try? Navigator.navigate(urn: "comments:\(mediaId)")
}

func mediaCell(_ cell: MediaCell, didSelectUserName userId: String) {
try? Navigator.navigate(urn: "user:\(userId)")
}

For these three cases, you simply want to navigate to another screen. Therefore, all you need to do is tell the Navigator where you want to go. For simplicity, you use try? to deal with any code that throws. Build and run the app. Search for your favorite post in the feed, and tap on the author, the post comments or likes to go to the target screen. The app behaves the same as it did before, but the code is now clean and declarative. Now do the same with UserController.swift. Add the following import to the top of the file:

1
import Compass

Replace the code after // MARK: - UserViewDelegate with the following:

1
2
3
4
5
6
7
func userView(_ view: UserView, didViewFollower userId: String) {
try? Navigator.navigate(urn: "follower:\(userId)")
}

func userView(_ view: UserView, didViewFollowing userId: String) {
try? Navigator.navigate(urn: "following:\(userId)")
}

Your task now is to refactor with the last route LogoutRoute. Open MenuController.swift and add the following to the top:

1
import Compass

Remove the logout method altogether. Find the following:

indexPath.section
1
2
  logout()
}

…and replace it with:

1
2
3
if indexPath.section == Section.account.rawValue, indexPath.row == 0 {
try? Navigator.navigate(urn: "logout")
}

Build and run the app, navigate to the menu and tap Logout. You should be taken to the login screen.

Handling Deep Linking

Deep linking allows your apps to be opened via a predefined URN. The system identifies each app via its URL scheme. For web pages, the scheme is usually http, https. For Instagram it is, quite handily, instagram. Use cases for this are inter-app navigation and app advertisements. For examples, the Messenger app uses this to open the user profile in the Facebook app, and Twitter uses this to open the App Store to install another app from an advertisement. In order for user to be redirected back to PhotoFeed, you need to specify a custom URL scheme for your app. Remember where you declared Navigator.scheme = "photofeed"? PhotoFeed just so happens to conform to this URL scheme, so deep links already worked — and you didn’t even know it! Build and run the app, then switch to Safari. Type photofeed:// in the address bar, then tap Go. That will trigger your app to open. The app opens, but PhotoFeed doesn’t parse any parameters in the URL to go anywhere useful. Time to change that! Your app responds to the URL scheme opening by implementing a UIApplicationDelegate method. Add the following after setupRouting in AppDelegate.swift:

1
2
3
4
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
try? Navigator.navigate(url: url)
return true
}

Navigator parses and handles this for you. Build and run again. Go to Safari app, type photofeed://user:self and tap Go. Photofeed will open and show the currently logged in users’ profile. Because you already had UserRoute, the requested URL was handled gracefully. Your app may already be presenting a particular screen when a routing request comes, but you’ve anticipated this by resetting the navigation controller or presentation stack to show the requested screen. This simple solution works for most cases. Again, it’s recommended you pick the topmost visible view controller as the current controller in Navigator.handle.

artboard 5

Deep linking is usually considered external navigation, in that the routing requests come from outside your app. Thanks to the central routing system that you developed, the code to handle external and internal routing requests is very much the same and involves no code duplication at all.

Routing with Push Notifications

Push notifications help engage users with your app. You may have received messages like “Hey, checkout today ‘s most popular stories” on Medium, “Your friend has a birthday today” on Facebook, … and when you tap those banners, you are taken straight to that particular screen. How cool is that? This is achievable with your URL routing approach. Imagine users tapping a push notification banner saying “You’re a celebrity on PhotoFeed — check out your profile now!” and being sent directly to their profile screen. To accomplish this, you simply have to embed the URN info into the push payload and handle that in your app.

Setting up

To start, you’ll need to specify your bundle ID. Go to Target Settings\General to change your bundle ID as push notification requires a unique bundle ID to work. Your project uses com.fantageek.PhotoFeed by default.

step1_bundleid

Next, you’ll need to register your App ID. Go to Member Center and register your App ID. Remember your Team ID, as you will need it in the final step. Also tick the Push Notification checkbox under Application Services.

step1_appid

Now you’ll need to generate your Authentication Key. Apple provides Token Authentication as a new authentication mechanism for push notifications. The token is easy to generate, works for all your apps, and mostly, it never expires. Still in Member Center, create a new Key and download it as a .p8 file. Remember your Key ID as you will need it in the final step.

step2_key

Next up: enabling push notification capability. Back in Xcode, go to Target Settings\Capabilities and enable Push Notifications, which will add PhotoFeed.entitlements to your project.

step3_capability

The next step is to register for push notifications. Open MainController.swift and add the following import to the top of MainController.swift:

1
import UserNotifications

You want to enable push notification only after login, so MainController is the perfect place. UserNotifications is recommended for app targeting iOS 10 and above.

1
2
3
4
5
6
7
8
9
10
11
12
override func viewDidLoad() {
super.viewDidLoad()

// [1] Register to get device token for remote notifications
UIApplication.shared.registerForRemoteNotifications()

// [2] Register to handle push notification UI
let options: UNAuthorizationOptions = [.alert, .sound, .badge]
UNUserNotificationCenter.current().requestAuthorization(options: options) { (granted, error) in
print(error as Any)
}
}

The permission dialog is shown once, so make sure you accept it. It’s time to handle the device token. Open AppDelegate.swift, and add the following to the end of extension AppDelegate:

1
2
3
4
5
6
7
8
9
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// [1] Parse to token string
let token = deviceToken.map {
return String(format: "%02.2hhx", $0)
}.joined()

// [2] Log it
print("Your device token is \(token)")
}

This is where you get device token if your app successfully connects to APNs. Normally, you would send this device token to the backend so they can organize , but in this tutorial we just log it. It is required in the tool to be able to target a particular device.

Handling payload

Open AppDelegate.swift and add the following to th end of extension AppDelegate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// [1] Always call the completion handler
defer {
completionHandler(.newData)
}

// [2] Convert into JSON dictionary
guard let json = userInfo as? [String: Any] else {
return
}

// [3] Parse to aps
guard let aps = json["aps"] as? [String: Any] else {
return
}

// [4] Parse to urn
guard let urn = aps["urn"] as? String else {
return
}

try? Navigator.navigate(urn: urn)
}

This method is called when your app receives push notification payload and is running. The above code is relatively straightforward: it first tries to parse the urn information from the payload, then tells Navigator to do the job . Build and run the app on the device, since push notifications won’t work on the simulator. Log in to the app if prompted. Once on the main screen, grant push notification permissions to the app in order to receive alerts. You should see the device token logged to your Xcode console.

Testing Push Notifications

In this tutorial, you’ll use a tool called PushNotifications to help you easily create push notifications for your app. Download the tool PushNotifications from here. This tool sends payloads directly to APNs.

step4_test

Choose iOS\Token to use Token Authentication, you get that by creating and downloading your Key from Certificates, Identifiers & Profiles. Browse for the .p8 auth key file that you downloaded earlier. Enter Team ID, you can check it by going to Membership Details Enter Key ID, this is the ID associated with the Key from the first step. Enter Bundle ID and device token. Paste the following into as. It is a traditional payload associated with the URN.

1
2
3
4
5
6
{
"aps":{
"alert":"You become a celebrity on PhotoFeed, checkout your profile now",
"urn": "user:self"
}
}

Since you’re debugging with Xcode, select Sandbox as environment.

Tap Send now. If your app is in the background, an alert will appear. Tapping it will take you to your app and show you your user profile. Bravo! You just implemented deep linking in push notification, thanks again to the URL routing.

Read more

Here is the final project with all the code from this tutorial. You now understand central routing patterns, have mastered Compass and even refactored a real-world app. However, there is no silver bullet that works well for all apps. You need to understand your requirements and adjust accordingly. If you want to learn more about other navigation patterns, here are a few suggestions:

Remember, it’s not only about the code, but also about the user experience that your app provides. So please make sure you conform to the guidelines Navigation in Human Interface Guidelines iOS.

Coordinator and FlowController

Issue #106

Every new architecture that comes out, either iOS or Android, makes me very excited. I’m always looking for ways to structure apps in a better way. But after some times, I see that we’re too creative in creating architecture, aka constraint, that is too far away from the platform that we’re building. I often think “If we’re going too far from the system, then it’s very hard to go back”

I like things that embrace the system. One of them is Coordinator which helps in encapsulation and navigation. Thanks to my friend Vadym for showing me Coordinator in action.

The below screenshot from @khanlou ‘s talk at CocoaHeads Stockholm clearly says many things about Coordinator


But after reading A Better MVC, I think we can leverage view controller containment to do navigation using UIViewController only.

Since I tend to call view controllers as LoginController, ProfileController, ... and the term flow to group those related screens, what should we call a Coordinator that inherits from UIViewController 🤔 Let’s call it FlowController 😎 .

The name is not that important, but the concept is simple. FlowController was also inspired by this Flow Controllers on iOS for a Better Navigation Control back in 2014. The idea is from awesome iOS people, this is just a sum up from my experience 😇

So FlowController can just a UIViewController friendly version of Coordinator. Let see how FlowController fits better into MVC

1. FlowController and AppDelegate

Your application starts from AppDelegate, in that you setup UIWindow. So we should follow the same “top down” approach for FlowController, starting with AppFlowController. You can construct all dependencies that your app need for AppFlowController, so that it can pass to other child FlowController.

AppDelegate is also considered Composition Root

Here is how to declare AppFlowController in AppDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct DependencyContainer: AuthServiceContainer, PhoneServiceContainer, NetworkingServiceContainer,
LocationServiceContainer, MapServiceContainer, HealthServiceContainer {

let authService: AuthServiceProtocol
let phoneService: PhoneService
let networkingService: NetworkingService
let locationService: LocationService
let mapService: MapService
let healthService: HealthService

static func make() -> DependencyContainer {
// Configure and make DependencyContainer here
}
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
var appFlowController: AppFlowController!

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
appFlowController = AppFlowController(
dependencyContainer: DependencyContainer.make()
)

window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = appFlowController
window?.makeKeyAndVisible()

appFlowController.start()

return true
}
}

Here are some hypothetical FlowController that you may encounter

  • AppFlowController: manages UIWindow and check whether to show onboarding, login or main depending on authentication state
    • OnboardingFlowController: manages UIPageViewController and maybe ask for some permissions
    • LoginFlowController: manages UINavigationController to show login, sms verification, forget password, and optionally start SignUpFlowController
    • MainFlowController: manages UITabBarController with each tab serving main features
      • FeedFlowController: show feed with list of items
      • ProfileFlowController: show profile
      • SettingsFlowController: show settings, and maybe call logout, this will delegates up the FlowController chain.

The cool thing about FlowController is it makes your code very self contained, and grouped by features. So it’s easy to move all related things to its own package if you like.

2. FlowController as container view controller

In general, a view controller should manage either sequence or UI, but not both.

Basically, FlowController is just a container view controller to solve the sequence, based on a simple concept called composition. It manages many child view controllers in its flow. Let’ say we have a ProductFlowController that groups together flow related to displaying products, ProductListController, ProductDetailController, ProductAuthorController, ProductMapController, … Each can delegate to the ProductFlowController to express its intent, like ProductListController can delegate to say “product did tap”, so that ProductFlowController can construct and present the next screen in the flow, based on the embedded UINavigationController inside it.

Normally, a FlowController just displays 1 child FlowController at a time, so normally we can just update its frame

1
2
3
4
5
6
7
final class AppFlowController: UIViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

childViewControllers.first?.view.frame = view.bounds
}
}

3. FlowController as dependency container

Each view controller inside the flow can have different dependencies, so it’s not fair if the first view controller needs to carry all the stuff just to be able to pass down to the next view controllers. Here are some dependencies

  • ProductListController: ProductNetworkingService
  • ProductDetailController: ProductNetworkingService, ImageDowloaderService, ProductEditService
  • ProductAuthorController: AuthorNetworkingService, ImageDowloaderService
  • ProductMapController: LocationService, MapService

Instead the FlowController can carry all the dependencies needed for that whole flow, so it can pass down to the view controller if needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct ProductDependencyContainer {
let productNetworkingService: ProductNetworkingService
let imageDownloaderService: ImageDownloaderService
let productEditService: ProductEditService
let authorNetworkingService: AuthorNetworkingService
let locationService: LocationService
let mapService: MapService
}

class ProductFlowController {
let dependencyContainer: ProductDependencyContainer

init(dependencyContainer: ProductDependencyContainer) {
self.dependencyContainer = dependencyContainer
}
}

extension ProductFlowController: ProductListControllerDelegate {
func productListController(_ controller: ProductListController, didSelect product: Product) {
let productDetailController = ProductDetailController(
productNetworkingService: dependencyContainer.productNetworkingService,
productEditService: dependencyContainer.productEditService,
imageDownloaderService: dependencyContainer.imageDownloaderService
)

productDetailController.delegate = self
embeddedNavigationController.pushViewController(productDetailController, animated: true)
}
}

Here are some ways that you can use to pass dependencies into FlowController

4. Adding or removing child FlowController

Coordinator

With Coordinator, you need to keep an array of child Coordinators, and maybe use address (=== operator) to identify them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Coordinator {
private var children: [Coordinator] = []

func add(child: Coordinator) {
guard !children.contains(where: { $0 === child }) else {
return
}

children.append(child)
}

func remove(child: Coordinator) {
guard let index = children.index(where: { $0 === child }) else {
return
}

children.remove(at: index)
}

func removeAll() {
children.removeAll()
}
}

FlowController

With FlowController, since it is UIViewController subclass, it has viewControllers to hold all those child FlowController. Just add these extensions to simplify your adding or removing of child UIViewController

1
2
3
4
5
6
7
8
9
10
11
12
13
extension UIViewController {
func add(childController: UIViewController) {
addChildViewController(childController)
view.addSubview(childController.view)
childController.didMove(toParentViewController: self)
}

func remove(childController: UIViewController) {
childController.willMove(toParentViewController: nil)
childController.view.removeFromSuperview()
childController.removeFromParentViewController()
}
}

And see in action how AppFlowController work with adding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final class AppFlowController: UIViewController {
func start() {
if authService.isAuthenticated {
startMain()
} else {
startLogin()
}
}

private func startLogin() {
let loginFlowController = LoginFlowController(
loginFlowController.delegate = self
add(childController: loginFlowController)
loginFlowController.start()
}

fileprivate func startMain() {
let mainFlowController = MainFlowController()
mainFlowController.delegate = self
add(childController: mainFlowController)
mainFlowController.start()
}
}

and with removing when the child FlowController finishes

1
2
3
4
5
6
extension AppFlowController: LoginFlowControllerDelegate {
func loginFlowControllerDidFinish(_ flowController: LoginFlowController) {
remove(childController: flowController)
startMain()
}
}

5. AppFlowController does not need to know about UIWindow

Coordinator

Usually you have an AppCoordinator, which is held by AppDelegate, as the root of your Coordinator chain. Based on login status, it will determine which LoginController or MainController will be set as the rootViewController, in order to do that, it needs to be injected a UIWindow

1
2
3
4
window = UIWindow(frame: UIScreen.main.bounds)
appCoordinator = AppCoordinator(window: window!)
appCoordinator.start()
window?.makeKeyAndVisible()

You can guess that in the start method of AppCoordinator, it must set rootViewController before window?.makeKeyAndVisible() is called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class AppCoordinator: Coordinator {
private let window: UIWindow

init(window: UIWindow) {
self.window = window
}

func start() {
if dependencyContainer.authService.isAuthenticated {
startMain()
} else {
startLogin()
}
}
}

FlowController

But with AppFlowController, you can treat it like a normal UIViewController, so just setting it as the rootViewController

1
2
3
4
5
6
appFlowController = AppFlowController(
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = appFlowController
window?.makeKeyAndVisible()

appFlowController.start()

6. LoginFlowController can manage its own flow

Supposed we have login flow based on UINavigationController that can display LoginController, ForgetPasswordController, SignUpController

Coordinator

What should we do in the start method of LoginCoordinator? Construct the initial controller LoginController and set it as the rootViewController of the UINavigationController? LoginCoordinator can create this embedded UINavigationController internally, but then it is not attached to the rootViewController of UIWindow, because UIWindow is kept privately inside the parent AppCoordinator.

We can pass UIWindow to LoginCoordinator but then it knows too much. One way is to construct UINavigationController from AppCoordinator and pass that to LoginCoordinator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final class AppCoordinator: Coordinator {
private let window: UIWindow

private func startLogin() {
let navigationController = UINavigationController()

let loginCoordinator = LoginCoordinator(navigationController: navigationController)

loginCoordinator.delegate = self
add(child: loginCoordinator)
window.rootViewController = navigationController
loginCoordinator.start()
}
}

final class LoginCoordinator: Coordinator {
private let navigationController: UINavigationController

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

func start() {
let loginController = LoginController(dependencyContainer: dependencyContainer)
loginController.delegate = self

navigationController.viewControllers = [loginController]
}
}

FlowController

LoginFlowController leverages container view controller so it fits nicely with the way UIKit works. Here AppFlowController can just add LoginFlowController and LoginFlowController can just create its own embeddedNavigationController.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class AppFlowController: UIViewController {
private func startLogin() {
let loginFlowController = LoginFlowController(
dependencyContainer: dependencyContainer
)

loginFlowController.delegate = self
add(childController: loginFlowController)
loginFlowController.start()
}
}

final class LoginFlowController: UIViewController {
private let dependencyContainer: DependencyContainer
private var embeddedNavigationController: UINavigationController!
weak var delegate: LoginFlowControllerDelegate?

init(dependencyContainer: DependencyContainer) {
self.dependencyContainer = dependencyContainer
super.init(nibName: nil, bundle: nil)

embeddedNavigationController = UINavigationController()
add(childController: embeddedNavigationController)
}

func start() {
let loginController = LoginController(dependencyContainer: dependencyContainer)
loginController.delegate = self

embeddedNavigationController.viewControllers = [loginController]
}
}

7. FlowController and responder chain

Coordinator

Sometimes we want a quick way to bubble up message to parent Coordinator, one way to do that is to replicate UIResponder chain using associated object and protocol extensions, like Inter-connect with Coordinator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension UIViewController {
private struct AssociatedKeys {
static var ParentCoordinator = "ParentCoordinator"
}

public var parentCoordinator: Any? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.ParentCoordinator)
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.ParentCoordinator, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}

open class Coordinator<T: UIViewController>: UIResponder, Coordinating {
open var parent: Coordinating?

override open var coordinatingResponder: UIResponder? {
return parent as? UIResponder
}
}

FlowController

Since FlowController is UIViewController, which inherits from UIResponder, responder chain happens out of the box

Responder objects—that is, instances of UIResponder—constitute the event-handling backbone of a UIKit app. Many key objects are also responders, including the UIApplication object, UIViewController objects, and all UIView objects (which includes UIWindow). As events occur, UIKit dispatches them to your app’s responder objects for handling.

8. FlowController and trait collection

FlowController

I very much like how Kickstarter uses trait collection in testing. Well, since FlowController is a parent view controller, we can just override its trait collection, and that will affect the size classes of all view controllers inside that flow.

As in A Better MVC, Part 2: Fixing Encapsulation

The huge advantage of this approach is that system features come free. Trait collection propagation is free. View lifecycle callbacks are free. Safe area layout margins are generally free. The responder chain and preferred UI state callbacks are free. And future additions to UIViewController are also free.

From setOverrideTraitCollection

When implementing a custom container view controller, you can use this method to change the traits of any embedded child view controllers to something more appropriate for your layout. Making such a change alters other view controller behaviors associated with that child

1
2
3
4
5
6
7
let trait = UITraitCollection(traitsFrom: [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular),
.init(userInterfaceIdiom: .phone)
])

appFlowController.setOverrideTraitCollection(trait, forChildViewController: loginFlowController)

9. FlowController and back button

Coordinator

One problem with UINavigationController is that clicking on the default back button pops the view controller out of the navigation stack, so Coordinator is not aware of that. With Coordinator you needs to keep Coordinator and UIViewController in sync, add try to hook up UINavigationControllerDelegate in order to clean up. Like in Back Buttons and Coordinators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Coordinator: UINavigationControllerDelegate {    
func navigationController(navigationController: UINavigationController,
didShowViewController viewController: UIViewController, animated: Bool) {

// ensure the view controller is popping
guard
let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from),
!navigationController.viewControllers.contains(fromViewController) else {
return
}

// and it's the right type
if fromViewController is FirstViewControllerInCoordinator) {
//deallocate the relevant coordinator
}
}
}

Or creating a class called NavigationController that inside manages a list of child coordinators. Like in Navigation coordinators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class NavigationController: UIViewController {
// MARK: - Inputs

private let rootViewController: UIViewController

// MARK: - Mutable state

private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]

// MARK: - Lazy views

private lazy var childNavigationController: UINavigationController =
UINavigationController(rootViewController: self.rootViewController)

// MARK: - Initialization

init(rootViewController: UIViewController) {
self.rootViewController = rootViewController

super.init(nibName: nil, bundle: nil)
}
}

FlowController

Since FlowController is just plain UIViewController, you don’t need to manually manage child FlowController. The child FlowController is gone when you pop or dismiss. If we want to listen to UINavigationController events, we can just handle that inside the FlowController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final class LoginFlowController: UIViewController {
private let dependencyContainer: DependencyContainer
private var embeddedNavigationController: UINavigationController!
weak var delegate: LoginFlowControllerDelegate?

init(dependencyContainer: DependencyContainer) {
self.dependencyContainer = dependencyContainer
super.init(nibName: nil, bundle: nil)

embeddedNavigationController = UINavigationController()
embeddedNavigationController.delegate = self
add(childController: embeddedNavigationController)
}
}

extension LoginFlowController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

}
}

10. FlowController and callback

We can use delegate pattern to notify FlowController to show another view controller in the flow

1
2
3
4
5
6
7
8
9
10
11
12
extension ProductFlowController: ProductListControllerDelegate {
func productListController(_ controller: ProductListController, didSelect product: Product) {
let productDetailController = ProductDetailController(
productNetworkingService: dependencyContainer.productNetworkingService,
productEditService: dependencyContainer.productEditService,
imageDownloaderService: dependencyContainer.imageDownloaderService
)

productDetailController.delegate = self
embeddedNavigationController.pushViewController(productDetailController, animated: true)
}
}

Another approach is to use closure as callback, as proposed by @merowing_, and also in his post Improve your iOS Architecture with FlowControllers

Using closures as triggers rather than delegate allows for more readable and specialized implementation, and multiple contexts

1
2
3
4
5
6
7
8
9
10
11
12
13
final class ProductFlowController {
func start() {
let productListController = ProductListController(
productNetworkingService: dependencyContainer.productNetworkingService
)

productListController.didSelectProduct = { [weak self] product in
self?.showDetail(for: product)
}

embeddedNavigationController.viewControllers = [productListController]
}
}

11. FlowController and deep linking

TBD. In the mean while, here are some readings about the UX

Please reconsidering your choice of libraries

Issue #105

Are you willing to take vaccines you don’t know about?

I like open source. I ‘ve made some and contributed to some. I also use other people ‘s open source libraries and learn a lot from them 😇

Open source can help us build better, faster and maybe more performant software by basing on other people ‘s hard work. We can also collaborate and make it better. But it’s also a double edges sword if you’re not judging carefully.

Putting a little bit more dedication

You can skip this post if your project is just for fun, and you don’t care about future maintenance. If you’re making client or product projects, you should care and put a little more dedication into it. The company and your teammates trust you to do the good work.

I’ve admitted that I ‘ve done node.js and electron.js. The choice is because it’s just a utility that I want to make quickly, and there ‘s many node.js packages that I can use immediately. I have little experience in node.js, so I accept the risk do get the work done. But if you’re experienced developer in your platform, and it’s your important project, then it’s a different story 💥

I’m kind of experimental person, so I always want to try new things. But I also learn the hard way to not over engineer, and to live as close to the system as possible. I just read Much ado about iOS app architecture and I agree with most of the points, that we shouldn’t fight the SDK and replace system frameworks.

To me, using 3rd libraries is like giving your life to someone else ‘s hands, that you can’t make any decision for your future. Please don’t just pod install and consider it done 🙏

What about the stars

People tend to follow those that have lots of followers, and to star a project with lots of stars. Don’t trust the stars. It means nearly nothing. The star is just the result of some marketing effort. Being featured or not in a newsletter can make 1k stars difference. Just because it was featured in a newsletter does not necessarily mean that it is good 😬

You should judge it yourself by checking how good the source code is, how many documentation are available, and whether there is unit tests or not. The author is just human, so he can’t help maintain the library forever. You’re taking a big risk if you don’t have good picture of the library.

The system, especially iOS, changes very often. There are some libraries that try to “replicate” system APIs or perform type checking for every possible types. It can be that the author pick the most common use cases, or just trying to provide a cleaner APIs to the user. But if things change, will that author be willing to fix that? Will you be stuck there and making another issue asking for help? We’re making life better, not traps for everyone to fall into 🙀

Here I don’t try to blame anyone, “you is not your work”. I just say that you should check it more thoroughly. You can consult your colleagues and discuss if it’s good to integrate. Most of the time, pulling a huge library just for some tiny syntactic sugar does not worth it

I just put some random links here and you can determine if you like or not

Please

Would you take a very big risk for so little return ? Always implement and imagine that you will be the next maintainer for the project, then you will act differently.

So the next time, please reconsider your choice of libraries. Take 1 step back and analyse a bit ❤️

Diff algorithm

Issue #99

I’ve been searching for efficient ways to diff collections, here are some interesting papers that I find

Myers

Wu

Wagner–Fischer

Common Longest Subsequence

Heckel

Hunt-Szymanski

Read more

How to use safeAreaLayoutGuide in iOS 10

Issue #98

The safeAreaLayoutGuide was introduced in iOS 11. And it is advised to stop using topLayoutGuide bottomLayoutGuide as these are deprecated.

To use safeAreaLayoutGuide, you need to do iOS version check

1
2
3
4
5
if #available(iOS 11.0, *) {
headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
} else {
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20)
}

Maybe we can introduce a common property that can be used across many iOS versions, let’s call it compatibleSafeAreaLayoutGuide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
extension UIView {
/// Use safeAreaLayoutGuide on iOS 11+, otherwise default to dummy layout guide
var compatibleSafeAreaLayoutGuide: UILayoutGuide {
if #available(iOS 11, *) {
return safeAreaLayoutGuide
} else {
if let layoutGuide = self.associatedLayoutGuide {
return layoutGuide
} else {
let layoutGuide = UILayoutGuide()
Constraint.on(
layoutGuide.topAnchor.constraint(equalTo: topAnchor),
layoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor),
layoutGuide.leftAnchor.constraint(equalTo: leftAnchor),
layoutGuide.rightAnchor.constraint(equalTo: rightAnchor)
)

self.associatedLayoutGuide = layoutGuide

return layoutGuide
}
}
}

private struct AssociatedKeys {
static var layoutGuide = "layoutGuide"
}

fileprivate var associatedLayoutGuide: UILayoutGuide? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.layoutGuide) as? UILayoutGuide
}

set {
if let newValue = newValue {
objc_setAssociatedObject(
self, &AssociatedKeys.layoutGuide,
newValue as UILayoutGuide?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}

This way we can simply do

1
headerView.topAnchor.constraint(equalTo: view.compatibleSafeAreaLayoutGuide.topAnchor, constant: 20)

Read more

Learning from Open Source Using Coordinator

Issue #97

The Coordinator pattern can be useful to manage dependencies and handle navigation for your view controllers. It can be seen from BackchannelSDK-iOS, take a look at BAKCreateProfileCoordinator for example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@implementation BAKCreateProfileCoordinator

- (instancetype)initWithUser:(BAKUser *)user navigationController:(UINavigationController *)navigationController configuration:(BAKRemoteConfiguration *)configuration {
self = [super init];
if (!self) return nil;

_navigationController = navigationController;
_user = user;
_profileViewController = [[BAKProfileFormViewController alloc] init];
[self configureProfileForm];
_configuration = configuration;

return self;
}

- (void)start {
[self.profileViewController updateDisplayName:self.user.displayName];
[self.navigationController pushViewController:self.profileViewController animated:YES];
}

- (void)profileViewControllerDidTapAvatarButton:(BAKProfileFormViewController *)profileViewController {
BAKChooseImageCoordinator *imageChooser = [[BAKChooseImageCoordinator alloc] initWithViewController:self.navigationController];
imageChooser.delegate = self;
[self.childCoordinators addObject:imageChooser];
[imageChooser start];
}

- (void)imageChooserDidCancel:(BAKChooseImageCoordinator *)imageChooser {
[self.childCoordinators removeObject:imageChooser];
}

Look how it holds navigationController as root element to do navigation, and how it manages childCoordinators

coordinator

Read more

Learning from Open Source Managing dependencies

Issue #96

Another cool thing about ios-oss is how it manages dependencies. Usually you have a lot of dependencies, and it’s good to keep them in one place, and inject it to the objects that need.

The Environment is simply a struct that holds all dependencies throughout the app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
A collection of **all** global variables and singletons that the app wants access to.
*/
public struct Environment {
/// A type that exposes endpoints for fetching Kickstarter data.
public let apiService: ServiceType

/// The amount of time to delay API requests by. Used primarily for testing. Default value is `0.0`.
public let apiDelayInterval: DispatchTimeInterval

/// A type that exposes how to extract a still image from an AVAsset.
public let assetImageGeneratorType: AssetImageGeneratorType.Type

/// A type that stores a cached dictionary.
public let cache: KSCache

/// ...
}

Then there’s global object called AppEnvironment that manages all these Environment in a stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public struct AppEnvironment {
/**
A global stack of environments.
*/
fileprivate static var stack: [Environment] = [Environment()]

/**
Invoke when an access token has been acquired and you want to log the user in. Replaces the current
environment with a new one that has the authenticated api service and current user model.

- parameter envelope: An access token envelope with the api access token and user.
*/
public static func login(_ envelope: AccessTokenEnvelope) {
replaceCurrentEnvironment(
apiService: current.apiService.login(OauthToken(token: envelope.accessToken)),
currentUser: envelope.user,
koala: current.koala |> Koala.lens.loggedInUser .~ envelope.user
)
}

/**
Invoke when we have acquired a fresh current user and you want to replace the current environment's
current user with the fresh one.

- parameter user: A user model.
*/
public static func updateCurrentUser(_ user: User) {
replaceCurrentEnvironment(
currentUser: user,
koala: current.koala |> Koala.lens.loggedInUser .~ user
)
}

public static func updateConfig(_ config: Config) {
replaceCurrentEnvironment(
config: config,
koala: AppEnvironment.current.koala |> Koala.lens.config .~ config
)
}

// Invoke when you want to end the user's session.
public static func logout() {
let storage = AppEnvironment.current.cookieStorage
storage.cookies?.forEach(storage.deleteCookie)

replaceCurrentEnvironment(
apiService: AppEnvironment.current.apiService.logout(),
cache: type(of: AppEnvironment.current.cache).init(),
currentUser: nil,
koala: current.koala |> Koala.lens.loggedInUser .~ nil
)
}

// The most recent environment on the stack.
public static var current: Environment! {
return stack.last
}

}

Then whenever there’s event that triggers dependencies update, we call it like

1
2
3
4
5
self.viewModel.outputs.logIntoEnvironment
.observeValues { [weak self] accessTokenEnv in
AppEnvironment.login(accessTokenEnv)
self?.viewModel.inputs.environmentLoggedIn()
}

The cool thing about Environment is that we can store and retrieve them

1
2
3
4
5
6
// Returns the last saved environment from user defaults.
public static func fromStorage(ubiquitousStore: KeyValueStoreType,
userDefaults: KeyValueStoreType) -> Environment {
// retrieval

}

And we can mock in tests

1
2
3
4
5
6
7
8
9
AppEnvironment.replaceCurrentEnvironment(
apiService: MockService(
fetchDiscoveryResponse: .template |> DiscoveryEnvelope.lens.projects .~ [
.todayByScottThrift,
.cosmicSurgery,
.anomalisa
]
)
)

Learning from Open Source Using Playground

Issue #94

One thing I like about kickstarter-ios is how they use Playground to quickly protoyping views.

We use Swift Playgrounds for iterative development and styling. Most major screens in the app get a corresponding playground where we can see a wide variety of devices, languages and data in real time.

This way we don’t need Injection or using React Native anymore. Take a look at all the pages https://github.com/kickstarter/ios-oss/tree/master/Kickstarter-iOS.playground/Pages

Read more

Indenting Swift code

Issue #93

Hi, here is how I indent my code. Let me know what you think 😉

Using 2 spaces indentation

When possible, configure your editor to use 2 spaces for tab size. You will love it ❤️

spaces

Move first parameter to new line

If there are many parameters, move the first parameter to a new line, and align the other parameters. Remember that the last parenthesis ) should align to the function call

1
2
3
4
5
6
7
let event = NSAppleEventDescriptor(
eventClass: UInt32(kASAppleScriptSuite),
eventID: UInt32(kASSubroutineEvent),
targetDescriptor: target,
returnID: Int16(kAutoGenerateReturnID),
transactionID: Int32(kAnyTransactionID)
)

You can do the same for function declaration

1
2
3
4
5
6
func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
// your code goes here
}

Shouldn’t use trailing closure if there are more than 2 closures

Here is how to use UIView.animate

1
2
3
4
5
6
7
8
9
10
11
12
13
UIView.animate(
withDuration: 5,
delay: 5,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseIn,
animations: {
self.tableView.alpha = 1
},
completion: { _ in
self.view.isHidden = true
}
)

Here is how to use RxSwift subscribe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
friendsObservable.subscribe(
onNext: { friends in

},
onError: { error in

},
onCompleted: {

},
onDisposed: {

}
)

Shouldn’t use trailing closure when chaining methods

Also, the next method call should start on same line

1
2
3
4
5
6
7
8
let items = [1, 2, 3, 4, 5]
let results = items.map({
return String($0)
}).flatMap({
return Int($0)
}).filter({
return $0 > 2
}).sorted()

Sync and async code in Swift

Issue #75

We should use DispatchQueue to build thread safe code. The idea is to prevent two read and write from happening at the same time from 2 different threads, which cause data corruption and unexpected behaviors. Note that you should try to avoid deadlock https://stackoverflow.com/questions/15381209/how-do-i-create-a-deadlock-in-grand-central-dispatch

All sync

Use try catch, together with serial queue. Use sync function to block current queue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getUser(id: String) throws -> User {
var user: User!
try serialQueue.sync {
user = try storage.getUser(id)
}

return user
}

func setUser(_ user: User) throws {
try serialQueue.sync {
try storage.setUser(user)
}
}

All async

Use Result, toget with serial queue. Use async function to return to current queue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Result<T> {
case value(T)
case error(Error)
}

func getUser(id: String, completion: (Result<User>) - Void) {
try serialQueue.async {
do {
user = try storage.getUser(id)
completion(.value(user))
} catch {
completion(.error(error))
}
}

return user
}

func setUser(_ user: User, completion: (Result<()>) -> Void) {
try serialQueue.async {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}

Sync read, async write

Use try catch for read, Result for write, together with concurrent queue. Use sync function for read to block current thread, while using async function with barrier flag for write to return to current queue. This is good for when multiple reads is preferred when there is no write. When write with barrier comes into the queue, other operations must wait.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func getUser(id: String) throws -> User {
var user: User!
try concurrentQueue.sync {
user = try storage.getUser(id)
}

return user
}

func setUser(_ user: User, completion: (Result<()>) -> Void) {
try concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(())
} catch {
completion(.error(error))
}
}
}

Testing for asynchrony

Before we could use dispatch_apply to submits a block to a dispatch queue for multiple invocations. Starting with Swift, the equivalence is concurrentPerform

1
2
3
4
DispatchQueue.concurrentPerform(iterations: 1000) { index in
let last = array.last ?? 0
array.append(last + 1)
}

Reference

How to use Given When Then in Swift tests

Issue #73

Spec

Using spec testing framework like Quick is nice, which enables BDD style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe("the 'Documentation' directory") {
it("has everything you need to get started") {
let sections = Directory("Documentation").sections
expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups"))
expect(sections).to(contain("Installing Quick"))
}

context("if it doesn't have what you're looking for") {
it("needs to be updated") {
let you = You(awesome: true)
expect{you.submittedAnIssue}.toEventually(beTruthy())
}
}
}

But in case you don’t want additional frameworks, and want to live closer to Apple SDKs as much as possible, here are few tips.

Naming

This is from the book that I really like The Art of Unit Testing. If you don’t mind the underscore, you can follow UnitOfWork_StateUnderTest_ExpectedBehavior structure

1
2
3
func testSum_NegativeNumberAs1stParam_ExceptionThrown()
func testSum_NegativeNumberAs2ndParam_ExceptionThrown()
func testSum_simpleValues_Calculated()

Given When Then

This is from BDD, and practised a lot in Cucumber. You can read more on https://martinfowler.com/bliki/GivenWhenThen.html.

First, add some more extensions to XCTestCase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import XCTest

extension XCTestCase {
func given(_ description: String, closure: () throws -> Void) throws {
try closure()
}

func when(_ description: String, closure: () throws -> Void) throws {
try closure()
}

func then(_ description: String, closure: () throws -> Void) throws {
try closure()
}
}

Then, in order to test, just follow given when then

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func testRemoveObject() throws {
try given("set to storage") {
try storage.setObject(testObject, forKey: key)
}

try when("remove object from storage") {
try storage.removeObject(forKey: key)
}

try then("there is no object in memory") {
let memoryObject = try? storage.memoryCache.object(forKey: key) as User
XCTAssertNil(memoryObject)
}

try then("there is no object on disk") {
let diskObject = try? storage.diskCache.object(forKey: key) as User
XCTAssertNil(diskObject)
}
}

I find this more interesting than comments. All are code and descriptive. It can also be developed further to throw the description text.


Updated at 2020-12-18 15:15:26

Optional of optional in Swift

Issue #58

Do you know that an optional can itself contain an optional, that contains another optional? In that case, we need to unwrap multiple times

optionals

You mostly see it when you try to access window

1
let window = UIApplication.shared.delegate?.window // UIWindow??

It is because delegate can be nil, and its window can be nil too.

1
window??.backgroundColor = .yellow

How to change year in Date in Swift

Issue #54

Today I’m trying to change the year of a Date object to 2000 in Swift.

1
let date = Date()

Firstly, I tried with date(bySetting:) but it does not work with past year. It simply returns nil

1
Calendar.current.date(bySetting: .year, value: 2000, of: date)

Secondly, I tried with dateComponents. The component.year has changed, but it calendar still returns the original date, very strange !!. No matter what timezone and calendar I use, it still has this problem

1
2
3
var component = calendar.dateComponents(in: TimeZone.current, from: base)
component.year = year
Calendar.current.date(from: component)

Finally, I tried to be more explicit, and it works 🎉

1
2
3
var component = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: base)
component.year = year
Calendar.current.date(from: component)

How to run UI Test with system alert in iOS

Issue #48

Continue my post https://github.com/onmyway133/blog/issues/45. When you work with features, like map view, you mostly need permissions, and in UITests you need to test for system alerts.

Add interruption monitor

This is the code. Note that you need to call app.tap() to interact with the app again, in order for interruption monitor to work

1
2
3
4
5
6
addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
alert.buttons["Allow"].tap()
return true
})

app.tap()

Note that you don’t always need to handle the returned value of addUIInterruptionMonitor

Only tap when needed

One problem with this approach is that when there is no system alert (you already touched to allow before), then app.tap() will tap on your main screen. In my app which uses map view, it will tap on some pins, which will present another screen, which is not correct.

Since app.alerts does not work, my 2nd attempt is to check for app.windows.count. Unfortunately, it always shows 5 windows whether alert is showing or not. I know 1 is for main window, 1 is for status bar, the other 3 windows I have no idea.

The 3rd attempt is to check that underlying elements (behind alert) can’t be touched, which is to use isHittable. This property does not work, it always returns true

Check the content

This uses the assumption that we only tests for when user hits Allow button. So only if alert is answered with Allow, then we have permission to display our content. For my map view, I check that there are some pins on the map. See https://github.com/onmyway133/blog/issues/45 on how to mock location and identify the pins

1
2
3
if app.otherElements.matching(identifier: "myPin").count == 0 {
app.tap()
}

When there is no permission

So how can we test that user has denied your request? In my map view, if user does not allow location permission, I show a popup asking user to go to Settings and change it, otherwise, they can’t interact with the map.

I don’t know how to toggle location in Privacy in Settings, maybe XCUISiriService can help. But 1 thing we can do is to mock the application

Before you launch the app in UITests, add some arguments

1
app.launchArguments.append("--UITests-mockNoLocationPermission")

and in the app, we need to check for this arguments

1
2
3
4
5
func checkLocationPermission() {
if CommandLine.arguments.contains("--UITests-mockNoLocationPermission") {
showNoLocationPopupAndAskUserToEnableInSettings()
}
}

That’s it. In UITests, we can test whether that no location permission popup appears or not

Updated at 2020-06-02 02:12:36

How to run UITests with map view in iOS

Issue #45

Mock a location

You should mock a location to ensure reliable test

Create the gpx file

Go to Xcode -> File -> New -> GPX File

gpx

It looks like

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<wpt lat="59.913590" lon="10.733750">
<name>Oslo S</name>
<time>2017-05-31T14:55:37Z</time>
</wpt>
<wpt lat="59.913590" lon="10.733750">
<name>Oslo S</name>
<time>2017-05-31T14:55:40Z</time>
</wpt>
</gpx>

The gpx file is very powerful, as it allows you to specify a route with different movement speed.

Provide one or more waypoints containing a latitude/longitude pair. If you provide one
waypoint, Xcode will simulate that specific location. If you provide multiple waypoints,
Xcode will simulate a route visiting each waypoint.

Optionally provide a time element for each waypoint. Xcode will interpolate movement
at a rate of speed based on the time elapsed between each waypoint. If you do not provide
a time element, then Xcode will use a fixed rate of speed. Waypoints must be sorted by time in ascending order.

Use the gpx file

  • Declare the gpx file in app target, not UITests target. Go to your app scheme -> Run -> Options

location

  • Go to Simulator -> Debug -> Location -> Custom Location and select that same location, just to make sure. It does not need to be the same, but I see that without Custom Location, it does not work in UITests

Test that you’re near the initial location

1
2
3
4
5
6
7
let map = app.maps.element(boundBy: 0)
let predicate = NSPredicate(format: "label CONTAINS 'City Hall'")
let cityHall = map.otherElements.matching(predicate).element(boundBy: 0)

// wait for the map to finish loading and zooming
wait(for: cityHall, timeout: 2)
XCTAssertTrue(cityHall.exists)

The wait function is from https://github.com/onmyway133/blog/issues/44

Test that you can interact with your custom pin

You need to specify accessibilityIdentifier, like

1
2
3
4
5
6
7
class MyPin: MKAnnotationView {
override func didMoveToSuperview() {
super.didMoveToSuperview()

accessibilityIdentifier = "myPin"
}
}

and then query for that pin. Not that it is not inside map, it is inside app

1
2
let pin = app.otherElements.matching(identifier: "myPin").element(boundBy: 0)
XCTAssertTrue(pin.exists)

You should use accessibilityIdentifier

accessibilityIdentifier is from UIAccessibilityIdentification protocol. You should not use accessibilityLabel, see https://github.com/kif-framework/KIF/issues/243

Given that accessibilityLabel is an outwardly-facing string that is actually used by accessibility screen readers (and should be localized to the device user’s language), Apple now provides an alternate property (iOS 5+) that is specifically intended for UI Automation purposes

How to use MainController in iOS

Issue #36

Usually in an app, we have these flows: onboarding, login, main. And we usually set OnboardingController, LoginController and MainController as the root view controller respectively depending on the state.

I find it useful to have the MainController as the container for main flow. It can be a tab controller, swipe menu controller or contains just 1 child view controller. The screens are provided by child view controllers, but the MainController does the following jobs

  • Status bar style

We usually need to call preferredStatusBarStyle on the parent controller. See https://stackoverflow.com/questions/19022210/preferredstatusbarstyle-isnt-called

  • App did become active

Usually when app is brought to foreground, we need to fetch logged in user profile to see if there’s changes. We do this by listening to app did become active in MainController.

  • Mock to open

This can be anti pattern. But in UI Tests, for laziness, we can just use some launch arguments and check to present some specific screens to test, because MainController is the root for main flow.

  • Logout

Because things originate from MainController, things can terminate in MainController. We can handle logout, clear states, and tell MainController to tell AppDelegate to switch to another root controller

How to handle Auto Layout with different screen sizes

Issue #35

Auto Layout is awesome. Just declare the constraints and the views are resized accordingly to their parent ‘s bounds changes. But sometimes it does not look good, because we have fixed values for padding, width, height, and even fixed font size.

Read more How to make Auto Layout more convenient in iOS

This can be solved by some degree using Size Class. The idea of size class is that we have many sets of constraints, and based on the device traits, we enabled some of them. This is more convenient to do in Storyboard (although very hard to reason about), but if we’re doing in code (my prefer way), then it is a lot of code. And a lot of code means a lot of bugs.

If you take a look at iOSRes, we see the ratio 16:9 (height:width)

  • iPhone SE (320 x 568): 1.775
  • iPhone 6 (375 x 667): 1.778
  • iPhone 6+ (414 x 736): 1.778

They mostly have the same ratio. So we can have a simple approach, that scale elements based on ratio. Given the fact that the designer usually designs for iPhone 6 size, we can make that a base.

In this approach, the content will scale up or down depending on its ratio. You may argue that the idea of bigger phone is to display more, not to show the same content bigger. You may be right, in that case you need to create different constraints and different UIs. But if you want simple solutions that work, this is one of them

This is the technique I used when doing Windows Phone development, but it applies to many platforms as well

Calculate the ratio

1
2
3
4
5
6
7
8
class Device {
// Base width in point, use iPhone 6
static let base: CGFloat = 375

static var ratio: CGFloat {
return UIScreen.main.bounds.width / base
}
}

Extension to make it convenient

We can have a computed property called adjusted that adjusts the size based on the ratio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension CGFloat {

var adjusted: CGFloat {
return self * Device.ratio
}
}

extension Double {

var adjusted: CGFloat {
return CGFloat(self) * Device.ratio
}
}

extension Int {

var adjusted: CGFloat {
return CGFloat(self) * Device.ratio
}
}

Use the ratio

You can adjust as much as you want

1
2
3
4
5
6
label.font = UIFont.systemFont(ofSize: 23.adjusted)

phoneTextField.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 30.adjusted),
phoneTextField.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -30.adjusted),

imageView.widthAnchor.constraint(equalToConstant: 80.adjusted), imageView.heightAnchor.constraint(equalToConstant: 90.adjusted),

NSApplicationDelegate and notification

Issue #34

In an iOS project, we often see this in AppDelegate

1
2
3
4
5
6
7
8
9
10
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

return true
}
}

But in a Cocoa project, we see this instead

1
2
3
4
5
6
7
8
9
10
11
12
13
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {



func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}

func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}

In this case the param is of type NSNotification

Delegate and notification

Reading Cocoa Core Competencies - Delegation

The delegate of most Cocoa framework classes is automatically registered as an observer of notifications posted by the delegating object. The delegate need only implement a notification method declared by the framework class to receive a particular notification message. Following the example above, a window object posts an NSWindowWillCloseNotification to observers but sends a windowShouldClose: message to its delegate.

So the pattern is that the delegate should strip the NS and Notification, like NSWindowWillCloseNotification to windowShouldClose:

How to define SDK and Deployment Target in iOS

Issue #33

I see that my answer to the question What’s the meaning of Base SDK, iOS deployment target, Target, and Project in xcode gets lots of views, so I think I need to elaborate more about it

Good read

base

Base SDK

  • We can’t configure this anymore, as Xcode will use the latest SDK. For Xcode 7, the SDK is iOS 9
  • If we upgrade Xcode, it will use the newer version of the SDK. Like Xcode 7.2, the SDK is iOS 9.1
  • Choosing the latest SDK for your project lets you use the new APIs introduced in the OS update that corresponds to that SDK. When new functionality is added as part of a system update, the system update itself does not typically contain updated header files reflecting the change. The SDKs, however, do contain updated header files.

Deployment Target

  • We can set in Xcode -> Target -> Deployment Info -> Deployment Target
  • State that we support this iOS version

What does it mean

So, a modern App might use iOS 9 as the Target SDK, and iOS 7 as the deployment target. This means that you can run on iOS 7, iOS 8 and iOS 9, and that you have available to you any iOS 9 calls when actually running on iOS 9.

.

Each .sdk directory resembles the directory hierarchy of the operating system release it represents: It has usr, System, and Developer directories at its top level. OS X .sdk directories also contain a Library directory. Each of these directories in turn contains subdirectories with the headers and libraries that are present in the corresponding version of the operating system with Xcode installed.

.

The libraries in an iOS or OS X SDK are stubs for linking only; they do not contain executable code but just the exported symbols. SDK support works only with native build targets.

So the SDK is just like stub and header only. It means that we can use certain APIs, but on OS that does not have the real symbols for those APIs, it crashes

available

Swift 2 introduces available construct that guards against failure when trying to use newer APIs.

Note that available is runtime, not compile time. All the code is inside your executable

1
2
3
4
5
if #available(iOS 9, OSX 10.10, *) {
// Code to execute on iOS 9, OS X 10.10
} else {

}

deprecated APIs

Always check to see if you are using deprecated APIs; though still available, deprecated APIs are not guaranteed to be available in the future

Compile time vs Runtime

1
2
3
4
5
#if (arch(i386) || arch(x86_64)) && os(iOS)
// code inside gets inserted into executable when builds for simulator
#else
// code inside gets inserted into executable when builds for device
#endif
1
2
3
4
5
#if os(OSX)
import Cocoa
#elseif os(iOS)
import UIKit
#endif
1
2
3
4
5
6
// All the code gets inserted into executable, but is run depending on the version of the OS
if #available(iOS 9, *) {
// use UIStackView
} else {
// show your manual Auto Layout skill
}

Weakly vs strongly linked

For example, suppose in Xcode you set the deployment target (minimum required version) to “OS X v10.5” and the base SDK (maximum allowed version) to “OS X v10.6”. During compilation, the compiler would weakly link interfaces that were introduced in OS X v10.6 while strongly linking interfaces defined in earlier versions of the OS. This would allow your application to run in OS X v10.5 and take advantage of newer features when available.

.

None of the (platform) frameworks is really “included in the bundle”. Instead, your app has a reference (“link”) to a framework once you add it to the “Link Binary with Library” build phase. The frameworks are pre-installed on the devices. When you run an app, all the app’s framework references are resolved by the dynamic linker (on the device), which means the framework code is loaded so your app can use it.

Reference


Updated at 2020-12-08 06:31:54

How to group digits in Swift

Issue #26

When working on Scale I think it’s good to have a way to group the digit so that it is easier to reason

Luckily, Swift already supports this. See The Swift Programming Language - Numeric Literals

Numeric literals can contain extra formatting to make them easier to read. Both integers and floats can be padded with extra zeros and can contain underscores to help with readability. Neither type of formatting affects the underlying value of the literal

1
2
3
let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

Talking about grouping digits after the decimal point, it is interesting too Convention of digit grouping after decimal point

So now we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum MetricUnit: Double {
case nano = 0.000_000_001
case micro = 0.000_001
case milli = 0.001
case centi = 0.01
case deci = 0.1
case base = 1
case deka = 10
case hecto = 100
case kilo = 1_000
case mega = 1_000_000
case giga = 1_000_000_000
case tera = 1_000_000_000_000
case peta = 1_000_000_000_000_000

static var defaultScale: Double {
return MetricUnit.base.rawValue
}
}

How to make lighter AppDelegate in iOS

Issue #24

There is Lighter View Controllers, and there is Lighter AppDelegate, too

Since working with iOS, I really like the delegate pattern, in which it helps us defer the decision to another party.

The iOS application delegates its event to AppDelegate, which over time will be a big mess. Usually, the AppDelegate is where you put your root view controller setup, crash tracking, push notification, debugging, … and we just somehow violent the Single Responsibility principle. Moreover, it makes us hard to reason about code in AppDelegate

Service

I like to think of each task in AppDelegate as a service. And the AppDelegate distributes the events into each service via ServiceDispatcher. Simple plain old composition and looping

I tend to have RootService as a place to setup root view controllers

It looks like this

ServiceDispatcher.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ServiceDispatcher : NSObject, UIApplicationDelegate {
let services: [UIApplicationDelegate]

init(services: [UIApplicationDelegate]) {
self.services = services
}

func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {

services.forEach { service in
service.application?(application, didFinishLaunchingWithOptions: launchOptions)
}

return true
}

func applicationDidBecomeActive(application: UIApplication) {
services.forEach { service in
service.applicationDidBecomeActive?(application)
}
}

func applicationWillResignActive(application: UIApplication) {
services.forEach { service in
service.applicationWillResignActive?(application)
}
}

func applicationWillEnterForeground(application: UIApplication) {
services.forEach { service in
service.applicationWillEnterForeground?(application)
}
}

func applicationDidEnterBackground(application: UIApplication) {
services.forEach { service in
service.applicationDidEnterBackground?(application)
}
}
}

RootService.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RootService : NSObject, UIApplicationDelegate {
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {

appDelegate().window = UIWindow(frame: UIScreen.mainScreen().bounds)
showHome()
appDelegate().window?.makeKeyAndVisible()

return true
}
}

extension RootService {
func showHome() {
let home = HomeWireframe().makeHome()
let navC = UINavigationController(rootViewController: home!)
appDelegate().window?.rootViewController = navC
}
}

extension RootService {
func appDelegate() -> AppDelegate {
return UIApplication.sharedApplication().delegate as! AppDelegate
}
}

AppDelegate.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
let serviceDispatcher = ServiceDispatcher(services: [RootService()])


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

serviceDispatcher.application(application, didFinishLaunchingWithOptions: launchOptions)

return true
}

func applicationWillResignActive(application: UIApplication) {
serviceDispatcher.applicationWillResignActive(application)
}

func applicationDidEnterBackground(application: UIApplication) {
serviceDispatcher.applicationDidEnterBackground(application)
}

func applicationWillEnterForeground(application: UIApplication) {
serviceDispatcher.applicationWillEnterForeground(application)
}

func applicationDidBecomeActive(application: UIApplication) {
serviceDispatcher.applicationDidBecomeActive(application)
}
}

I have more services like DebugService, PushNotificationService, CrashTrackingService, …

The downside to this approach is that in real life, there will be dependencies between those services, like that UserService must be called before RootService? In this case, I have to use comment to explain why I have that decision, which is hard for newcomers to understand at first. Take a look at How to Move Bootstrapping Code Out of AppDelegate for how dependencies are managed

JSDecoupledAppDelegate comes with another approach, in which service events are named according to the functions, like appStateDelegate, appDefaultOrientationDelegate, watchInteractionDelegate, …

But for me, Service and ServiceDispatcher suit my need

Reference

How to handle RefreshControl in iOS

Issue #20

The other day I was doing refresh control, and I saw this Swift Protocols with Default Implementations as UI Mixins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension Refreshable where Self: UIViewController
{
/// Install the refresh control on the table view
func installRefreshControl()
{
let refreshControl = UIRefreshControl()
refreshControl.tintColor = .primaryColor
refreshControl.addTarget(self, action: #selector(handleRefresh(_:)), for: .valueChanged)
self.refreshControl = refreshControl

if #available(iOS 10.0, *)
{
tableView.refreshControl = refreshControl
}
else
{
tableView.backgroundView = refreshControl
}
}
}

Protocol extension is cool but somehow I’m not a fan of it. I always consider composition first, to extract the specific task to one entity that does that well. It looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RefreshHandler: NSObject {
let refresh = PublishSubject<Void>()
let refreshControl = UIRefreshControl()

init(view: UIScrollView) {
super.init()
view.addSubview(refreshControl)
refreshControl.addTarget(self, action: #selector(refreshControlDidRefresh(_: )), for: .valueChanged)
}

// MARK: - Action

func refreshControlDidRefresh(_ control: UIRefreshControl) {
refresh.onNext(())
}

func end() {
refreshControl.endRefreshing()
}
}

It is a bit Rx, we can use block if we like, but the idea is we can declare this RefreshHandler and use it everywhere we want

1
2
3
4
5
6
refreshHandler = RefreshHandler(view: scrollView)

refreshHandler.refresh
.startWith(())
.bindTo(viewModel.input.fetch)
.addDisposableTo(bag)

Swift snippets

Issue #17

I always forget how to write correct #available( or #if swift(>=3.0) or just lazy to write required init?(coder aDecoder: NSCoder) every time I make a subclass. That’s why I made SwiftSnippets to save time for these tedious tasks. Installation is easy with script, so you should give it a try.

I can’t recommend this enough, it saves me tons of time