How to enable black mode in Google Maps in iOS

Issue #246

Use GMSMapStyle https://developers.google.com/maps/documentation/android-sdk/styling
Export styling json from https://mapstyle.withgoogle.com/

1
2
let mapStyleUrl = Bundle.main.url(forResource: "mapStyle", withExtension: "json")!
mapView.mapStyle = try? GMSMapStyle(contentsOfFileURL: mapStyleUrl)

To change overall color, search for mostly "elementType": "geometry" and "featureType": "water"

1
2
3
4
5
6
7
8
{
"elementType": "geometry",
"stylers": [
{
"color": "#424242"
}
]
}
1
2
3
4
5
6
7
8
9
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#2E2E2E"
}
]
}

Favorite WWDC 2018 sessions

Issue #245

Original post https://medium.com/fantageek/my-favourite-wwdc-2018-sessions-363d3fc9c9d5


Favourite WWDC 2018 sessions

This year I failed the lottery ticket to WWDC, and I also missed the keynote live stream because I was sailing on the Christian Radich outside Oslo fjord that day. Luckily all the videos are available on Apple Developer site very shortly, and we can watch them now on Chrome or the unofficial WWDC app on macOS. I recommend the WWDC macOS app as it allows to mark favourites and filter, also offers the ability to adjust play speed to 1.25 or 1.5 saves me some time watching.

This year WWDC focuses a lot on privacy, stability, and speed, which are all I wish, so many thanks to Apple engineers who made that happen, and the resit to install the so called more stable iOS 12 is real. As an iOS engineers, I like to focus more about the necessary things to me, that is about the Swift programming language, new changes in Cocoa Touch, enhancements in Xcode and testing tricks. I also like to explore more about machine learning so I’m very glad that Apple is investing more into this technology with the introduction of Turi Create and Create ML.

To me, APIs come and get deprecated very often and it’s good to know them, but the most important thing is to invest in your programming, debugging and testing skill which you can apply in many other platforms.

Continued from last year favourites list, below are my favourite sessions with personal notes. Things are listed in no particular order. Hope you find it useful.

Platforms State of the Union

If you don’t have time, you should watch only this session. Platform State of the Union is like keynote for developers as it highlights important changes.

  • Privacy: Apple confirms on its commitment in privacy and security, also introduces password management feature and auto fill on iOS 12. Generating strong password, integrating with 3rd password management and quickly populating OTP field from SMS message have never been easier. GateKeeper gets some improvements as well and begins to require app to be notarised.

  • iOS 12: huge improvement in performance, Siri gets smarter with Shortcut support, group calling in FaceTime and grouped notification. Also for emoji fan, Memoji was introduced.

  • macOS 10.14 Mojave: more with Dark Mode. They demo mostly with Xcode in dark mode, which looks so cool. This year WWDC banner give hints about iOS and macOS cross-platform apps, which is partially true with Marzipan, a way to allow iOS apps to run on the mac.

  • Xcode 10: with improvements in code editing and source control changes bar indicator. Debugging with memory debug tool, LLDB performance enhancement and particular the new build system completely rewritten in Swift with parallel tasks are exciting news.

  • Swift 4.2: if you follow swift repo then Swift 4.2 may not be very surprising. There are also announcements for Swift 5 plan.

  • Machine Learning: is never hotter than this. This year we see huge investments in machine learning with Create ML, Turi Create, Natural Language frameworks, CoreML 2, new detection capabilities in Vision.

  • ARKit 2, watchOS 5, tvOS 12, AppStore Connect and AppStore Connect APIs are some other important news you don’t want to miss.

What’s new in Swift

Together with this session, I recommend you to read What’s new in Swift 4.2 summary which is very succinct. Besides improvement in complication and runtime, Swift 4.2 offers some new features: iterable enum case, synthesised Equatable and Hashable, handy functions for shuffling, random generating. To me, the need to explicitly handle Implicitly unwrapped optional is also a reasonable change.

What’s New in Cocoa Touch

This is a brief introduction to all changes coming to iOS 12, together with tips on how to be a good iOS citizen. Learn what can affect scrolling experience and prefetching technique, memory consumption and automatic backing stores, how to get the best from UIImage and UIImageView . AutoLayout engine got a lot of performance improvement so it won’t bother you any more. To me the most satisfying is to get all the UIKit notifications and classes reorganised under nested types, which makes code reasoning very easy.

Getting the Most out of Playgrounds in Xcode

I’ve written about Playground before and I’m very glad that Apple also invests a lot in it. The way people can interact and train model [Create ML](http://Introducing Create ML) in Playground is mesmerising. People may question how Playground works so well in session’s demos, but we can’t resist the new changes coming to Playground like Step by Step execution, markup rendering improvements and also how easy it is to consume custom frameworks. We can also now publish our own Playground through subscription.

What’s New in Core ML

Apple starts the machine learning trend last year with the introduction of Core ML. We might be excited and frustrated at the same time as Core ML is powerful but there’s no way we can customise it. Now the 2 parts tell us how to implement custom layer and model, techniques to reduce model size like quantisation and flexible model. This makes the foundation for improvement in Vision in object tracking and the debut of Natural Language framework. Machine learning has never been easier.

What’s New in Testing

I can’t miss any testing sessions as it is part of every day’s work. How can your program avoids regression bugs and ready for refactoring without any tests?

This session shows improvement in coverage and the introduction of xccov tool to help us build automation on top of coverage report. Parallel distributed testing in Xcode 10 can save us some time to have coffee. Another wonderful news is that tests have multiple order execution mode to avoid bugs due to implicit dependencies.

Testing Tips & Tricks 🌟

This is my most favourite. The session starts with a pyramid of tests with unit integration and end-to-end test analogy explanation, then to some very cool tips and tricks.

  • Testing network request: I like the separation of APIRequest and APIRequestLoader with URLSession , dependency injection with default parameter and the customisation of URLProtocol in URLSessionConfiguration

  • Testing notification: Notification is system wide and I try to avoid it as much as possible. This shows how to inject dependency with default parameter and use of own NotificationCenter instead of NotificationCenter.default to ease testing

  • Testing location: build abstraction with LocationProvider and LocationFetcher . How to use custom protocol and protocol for delegate to mock during test

  • Testing timer: how to use and mock RunLoop behaviour with Timer

Advanced Debugging with Xcode and LLDB

LLDB has been improved to enable to debugging reliability experience, issues with AST context corruption, Swift type resolution are now things in the past. We can review how to use some common LLDB commands with handy arguments, and how to use Xcode breakpoint to its best.

A Tour of UICollectionView 🌟

I begin to use UICollectionView more than UITableView , and it also has same code as NSCollectionView,which is more comfortable solution than the horrible NSTableView .

  • Item size in UICollectionViewLayout : I often rely on UICollectionViewDelegateFlowLayout to specify item size but after watching this session, I feel like moving size related code to Layout object feels more like a good way to go

  • Mosaic layout: This is not new, but good to watch again. You learn how to implement custom layout using cached layout attributes

  • Data Source update: I didn’t expect Apple mentions this, but it is a good lesson on how UICollectionView handles batch update. I ‘ve written about this before in my A better way to update UICollectionView data in Swift with diff framework and that post gets a lot of attractions. In this session we need to remember that *ordering matters in data source update, but not in collection view update *❗️❗️❗️

Swift Generics

Generic was a core feature of Swift since the beginning, we all know that it helps us to implement generic code that can work with many types in a type safe manner. This session reminds that I ‘ve never learned enough, especially the reasonable design behind it.

The sessions showcases Collection protocol and its protocol inheritances: MutableCollection , BidirectionalCollection , RandomAccessCollection and how they can be combined to provide generic functionalities for conformers. The associatedtype requirement in each protocol, especially Index and Element, is very minimal and has enough constraints for the protocol to implement lots of necessary functionalities in its protocol extension, which is eye opening for me. I very like to read open source, so looking at the source code for such protocols helps me understand more.

The part about Fisher Yates shuffle algorithm details how we can come up with necessary protocol while still make them generic

Pay attention to when they mention count and map , you can learn more how each concrete type can hook into the customisation point in protocol extension

Finally learn the Liskov substitution principle with protocol in class inheritance. You should also Using Collections Effectively for how to utilise handy functions in Collection.

Data You Can Trust

Although Codable has a lot to offers in term of data integrity, this is good to know about to make sure the data you receive is actually the right data in correct format and structure. CommonCrypto is also part of new iOS SDK so you don’t need my Arcane library to handle encryption and hashing in your apps.

Embracing Algorithms

This is the most pleasant to watch as it is like a conversation between the speaker and the imaginary manager Crusty. Here I learn how to be aware of algorithm complexity and also how to utilise built in Foundation functions which are already optimised for performance.

After this session I can’t help myself but going to Swift repo to read the Algorithms.swift file immediately.

Image and Graphics Best Practices

Learn how image encoding and decoding works through data and image buffer and how that affects memory and performance. There are techniques like downsampling that can tackle this problem. This also recommends against using backing store, and instead, use UIImageView

A Guide to Turi Create

I’ve written about Turi Create before, but it is just scratching the surface of the many tasks offered by Turi. This year Apple releases Turi Create 5 with style transfer task, Vision Feature Print, GPU acceleration and recommender model improvements. I can’t wait to explore. And if you take a look at MLDataTable in Create ML framework, it looks like this has Turi ‘s SFrame under the hood.

That’s it. Thanks for reading. What are your favourite sessions this year? Please share in the comment section below

How to use CAReplicatorLayer to make activity indicator in iOS

Issue #230

CAReplicatorLayer is a layer that creates a specified number of sublayer copies with varying geometric, temporal, and color transformations

Here we use instanceTransform which applies transformation matrix around the center of the replicator layer

Below is how we use replicatorLayer to replicate lots of line and rotate them around the center.

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
let replicatorLayer = CAReplicatorLayer()
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))

let line = CALayer()
let lineCount: Int = 12
let duration: TimeInterval = 1.0
let lineSize: CGSize = CGSize(width: 20, height: 6)
let lineColor: UIColor = UIColor.darkGray

let angle = CGFloat.pi * 2 / CGFloat(lineCount)
let rotation = CATransform3DMakeRotation(angle, 0, 0, 1.0)

replicatorLayer.instanceTransform = rotation
replicatorLayer.instanceCount = lineCount
replicatorLayer.instanceDelay = duration / TimeInterval(lineCount)

line.backgroundColor = lineColor.cgColor
line.frame.size = lineSize
line.cornerRadius = lineSize.height / 2

animation.fromValue = 1.0
animation.toValue = 0.0
animation.repeatCount = Float.greatestFiniteMagnitude
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.duration = duration

replicatorLayer.addSublayer(line)
layer.addSublayer(replicatorLayer)

// x:
// y: half the height, changing affects rotation of lines
line.position = CGPoint(x: 48, y: 75)

line.add(animation, forKey: nil)

Pay attention to position of the line. The larger the x, the closer to center. y should be half the height of the replicator layer size, changing it affects the skewness of the line.

indicator

How to do rotation for CALayer in iOS

Issue #229

Use keypath

1
2
3
4
5
6
let animation = CASpringAnimation(keyPath: #keyPath(CALayer.transform))
animation.fromValue = 0
animation.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)

animation.toValue = CGFloat.pi / 4

Avoid setting frame many times

Otherwise, frame is the box that covers the rotation transform, and backgroundColor now fills that huge box

abc

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

guard line.frame.width <= 0 else {
return
}

line.backgroundColor = UIColor.red.cgColor
line.cornerRadius = 3
line.frame.size = CGSize(width: bounds.width*0.6, height: 6)
line.position = layer.position
}

Auto Layout

Avoid using Auto Layout for the rotated view

How to not use isRemovedOnCompletion for CAAnimation in iOS

Issue #228

CAAnimation is about presentation layer, after animation completes, the view snaps back to its original state. If we want to keep the state after animation, then the wrong way is to use CAMediaTimingFillMode.forward and isRemovedOnCompletion

Animation never ends

forwards https://developer.apple.com/documentation/quartzcore/camediatimingfillmode/1427658-forwards

The receiver remains visible in its final state when the animation is completed.

isRemovedOnCompletion

https://developer.apple.com/documentation/quartzcore/caanimation/1412458-isremovedoncompletion

When true, the animation is removed from the target layer’s animations once its active duration has passed. Defaults to true.

1
2
layer.fillMode = .forwards
animation. isRemovedOnCompletion = false

This is to tell the animation to never ends and keep its last presentation state. Wrong approach ❗️

Set final state before calling animation

The presentation state is just for animation, the source of truth lies in the layer itself. We need to set the final state before calling animation

1
2
3
4
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

shapeLayer.strokeEnd = 1.0
shapeLayer.add(animation, forKey: "")

How to make simple search box in iOS

Issue #227

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
60
61
62
final class SearchBox: UIView {
lazy var textField: UITextField = {
let textField = UITextField()
let imageView = UIImageView(image: R.image.search()!)
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit
textField.leftView = imageView
textField.leftViewMode = .always

return textField
}()

lazy var filterButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(R.image.filter()!, for: .normal)
button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

return button
}()

lazy var backView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.layer.borderColor = R.color.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.shadowOffset = CGSize(width: 1, height: 1)
view.layer.shadowOpacity = 0.8
view.layer.shadowColor = R.color.lightGray.cgColor

return view
}()

override init(frame: CGRect) {
super.init(frame: frame)

setup()
}

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

private func setup() {
addSubviews([backView, textField, filterButton])

NSLayoutConstraint.on([
filterButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
filterButton.centerYAnchor.constraint(equalTo: centerYAnchor),
filterButton.heightAnchor.constraint(equalToConstant: 44),
filterButton.widthAnchor.constraint(equalToConstant: 44),

textField.centerYAnchor.constraint(equalTo: centerYAnchor),
textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16),
textField.rightAnchor.constraint(equalTo: filterButton.leftAnchor, constant: -8)
])

NSLayoutConstraint.on([
backView.pinEdges(view: self, inset: UIEdgeInsets(top: 4, left: 4, bottom: -4, right: -4))
])
}
}

To apply padding to leftView, increase width and use contentMode

1
2
imageView.frame.size = CGSize(width: 20 + 8, height: 20)
imageView.contentMode = .scaleAspectFit

To make image in button smaller, use imageEdgeInsets with all positive values
To have round and shadow, specify shadowOpacity, cornerRadius, shadowOffset

searchbox

How to capture video in iOS simulator

Issue #226

Take screenshot

1
xcrun simctl io booted screenshot image.png

Record video

1
xcrun simctl io booted recordVideo video.mp4

How to use custom fonts in iOS

Issue #225

1
2
3
4
5
6
7
8
9
10
11
12
13
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>
<string>OpenSans-BoldItalic.ttf</string>
<string>OpenSans-ExtraBold.ttf</string>
<string>OpenSans-ExtraBoldItalic.ttf</string>
<string>OpenSans-Italic.ttf</string>
<string>OpenSans-Light.ttf</string>
<string>OpenSans-LightItalic.ttf</string>
<string>OpenSans-Regular.ttf</string>
<string>OpenSans-SemiBold.ttf</string>
<string>OpenSans-SemiBoldItalic.ttf</string>
</array>

Read Adding a Custom Font to Your App

The name of the font isn’t always obvious, and rarely matches the font file name. A quick way to find the font name is to get the list of fonts available to your app, which you can do with the following code:

1
2
3
4
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}

How to create UITabBarController programmatically in iOS

Issue #224

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let tabBarController = UITabBarController()

let navigationController1 = UINavigationController(rootViewController: viewController1)
let navigationController2 = UINavigationController(rootViewController: viewController2)
let navigationController3 = UINavigationController(rootViewController: viewController3)

navigationController2.isNavigationBarHidden = true

navigationController1.tabBarItem.image = R.image.profile()
navigationController2.tabBarItem.image = R.image.books()
navigationController3.tabBarItem.image = R.image.settings()

tabBarController.tabBar.tintColor = .yellow
tabBarController.viewControllers = [navigationController1, navigationController2, navigationController3]

Use tintColor instead of the deprecated selectedImageTintColor to indicate selected item color.

For icon size, check Tab Bar Icon Size, usually 50x50 for 2x and 75x75 for 3x

In portrait orientation, tab bar icons appear above tab titles. In landscape orientation, the icons and titles appear side-by-side. Depending on the device and orientation, the system displays either a regular or compact tab bar. Your app should include custom tab bar icons for both sizes.

tab

How to ignore App Transport Security in iOS

Issue #221

Ignore a host

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
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<true/>
<key>NSThirdPartyExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSRequiresCertificateTransparency</key>
<false/>
</dict>
</dict>
</dict>

Ignore all

1
2
3
4
5
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

How to use Stripe and Apple Pay in iOS

Issue #219

Show basic add card in iOS

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
import UIKit
import Stripe

final class MainController: UIViewController {

func showPayment() {
let addCardViewController = STPAddCardViewController()
addCardViewController.delegate = self
let navigationController = UINavigationController(rootViewController: addCardViewController)

present(navigationController, animated: true, completion: nil)
}
}

extension MainController: STPAddCardViewControllerDelegate {
func addCardViewControllerDidCancel(_ addCardViewController: STPAddCardViewController) {
dismiss(animated: true, completion: nil)
}

func addCardViewController(_ addCardViewController: STPAddCardViewController, didCreateToken token: STPToken, completion: @escaping STPErrorBlock) {
_ = token.tokenId
completion(nil)
dismiss(animated: true, completion: nil)
}
}

Generate ephemeral key

https://stripe.com/docs/mobile/ios/standard#ephemeral-key

In order for our prebuilt UI elements to function, you’ll need to provide them with an ephemeral key, a short-lived API key with restricted API access. You can think of an ephemeral key as a session, authorizing the SDK to retrieve and update a specific Customer object for the duration of the session.

Backend in Go

https://github.com/stripe/stripe-go

Need a secret key by going to Stripe dashboard -> Developers -> API keys -> Secret key

1
stripe.Key = "sk_key"

Need customer id. We can manually create one in Stripe dashboard -> Customers

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/ephemeralkey"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/ephemeral_keys", generateEphemeralKey)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

type EphemeralKeysRequest struct {
ApiVersion string `json:"api_version"`
}

func generateEphemeralKey(w http.ResponseWriter, r *http.Request) {
customerId := "cus_Eys6aeP5xR89ab"

decoder := json.NewDecoder(r.Body)
var t EphemeralKeysRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

stripeVersion := t.ApiVersion
if stripeVersion == "" {
log.Printf("Stripe-Version not found\n")
w.WriteHeader(400)
return
}
params := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerId),
StripeVersion: stripe.String(stripeVersion),
}

key, err := ephemeralkey.New(params)
if err != nil {
log.Printf("Stripe bindings call failed, %v\n", err)
w.WriteHeader(500)
return
}
w.Write(key.RawJSON)
}

iOS client

Networking client uses How to make simple networking client in Swift

Need an object that conforms to STPCustomerEphemeralKeyProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class EphemeralKeyClient: NSObject, STPCustomerEphemeralKeyProvider {
let client = NetworkClient(baseUrl: URL(string: "http://localhost:8080")!)

func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
var options = Options()
options.httpMethod = .post
options.path = "ephemeral_keys"
options.parameters = [
"api_version": apiVersion
]

client.makeJson(options: options, completion: { result in
switch result {
case .success(let json):
completion(json, nil)
case .failure(let error):
completion(nil, error)
}
})
}
}

Setting up STPCustomerContext and STPPaymentContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class MainController: UIViewController {
let client = EphemeralKeyClient()
let customerContext: STPCustomerContext
let paymentContext: STPPaymentContext

init() {
self.customerContext = STPCustomerContext(keyProvider: client)
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
paymentContext.delegate = self
paymentContext.hostViewController = self
}

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

func start() {
paymentContext.presentShippingViewController()
}
}

Handle charge

https://stripe.com/docs/charges

Backend in Go

If we use stripe_id from card, which has the form of card_xxx, we need to include customer info

If we use token, which has the form tok_xxx, then no need for customer info

From STPPaymentResult

When you’re using STPPaymentContext to request your user’s payment details, this is the object that will be returned to your application when they’ve successfully made a payment. It currently just contains a source, but in the future will include any relevant metadata as well. You should pass source.stripeID to your server, and call the charge creation endpoint. This assumes you are charging a Customer, so you should specify the customer parameter to be that customer’s ID and the source parameter to the value returned here. For more information, see https://stripe.com/docs/api#create_charge

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
package main

import (
"net"
"encoding/json"
"fmt"
"net/http"
"log"
"os"
"github.com/stripe/stripe-go/charge"
)

func main() {
stripe.Key = "sk_test_mM2MkqO61n7vvbVRfeYmBgWm00Si2PtWab"

http.HandleFunc("/request_charge", handleCharge)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

var customerId = "cus_Eys6aeP5xR89ab"

type PaymentResult struct {
StripeId string `json:"stripe_id"`
}

func handleCharge(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t PaymentResult
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend"),
Customer: stripe.String(customerId),
}

params.SetSource(t.StripeId)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

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
final class PaymentClient {
let client = NetworkClient(baseUrl: URL(string: "http://192.168.210.219:8080")!)

func requestCharge(source: STPSourceProtocol, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge"
options.parameters = [
"stripe_id": source.stripeID
]

client.makeJson(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}
}

paymentContext.requestPayment()

extension MainController: STPPaymentContextDelegate {
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) {
client.requestCharge(source: paymentResult.source, completion: { result in
switch result {
case .success:
completion(nil)
case .failure(let error):
completion(error)
}
})
}
}

Token from card

Use STPAPIClient.shared().createToken to get token from card https://stripe.com/docs/mobile/ios/custom#collecting-card-details

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cardParams = STPCardParams()
cardParams.number = "4242424242424242"
cardParams.expMonth = 10
cardParams.expYear = 2021
cardParams.cvc = "123"

STPAPIClient.shared().createToken(withCard: cardParams) { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
// Present error to user...
return
}

submitTokenToBackend(token, completion: { (error: Error?) in
if let error = error {
// Present error to user...
}
else {
// Continue with payment...
}
})
}

Payment options and shipping view controllers

Instead of using paymentContext

1
2
paymentContext.pushShippingViewController()
paymentContext.pushPaymentOptionsViewController()

We can use view controllers https://stripe.com/docs/mobile/ios/custom#stppaymentoptionsviewcontroller directly with STPPaymentOptionsViewController and STPShippingAddressViewController. Then implement STPPaymentOptionsViewControllerDelegate and STPShippingAddressViewControllerDelegate

Register merchant Id and Apple Pay certificate

https://stripe.com/docs/apple-pay/apps

Get Certificate signing request file from Stripe https://dashboard.stripe.com/account/apple_pay

We can’t register merchant id with Enterprise account

Use Apple Pay

Go backend

Use token

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
type ApplePayRequest struct {
Token string `json:"token"`
}

func handleChargeUsingApplePay(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var t ApplePayRequest
err := decoder.Decode(&t)
if err != nil {
panic(err)
}

params := &stripe.ChargeParams{
Amount: stripe.Int64(150),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Description: stripe.String("Charge from my Go backend for Apple Pay"),
}

params.SetSource(t.Token)
ch, err := charge.New(params)
if err != nil {
fmt.Fprintf(w, "Could not process payment: %v", err)
fmt.Println(ch)
w.WriteHeader(400)
}

w.WriteHeader(200)
}

iOS client

Update client to send STPToken

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
final class PaymentClient: NSObject {
let client = NetworkClient(baseUrl: URL(string: "localhost:8080")!)

func requestCharge(token: STPToken, completion: @escaping (Result<(), Error>) -> Void) {
var options = Options()
options.httpMethod = .post
options.path = "request_charge_apple_pay"
options.parameters = [
"token": token.tokenId
]

client.make(options: options, completion: { result in
completion(result.map({ _ in () }))
})
}

func useApplePay(payment: PKPayment, completion: @escaping (Result<(), Error>) -> Void) {
STPAPIClient.shared().createToken(with: payment, completion: { (token: STPToken?, error: Error?) in
guard let token = token, error == nil else {
return
}

self.requestCharge(token: token, completion: completion)
})
}
}

Use PKPaymentAuthorizationViewController, not PKPaymentAuthorizationController

https://developer.apple.com/documentation/passkit/pkpaymentauthorizationcontroller

The PKPaymentAuthorizationController class performs the same role as the PKPaymentAuthorizationViewController class, but it does not depend on the UIKit framework. This means that the authorization controller can be used in places where a view controller cannot (for example, in watchOS apps or in SiriKit extensions).

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
extension MainController {
func showApplePay() {
let merchantId = "merchant.com.onmyway133.MyApp"
let paymentRequest = Stripe.paymentRequest(withMerchantIdentifier: merchantId, country: "US", currency: "USD")
paymentRequest.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Rubber duck", amount: 1.5)
]

guard Stripe.canSubmitPaymentRequest(paymentRequest) else {
assertionFailure()
return
}

guard let authorizationViewController = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest) else {
assertionFailure()
return
}

authorizationViewController.delegate = self
innerNavigationController.present(authorizationViewController, animated: true, completion: nil)
}
}

extension MainController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
controller.dismiss(animated: true, completion: nil)
}

func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {

client.useApplePay(payment: payment, completion: { result in
switch result {
case .success:
completion(.init(status: .success, errors: nil))
case .failure(let error):
completion(.init(status: .failure, errors: [error]))
}
})
}
}

Showing Apple Pay option

From appleMerchantIdentifier

The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay.

1
2
3
4
5
if Stripe.deviceSupportsApplePay() {
STPPaymentConfiguration.shared().appleMerchantIdentifier = "merchant.com.onmyway133.MyApp"
}

paymentContext.pushPaymentOptionsViewController()

requestPayment not showing UI

From requestPayment

Requests payment from the user. This may need to present some supplemental UI to the user, in which case it will be presented on the payment context’s hostViewController. For instance, if they’ve selected Apple Pay as their payment method, calling this method will show the payment sheet. If the user has a card on file, this will use that without presenting any additional UI. After this is called, the paymentContext:didCreatePaymentResult:completion: and paymentContext:didFinishWithStatus:error: methods will be called on the context’s delegate.

Use STPPaymentOptionsViewController to show cards and Apple Pay options

Code for requestPayment

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
- (void)requestPayment {
WEAK(self);
[[[self.didAppearPromise voidFlatMap:^STPPromise * _Nonnull{
STRONG(self);
return self.loadingPromise;
}] onSuccess:^(__unused STPPaymentOptionTuple *tuple) {
STRONG(self);
if (!self) {
return;
}

if (self.state != STPPaymentContextStateNone) {
return;
}

if (!self.selectedPaymentOption) {
[self presentPaymentOptionsViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self requestPaymentShouldPresentShippingViewController]) {
[self presentShippingViewControllerWithNewState:STPPaymentContextStateRequestingPayment];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPCard class]] ||
[self.selectedPaymentOption isKindOfClass:[STPSource class]]) {
self.state = STPPaymentContextStateRequestingPayment;
STPPaymentResult *result = [[STPPaymentResult alloc] initWithSource:(id<STPSourceProtocol>)self.selectedPaymentOption];
[self.delegate paymentContext:self didCreatePaymentResult:result completion:^(NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
if (error) {
[self didFinishWithStatus:STPPaymentStatusError error:error];
} else {
[self didFinishWithStatus:STPPaymentStatusSuccess error:nil];
}
});
}];
}
else if ([self.selectedPaymentOption isKindOfClass:[STPApplePayPaymentOption class]]) {
// ....

Payment options

1
2
3
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didSelect paymentOption: STPPaymentOption) {
// No op
}

After user selects payment option, the change is saved in dashboard https://dashboard.stripe.com/test/customers, but for card only. Select Apple Pay does not reflect change in web dashboard.

Apple pay option is added manually locally, from STPCustomer+SourceTuple.m 😲

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
- (STPPaymentOptionTuple *)filteredSourceTupleForUIWithConfiguration:(STPPaymentConfiguration *)configuration {
id<STPPaymentOption> _Nullable selectedMethod = nil;
NSMutableArray<id<STPPaymentOption>> *methods = [NSMutableArray array];
for (id<STPSourceProtocol> customerSource in self.sources) {
if ([customerSource isKindOfClass:[STPCard class]]) {
STPCard *card = (STPCard *)customerSource;
[methods addObject:card];
if ([card.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = card;
}
}
else if ([customerSource isKindOfClass:[STPSource class]]) {
STPSource *source = (STPSource *)customerSource;
if (source.type == STPSourceTypeCard
&& source.cardDetails != nil) {
[methods addObject:source];
if ([source.stripeID isEqualToString:self.defaultSource.stripeID]) {
selectedMethod = source;
}
}
}
}

return [STPPaymentOptionTuple tupleWithPaymentOptions:methods
selectedPaymentOption:selectedMethod
addApplePayOption:configuration.applePayEnabled];
}

STPApplePayPaymentOptionis not available inpaymentContext.paymentOptions` immediately

Change selected payment option

In STPPaymentContext

setSelectedPaymentOption is read only and trigger paymentContextDidChange, but it checks if the new selected payment option is equal to existing selected payment option

1
2
3
4
5
6
7
8
9
10
11
- (void)setSelectedPaymentOption:(id<STPPaymentOption>)selectedPaymentOption {
if (selectedPaymentOption && ![self.paymentOptions containsObject:selectedPaymentOption]) {
self.paymentOptions = [self.paymentOptions arrayByAddingObject:selectedPaymentOption];
}
if (![_selectedPaymentOption isEqual:selectedPaymentOption]) {
_selectedPaymentOption = selectedPaymentOption;
stpDispatchToMainThreadIfNecessary(^{
[self.delegate paymentContextDidChange:self];
});
}
}

There is retryLoading which is called at init

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
- (void)retryLoading {
// Clear any cached customer object before refetching
if ([self.apiAdapter isKindOfClass:[STPCustomerContext class]]) {
STPCustomerContext *customerContext = (STPCustomerContext *)self.apiAdapter;
[customerContext clearCachedCustomer];
}
WEAK(self);
self.loadingPromise = [[[STPPromise<STPPaymentOptionTuple *> new] onSuccess:^(STPPaymentOptionTuple *tuple) {
STRONG(self);
self.paymentOptions = tuple.paymentOptions;
self.selectedPaymentOption = tuple.selectedPaymentOption;
}] onFailure:^(NSError * _Nonnull error) {
STRONG(self);
if (self.hostViewController) {
[self.didAppearPromise onSuccess:^(__unused id value) {
if (self.paymentOptionsViewController) {
[self appropriatelyDismissPaymentOptionsViewController:self.paymentOptionsViewController completion:^{
[self.delegate paymentContext:self didFailToLoadWithError:error];
}];
} else {
[self.delegate paymentContext:self didFailToLoadWithError:error];
}
}];
}
}];
[self.apiAdapter retrieveCustomer:^(STPCustomer * _Nullable customer, NSError * _Nullable error) {
stpDispatchToMainThreadIfNecessary(^{
STRONG(self);
if (!self) {
return;
}
if (error) {
[self.loadingPromise fail:error];
return;
}
if (!self.shippingAddress && customer.shippingAddress) {
self.shippingAddress = customer.shippingAddress;
self.shippingAddressNeedsVerification = YES;
}

STPPaymentOptionTuple *paymentTuple = [customer filteredSourceTupleForUIWithConfiguration:self.configuration];

[self.loadingPromise succeed:paymentTuple];
});
}];
}

Which in turns call STPCustomerEphemeralKeyProvider. As stripe does not save Apple Pay option in dashboard, this method return list of card payment options, together with the default card as selected payment option 😲

Although the new STPCard has a different address, it is the exact same card with the same info, and the isEqual method of STPCard is

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)isEqualToCard:(nullable STPCard *)other {
if (self == other) {
return YES;
}

if (!other || ![other isKindOfClass:self.class]) {
return NO;
}

return [self.stripeID isEqualToString:other.stripeID];
}

I raised an issue How to change selected payment option? hope it gets resolved soon 😢

Understanding AVFoundation and MediaPlayer frameworks in iOS

Issue #210

Depending on what features we want to achieve, we need to go with either AVFoundation or MediaPlayer framework. As someone who worked with many apps that involve media playback, here are some of my observations

MPMoviePlayerController vs AVPlayer

At first, I use MPMoviePlayerController because it is very simple, in fact, it is a wrapper around AVPlayer. It offers a lot of useful notifications and properties.

But when my requirements change, a lot more features are needed, I need to change to AVPlayer to have more control. And that is the wisest decision I’ve ever made

You should use AVPlayer as soon as possible. Using it cost you just a little longer time than MPMoviePlayerController, but you have a lot of control.

Custom controls

When building your own player, the built in controls of MPMoviePlayerController may not satisfy your need. I see many questions on SO are about custom controls.

MPMoviePlayerController

You have to set controlStyle to MPMovieControlStyleNone, set up Timer because currentPlaybackTime is not KVO compliance

AVPlayer

AVPlayer has no built in controls, but it has addPeriodicTimeObserverForInterval:queue:usingBlock: that makes handling the current time easily. The nicer thing about periodTimeObserver is that “The block is also invoked whenever time jumps and whenever playback starts or stops”

Notification

MPMoviePlayerController

It has a lot of useful notifications, like MPMoviePlayerNowPlayingMovieDidChangeNotification, MPMoviePlayerLoadStateDidChangeNotification, MPMovieDurationAvailableNotification, …

AVPlayer

The AVPlayerItem has some notifications AVPlayerItemDidPlayToEndTimeNotification, AVPlayerItemPlaybackStalledNotification, …
If you want to have those same notifications as MPMoviePlayerController, you have to KVO some properties of AVPlayer like currentItem, currentTime, duration, … You have to read documents to make sure which property is KVO compliance

Seek

MPMoviePlayerController

You can change the currentPlaybackTime to seek, but it results in jumping, because of efficiency and buffer status. I have to manually disable slider during seeking

AVPlayer

AVPlayer has this seekToTime:toleranceBefore:toleranceAfter:completionHandler: which allows you to specify the tolerance. Very nice

Subtitle

MPMoviePlayerController

I have to schedule a timer that “wake” and “sleep” at the beginning and end time of a certain subtitle marker, respectively.

AVPlayer

AVPlayer has this addBoundaryTimeObserverForTimes:queue:usingBlock: that perfectly suits my need. I setup 2 boundary time observers, 1 for the beginning times, 1 for the end times. The beginning times is an array containing all beginning timestamps that a subtitle marker appears.

Time scale

AVPlayer uses CMTime that offers timescale, which is a more comfortable way to specify more accurate time

Volume

AVPlayer has volume property (relative to system volume) that allows me to programmatically changes the player volume

Playable duration

MPMoviePlayerController

It has playableDuration property

AVPlayer

You have to compute yourself. See http://stackoverflow.com/questions/6815316/how-can-i-get-the-playable-duration-of-avplayer

Full screen

MPMoviePlayerController achieves full screen mode by creating another UIWindow, you learn from this to support full screen using AVPlayer, too

Movie Source Type

MPMoviePlayerController

It has movieSourceType that provides clues to the playback system, hence improving load time

“ If you know the source type of the movie, setting the value of this property before playback begins can improve the load times for the movie content.”

AVPlayer

Read Handling Different Types of Asset on AV Foundation Programming Guide

“To create and prepare an HTTP live stream for playback. Initialize an instance of AVPlayerItem using the URL. (You cannot directly create an AVAsset instance to represent the media in an HTTP Live Stream.)”

AVAsset

AVPlayer allows you to access AVAsset, which provides you more information about the playback and load state

Allow a range within a video to be playable

AVPlayerItem has forwardPlaybackEndTime and reversePlaybackEndTime that is used to specify a range that the player can play. When forwardPlaybackEndTime is specified and the playhead passes this points, AVPlayerItem will trigger AVPlayerItemDidPlayToEndTimeNotification

But it doesn’t work in my case forwardPlaybackEndTime does not work

You can have many instances of AVPlayer playing at the same time

Read more

  1. AV Foundation Programming Guide
  2. WWDC 2011 Session 405 Exploring AV Foundation
  3. WWDC 2011 Session 415 Working With Media In AV Foundation
  4. WWDC 2014 Session 503 Mastering Modern Media Playback
  5. WWDC 2011 Session 406 AirPlay and External Displays in iOS apps
  6. AVPlayerDemo
  7. VKVideoPlayer
  8. ALMoviePlayerController
  9. AV Foundation Playing Videos
  10. AVPlayer and MPMoviePlayerController differences
  11. MPMoviePlayer Tips

How to handle reachability in iOS

Issue #209

Here are what I learn about reachability handling in iOS, aka checking for internet connection. Hope you will find it useful, too.

This post starts with techniques from Objective age, but many of the concepts still hold true

The naive way

Some API you already know in UIKit can be used for checking internet connection. Most of them are synchronous code, so you ‘d better call them in a background thread

1
2
3
4
5
6
7
8
- (BOOL)connectedToInternet
{
NSString *string = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.google.com"]
encoding:NSUTF8StringEncoding
error:nil];

return string ? YES : NO;
}
1
2
3
4
5
6
7
8
9
10
- (BOOL)connectedToInternet
{
NSURL *url = [NSURL URLWithString:@"http://www.google.com"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"HEAD"];
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error: NULL];

return ([response statusCode] == 200) ? YES : NO;
}

Using SystemConfiguration framework

After importing the SystemConfiguration framework, you can use either SCNetworkReachabilityGetFlags to synchronously get the reachability status, or provide a callback to SCNetworkReachabilitySetCallback to be notified about reachability status change.

Note that SCNetworkReachabilityGetFlags is synchronous.

The System Configuration framework reachability API () operates synchronously by default. Thus, seemingly innocuous routines like SCNetworkReachabilityGetFlags can get you killed by the watchdog. If you’re using the reachability API, you should use it asynchronously. This involves using the SCNetworkReachabilityScheduleWithRunLoop routine to schedule your reachability queries on the run loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL) isConnectionAvailable
{
SCNetworkReachabilityFlags flags;
BOOL receivedFlags;

SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(CFAllocatorGetDefault(), [@"dipinkrishna.com" UTF8String]);
receivedFlags = SCNetworkReachabilityGetFlags(reachability, &flags);
CFRelease(reachability);

if (!receivedFlags || (flags == 0) )
{
return FALSE;
} else {
return TRUE;
}
}

Note that SCNetworkReachabilitySetCallback notifies only when reachability status changes

Assigns a client to the specified target, which receives callbacks when the reachability of the target changes

Using some libraries

Libraries make our life easier, but to live well with them, you must surely understand them. There are many reachability libraries on Github, but here I want to mention the most popular: Reachability from tonymillion and AFNetworkReachabilityManager (a submodule of AFNetworking) from mattt. Both use SystemConfiguration under the hood.

Reachability

Some people use Reachability like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)testInternetConnection
{
internetReachableFoo = [Reachability reachabilityWithHostname:@"www.google.com"];

// Internet is reachable
internetReachableFoo.reachableBlock = ^(Reachability*reach)
{
// Update the UI on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Yayyy, we have the interwebs!");
});
};

// Internet is not reachable
internetReachableFoo.unreachableBlock = ^(Reachability*reach)
{
// Update the UI on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Someone broke the internet :(");
});
};

[internetReachableFoo startNotifier];
}

Looking into the method “startNotifier”, you will see that it only uses SCNetworkReachabilitySetCallback and it means this callback will only be called if reachability status changes.

If you want to know the reachability status directly, for example, the reachability status at app launch, you must use the method “isReachable”. This method under the hood uses SCNetworkReachabilityGetFlags which is synchronous, and it locks the calling thread.

Reachability has reachabilityForLocalWiFi, which is interesting :)

1
2
3
4
5
6
7
8
9
10
11
+(Reachability*)reachabilityForLocalWiFi
{
struct sockaddr_in localWifiAddress;
bzero(&localWifiAddress, sizeof(localWifiAddress));
localWifiAddress.sin_len = sizeof(localWifiAddress);
localWifiAddress.sin_family = AF_INET;
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM);

return [self reachabilityWithAddress:&localWifiAddress];
}

AFNetworkReachabilityManager

With AFNetworkReachabilityManager, all you have to do is

1
2
3
4
5
6
7
- (void)trackInternetConnection
{
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
// Handle the status
}];
}

What is nice about AFNetworkReachabilityManager is that in the “startMonitoring” method, it both uses SCNetworkReachabilitySetCallback and calls AFNetworkReachabilityStatusForFlags to get the initial reachability status in a background thread, and calls the AFNetworkReachabilityStatusBlock. So in the user ‘s point of view, all we care about is the AFNetworkReachabilityStatusBlock handler.

AFNetworking has all the features that Reachability has, and its code is well structured. Another cool thing about it is that it is already in your AFNetworking pod. It’s hard to find projects without AFNetworking these days

isReachableViaWWAN vs isReachableViaWiFi

Take a look at the method AFNetworkReachabilityStatusForFlags and you will know the story

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static AFNetworkReachabilityStatus AFNetworkReachabilityStatusForFlags(SCNetworkReachabilityFlags flags) {
[...]
status = AFNetworkReachabilityStatusUnknown;
if (isNetworkReachable == NO) {
status = AFNetworkReachabilityStatusNotReachable;
}
#if TARGET_OS_IPHONE
else if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) {
status = AFNetworkReachabilityStatusReachableViaWWAN;
}
#endif
else {
status = AFNetworkReachabilityStatusReachableViaWiFi;
}

return status;
}

isReachableViaWWAN is supposed to be for iOS Device

How to use AFNetworkReachabilityManager

I’ve asked a question here Issue 2262, you should take a look at it

The safe way is not to use the sharedManager, but use managerForDomain

1
2
3
4
5
6
7
8
AFNetworkReachabilityManager *afReachability = [AFNetworkReachabilityManager managerForDomain:@"www.google.com"];
[afReachability setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if (status < AFNetworkReachabilityStatusReachableViaWWAN) {
[FTGAlertView showMessage:@"No internet connection"];
}
}];

[afReachability startMonitoring];

You should read the question 7 and 8 in the Reference below to know more about SCNetworkReachabilityCreateWithName vs SCNetworkReachabilityCreateWithAddress, and about the zero address

Reachability.swift

In Swift, there is this popular Reachability.swift to check for network reachability status

Connectivity

Sometimes, a more robust way is just to ping certain servers, that how’s Connectivy works

Also, read more Solving the Captive Portal Problem on iOS

In order to detect that it has connected to a Wi-Fi network with a captive portal, iOS contacts a number of endpoints hosted by Apple — an example being https://www.apple.com/library/test/success.html. Each endpoint hosts a small HTML page of the form:

Read more

  1. Reachability
  2. AFNetworkReachabilityManager
  3. How to check for internet connection synchronously?
  4. iOS: Check whether internet connection is available
  5. Check if Active Internet Connection Exists on iOS Device
  6. Technical Q&A QA1693 Synchronous Networking On The Main Thread
  7. How to check for network reachability on iOS in a non-blocking manner?
  8. understanding INADDR_ANY for socket programming - c

How to zoom in double in MapKit

Issue #183

1
2
3
4
5
6
7
8
9
10
11
12
func zoomInDouble(coordinate: CLLocationCoordinate2D) {
let region = mapView.region
let zoomInRegion = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(
latitudeDelta: region.span.latitudeDelta * 0.5,
longitudeDelta: region.span.longitudeDelta * 0.5
)
)

mapView.setRegion(zoomInRegion, animated: true)
}

How to select cluster annotation in MapKit

Issue #182

1
2
3
4
5
6
7
8
9
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let coordinate = view.annotation?.coordinate else {
return
}

if (view.annotation is MKClusterAnnotation) {
zoomInDouble(coordinate: coordinate)
}
}

How to cluster annotations in MapKit in iOS 11

Issue #181

https://developer.apple.com/documentation/mapkit/mkannotationview/decluttering_a_map_with_mapkit_annotation_clustering

1
2
3
4
5
6
7
8
9
10
11
final class AnnotationView: MKMarkerAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

clusteringIdentifier = String(describing: ClusterView.self)
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
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
final class ClusterView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
displayPriority = .defaultHigh
}

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

override func prepareForDisplay() {
super.prepareForDisplay()

guard let annotation = annotation as? MKClusterAnnotation else {
return
}

let count = annotation.memberAnnotations.count
image = self.image(annotation: annotation, count: count)
}

func image(annotation: MKClusterAnnotation, count: Int) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 40.0, height: 40.0))
image = renderer.image { _ in
UIColor.purple.setFill()
UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).fill()
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 20.0)
]

let text = "\(count)"
let size = text.size(withAttributes: attributes)
let rect = CGRect(x: 20 - size.width / 2, y: 20 - size.height / 2, width: size.width, height: size.height)
text.draw(in: rect, withAttributes: attributes)
}

return image
}
}
1
mapView.register(ClusterView.self, forAnnotationViewWithReuseIdentifier: String(describing: ClusterView.self))

Using CircleCI 2.0

Issue #158

We ‘ve been using CircleCI for many of our open source projects. Since the end of last year 2017, version 2.0 began to come out, and we think it’s good time to try it now together with Swift 4.1 and Xcode 9.3

The problem with version 2.0 is it’s so powerful and has lots of cool new features like jobs and workflows, but that requires going to documentation for how to migrate configuration file, especially Search and Replace Deprecated 2.0 Keys

Creating config.yml

The first thing is to create a new config.yml inside folder .circleci

Copy your existing circle.yml file into a new directory called .circleci at the root of your project repository.

Next is to declare version and jobs

Add version: 2 to the top of the .circleci/config.yml file.

Checking xcodebuild

For simple cases, we just use xcodebuild to build and test the project, so it’s good to try it locally to avoid lots of trial commits to trigger CircleCI. You can take a look at this PR https://github.com/hyperoslo/Cheers/pull/20

Before our configuration file for version 1.0 looks like this

1
- set -o pipefail && xcodebuild -project Cheers.xcodeproj -scheme "Cheers-iOS" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8,OS=11.0' -enableCodeCoverage YES test

Now we should put pipefail inside shell, follow https://github.com/CircleCI-Public/circleci-demo-ios/blob/master/.circleci/config.yml

shell: /bin/bash –login -o pipefail

Now is the actual trying xcodebuild, after many failures due to destination param

1
2
3
4
5
6
xcodebuild: error: Unable to find a destination matching the provided destination specifier:
{ platform:iOS Simulator, OS:11.3 }

Missing required device specifier option.
The device type “iOS Simulator” requires that either “name” or “id” be specified.
Please supply either “name” or “id”.
1
xcodebuild: error: option 'Destination' requires at least one parameter of the form 'key=value'

I found this to work, run this in the same folder as your xcodeproj

1
xcodebuild -project Cheers.xcodeproj -scheme "Cheers-iOS" -sdk iphonesimulator -destination "platform=iOS Simulator,OS=11.3,name=iPhone X" -enableCodeCoverage YES test

Adding workflow

Version 2.0 introduces workflow which helps organising jobs

A workflow is a set of rules for defining a collection of jobs and their run order. Workflows support complex job orchestration using a simple set of configuration keys to help you resolve failures sooner.

For our simple use cases, we add this workflow

1
2
3
4
5
workflows:
version: 2
build-and-test:
jobs:
- build-and-test

Collecting Test Metadata

CircleCI collects test metadata from XML files and uses it to provide insights into your job

Final

Use below as template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: 2.1
jobs:
build_test:
macos:
xcode: "11.0"
shell: /bin/bash --login -o pipefail
steps:
- checkout
- run:
command: |
curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf
pod install
- run:
command: xcodebuild -workspace MyApp.xcworkspace -scheme "MyApp" -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone X,OS=12.2" -enableCodeCoverage YES test
- store_test_results:
path: test-results

workflows:
version: 2.1
primary:
jobs:
- build_test

Read more

UITableViewCell and Model

Issue #154

The most common UI element in iOS is UITableView, and the most common task is to display the UITableViewCell using the model.

Although the title specifies UITableViewCell, but the problem involves other views (UICollectionView, custom view, …) as well

There are many debates about this, so here I want to make a summary, all in my opinion

It started with

UITableViewCell Is Not a Controller
This article follows strict MVC and states we should not pass model object to cell and let cell manipulate directly on the model. He shows several examples that point out this job is not suitable for the cell. Instead this job should be done on the Controller (UIViewController, UITableViewDataSource, …)

Category for simple case

Skinnier Controllers Using View Categories
This article states that we should keep ViewController skinnier by transfering the job (mapping model object to cell) to the cell category

Using subclassing

UITableViewCell Is Not a Controller, But…
This articles explains the beauty of subclassing to take advantage of Polymorphism. In the theming example he gives, we see that Controller ‘s job now is to select the correct cell subclass, and the subclass ‘s job is to know how to use the model
“When a UITableViewCell subclass accepts a model object parameter and updates its constituent subviews as I have described, it is behaving as a data transformer, not a controller”

Model Presenter

Model View Controller Presenter
This article shows that using subclassing and category will have duplication implementation when you have more cells and models. After several optimizations, he finally gets to the Model Presenter, which is the center object who knows how to represent the model in different view.
“This is an object that knows how to represent every aspect of a certain model”

MVVM

MVC, MVVM, FRP, And Building Bridges
This article explains MVVM but it touches our problem directly. The problem with cell and model actually is

  1. How to map the model to the cell
  2. Who will do this job? The cell, Controller or another Model Mapping object ?

The ViewModel is actually who does this work, which is to transform the model to something the view can easily use

“A table view data source is none of these things. It’s purely a layer between the table view and the model. The model defines lists of things, but the table view data source transform those lists into sections and rows. It also returns the actual table view cells, but that’s not what I’m focusing on here. The key is its role as a middle-tier data transformer.”

Do we access the cell ‘s subviews

Paul on UITableViewCell
Brent follows with another post explaining how cell ‘s subviews should not be accessed outside of the cell

Data Source

Clean table view code
This article deals with Bridging the Gap Between Model Objects and Cells.
“At some point we have to hand over the data we want to display into the view layer. Since we still want to maintain a clear separation between the model and the view, we often offload this task to the table view’s data source. This kind of code clutters the data source with specific knowledge about the design of the cell. We are better off factoring this out into a category of the cell class”

This together with Lighter View Controllers shows a practical example that deals with most cases

Reference

  1. Should UITableViewCells Know About Model Objects?

Learning from Open Source Generic Factory

Issue #148

From https://github.com/devxoul/Pure/blob/master/Sources/Pure/FactoryModule.swift

1
2
3
4
5
6
7
8
public protocol FactoryModule: Module {

/// A factory for `Self`.
associatedtype Factory = Pure.Factory<Self>

/// Creates an instance of a module with a dependency and a payload.
init(dependency: Dependency, payload: Payload)
}

From https://github.com/devxoul/Pure/blob/master/Sources/Pure/Factory.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
open class Factory<Module: FactoryModule> {
private let dependencyClosure: () -> Module.Dependency

/// A static dependency of a module.
open var dependency: Module.Dependency {
return self.dependencyClosure()
}

/// Creates an instance of `Factory`.
///
/// - parameter dependency: A static dependency which should be resolved in a composition root.
public init(dependency: @autoclosure @escaping () -> Module.Dependency) {
self.dependencyClosure = dependency
}

/// Creates an instance of a module with a runtime parameter.
///
/// - parameter payload: A runtime parameter which is required to construct a module.
open func create(payload: Module.Payload) -> Module {
return Module.init(dependency: self.dependency, payload: payload)
}
}

From https://github.com/devxoul/Pure/blob/master/Tests/PureTests/PureSpec.swift#L72

1
2
3
4
5
6
let factory = FactoryFixture<Dependency, Payload>.Factory(dependency: .init(
networking: "Networking A"
))
let instance = factory.create(payload: .init(id: 100))
expect(instance.dependency.networking) == "Networking A"
expect(instance.payload.id) == 100

How to use standalone UINavigationBar in iOS

Issue #144

There are times we want the same UIViewController to look good when it’s presented modally or pushed from UINavigationController stack. Take a look at BarcodeScanner and the PR https://github.com/hyperoslo/BarcodeScanner/pull/82

When it is presented, we need a header view so that we can show a title and a close button. We can create a custom HeaderView that inherits from UIView or either embed it in a UINavigationController before presenting.

If we go with the controller being embedded in UINavigationController approach, it will collide with the other UINavigationController when it is pushed.

If we go with custom HeaderView, then we need to layout the view so that it looks good on both portrait and landscape, and on iPhone X that as safeAreaLayoutGuide.

Using standalone UINavigationBar

Since UINavigationController uses UINavigationBar under the hood, which uses UINavigationItem info to present the content. We can imitate this behavior by using a standalone UINavigationBar. See Adding Content to a Standalone Navigation Bar

In the vast majority of scenarios you will use a navigation bar as part of a navigation controller. However, there are situations for which you might want to use the navigation bar UI and implement your own approach to content navigation. In these situations, you can use a standalone navigation bar.

A navigation bar manages a stack of UINavigationItem objects

The beauty is that our standalone UINavigationBar and that of UINavigationController are the same, use the same UINavigationItem and no manual layout are needed

Declare UINavigationItem

We can just set properties like we did with a normal navigationItem

1
2
3
let standaloneItem = UINavigationItem()
standaloneItem.leftBarButtonItem = UIBarButtonItem(customView: closeButton)
standaloneItem.titleView = UILabel()

Adding UINavigationBar

Customise your bar, then declare layout constraints. You only need to pin left, right, and top. Note that you need to implement UINavigationBarDelegate to attach bar to status bar, so that it appears good on iPhone X too

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let navigationBar = UINavigationBar()
navigationBar.isTranslucent = false
navigationBar.delegate = self
navigationBar.backgroundColor = .white
navigationBar.items = [standaloneItem]

navigationBar.translatesAutoresizingMaskIntoConstraints = false
navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

if #available(iOS 11, *) {
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
} else {
navigationBar.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
1
2
3
4
5
extension BarcodeScannerController: UINavigationBarDelegate {
public func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}

Inside UINavigationController

When this UIViewController is pushed from a UINavigationController stack, we just need to hide our standalone navigationBar. If we prefer the default back button, we don’t need to set leftBarButtonItem

On iOS 10, you need to call sizeToFit for any items in UINavigationItem for it to get actual size

1
2
3
4
5
6
7
8
9
10
11
12
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

if navigationController != nil {
let label = HeaderElement.makeLabel()
label.sizeToFit()
navigationItem.titleView = label
navigationBar.isHidden = true
} else {
navigationBar.isHidden = false
}
}

How to deal with animation in UITests in iOS

Issue #143

Today I was writing tests and get this error related to app idle

1
t =    23.06s         Assertion Failure: <unknown>:0: Failed to scroll to visible (by AX action) Button, 0x6000003827d0, traits: 8858370049, label: 'cart', error: Error -25204 performing AXAction 2003 on element <XCAccessibilityElement: 0x7fc391a2bd60> pid: 91461, elementOrHash.elementID: 140658975676048.128

It turns out that the project uses a HUD that is performing some progress animation. Even it was being called HUD.hide(), the problem still exists.

1
2
3
4
t =    31.55s     Wait for no.example.MyApp to idle
t = 91.69s App animations complete notification not received, will attempt to continue.
t = 91.70s Tap Target Application 0x6040002a1260
t = 91.70s Wait for no.example.MyApp to id

No matter how I call sleep,wait`, still the problem

1
2
3
sleep(10)
app.tap()
_ = checkoutButton.waitForExistence(timeout: 10)

The fix is to disable animation. Start with setting argument when running tests

1
2
app.launchArguments.append("--UITests")
app.launch

Then in AppDelegate

1
2
3
4
5
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if CommandLine.arguments.contains("--UITests") {
UIView.setAnimationsEnabled(false)
}
}

How to use R.swift in UITest in iOS

Issue #138

Here is how to use R.swift in UITest target

  • Add Localizable.strings to UITest target
  • Declare pod
1
2
3
target 'MyAppUITests' do
pod 'R.swift', '~> 4.0'
end
  • In UITest target settings, add $(FRAMEWORK_SEARCH_PATHS) to Runpath Search Path
  • Add R.generated.swift to UITest target

Hiding back button in navigation bar in iOS

Issue #137

Use a custom NavigationController

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

class NavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.tintColor = .white
navigationBar.barStyle = .black
navigationBar.isTranslucent = false
}

override func pushViewController(_ viewController: UIViewController, animated: Bool) {
viewController.navigationItem.backBarButtonItem =
UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
super.pushViewController(viewController, animated: animated)
}
}

Using dlopen in iOS

Issue #133

With dlopen we can make uses of some private frameworks. It will be fun

From iPhone X home button

1
2
3
4
5
6
7
#import <dlfcn.h>

// somewhere in viewDidLoad
dlopen([binaryPath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
UIView *const view = [[NSClassFromString(@"SBHomeGrabberView") alloc] init];
[view sizeToFit];
[self.view addSubview:view];

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(

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.