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 aboutmachinelearning 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.
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.
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.
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.
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.
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 2parts 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.
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.
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
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.
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 *❗️❗️❗️
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
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.
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.
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
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
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)
// 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.
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
To make image in button smaller, use imageEdgeInsets with all positive values To have round and shadow, specify shadowOpacity, cornerRadius, shadowOffset
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 inUIFont.familyNames.sorted() { let names = UIFont.fontNames(forFamilyName: family) print("Family: \(family) Font names: \(names)") }
let navigationController1 = UINavigationController(rootViewController: viewController1) let navigationController2 = UINavigationController(rootViewController: viewController2) let navigationController3 = UINavigationController(rootViewController: viewController3)
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.
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.
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
finalclassMainController: UIViewController{ let client = EphemeralKeyClient() let customerContext: STPCustomerContext let paymentContext: STPPaymentContext
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
type ApplePayRequest struct { Token string`json:"token"` }
funchandleChargeUsingApplePay(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) }
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).
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.
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
funcpaymentOptionsViewController(_ 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 😲
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
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
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
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.”
“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
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
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
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.
// 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 elseif ((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
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:
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 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
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.
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
How to map the model to the cell
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
/// A static dependency of a module. openvar dependency: Module.Dependency { returnself.dependencyClosure() }
/// Creates an instance of `Factory`. /// /// - parameter dependency: A static dependency which should be resolved in a composition root. publicinit(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. openfunccreate(payload: Module.Payload) -> Module { returnModule.init(dependency: self.dependency, payload: payload) } }
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
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
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
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
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 🙀
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 🙀
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
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(
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.
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 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
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.
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
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.
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
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.
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
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.
Deletions
If we go from “kit” to an empty string “”, we need 3 deletions
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
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
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”
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’
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
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
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.
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.
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:
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:
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.
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.
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.
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
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.
// [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 = { [weakself] location in guardlet selectedController = self?.mainController?.selectedViewController else { return }
// [5] Choose the current visible controller let currentController = (selectedController as? UINavigationController)?.topViewController ?? selectedController
Declare a scheme for Compass to work. This is your application URL scheme. This shines when you wish to support deep linking .
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.
A Navigator can manage multiple routers. In this case, you only register one router.
This is where you supply the handling closure. Navigator uses this to handle a resolved location request.
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.
Finally, since Router is a composite Routable, you dispatch to it the Location.
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
publicprotocolRoutable{ funcnavigate(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.
This is called when you touch the post author on the feed. Here’s what’s happening:
UserRoute deals with user:{userId} urn, so location.arguments["userId"] should contain the correct userId to inject into UserController.
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:
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:
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:
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:
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:
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:
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:
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.
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.
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.
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.
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.
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
overridefuncviewDidLoad() { 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 asAny) } }
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:
// [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:
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.
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.