iOS on Zen

NSDevelopment blog

Using user session in iOS

Intro

Hello everyone. Topic that I’d like to discuss today is quite simple. What is it about and why do I need to talk about it? Usually when user gets registered / logged in there is no strict way to describe this state. Obviously when the app authorized the user we can retrieve bunch of information about it like: APIClient.sharedClient().token != nil or UserStore.sharedStore().currentUser != nil, or performing a DB query where User.isCurrent == true or all this at the same time. Literally there are lot of runtime attributes indicating about logged in user but nothing that combines it into a single object. Basically User Session is that missing abstraction that gives us a way to definitely describe current user state: logged in or not. I belive most of readers faced with logout case, whe you have stop all running services, probably remove user’s disk data, etc. I saw many places to handle logout cleanup but none of them feels comfortable: App Delegates, UIViewController that handles logout and so on. User Session is something that: - an object that definitely describes current login state - gives us a correct place to both start and stop services - we’ll talk today

Implementation

User Session abstraction neither new topic or a rocket science. It is used in most of the backend services, however it didn’t receive the attention it deserves in iOS-community (IMO). For core implementation we need 3 classes:

  • User session itself
  • User session controller to open, close and restore sessions as well as keep strong reference to current session
  • User session prototype to encapsulate any login data (user, token, etc) before new session is opened

User Session Prototype

Lets start from user session and its prototype. Assume that both sign in / up request returns the following response:

1
2
3
4
5
6
7
8
9
{
    "result": {
        "user": {
            "id": "ee977806d72865",
            "email": "foo@bar.com",
            "name": "Foo Bar"
        }
    }
}

In order to be passed around this JSON it needs to be encapsulated into an object, not a plain Dictionary:

UserSessionPrototype.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct UserSessionPrototype {

    let identifier: String
    let userInfo: [String: AnyObject]

    init(response: [String: AnyObject]) {
        // Dirty parsing to simplify our example
        let result = response["result"] as! [String: AnyObject]
        let user = result["user"] as! [String: AnyObject]

        identifier = user["id"] as! String
        userInfo = user
    }
}

UserSessionPrototype contains just 2 properties:

  • identifier of the user, which will be used to build UserSession.identifier. This is quite useful for preserving of session’s data between logout / login actions
  • userInfo: simple Dictionary to keep all user data for session bootstrap

Prototype not necessarily should be so simple. It may combine any login data that is required for correct session bootstrap such as tokens, session trial limit and so on.

User Session

What about UserSession? Obviously it should support init with prototype, restoration. What else? It needs to be able to bootstrap initial state and place to handle start & stop of some services (timeline background update, etc).

UserSession.swift
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
class UserSession {

    let identifier: String // to uniquely identify session
    private let sessionPrototype: UserSessionPrototype?

    // MARK: - Init

    init(prototype: UserSessionPrototype) {
        identifier = prototype.identifier // any hashing function such as sha1 can be added here
        sessionPrototype = prototype
    }

    init?(restorationIdentifier identifier: String) {
        self.identifier = identifier
        sessionPrototype = nil

        let canRestore = true // check whether session for passed identifier can be successfuly restored or not
        if !canRestore {
            return nil
        }
    }

    // MARK: - Open / Close

    func open() {
        if let prototype = sessionPrototype {
            bootstrapFromPrototype(prototype)
        }

        // point to start any related services
    }

    func close() {
        // point to stop any related services
    }

    // MARK: - Bootstrapping

    private func bootstrapFromPrototype(prototype: UserSessionPrototype) {
        // map user data to a new user, setup db, etc
    }
}

Now lets talk about session restoration: in order to perform correct init you’re free to check anything. In my real-world case I checked whether keychain has a valid token for passed identifier.

What else we need to add to the session? Probably sort of a flag to definitely describe session’s current state. It can be effectively used for further development like auth token refresh, logout and so on:

  • Undefined - session hasn’t been opened yet
  • Opened - default session state
  • Closed - session has been closed
  • Invalid - user session bootstap failed or auth token has been invalidated
UserSession.swift
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
enum State {
    case Undefined, Opened, Closed, Invalid
}

private(set) var state: State = .Undefined

func open() -> Bool {
    precondition(state == .Undefined)

    if let prototype = sessionPrototype where !bootstrapFromPrototype(prototype) {
        state = .Invalid
        return false
    }

    // point to start any related services

    state = .Opened

    return true
}

func close() {
    precondition(state == .Opened || state == .Invalid)
    // point to stop any related services

    state = .Closed
}

// MARK: - Bootstrapping

private func bootstrapFromPrototype(prototype: UserSessionPrototype) -> Bool {
    // map user data to a new user, setup db, etc

    return true // bootstrap completed successfuly
}

User Session Controller

Now it is time to add controller to keep an eye on active session. As was mentioned above main responsibilities are:

  • Open a new session as a result of sign up / in
  • Close session
  • Restore session
  • Keep a strong reference to the opened session
UserSessionController.swift
1
2
3
4
5
6
7
8
9
10
11
class UserSessionController {

    // MARK: - UserSession

    private(set) userSession: UserSession? {
        didSet {
            oldValue?.close()
            userSession?.open()
        }
    }
}

Let’s start from opening of a new session. In this example I’m not going to cover full flow of sign up / in since it is quite straightforward. For now lets assume, that we have APIClient, that talks to the backend. Result of login operation is a UserSessionPrototype that we implemented earlier or NSError in case of network error:

UserSessionController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private let apiClient = AuthorizationAPIClient()

func openSession(#username: String, password: String, completion: (UserSession?, NSError?) -> Void) {
    let requestCompletion: (UserSessionPrototype?, NSError?) = { prototype, error in
        if let prototype = prototype {
            self.userSession = UserSession(prototype: prototype)
            completion(self.userSession, nil)
        } else {
            completion(nil, error) // preferrable to map error to the UserSessionControllet's level
        }
    }

    apiClient.loginWithUsername(username, password: password, completion: requestCompletion)
}

It is generally preferable to return an object that can cancel async operation. Void as a return type of any async operation should be ommited, but to keep our example simple I’m ignoring this rule

Session closing is a oneline function:

UserSessionController.swift
1
2
3
func closeSession() {
    userSession = nil
}

Now it is time for restoration! What do we need for it? Obviously to preserve opened user session’s identifier between apllication launches. Best place to update persistent identifier value is userSession’s didSet. For storage lets go with NSUserDefaults:

UserSessionController.swift
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
// MARK: - Init

let userDefaults: NSUserDefaults
init(userDefaults: NSUserDefaults) {
    self.userDefaults = userDefaults
}

// MARK: - Restoration

private static let userSessionKey = "your.app.bundle.id.userSession"
private var userSessionIdentifier: String? {
    get {
        return userDefaults.objectForKey(UserSessionController.userSessionKey) as? String
    }
    set {
        userDefaults.setObject(newValue, forKey: UserSessionController.userSessionKey)
        userDefaults.synchronize()
    }
}

func restoreUserSession() -> UserSession? {
    assert(userSession == nil, "It is illegal restore session while ")

    if let identifier = userSessionIdentifier, let session = UserSession(restorationIdentifier: identifier) {
        self.userSession = session
        return session
    }

    return nil
}

var canRestoreUserSession: Bool {
    return userSessionIdentifier != nil
}

// MARK: - UserSession

private(set) var userSession: UserSession? {
    didSet {
        oldValue?.close()
        userSession?.open()

        userSessionIdentifier = userSession?.identifier
    }
}

Also we need to add initialization of ‘UserSessionController’ into the root of the app:

AppDelegate.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let userSessionController = UserSessionController(userDefaults: NSUserDefaults.standardUserDefaults())

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        if userSessionController.canRestoreUserSession {
            // lets restore it
        }  else {
            // we need to show login
        }

        return true
    }
}

Extras

Ok it seems that we’re done with basic implementation. Is there anything useful we can add out of the box? User’s data management convenience! By a simple path utility added to the session we can easily distinguish any user’s files physically, so you no longer have to care about possible collisions.

Briefly our Documents and ‘Caches’ path will be dependand on UserSession.identifier:

UserSessionPathBuilder.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserSessionPathBuilder {

    private let identifier: String
    init(userSession: UserSession) {
        identifier = userSession.identifier
    }

    private func pathForDirectory(directory: NSSearchPathDirectory) -> String {
        let directory = NSSearchPathForDirectoriesInDomains(directory, .UserDomainMask, true).first as! String
        return directory.stringByAppendingPathComponent("user_session")
    }

    // ~/Documents/user_session/identifier/
    func documentDirectory() -> String {
        return pathForDirectory(.DocumentDirectory).stringByAppendingPathComponent(identifier)
    }

    // ~/Library/Caches/user_session/identifier/
    func cachesDirectory() -> String {
        return pathForDirectory(.CachesDirectory).stringByAppendingPathComponent(identifier)
    }
}

Adding this to the UserSession:

UserSession.swift
1
2
3
lazy private(set) var pathBuilder: UserSessionPathBuilder = { [unowned self] in
    return UserSessionPathBuilder(userSession: self)
}()

Now when you’re going to setup CoreData stack there is a great utility for “path consulting”. For sure, final desicion of where to store any user’s data is up to the implementation but existence of such path utility pushes developer to do it right ;)

Summary

UserSession is a great %your_app_name% citizen:

  • amazing starting point to stop using sharedManagers and take control over of your services!
  • gives you entry point to perform any neccessary logout cleanup
  • helps to store your data more organized
  • can indicates about invalidated auth tokens by updating state value
  • abstraction that I’d like to know about few years earlier

Check out sample source code on the github!

Special thanks for help to Alex Denisov and Paul Taykalo!

Comments