Modern iOS development

21/04/2020

Alright, here is a bit of a disclaimer before we embark on this journey - pretty much through all of my career I was developing micro-services and sometimes front-ends for them, think of the stacks like Go, Java, JavaSript or Typescript and just recently I’ve decided to try myself in the absolutely new field (for me) - mobile development, specifically for iOS, so this post covers my personal experience as a newbie and some findings along the way.

Disclaimer #1

Software versions at the time of writing this post:
- Xcode 11.4
- Swift 5.2

Disclaimer #2

All written in here is purely personal perspective.

Motivation

Obviously it is a huge step and before blindly accepting it and plunging into this I always try to find my “Why?”. “Why?” is super important, essentially it is the thing that will keep you motivated and trying, it is a north star that helps you stay on the course and keep going no mater what. It should be something that will bring a value maybe just personal, maybe for the others too.

So I’ve decided to build a tool for capturing thoughts - important bits of information in order to share them and make sense of them…Pretty vague huh? :) But this is all I can share for now (stay tuned).

I’ve also registered domain for it capcha.app, though not much to see there at the moment. As you can see the name is Capcha, think of it as a slang word for “capture”, also it is only a single letter away from CAPTCHA.

So here is my motivation - I’m building the best personal tool out here :-)

First steps

After figuring out the driving idea, it is the time to understand the user and as a result the platforms to build upon. Hence it is a personal tool for capturing important information to share and make sense of it, I’ve figured that it should be a mobile app. I am an owner of the iPhone, therefore the platform is iOS.

Hello world

Apple did a great job with providing step-by-step tutorials for new starters, I really like the approach with having explanation steps on the left side of the web-page and the code with a previews on the right side, so that when you scroll further down the code is being updating dynamically (sometimes in-place), this is awesome!

SwiftUI_Tutorials_Handling_User_Input
(from SwiftUI Tutorials - Handling User Input)

It really helps to jump right into the application development and to grasp the basics, so after couple of evenings you would have a rough idea about how UI bits are being put together.

From now on, you are on your own

Okay, let’s add authentication and allow data sharing in-between main application and its extension, should be simple, right? OMG I didn’t have a slightest idea about how hard it could be for the beginner…

Authentication

Authentication allows to prove user’s identity and separate content ownership per user. You can build and maintain your own authentication/identity stack, with a proper security, password/user store and etc or you can just rely on the existing solutions. All the big players today have Identity-as-a-Service offerings.

In this article I’ll be focusing on the Google’s identity hence the experience from the previous project. Another important detail worth mentioning is that I would like to maintain a reasonable amount of the control over the authentication process by proxy-ing all calls through the Capcha’s micro-service, which will also serve as a Back-End for the mobile app.

There is a dedicated class for authenticating a user through a Web Service called ASWebAuthenticationSession, most of the documentation around it is pretty clear, but there is a part about custom scheme for the callback redirect address after successful authentication:

// Use the URL and callback scheme specified by the authorization provider.
guard let authURL = URL(string: "https://example.com/auth") else { return }
let scheme = "exampleauth"

// Initialize the session.
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: scheme)
{ callbackURL, error in
    // Handle the callback.
}

and then inside the callback itself:

// The callback URL format depends on the provider. For this example:
//   exampleauth://auth?token=1234
let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems

so in other words, it is not the http or https before the colon and double slash (://) inside the redirect URL, but something that you’ll need to come up with, some unique keyword for your own scheme, for example in my case it is capchaapp://zoomio.org/auth. It is important to have this redirect URL for the client (mobile app) and the web-service (API back-end) so that the ASWebAuthenticationSession will catch once callback will be initiated and invoke provided completionHandler argument to its constructor.

Another very important detail, which is not mentioned in the Apple’s docs, is the fact that ASWebAuthenticationSession doesn’t store any session cookies provided on the successful authentication, which is opposite to what Web browsers do. IMHO it is a bit crazy, but you’d need to rely on the query arguments in the callback redirect URL in order to provide authentication token back to your mobile app, in my case it looks like this - capchaapp://zoomio.org/auth?token=123TokenXyz.

HTTP client

In short, URLSession inside the Swift’s STD lib is a bit low-level and it is based on the callbacks, which is sometimes quite a PITA, it bubbles up callback-oriented coding style from bottom to the top. For example, here is how usual HTTP call looks like:

// First define an HTTP call task.
let task = session.dataTask(with: request) { data, response, error in
    // Handle error ...
    // Then parse incoming data ...
}

// And this will execute a call.
task.resume()

// You can also return task to the caller, 
// to allow caller to cancel it.
return task

One way of coping with it is to use semaphores:

var result: Data?
var err: Error?

let semaphore = DispatchSemaphore(value: 0)

let task = session.dataTask(with: request) { data, response, error in
    // Handle HTTP error ...
    result = data
    err = error
}

// And this will execute a call.
task.resume()

semaphore.signal()

_ = semaphore.wait(wallTimeout: .distantFuture)

return result, err

Data share in between main app and extension

So now after authentication is in-place along with the HTTP calls it is time to talk about how to share information in-between the application and its extension. Extensions in iOS are used for various purposes, there are many of them. In my particular use case I needed a way to make HTTP calls from the extension to the API exposed by the Capcha Web Service. Said calls needed to be made on behalf of the user (yup here comes authentication again). But hence user is already authenticated in the main application why to go through the authentication in the extension again? Here comes a short story about the App Groups.

There is a section inside the Apple’s “App Extension Programming Guide” called “Sharing Data with Your Containing App”, the suggestion is to rely on the NSUserDefaults which serves as key value store, so that you can set a value under the certain key in your main app and then retrieve it inside the extension by the same key.

It is worth mentioning that the whole guide is pretty outdated, for example in Swift 5 NSUserDefaults becomes UserDefaults, but the bigger problem here is in configuring the application and extension so that the UserDefaults will actually work for you. The page references “Adding an App to an App Group” which suggests to add following:

<key>com.apple.security.application-groups</key>
<array>
    <string>DG29478A379Q6483R9214.HolstFirstAppSuite</string>
    <string>DG29478A379Q6483R9214.HolstSecondAppSuite</string>
</array>

where values “must consist of your development team ID, followed by a period, followed by an arbitrary name chosen by your development team”. This is it… it doesn’t tell where it needs to be added and how.

As a first attempt I’ve ended up creating a key com.apple.security.application-groups of the array type inside the both Info.plist files for app and extension, and providing values as said in the docs, oh yeah, btw you’d need to have the Apple’s developer ID ready and you if you don’t you would have to pay 99 USD for it (annually). But it didn’t work.

Thanks to this outdated article “Making a Share Extension that accepts text and URLs in combination with CoreData Swift 3” I’ve realised that “App Groups” can be created inside the Xcode’s UI, though the examples in the article aren’t relevant anymore, after some trial and error I’ve found that it is done via the “+” sign in the right side of the top navigation bar.

Capcha XCode Project

Clicking on it brings up the “Capabilities” where you can select “App Groups” and add your group into both targets app and extension (e.g. group.com.example.Awesomeness), so that it can be referred when instantiating UserDefaults like this:

let defaults = UserDefaults(suiteName: "group.com.example.Awesomeness")

Once it is done, congrats you are able to securely share data in-between the main application and its extension.

And finally I was able to start working on the main application’s logic, oh boy it’s been a journey…

Summary

Swift is pretty expressive programming language, it has a lot of ways to do one thing and there might be a lot going on in a single line. I’m not a huge fan of such things.

There are good parts, such as nil values handling with variableName? and variableName!, type aliases and extensions are good too.

I would definitely provide a better default abstraction for URLSession in standard library and in fact people already do it all over the place with the tons of packages for HTTP client (Alamofire for example).

It looks like everything changes quite fast in the world of Swift and Xcode and documentation (apart from the UI tutorials) definitely needs to keep up with it, otherwise people might struggle.

This is it for today, I hope that it might help someone else that might be going through the similar process right now.