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(
You can create a set with any element type that conforms to the Hashable protocol. By default, most types in the standard library are hashable, including strings, numeric and Boolean types, enumeration cases without associated values, and even sets themselves.
The Hashable protocol inherits from the Equatable protocol, so you must also add an equal-to operator (==) function for your custom type.
1 2 3 4 5 6 7 8
publicprotocolHashable : Equatable{
/// The hash value. /// /// Hash values are not guaranteed to be equal across different executions of /// your program. Do not save hash values to use during a future execution. publicvar hashValue: Int { get } }
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.
Every new architecture that comes out, either iOS or Android, makes me very excited. I’m always looking for ways to structure apps in a better way. But after some times, I see that we’re too creative in creating architecture, aka constraint, that is too far away from the platform that we’re building. I often think “If we’re going too far from the system, then it’s very hard to go back”
I like things that embrace the system. One of them is Coordinator which helps in encapsulation and navigation. Thanks to my friend Vadym for showing me Coordinator in action.
The below screenshot from @khanlou ‘s talk at CocoaHeads Stockholm clearly says many things about Coordinator
But after reading A Better MVC, I think we can leverage view controller containment to do navigation using UIViewController only.
Since I tend to call view controllers as LoginController, ProfileController, ... and the term flow to group those related screens, what should we call a Coordinator that inherits from UIViewController 🤔 Let’s call it FlowController 😎 .
The name is not that important, but the concept is simple. FlowController was also inspired by this Flow Controllers on iOS for a Better Navigation Control back in 2014. The idea is from awesome iOS people, this is just a sum up from my experience 😇
So FlowController can just a UIViewController friendly version of Coordinator. Let see how FlowController fits better into MVC
Your application starts from AppDelegate, in that you setup UIWindow. So we should follow the same “top down” approach for FlowController, starting with AppFlowController. You can construct all dependencies that your app need for AppFlowController, so that it can pass to other child FlowController.
let authService: AuthServiceProtocol let phoneService: PhoneService let networkingService: NetworkingService let locationService: LocationService let mapService: MapService let healthService: HealthService
staticfuncmake() -> DependencyContainer { // Configure and make DependencyContainer here } }
Here are some hypothetical FlowController that you may encounter
AppFlowController: manages UIWindow and check whether to show onboarding, login or main depending on authentication state
OnboardingFlowController: manages UIPageViewController and maybe ask for some permissions
LoginFlowController: manages UINavigationController to show login, sms verification, forget password, and optionally start SignUpFlowController
MainFlowController: manages UITabBarController with each tab serving main features
FeedFlowController: show feed with list of items
ProfileFlowController: show profile
SettingsFlowController: show settings, and maybe call logout, this will delegates up the FlowController chain.
The cool thing about FlowController is it makes your code very self contained, and grouped by features. So it’s easy to move all related things to its own package if you like.
2. FlowController as container view controller
In general, a view controller should manage either sequence or UI, but not both.
Basically, FlowController is just a container view controller to solve the sequence, based on a simple concept called composition. It manages many child view controllers in its flow. Let’ say we have a ProductFlowController that groups together flow related to displaying products, ProductListController, ProductDetailController, ProductAuthorController, ProductMapController, … Each can delegate to the ProductFlowController to express its intent, like ProductListController can delegate to say “product did tap”, so that ProductFlowController can construct and present the next screen in the flow, based on the embedded UINavigationController inside it.
Normally, a FlowController just displays 1 child FlowController at a time, so normally we can just update its frame
Each view controller inside the flow can have different dependencies, so it’s not fair if the first view controller needs to carry all the stuff just to be able to pass down to the next view controllers. Here are some dependencies
structProductDependencyContainer{ let productNetworkingService: ProductNetworkingService let imageDownloaderService: ImageDownloaderService let productEditService: ProductEditService let authorNetworkingService: AuthorNetworkingService let locationService: LocationService let mapService: MapService }
classProductFlowController{ let dependencyContainer: ProductDependencyContainer
With FlowController, since it is UIViewController subclass, it has viewControllers to hold all those child FlowController. Just add these extensions to simplify your adding or removing of child UIViewController
5. AppFlowController does not need to know about UIWindow
Coordinator
Usually you have an AppCoordinator, which is held by AppDelegate, as the root of your Coordinator chain. Based on login status, it will determine which LoginController or MainController will be set as the rootViewController, in order to do that, it needs to be injected a UIWindow
Supposed we have login flow based on UINavigationController that can display LoginController, ForgetPasswordController, SignUpController
Coordinator
What should we do in the start method of LoginCoordinator? Construct the initial controller LoginController and set it as the rootViewController of the UINavigationController? LoginCoordinator can create this embedded UINavigationController internally, but then it is not attached to the rootViewController of UIWindow, because UIWindow is kept privately inside the parent AppCoordinator.
We can pass UIWindow to LoginCoordinator but then it knows too much. One way is to construct UINavigationController from AppCoordinator and pass that to LoginCoordinator
LoginFlowController leverages container view controller so it fits nicely with the way UIKit works. Here AppFlowController can just add LoginFlowController and LoginFlowController can just create its own embeddedNavigationController.
Sometimes we want a quick way to bubble up message to parent Coordinator, one way to do that is to replicate UIResponder chain using associated object and protocol extensions, like Inter-connect with Coordinator
Since FlowController is UIViewController, which inherits from UIResponder, responder chain happens out of the box
Responder objects—that is, instances of UIResponder—constitute the event-handling backbone of a UIKit app. Many key objects are also responders, including the UIApplication object, UIViewController objects, and all UIView objects (which includes UIWindow). As events occur, UIKit dispatches them to your app’s responder objects for handling.
8. FlowController and trait collection
FlowController
I very much like how Kickstarter uses trait collection in testing. Well, since FlowController is a parent view controller, we can just override its trait collection, and that will affect the size classes of all view controllers inside that flow.
The huge advantage of this approach is that system features come free. Trait collection propagation is free. View lifecycle callbacks are free. Safe area layout margins are generally free. The responder chain and preferred UI state callbacks are free. And future additions to UIViewController are also free.
When implementing a custom container view controller, you can use this method to change the traits of any embedded child view controllers to something more appropriate for your layout. Making such a change alters other view controller behaviors associated with that child
One problem with UINavigationController is that clicking on the default back button pops the view controller out of the navigation stack, so Coordinator is not aware of that. With Coordinator you needs to keep Coordinator and UIViewController in sync, add try to hook up UINavigationControllerDelegate in order to clean up. Like in Back Buttons and Coordinators
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
extensionCoordinator: UINavigationControllerDelegate{ funcnavigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) { // ensure the view controller is popping guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from), !navigationController.viewControllers.contains(fromViewController) else { return } // and it's the right type if fromViewController isFirstViewControllerInCoordinator) { //deallocate the relevant coordinator } } }
Or creating a class called NavigationController that inside manages a list of child coordinators. Like in Navigation coordinators
Since FlowController is just plain UIViewController, you don’t need to manually manage child FlowController. The child FlowController is gone when you pop or dismiss. If we want to listen to UINavigationController events, we can just handle that inside the FlowController
Are you willing to take vaccines you don’t know about?
I like open source. I ‘ve made some and contributed to some. I also use other people ‘s open source libraries and learn a lot from them 😇
Open source can help us build better, faster and maybe more performant software by basing on other people ‘s hard work. We can also collaborate and make it better. But it’s also a double edges sword if you’re not judging carefully.
Putting a little bit more dedication
You can skip this post if your project is just for fun, and you don’t care about future maintenance. If you’re making client or product projects, you should care and put a little more dedication into it. The company and your teammates trust you to do the good work.
I’ve admitted that I ‘ve done node.js and electron.js. The choice is because it’s just a utility that I want to make quickly, and there ‘s many node.js packages that I can use immediately. I have little experience in node.js, so I accept the risk do get the work done. But if you’re experienced developer in your platform, and it’s your important project, then it’s a different story 💥
I’m kind of experimental person, so I always want to try new things. But I also learn the hard way to not over engineer, and to live as close to the system as possible. I just read Much ado about iOS app architecture and I agree with most of the points, that we shouldn’t fight the SDK and replace system frameworks.
To me, using 3rd libraries is like giving your life to someone else ‘s hands, that you can’t make any decision for your future. Please don’t just pod install and consider it done 🙏
What about the stars
People tend to follow those that have lots of followers, and to star a project with lots of stars. Don’t trust the stars. It means nearly nothing. The star is just the result of some marketing effort. Being featured or not in a newsletter can make 1k stars difference. Just because it was featured in a newsletter does not necessarily mean that it is good 😬
You should judge it yourself by checking how good the source code is, how many documentation are available, and whether there is unit tests or not. The author is just human, so he can’t help maintain the library forever. You’re taking a big risk if you don’t have good picture of the library.
The system, especially iOS, changes very often. There are some libraries that try to “replicate” system APIs or perform type checking for every possible types. It can be that the author pick the most common use cases, or just trying to provide a cleaner APIs to the user. But if things change, will that author be willing to fix that? Will you be stuck there and making another issue asking for help? We’re making life better, not traps for everyone to fall into 🙀
Here I don’t try to blame anyone, “you is not your work”. I just say that you should check it more thoroughly. You can consult your colleagues and discuss if it’s good to integrate. Most of the time, pulling a huge library just for some tiny syntactic sugar does not worth it
I just put some random links here and you can determine if you like or not
Would you take a very big risk for so little return ? Always implement and imagine that you will be the next maintainer for the project, then you will act differently.
So the next time, please reconsider your choice of libraries. Take 1 step back and analyse a bit ❤️
Another cool thing about ios-oss is how it manages dependencies. Usually you have a lot of dependencies, and it’s good to keep them in one place, and inject it to the objects that need.
The Environment is simply a struct that holds all dependencies throughout the app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/** A collection of **all** global variables and singletons that the app wants access to. */ publicstructEnvironment{ /// A type that exposes endpoints for fetching Kickstarter data. publiclet apiService: ServiceType
/// The amount of time to delay API requests by. Used primarily for testing. Default value is `0.0`. publiclet apiDelayInterval: DispatchTimeInterval
/// A type that exposes how to extract a still image from an AVAsset. publiclet assetImageGeneratorType: AssetImageGeneratorType.Type
/// A type that stores a cached dictionary. publiclet cache: KSCache /// ... }
Then there’s global object called AppEnvironment that manages all these Environment in a stack
publicstructAppEnvironment{ /** A global stack of environments. */ fileprivatestaticvar stack: [Environment] = [Environment()]
/** Invoke when an access token has been acquired and you want to log the user in. Replaces the current environment with a new one that has the authenticated api service and current user model. - parameter envelope: An access token envelope with the api access token and user. */ publicstaticfunclogin(_ envelope: AccessTokenEnvelope) { replaceCurrentEnvironment( apiService: current.apiService.login(OauthToken(token: envelope.accessToken)), currentUser: envelope.user, koala: current.koala |> Koala.lens.loggedInUser .~ envelope.user ) }
/** Invoke when we have acquired a fresh current user and you want to replace the current environment's current user with the fresh one. - parameter user: A user model. */ publicstaticfuncupdateCurrentUser(_ user: User) { replaceCurrentEnvironment( currentUser: user, koala: current.koala |> Koala.lens.loggedInUser .~ user ) }
// Invoke when you want to end the user's session. publicstaticfunclogout() { let storage = AppEnvironment.current.cookieStorage storage.cookies?.forEach(storage.deleteCookie)
// The most recent environment on the stack. publicstaticvar current: Environment! { return stack.last }
}
Then whenever there’s event that triggers dependencies update, we call it like
1 2 3 4 5
self.viewModel.outputs.logIntoEnvironment .observeValues { [weakself] accessTokenEnv in AppEnvironment.login(accessTokenEnv) self?.viewModel.inputs.environmentLoggedIn() }
The cool thing about Environment is that we can store and retrieve them
1 2 3 4 5 6
// Returns the last saved environment from user defaults. publicstaticfuncfromStorage(ubiquitousStore: KeyValueStoreType, userDefaults: KeyValueStoreType) -> Environment { // retrieval
One thing I like about kickstarter-ios is how they use Playground to quickly protoyping views.
We use Swift Playgrounds for iterative development and styling. Most major screens in the app get a corresponding playground where we can see a wide variety of devices, languages and data in real time.
Hi, here is how I indent my code. Let me know what you think 😉
Using 2 spaces indentation
When possible, configure your editor to use 2 spaces for tab size. You will love it ❤️
Move first parameter to new line
If there are many parameters, move the first parameter to a new line, and align the other parameters. Remember that the last parenthesis ) should align to the function call
Use try catch for read, Result for write, together with concurrent queue. Use sync function for read to block current thread, while using async function with barrier flag for write to return to current queue. This is good for when multiple reads is preferred when there is no write. When write with barrier comes into the queue, other operations must wait.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
funcgetUser(id: String)throws -> User { var user: User! try concurrentQueue.sync { user = try storage.getUser(id) }
Before we could use dispatch_apply to submits a block to a dispatch queue for multiple invocations. Starting with Swift, the equivalence is concurrentPerform
1 2 3 4
DispatchQueue.concurrentPerform(iterations: 1000) { index in let last = array.last ?? 0 array.append(last + 1) }
Using spec testing framework like Quick is nice, which enables BDD style.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
describe("the 'Documentation' directory") { it("has everything you need to get started") { let sections = Directory("Documentation").sections expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups")) expect(sections).to(contain("Installing Quick")) }
context("if it doesn't have what you're looking for") { it("needs to be updated") { let you = You(awesome: true) expect{you.submittedAnIssue}.toEventually(beTruthy()) } } }
But in case you don’t want additional frameworks, and want to live closer to Apple SDKs as much as possible, here are few tips.
Secondly, I tried with dateComponents. The component.year has changed, but it calendar still returns the original date, very strange !!. No matter what timezone and calendar I use, it still has this problem
1 2 3
var component = calendar.dateComponents(in: TimeZone.current, from: base) component.year = year Calendar.current.date(from: component)
Finally, I tried to be more explicit, and it works 🎉
1 2 3
var component = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: base) component.year = year Calendar.current.date(from: component)
Continue my post https://github.com/onmyway133/blog/issues/45. When you work with features, like map view, you mostly need permissions, and in UITests you need to test for system alerts.
Add interruption monitor
This is the code. Note that you need to call app.tap() to interact with the app again, in order for interruption monitor to work
1 2 3 4 5 6
addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in alert.buttons["Allow"].tap() returntrue })
app.tap()
Note that you don’t always need to handle the returned value of addUIInterruptionMonitor
Only tap when needed
One problem with this approach is that when there is no system alert (you already touched to allow before), then app.tap() will tap on your main screen. In my app which uses map view, it will tap on some pins, which will present another screen, which is not correct.
Since app.alerts does not work, my 2nd attempt is to check for app.windows.count. Unfortunately, it always shows 5 windows whether alert is showing or not. I know 1 is for main window, 1 is for status bar, the other 3 windows I have no idea.
The 3rd attempt is to check that underlying elements (behind alert) can’t be touched, which is to use isHittable. This property does not work, it always returns true
Check the content
This uses the assumption that we only tests for when user hits Allow button. So only if alert is answered with Allow, then we have permission to display our content. For my map view, I check that there are some pins on the map. See https://github.com/onmyway133/blog/issues/45 on how to mock location and identify the pins
1 2 3
if app.otherElements.matching(identifier: "myPin").count == 0 { app.tap() }
When there is no permission
So how can we test that user has denied your request? In my map view, if user does not allow location permission, I show a popup asking user to go to Settings and change it, otherwise, they can’t interact with the map.
I don’t know how to toggle location in Privacy in Settings, maybe XCUISiriService can help. But 1 thing we can do is to mock the application
Before you launch the app in UITests, add some arguments
The gpx file is very powerful, as it allows you to specify a route with different movement speed.
Provide one or more waypoints containing a latitude/longitude pair. If you provide one waypoint, Xcode will simulate that specific location. If you provide multiple waypoints, Xcode will simulate a route visiting each waypoint.
Optionally provide a time element for each waypoint. Xcode will interpolate movement at a rate of speed based on the time elapsed between each waypoint. If you do not provide a time element, then Xcode will use a fixed rate of speed. Waypoints must be sorted by time in ascending order.
Use the gpx file
Declare the gpx file in app target, not UITests target. Go to your app scheme -> Run -> Options
Go to Simulator -> Debug -> Location -> Custom Location and select that same location, just to make sure. It does not need to be the same, but I see that without Custom Location, it does not work in UITests
Test that you’re near the initial location
1 2 3 4 5 6 7
letmap = app.maps.element(boundBy: 0) let predicate = NSPredicate(format: "label CONTAINS 'City Hall'") let cityHall = map.otherElements.matching(predicate).element(boundBy: 0)
// wait for the map to finish loading and zooming wait(for: cityHall, timeout: 2) XCTAssertTrue(cityHall.exists)
Given that accessibilityLabel is an outwardly-facing string that is actually used by accessibility screen readers (and should be localized to the device user’s language), Apple now provides an alternate property (iOS 5+) that is specifically intended for UI Automation purposes
Usually in an app, we have these flows: onboarding, login, main. And we usually set OnboardingController, LoginController and MainController as the root view controller respectively depending on the state.
I find it useful to have the MainController as the container for main flow. It can be a tab controller, swipe menu controller or contains just 1 child view controller. The screens are provided by child view controllers, but the MainController does the following jobs
Usually when app is brought to foreground, we need to fetch logged in user profile to see if there’s changes. We do this by listening to app did become active in MainController.
Mock to open
This can be anti pattern. But in UI Tests, for laziness, we can just use some launch arguments and check to present some specific screens to test, because MainController is the root for main flow.
Logout
Because things originate from MainController, things can terminate in MainController. We can handle logout, clear states, and tell MainController to tell AppDelegate to switch to another root controller
Auto Layout is awesome. Just declare the constraints and the views are resized accordingly to their parent ‘s bounds changes. But sometimes it does not look good, because we have fixed values for padding, width, height, and even fixed font size.
This can be solved by some degree using Size Class. The idea of size class is that we have many sets of constraints, and based on the device traits, we enabled some of them. This is more convenient to do in Storyboard (although very hard to reason about), but if we’re doing in code (my prefer way), then it is a lot of code. And a lot of code means a lot of bugs.
If you take a look at iOSRes, we see the ratio 16:9 (height:width)
iPhone SE (320 x 568): 1.775
iPhone 6 (375 x 667): 1.778
iPhone 6+ (414 x 736): 1.778
They mostly have the same ratio. So we can have a simple approach, that scale elements based on ratio. Given the fact that the designer usually designs for iPhone 6 size, we can make that a base.
In this approach, the content will scale up or down depending on its ratio. You may argue that the idea of bigger phone is to display more, not to show the same content bigger. You may be right, in that case you need to create different constraints and different UIs. But if you want simple solutions that work, this is one of them
This is the technique I used when doing Windows Phone development, but it applies to many platforms as well
Calculate the ratio
1 2 3 4 5 6 7 8
classDevice{ // Base width in point, use iPhone 6 staticlet base: CGFloat = 375
staticvar ratio: CGFloat { returnUIScreen.main.bounds.width / base } }
Extension to make it convenient
We can have a computed property called adjusted that adjusts the size based on the ratio
The delegate of most Cocoa framework classes is automatically registered as an observer of notifications posted by the delegating object. The delegate need only implement a notification method declared by the framework class to receive a particular notification message. Following the example above, a window object posts an NSWindowWillCloseNotification to observers but sends a windowShouldClose: message to its delegate.
So the pattern is that the delegate should strip the NS and Notification, like NSWindowWillCloseNotification to windowShouldClose:
We can’t configure this anymore, as Xcode will use the latest SDK. For Xcode 7, the SDK is iOS 9
If we upgrade Xcode, it will use the newer version of the SDK. Like Xcode 7.2, the SDK is iOS 9.1
Choosing the latest SDK for your project lets you use the new APIs introduced in the OS update that corresponds to that SDK. When new functionality is added as part of a system update, the system update itself does not typically contain updated header files reflecting the change. The SDKs, however, do contain updated header files.
Deployment Target
We can set in Xcode -> Target -> Deployment Info -> Deployment Target
State that we support this iOS version
What does it mean
So, a modern App might use iOS 9 as the Target SDK, and iOS 7 as the deployment target. This means that you can run on iOS 7, iOS 8 and iOS 9, and that you have available to you any iOS 9 calls when actually running on iOS 9.
.
Each .sdk directory resembles the directory hierarchy of the operating system release it represents: It has usr, System, and Developer directories at its top level. OS X .sdk directories also contain a Library directory. Each of these directories in turn contains subdirectories with the headers and libraries that are present in the corresponding version of the operating system with Xcode installed.
.
The libraries in an iOS or OS X SDK are stubs for linking only; they do not contain executable code but just the exported symbols. SDK support works only with native build targets.
So the SDK is just like stub and header only. It means that we can use certain APIs, but on OS that does not have the real symbols for those APIs, it crashes
available
Swift 2 introduces available construct that guards against failure when trying to use newer APIs.
Note that available is runtime, not compile time. All the code is inside your executable
1 2 3 4 5
if #available(iOS 9, OSX10.10, *) { // Code to execute on iOS 9, OS X 10.10 } else {
#if (arch(i386) || arch(x86_64)) && os(iOS) // code inside gets inserted into executable when builds for simulator #else // code inside gets inserted into executable when builds for device #endif
// All the code gets inserted into executable, but is run depending on the version of the OS if #available(iOS 9, *) { // use UIStackView } else { // show your manual Auto Layout skill }
For example, suppose in Xcode you set the deployment target (minimum required version) to “OS X v10.5” and the base SDK (maximum allowed version) to “OS X v10.6”. During compilation, the compiler would weakly link interfaces that were introduced in OS X v10.6 while strongly linking interfaces defined in earlier versions of the OS. This would allow your application to run in OS X v10.5 and take advantage of newer features when available.
.
None of the (platform) frameworks is really “included in the bundle”. Instead, your app has a reference (“link”) to a framework once you add it to the “Link Binary with Library” build phase. The frameworks are pre-installed on the devices. When you run an app, all the app’s framework references are resolved by the dynamic linker (on the device), which means the framework code is loaded so your app can use it.
Numeric literals can contain extra formatting to make them easier to read. Both integers and floats can be padded with extra zeros and can contain underscores to help with readability. Neither type of formatting affects the underlying value of the literal
1 2 3
let paddedDouble = 000123.456 let oneMillion = 1_000_000 let justOverOneMillion = 1_000_000.000_000_1
publicenumMetricUnit: Double{ case nano = 0.000_000_001 case micro = 0.000_001 case milli = 0.001 case centi = 0.01 case deci = 0.1 case base = 1 case deka = 10 case hecto = 100 case kilo = 1_000 case mega = 1_000_000 case giga = 1_000_000_000 case tera = 1_000_000_000_000 case peta = 1_000_000_000_000_000
Since working with iOS, I really like the delegate pattern, in which it helps us defer the decision to another party.
The iOS application delegates its event to AppDelegate, which over time will be a big mess. Usually, the AppDelegate is where you put your root view controller setup, crash tracking, push notification, debugging, … and we just somehow violent the Single Responsibility principle. Moreover, it makes us hard to reason about code in AppDelegate
Service
I like to think of each task in AppDelegate as a service. And the AppDelegate distributes the events into each service via ServiceDispatcher. Simple plain old composition and looping
I tend to have RootService as a place to setup root view controllers
services.forEach { service in service.application?(application, didFinishLaunchingWithOptions: launchOptions) }
returntrue }
funcapplicationDidBecomeActive(application: UIApplication) { services.forEach { service in service.applicationDidBecomeActive?(application) } }
funcapplicationWillResignActive(application: UIApplication) { services.forEach { service in service.applicationWillResignActive?(application) } }
funcapplicationWillEnterForeground(application: UIApplication) { services.forEach { service in service.applicationWillEnterForeground?(application) } }
funcapplicationDidEnterBackground(application: UIApplication) { services.forEach { service in service.applicationDidEnterBackground?(application) } } }
I have more services like DebugService, PushNotificationService, CrashTrackingService, …
The downside to this approach is that in real life, there will be dependencies between those services, like that UserService must be called before RootService? In this case, I have to use comment to explain why I have that decision, which is hard for newcomers to understand at first. Take a look at How to Move Bootstrapping Code Out of AppDelegate for how dependencies are managed
JSDecoupledAppDelegate comes with another approach, in which service events are named according to the functions, like appStateDelegate, appDefaultOrientationDelegate, watchInteractionDelegate, …
But for me, Service and ServiceDispatcher suit my need
extensionRefreshablewhereSelf: UIViewController { /// Install the refresh control on the table view funcinstallRefreshControl() { let refreshControl = UIRefreshControl() refreshControl.tintColor = .primaryColor refreshControl.addTarget(self, action: #selector(handleRefresh(_:)), for: .valueChanged) self.refreshControl = refreshControl if #available(iOS 10.0, *) { tableView.refreshControl = refreshControl } else { tableView.backgroundView = refreshControl } } }
Protocol extension is cool but somehow I’m not a fan of it. I always consider composition first, to extract the specific task to one entity that does that well. It looks like this
I always forget how to write correct #available( or #if swift(>=3.0) or just lazy to write required init?(coder aDecoder: NSCoder) every time I make a subclass. That’s why I made SwiftSnippets to save time for these tedious tasks. Installation is easy with script, so you should give it a try.
I can’t recommend this enough, it saves me tons of time