Using assembly

Issue #112

I’m very fascinated when people use assembly to perform investigation, that’s just mind blowing 💥 . Here are some of the favorite use cases

URL Routing with Compass

Issue #110

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


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

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

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

Getting Started

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

artboard

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

Registering Your App on Instagram

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

artboard 2

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

Compass 101

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

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

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

Route Patterns

This is how you declare a routing schema in Compass:

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

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

The Navigator

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

artboard 3

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

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

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

Location

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

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

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

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

let arguments = location.arguments

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

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

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

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

Router to the Rescue

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

1
pod install

The version of Compass used in this tutorial is 5.1.0.

Registering a Router

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

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

1
import Compass

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

1
var postLoginRouter = Router()

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

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

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

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

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

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

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

Here’s what you do in the above method:

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

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

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

1
setupRouting()

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

Implementing the Route Handlers

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

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

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

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

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

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

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

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

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

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

The remaining Route

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

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

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

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

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

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

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

The LogoutRoute

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

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

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

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

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

Refactoring Time

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

1
import Compass

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

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

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

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

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

1
import Compass

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

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

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

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

1
import Compass

Remove the logout method altogether. Find the following:

indexPath.section
1
2
  logout()
}

…and replace it with:

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

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

Handling Deep Linking

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

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

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

artboard 5

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

Routing with Push Notifications

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

Setting up

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

step1_bundleid

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

step1_appid

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

step2_key

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

step3_capability

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

1
import UserNotifications

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

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

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

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

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

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

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

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

Handling payload

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

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

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

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

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

try? Navigator.navigate(urn: urn)
}

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

Testing Push Notifications

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

step4_test

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

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

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

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

Read more

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

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

BuddyBuild and gradle.properties

Issue #109

People advise against storing keys inside build.gradles. We should store them on 1Password and populate our gradle.properties, so don’t track this file in git. Here is .gitignore file

1
2
3
4
5
6
7
8
*.iml

/build
/gradle.properties
/local.properties

.gradle
.idea

There are several ways to help BuddyBuild know about our gradle.properties

1. Using Environment variables

But when configuring the project on BuddyBuild, it complains about key not found. The solution is to use Environment variables

key

Then in your build.gradle, you can

1
buildConfigField 'String', 'MY_KEY', System.getenv("MY_KEY") ?: MY_KEY

This is because gradle does not know about environment variables. The System.getenv("MY_KEY") is for BuddyBuild, and the default MY_KEY is for gradle.properties.

Next is to remove this duplication. We can use Groovy Binding. build.gradle does the import import groovy.lang.Binding automatically for us

1
2
3
4
5
6
7
8
String environmentKey(variable) {
for (Object var : binding.variables) {
if (var.value == variable) {
return System.getenv(var.key) ?: variable
}
}
return ""
}
1
buildConfigField 'String', 'MY_KEY', environmentKey(MY_KEY)

2. Using Secured File 👍

BuddyBuild allows us to define Secured File, here we can upload our gradle.properties

secure files

And we can use Prebuild script to copy this secured file to our project. BuddyBuild suggests using buddybuild_prebuild.sh but then build fails in Build file '/tmp/sandbox/workspace/app/build.gradle'

So, create a script called buddybuild_postclone.sh

1
2
3
#!/usr/bin/env bash

cp $BUDDYBUILD_SECURE_FILES/gradle.properties $BUDDYBUILD_WORKSPACE/gradle.properties

Communication between Fragment and Activity

Issue #108

There’s always need for communication, right 😉 Suppose we have OnboardingActivity that has several OnboardingFragment. Each Fragment has a startButton telling that the onboarding flow has finished, and only the last Fragment shows this button.

Here are several ways you can do that

1. EventBus 🙄

Nearly all articles I found propose this https://github.com/greenrobot/EventBus, but I personally don’t like this idea because components are loosely coupled, every component and broadcast can listen to event from a singleton, which makes it very hard to reason when the project scales

1
data class OnboardingFinishEvent()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class OnboardingActivity: AppCompatActivity() {
override fun onStart() {
super.onStart()

EventBus.getDefault().register(this)
}

override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onOnboardingFinishEvent(event: OnboardingFinishEvent) {
// finish
}
}
1
2
3
4
5
6
7
8
9
class OnboardingFragment: Fragment() {
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
EventBus.getDefault().post(OnboardingFinishEvent())
}
}
}

Read more

2. Otto 🙄

This https://github.com/square/otto was deprecated in favor of RxJava and RxAndroid

3. RxJava 🙄

We can use simple PublishSubject to create our own RxBus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject

// Use object so we have a singleton instance
object RxBus {

private val publisher = PublishSubject.create<Any>()

fun publish(event: Any) {
publisher.onNext(event)
}

// Listen should return an Observable and not the publisher
// Using ofType we filter only events that match that class type
fun <T> listen(eventType: Class<T>): Observable<T> = publisher.ofType(eventType)
}
1
2
3
4
// OnboardingFragment.kt
startButton.onClick {
RxBus.publish(OnboardingFinishEvent())
}
1
2
3
4
5
6
7
8
// OnboardingActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

RxBus.listen(OnboardingFinishEvent::class.java).subscribe({
// finish
})
}

4. Interface

This is advised here Communicating with Other Fragments. Basically you define an interface OnboardingFragmentDelegate that whoever conforms to that, can be informed by the Fragment of events. This is similar to Delegate pattern in iOS 😉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface OnboardingFragmentDelegate {
fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment)
}

class OnboardingFragment: Fragment() {
var delegate: OnboardingFragmentDelegate? = null

override fun onAttach(context: Context?) {
super.onAttach(context)

if (context is OnboardingFragmentDelegate) {
delegate = context
}
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
delegate?.onboardingFragmentDidClickStartButton(this)
}
}
}
1
2
3
4
5
6
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
override fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment) {
onboardingService.hasShown = true
startActivity<LoginActivity>()
}
}

5. ViewModel

We can learn from Share data between fragments to to communication between Fragment and Activity, by using a shared ViewModel that is scoped to the activity. This is a bit overkill

1
2
3
class OnboardingSharedViewModel: ViewModel() {
val finish = MutableLiveData<Unit>()
}
1
2
3
4
5
6
7
8
9
10
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this).get(OnboardingSharedViewModel::class.java)

viewModel.finish.observe(this, Observer {
startActivity<LoginActivity>()
})
}
}

Note that we need to call ViewModelProviders.of(activity) to get the same ViewModel with the activity

1
2
3
4
5
6
7
8
9
10
class OnboardingFragment: Fragment() {
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val viewModel = ViewModelProviders.of(activity).get(OnboardingSharedViewModel::class.java)
startButton.onClick({
viewModel.finish.value = Unit
})
}
}

7. Lambda

Create a lambda in Fragment, then set it on onAttachFragment. It does not work for now as there is no OnboardingFragment in onAttachFragment 😢

1
2
3
4
5
6
7
8
9
10
11
class OnboardingFragment: Fragment() {
var didClickStartButton: (() -> Unit)? = null

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

startButton.onClick {
didClickStartButton?.invoke()
}
}
}
1
2
3
4
5
6
7
8
9
10
11
class OnboardingActivity: AppCompatActivity() {
override fun onAttachFragment(fragment: Fragment?) {
super.onAttachFragment(fragment)

(fragment as? OnboardingFragment).let {
it?.didClickStartButton = {
// finish
}
}
}
}

8. Listener in Bundle 🙄

Read more

Coordinator and FlowController

Issue #106

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

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

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


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

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

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

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

1. FlowController and AppDelegate

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

AppDelegate is also considered Composition Root

Here is how to declare AppFlowController in AppDelegate

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

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

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

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
var appFlowController: AppFlowController!

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

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

appFlowController.start()

return true
}
}

Here are some hypothetical FlowController that you may encounter

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

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

2. FlowController as container view controller

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

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

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

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

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

3. FlowController as dependency container

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

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

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

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

class ProductFlowController {
let dependencyContainer: ProductDependencyContainer

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

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

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

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

4. Adding or removing child FlowController

Coordinator

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

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

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

children.append(child)
}

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

children.remove(at: index)
}

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

FlowController

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

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

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

And see in action how AppFlowController work with adding

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

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

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

and with removing when the child FlowController finishes

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

5. AppFlowController does not need to know about UIWindow

Coordinator

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

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

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

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

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

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

FlowController

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

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

appFlowController.start()

6. LoginFlowController can manage its own flow

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

Coordinator

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

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

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

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

let loginCoordinator = LoginCoordinator(navigationController: navigationController)

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

final class LoginCoordinator: Coordinator {
private let navigationController: UINavigationController

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

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

navigationController.viewControllers = [loginController]
}
}

FlowController

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

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

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

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

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

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

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

embeddedNavigationController.viewControllers = [loginController]
}
}

7. FlowController and responder chain

Coordinator

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

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

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

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

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

FlowController

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

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

8. FlowController and trait collection

FlowController

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

As in A Better MVC, Part 2: Fixing Encapsulation

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

From setOverrideTraitCollection

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

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

appFlowController.setOverrideTraitCollection(trait, forChildViewController: loginFlowController)

9. FlowController and back button

Coordinator

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

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

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

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

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

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

private let rootViewController: UIViewController

// MARK: - Mutable state

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

// MARK: - Lazy views

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

// MARK: - Initialization

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

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

FlowController

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

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

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

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

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

}
}

10. FlowController and callback

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

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

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

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

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

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

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

embeddedNavigationController.viewControllers = [productListController]
}
}

11. FlowController and deep linking

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

Please reconsidering your choice of libraries

Issue #105

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

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

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

Putting a little bit more dedication

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

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

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

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

What about the stars

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

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

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

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

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

Please

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

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

How to make generic extension with associatedtype protocol in Swift

Issue #104

I like extensions, and I like to group them under 1 common property to easily access. This also makes it clear that these all belong to the same feature and not to confuse with Apple properties.

This is how I do it in Anchor and On

1
2
3
4
5
6
activate(
a.anchor.top.left,
b.anchor.top.right,
c.anchor.bottom.left,
d.anchor.bottom.right
)
1
2
3
4
5
6
7
textField.on.text { text in
print("textField text has changed")
}

textField.on.didEndEditing { text in
print("texField has ended editing")
}

Generic extension

For On, it is a bit tricky as it needs to adapt to different NSObject subclasses. And to make auto completion work, meaning that each type of subclass gets its own function hint, we need to use generic and associatedtype protocol.

You can take a look at Container and OnAware

1
2
3
4
5
6
7
public class Container<Host: AnyObject>: NSObject {
unowned let host: Host

init(host: Host) {
self.host = host
}
}
1
2
3
4
5
public protocol OnAware: class {
associatedtype OnAwareHostType: AnyObject

var on: Container<OnAwareHostType> { get }
}

RxCocoa

RxSwift has its RxCocoa that does this trick too, so that you can just declare

1
2
3
button.rx.tap
textField.rx.text
alertAction.rx.isEnabled

The power lies in the struct Reactive and ReactiveCompatible protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public struct Reactive<Base> {
/// Base object to extend.
public let base: Base

/// Creates extensions with base object.
///
/// - parameter base: Base object.
public init(_ base: Base) {
self.base = base
}
}

public protocol ReactiveCompatible {
/// Extended type
associatedtype CompatibleType

/// Reactive extensions.
static var rx: Reactive<CompatibleType>.Type { get set }

/// Reactive extensions.
var rx: Reactive<CompatibleType> { get set }
}

extension ReactiveCompatible {
/// Reactive extensions.
public static var rx: Reactive<Self>.Type {
get {
return Reactive<Self>.self
}
set {
// this enables using Reactive to "mutate" base type
}
}

/// Reactive extensions.
public var rx: Reactive<Self> {
get {
return Reactive(self)
}
set {
// this enables using Reactive to "mutate" base object
}
}
}

Here UIButton+Rx you can see how it can be applied to UIButton

1
2
3
4
5
6
7
extension Reactive where Base: UIButton {

/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
return controlEvent(.touchUpInside)
}
}

How to make simple Binding in MVVM in iOS

Issue #103

If you use MVVM or any other kinds of helper classes, then there’s need to report back the result to the caller. In simple cases, without asynchronous chaining, RxSwift is a bit overkill, you can just implement your own Binding. Basically, it is just observer pattern, or closure in its simplest form.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Binding<T> {
var value: T {
didSet {
listener?(value)
}
}

private var listener: ((T) -> Void)?

init(value: T) {
self.value = value
}

func bind(_ closure: @escaping (T) -> Void) {
closure(value)
listener = closure
}
}

Then you can declare it like

1
2
3
4
5
6
7
8
9
10
11
12
13
class ViewModel {
let friends = Binding<[User]>(value: [])

init() {
getFacebookFriends {
friends.value = $0
}
}

func getFacebookFriends(completion: ([User]) -> Void) {
// Do the work
}
}

Finally, this is how you listen to the result via callback

1
2
3
4
5
6
7
override func viewDidLoad() {
super.viewDidLoad()

viewModel.friends.bind { friends in
self.friendsCountLabel.text = "\(friends.count)"
}
}

How to use custom UINavigationBar in iOS

Issue #102

Today I was reading the project in IGListKit Tutorial: Better UICollectionViews, I encounter something I often overlook

1
2
let nav = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
nav.pushViewController(FeedViewController(), animated: false)

So we can provide custom navigationBarClass and toolbarClass for UINavigationController. This RRMaterialNavigationBar also has cool implementation of a material UINavigationBar.

Together with UINavigationBarDelegate we can do some cool things

It's good to have a CI

Issue #101

I have Unit tests and UI tests pass on my simulator and device, locally. But when I make the build on Buddybuild, it fails with the reason Activity cannot be used after its scope has completed. People seem to have the same issue too.

Taking a look at the log in Buddybuild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 t =     8.89s     Staging: UIStatusBarWindow
t = 8.95s Staging: (CoreFoundation) Sending Updated Preferences to System CFPrefsD
t = 8.95s Staging: Setup BuddybuildSDK
t = 8.98s Staging: [BuddyBuildSDK] In app store - Instant Replay Disabled
t = 8.98s Staging: Setting up the remote notifications for UI Tests video recording
t = 9.07s CL: CLLocationManager
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"CLLocationManager", "event":activity, "_cmd":initWithEffectiveBundleIdentifier:bundle:, "self":"0x600000205140", "identifier":(null), "bundle":(null)}
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Core] {"msg":"state transition", "event":state_transition, "state":LocationManager, "id":"0x600000205140", "property":init, "new":'00 00 00 00 00 00 F0 BF 00 00 00 00 00 00 F0 BF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 3F 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00'}
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Core] {"msg":"state transition", "event":state_transition, "state":LocationManager, "id":"0x600000205140", "property":lifecycle, "old":"0x0", "new":"0x6040000ddd50"}
t = 9.07s CL: _CLClientCreateWithBundleIdentifierAndPath
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"_CLClientCreateWithBundleIdentifierAndPath", "event":activity, "effectiveBundleIdentifier":(null), "effectiveBundlePath":(null)}
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"client allocated", "client":"0x7f84c64e0990"}
t = 9.07s CL: _CLClientCreateConnection
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"_CLClientCreateConnection", "event":activity, "client":"0x7f84c64e0990"}
t = 9.07s CL: Sending cached messages to daemon
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"Sending cached messages to daemon", "event":activity}
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Client] #Warning No cached registration message
t = 9.07s Staging: (CoreLocation) [com.apple.locationd.Core.Core] {"msg":"state transition", "event":state_transition, "state":LocationManager, "id":"0x600000205140", "property":pausesLocationUpdatesAutomatically, "old":0, "new":1}
t = 9.10s CL: CLLocationManager
t = 9.11s Staging: (CoreLocation) [com.apple.locationd.Core.Client] {"msg":"CLLocationManager", "event":activity, "_cmd":setDelegate:, "self":"0x600000205140", "delegate":"0x60400028bef0"}
t = 9.12s error: unexpectedly found nil while unwrapping an Optional value
t = 9.12s Unable to monitor event loop
t = 10.13s Tap "Onboarding.Continue" Button
t = 10.13s Wait for no.hyper.MyApp-Staging to idle
t = 10.16s Find the "Onboarding.Continue" Button
t = 11.28s Assertion Failure: <unknown>:0: no.hyper.MyApp-Staging crashed in MyApp_Staging.AppDelegate.(makeDependencyContainer in _5D394B3D7D393F9C3C550E61780517BB)() -> MyApp_Staging.DependencyContainer
t = 11.33s Wait for com.apple.springboard to idle

Did you see unexpectedly found nil while unwrapping an Optional value? It crashed in CLLocationManager. It is because when location changes, CLLocationManager needs to report it via didUpdateLocations function, but we haven’t implemented it. Strangely that it didn’t happen when testing locally.

The proposed fix is to implement a dummy method with no operation

1
2
3
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// No op
}

But it is not the solution. It appears that BuddyBuild is doing some hacks with how push notification and UIWindow, hence causing the issue.

Diff algorithm

Issue #99

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

Myers

Wu

Wagner–Fischer

Common Longest Subsequence

Heckel

Hunt-Szymanski

Read more

How to use safeAreaLayoutGuide in iOS 10

Issue #98

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

To use safeAreaLayoutGuide, you need to do iOS version check

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

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

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

self.associatedLayoutGuide = layoutGuide

return layoutGuide
}
}
}

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

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

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

This way we can simply do

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

Read more

Learning from Open Source Using Coordinator

Issue #97

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

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

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

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

return self;
}

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

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

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

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

coordinator

Read more

Learning from Open Source Managing dependencies

Issue #96

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

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

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

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

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

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

/// ...
}

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

}

And we can mock in tests

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

Learning from Open Source Using Playground

Issue #94

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

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

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

Read more

Indenting Swift code

Issue #93

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

Using 2 spaces indentation

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

spaces

Move first parameter to new line

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

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

You can do the same for function declaration

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

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

Here is how to use UIView.animate

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

Here is how to use RxSwift subscribe

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

},
onError: { error in

},
onCompleted: {

},
onDisposed: {

}
)

Shouldn’t use trailing closure when chaining methods

Also, the next method call should start on same line

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

Testing keychain in iOS

Issue #92

Today I was upgrading Keychain to swift 4, and take this opportunity to fix the test. The tests pass on macOS, but on iOS, I get -25300 error for

1
var status = SecItemCopyMatching(query as CFDictionary, nil)

It is because there is no Keychain entitlement for test target. But this is a framework, how can I add entitlement 🤔 The solution is to use a Test Host to host the XCTest tests. See my pull request

Create test host target

target

First create an iOS app to act as a test host, you can name it TestHost_iOS

Enable Keychain capability

Then enable Keychain capability to let Xcode automatically create an entitlement file for you. Note that you can just enter the Keychain group. You don’t need go to Apple Developer dashboard to configure anything

keychain

Specify Test Host

Then in you test target, specify Test Host by using $(BUILT_PRODUCTS_DIR)/TestHost_iOS.app/TestHost_iOS

test host

Now run your test again, it should pass 🎉

Learning from Open Source Making macOS app in code

Issue #91

I’m familiar with the whole app structure that Xcode gives me when I’m creating new macOS project, together with Storyboard. The other day I was reading touch-bar-simulator and see how it declares app using only code. See this main.swift

1
2
3
4
5
6
7
8
9
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()

final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
let controller = IDETouchBarSimulatorHostWindowController.simulatorHostWindowController()!
lazy var window: NSWindow = self.controller.window!
}

String manipulation in Apple Script

Issue #89

Today I find that AppleScript allows us to import Foundation, with that we have lots of power, including NSString. See my script

1
2
3
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString

Here is how I can remove last path component from a string

1
2
3
4
5
on myRemoveLastPath(myPath)
set myString to NSString's stringWithString:myPath
set removedLastPathString to myString's stringByDeletingLastPathComponent
removedLastPathString as text
end myRemoveLastPath

You need to cast to NSString with NSString's stringWithString: and cast back to Apple Script string with as text. The 's is how you can invoke functions.

One more thing is that we can support parameters to function, like this script

1
2
3
4
5
on remove:remove_string fromString:source_string	
set s_String to NSString's stringWithString:source_string
set r_String to NSString's stringWithString:remove_string
return s_String's stringByReplacingOccurrencesOfString:r_String withString:""
end remove:fromString:

How to call function inside Apple Script

Issue #88

I ‘ve been using Apple Script to for my Finder extension FinderGo. Because of sandboxing, all scripts must lie inside Application Scripts folder.

Today, I was rewriting my Xcode extension XcodeWay. Before Xcode 8, we could use Xcode plugin and all kinds of magic to make our dreams come true https://github.com/onmyway133/XcodeWay/blob/1.0/XcodeWay/Helper/FTGEnvironmentManager.m#L50. But then it does not work since Xcode Source Editor Extension was introduced. So I rewrote XcodeWay as an extension https://github.com/onmyway133/XcodeWay/releases/tag/1.1.0

Extension must run inside sandbox. If you switch App Sandbox in your XcodeWayExtensions.entitlements to NO, it won’t load. So sandbox restricts me a lot in what kinds of things I want to do. And under Xcode 9, I can’t use NSWorkspace to open Finder.

So I think I could use Apple Script too, and it worked like a charm. The only restriction is code reuse, since I only know how to run an entire script. One way is to import other Apple scripts https://stackoverflow.com/questions/2606136/import-applescript-methods-in-another-applescript but I think I will write all the functions inside 1 script, and find out how to call specific function.

By function, I also mean handler, procedure. I come across this snippet Scriptinator that pretty much inspires me, thanks to open source.

So here is my script that contains lots of functions . And here is ScriptRunner that explains how to build NSAppleEventDescriptor. Note that you need to import Carbon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Carbon

func eventDescriptior(functionName: String) -> NSAppleEventDescriptor {
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
let target = NSAppleEventDescriptor(
descriptorType: typeProcessSerialNumber,
bytes: &psn,
length: MemoryLayout<ProcessSerialNumber>.size
)

let event = NSAppleEventDescriptor(
eventClass: UInt32(kASAppleScriptSuite),
eventID: UInt32(kASSubroutineEvent),
targetDescriptor: target,
returnID: Int16(kAutoGenerateReturnID),
transactionID: Int32(kAnyTransactionID)
)

let function = NSAppleEventDescriptor(string: functionName)
event.setParam(function, forKeyword: AEKeyword(keyASSubroutineName))

return event
}

How to use Input and output container in Swift

Issue #87

This is a very nifty trick from ios-oss which was built around MVVM pattern. It uses protocol to define input and output, and a container protocol to contain them. Take https://github.com/kickstarter/ios-oss/blob/1f5643f6a769995ccd1bb3826699745e64597ab7/Library/ViewModels/LoginViewModel.swift for example

public protocol LoginViewModelInputs {

}

public protocol LoginViewModelOutputs {

}

public protocol LoginViewModelType {
  var inputs: LoginViewModelInputs { get }
  var outputs: LoginViewModelOutputs { get }
}

public final class LoginViewModel: LoginViewModelType, LoginViewModelInputs, LoginViewModelOutputs {
  public var inputs: LoginViewModelInputs { return self }
  public var outputs: LoginViewModelOutputs { return self }
}

Look how LoginViewModel conforms to 3 protocols. And when you access its input or output properties, you are constrained to only LoginViewModelInputs and LoginViewModelOutputs

Fixing login hanging in macOS High Sierra

Issue #86

Today I met a strange problem. After I enter my password, the progress bar runs to the end, and it is stuck there forever. No matter how many times I try to restart.

I finally need to go to Recovery mode by pressing Cmd+R at start up. I then select Get Help Online to open Safari. Strangely enough I wasn’t connected to Internet

After select the wifi icon on the status bar to connect internet, I then restart and can login again. It seems that macOS is checking for something before allowing user to login

What about performance?

Issue #85

That’s the question I hear often when people are introduced to a new framework. It’s a valid concern. But it seems to me that they ask this just for fun. To my surprise, most people just don’t care, and the frameworks with the most stars often perform the worst.

Now take a look back at performance. Here are some benchmarks

From https://github.com/ibireme/YYModel, compare different JSON mappers for ObjC

From https://github.com/bwhiteley/JSONShootout, compare different JSON mappers for Swift

From https://github.com/onmyway133/DeepDiff#among-different-frameworks, compare different diffing frameworks

I use it because it has many stars

Take a look at the stars, the ones with the most stars often perform the slowest 🙀

I don’t say that more stars mean better. I don’t believe in stars. Stars may just be a result of your marketing effort. The same framework, without any code change, but after featured in some newsletters, gets additional thousand stars. The code remains the same, so what do stars really tell here?

I’m not talking about closed source. I like open source. When deciding an open source framework, there are many factors. It can be issues and pull requests that indicate how the community care about it. It can be good code and good tests, that make it easy to maintain. It can be good documentation, that says how much dedication the developers have put in.

And here’s the fact, when you see a project with many stars, you tend to star it too 😉 for the sake of bookmarking. Stars mean little, but they give us some ideas on how popular a project is.

I just need to get work done

OK.

What about performance?

Honestly, do you really care?

Dear SDK developers

Issue #84

Dear SDK developers,

  • Please don’t do swizzling. Give us instructions on where to call your APIs instead of doing unnecessary swizzling. You’re making it for developers to use, and they know how to write code. Most of the time, you don’t know how to do swizzling right ⚠️
  • Please don’t use configuration file. If possible, please consider configuration via pure code, instead of the plist file that is more exposable in the app bundle.
  • Please don’t make assumption about the app. Your assumptions about the app the SDK is going to be integrated are not always correct. Every app has its own use case and view hierarchy.
  • Please provide proper releases. Tag and make releases for new changes in the SDK. If there are major changes, there should be migration guide and affect of that to older apps. Also, please write proper change logs, either in release notes or change log file.
  • Please don’t leave us with a dump header file. Please add documentations to all the functions.
  • Please improve the README. To avoid all the guessing, please improve the README with detail instructions.
  • Please include a sample demo project. When making a demo project, you possible have chances to deal with potential problems. And developers have a clue on which steps they might be missing.
  • Please open source it. You’re selling your service, not the code. By open sourcing it, the code is clear to everybody, and they might help you with bug fixing and suggestion.

To be honest, I was very scared when asked to integrate the SDK with poor documentation and closed source. It’s like playing the guessing and praying game.

If the service is not important to you, and you’re doing it for fun, then you can ignore these requests. But if you’re serious about it, then please consider doing. We know you can do better 💪

Thanks ❤️

How to migrate Codable object in Swift

Issue #83

As of swift 4 migration, we updated Cache to fully take advantage of Codable. It works for most cases, as we should usually declare our entity as typed safe object instead of array or json dictionary. And by conforming to Codable, it is easily encoded and decoded to and from json data. And persisting them to Cache is as easy as eating cookie.

The other day, I saw someone asking on how to migrate if the model changes https://github.com/hyperoslo/Cache/issues/153, and he likes the way Realm does https://realm.io/docs/swift/latest/#migrations

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

Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
// The enumerateObjects(ofType:_:) method iterates
// over every Person object stored in the Realm file
migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
// combine name fields into a single field
let firstName = oldObject!["firstName"] as! String
let lastName = oldObject!["lastName"] as! String
newObject!["fullName"] = "\(firstName) \(lastName)"
}
}
})

I think we can rely on Codable to the migration. FYI, here is the PR https://github.com/hyperoslo/Cache/pull/154

Class name change

I see Codable is based on json, and the importance of json is its data structure, not the class name. So if you change the class name, it still works.

First, we save model of type Person, later we load model of type Alien. It works because the structure stays the same

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Person: Codable {
let firstName: String
let lastName: String
}

struct Alien: Codable {
let firstName: String
let lastName: String
}

let person = Person(firstName: "John", lastName: "Snow")
try! storage.setObject(person, forKey: "person")

// As long as it has same properties, it works too
let cachedObject = try! storage.object(ofType: Alien.self, forKey: "person")
XCTAssertEqual(cachedObject.firstName, "John")

Property change

If the property changes, then you need to do a little work of migration.

First, we save model of type Person1, it has just fullName. Later we change the model to Person2 with some new properties. To do the migration, we need to load model with old Person1 first, then construct a new model Person2 based on this Person1. Finally, save that to Cache with the same key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Person1: Codable {
let fullName: String
}

struct Person2: Codable {
let firstName: String
let lastName: String
}

// Firstly, save object of type Person1
let person = Person1(fullName: "John Snow")

try! storage.setObject(person, forKey: "person")
XCTAssertNil(try? storage.object(ofType: Person2.self, forKey: "person"))

// Later, convert to Person2, do the migration, then overwrite
let tempPerson = try! storage.object(ofType: Person1.self, forKey: "person")
let parts = tempPerson.fullName.split(separator: " ")
let migratedPerson = Person2(firstName: String(parts[0]), lastName: String(parts[1]))
try! storage.setObject(migratedPerson, forKey: "person")

XCTAssertEqual(
try! storage.object(ofType: Person2.self, forKey: "person").firstName,
"John"
)

How to handle alert in UITests in iOS

Issue #82

Usually in an app, you have the onboarding with steps that require push notification and location permission to be turned on. And you want to automate these steps via UITests

Firstly, you need to add interruption handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
addUIInterruptionMonitor(withDescription: "Alert") {
element in
do {
// Push Notification
let button = element.buttons["Allow"]
let title = element.staticTexts["“MyAwesomeApp” Would Like to Send You Notifications"]
if title.exists && button.exists {
button.tap()
}
}

do {
// Location
let button = element.buttons["Only While Using the App"]
if button.exists {
button.tap()
}
}

return true
}
}

Then you need to call tap on XCUIApplication to make the app responsive

1
2
turnOnPushNotificationButton.tap()
tap()

Sometimes the alert handling is slow and you get Did not receive view did disappear notification within 2.0s. Well, the workaround is to wait for the element on next onboarding step to appear. Starting with Xcode 9, you can use waitForExistence.

This is how you can go to last step after user has enabled push notification

1
2
let label = staticTexts["Congratulation. You've granted us permission. Now enjoy the app."]
_ = label.waitForExistence(timeout: 5)