Lambda Labs Week IV

7 minute read

Individual Accomplishments

This week I did a lot to clean up/polish the UI and UX. I made various improvements based on user feedback. I changed the result of the slider to be to show the user their options instead of just cancelling. I added a logout button. I added arrows to show if there is more data you can swipe to on the stats view. I fixed several bugs with the alarm time picker. I fixed the bug where you had to tap the login button twice to get it to do anything. I also reworked the login flow so that it will only show you the login screen if it needs input from you to authenticate, otherwise it will just do it in the background. I also started searching for sounds I could use for the alarm, and thinking through how I would build an interface for picking which one you wanted and it occurred to me that I could just integrate with MusicKit and let the user pick whatever song they want from their own library. So I started working to get that integrated.

Sleep Tracking Screen Updated Stats Screen

Detailed Analysis

I think the most interesting thing I worked on this week was the reworks that I made to the login flow. Before, I had the login view as the initial view controller, I would attempt to sign in silently in the background, and if that was successful, it would segue to the main tab bar controller of the app. If it wasn’t successful, it would wait for input from the user. “Signing in silently” involved making a call to sign in to Google in the background, and if that was successful, making a call to our back end to authenticate the user and get a token. So two network calls had to be made and successfully completed before moving past the login screen.

The problem with this is that it would show the login screen to the user every time they launched the app, even if they didn’t need to login. So I made a few changes to get around this. The first thing I did was to save the user’s information when they successfully log in, the non-sensitive stuff in UserDefaults and the sensitive stuff in the keychain. I also made an initializer that would attempt to make a User from that information and a convenience method for wiping that information out when a user logs out. Then, in the AppDelegate, in app DidFinishLaunching, I try to make a User from the saved data and if it is successful I present the main interface directly, so the user never sees the login screen. If it isn’t successful, I just present the login screen.

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
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    application.isIdleTimerDisabled = true

    // Other stuff...

    if User.loadUser() {
        presentLoggedInVC(animated: false)
    } else {
        presentLoginVC(animated: false)
    }

    return true
}

private func presentLoggedInVC(animated: Bool = true) {
    if User.current != nil {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let tabBarController = storyboard.instantiateViewController(withIdentifier: .tabBarController)
        setRootViewController(tabBarController, animated: animated)
    }
}

private func presentLoginVC(animated: Bool = true) {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let loginViewController = storyboard.instantiateViewController(withIdentifier: .loginViewController)
    setRootViewController(loginViewController, animated: animated)
}

func setRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard animated, let window = self.window else {
        self.window?.rootViewController = vc
        self.window?.makeKeyAndVisible()
        return
    }

    DispatchQueue.main.async {
        window.rootViewController = vc
        window.makeKeyAndVisible()
        UIView.transition(with: window, duration: 0.3, options: .transitionFlipFromRight, animations: nil, completion: nil)
    }
}

This already felt a lot better from the user’s perspective, but there was a problem in that the token saved in the keychain might not be valid anymore – so the user would be able to use the app like normal, but when they went to post their sleep data it wouldn’t go through because of the invalid token. That wouldn’t be good! So when the user is made from saved information, it also fires off a network request to our back end to see if the token is still valid. If it isn’t, the app attempts to sign in silently in the background. If it is successful, great. It just updates the token. If it isn’t successful, it interrupts the user with the login screen so they don’t inadvertently keep using it with invalid credentials. Not the most elegant solution in the world, but it is better than it was.

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
static func updateUserData() {
        guard let currentUser = User.current else { return }
        let requestURL = URL(string: .baseURLString)!.appendingPathComponent("users").appendingPathComponent("\(currentUser.sleepstaID)")

        var request = URLRequest(url: requestURL)
        request.addValue("application/json", forHTTPHeaderField: "Content-type")
        request.addValue(currentUser.sleepstaToken, forHTTPHeaderField: "Authorization")

        URLSession.shared.dataTask(with: request) { (data, _, error) in

          guard let signIn = GIDSignIn.sharedInstance() else { fatalError("No Google Sign In instance, something has gone wrong!") }

            // If an error comes back, it means the token is expired, so log back in with Google
            if let error = error {
                print("Error GETting user info: \(error.localizedDescription)")
                if let bool = signIn.hasAuthInKeychain(), bool {
                  DispatchQueue.main.async {
                      signIn.signInSilently()
                  }
                } else {
                    signIn.disconnect()
                }
                return
            }

            guard let data = data else {
                return
            }

            do {
                let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
                if jsonObject is [String: Any] {
                    // There's a problem, the token isn't valid.
                    if signIn.hasAuthInKeychain() {
                        DispatchQueue.main.async {
                            signIn.signInSilently()
                        }
                    } else {
                        signIn.disconnect()
                    }
                } else {
                    // TODO: Update any user info that has changed
                }
            } catch {
                NSLog("Error decoding data: \(error.localizedDescription)")
            }

        }.resume()

Reflections

I don’t know about the rest of my team, but I have a hard time getting the app to work well if it doesn’t also look halfway decent. So I probably spent more time than I should have messing with UI/UX in the previous weeks as I developed the functionality of the app. That means that what I’ve been working on hasn’t really shifted all that much this week, other than that I’ve been thinking more about edge cases this week than ‘the golden path’. I always do my best to think through how to make it obvious to do the thing that you’re supposed to do. I always do my best to make things feel balanced and look right. I always try to see the app as someone would for the first time. So there hasn’t been too much to change. It has mostly been polishing small things.