Easy Way to Implement Demo Mode in iOS Apps

  —  
 read

Ever thought about adding demo mode to your application? If I were guessing, I'd say most of you probably haven't. It's not a bad thing, it just means you haven't yet encountered a problem or use case requiring you to build one. However, there are a lot of scenarios when the demo mode comes in handy.

In this article, I'll show you that implementing a demo mode doesn't require a lot of time, all the while keeping your existing logic pretty much intact. While we're at it, the solution I'm going to show you will also make your development life easier in cases where the API is still work in progress, or just flat out refusing to cooperate.

Demo mode use cases

One example that comes to mind would be in AppStore reviews for apps which require a login but can't really provide a test user. Think of all the banking apps that require users to be legal entities in the app's origination country.

Other use cases could be apps that would like to show off their premium features without providing a trial period, or maybe those with sensitive data management that would like to familiarize their users with the application's feature flow before throwing them into the "lion's den".

My team has encountered similar cases in the projects we've worked on, so we had to come up with a scalable and maintainable way of making it work.

Implementing demo mode inside an app

When starting to think about implementing demo mode inside an app, your first thought might be to add an additional parameter to your API call site specifying whether the call should be executed or mocked. 

This would work, but it would also require a lot of "copypasta", and every new API call you implement should also have the same logic. Since we're aiming for scalability and maintainability, this approach isn't the way to go. As your app grows, it would introduce a lot of switch-cases and logic duplication and frankly, it would get tiring quickly.

That is why we've decided to make something better  -  a demo manager.

You're probably thinking "Another manager? But I've already got a ton of them and I've read several blog posts telling me that they're bad".

This could be a common response to my proposal since developers like to introduce managers everywhere, even in places where they don't really make much sense. But bear with me for a minute, this is a use case where a manager fits like a glove.

The hidden gems

Our demo manager appears quite basic when you look at it from the outside. After all, it only has a single state parameter isEnabled telling us whether our API calls are being actively mocked or not. However, its "hidden" responsibilities are where the money's at, so let's take a look at them.

The first responsibility is hooking onto every API call your application makes, while the second one builds upon the first one by filtering those API calls in the following way:

  • if it should be mocked -> read the response from a stored file and forward it as a response to your business logic,
  • if it shouldn't be mocked -> make an API call to the server (same as before).

For API call mocking, we're going to use OHTTPStubs, a library designed to stub network requests by using method swizzling. It works with NSURLConnection, NSURLSession, AFNetworking, Alamofire or any networking framework that uses Cocoa's URL Loading System, so it's perfect for our use case.

Of course, if you don't like adding pods to your projects, you could implement your own hooking logic, but that isn't the point of this tutorial so I'll leave that to the brave.

Getting down to the code

Now that we've covered the what and why, let's move on to the how. We'll start with the top-down approach, building our demo manager in several steps.

import UIKit
import OHHTTPStubs

class DemoManager {

    static var instance = DemoManager()

    var isEnabled: Bool {
        get { return !OHHTTPStubs.allStubs().isEmpty }
        set (newValue) {
            switch newValue {
            case true: setupStubResponses()
            case false: OHHTTPStubs.removeAllStubs()
            }
        }
    }

    private init() {}

}

As you can see, our demo manager is a singleton, with one variable for switching the mocking on/off. It might look simple, but this single point of entry provides us with great flexibility . If you want to mock everything in the application, all you have to do is just set the isEnabled parameter to true when the application starts and you're golden. 

However, if you want a bit more specificity, you have free reign to do so. For example, your API call mocking can start only when you get to a screen which still doesn't have an API implementation ready on the backend side, and can be turned off when you navigate off this screen.

private extension DemoManager {

    func setupStubResponses() {
        stubSomething()
        stubUpdatingSomething()
        stubSomethingWithPagination()
    }

    func stubSomething() {
        stubResponse(
            containing: "/apiPath/somethingMockedPath",
            statusCode: 200,
            from: "something_mocked"
        )
    }

    func stubUpdatingSomething() {
        stubResponse(
            containing: "/apiPath/somethingToUpdate",
            method: .patch,
            statusCode: 200
        )
    }

    func stubSomethingWithPagination() {
        stubResponse(
            containing: "/apiPath/somethingPaginated",
            queryParamPart: "cursor=0",
            statusCode: 200,
            from: "something_paginated_first_page"
        )

        stubResponse(
            containing: "/apiPath/somethingPaginated",
            queryParamPart: "cursor=1",
            statusCode: 200,
            from: "something_paginated_second_page"
        )
    }

}

Here we can see the implementation of our setupStubResponses() method.

It stubs different API calls with responses from JSON files - something_mocked, something_paginated_first_page and something_paginated_second_page, while the second call is mocked with just a 200 code telling us that the update was successful. 

Each call is mocked using the stubResponse() method with individual filtering parameters. To understand that there isn't some black box magic involved, let's take a look at what's going on under the hood here.

private extension DemoManager {

     /**
     Used for creating stubs for API mocking.
     For distinguishing which stub should be used for which API call you should use the following parameters:
        - `urlPart`
        - `queryParamPart`
        - `requiredHeaderValues`
        - `method`
     Successfuly mocked response is defined by:
        - `statusCode`
        - `headers`
        - `fileName`
     - Parameter urlPart: Specifies URL path that should be mocked
     - Parameter queryParamPart: Used for mocking calls with specific query parameters
     - Parameter requiredHeaderValues: Used for mocking calls with specific header values, i.e. can be used for differentiating multiple API calls only by the values in their headers
     - Parameter body: Used for mocking calls with specific parameters in their body
     - Parameter method: The HTTP request method
     - Parameter statusCode: Mocked response status code (can be used for mocking errors)
     - Parameter headers: Header values sent in mocked response
     - Parameter fileName: JSON file from which response data should be read
     */
    func stubResponse(
        containing urlPart: String,
        queryParamPart: String? = nil,
        requiredHeaderValues: String?...,
        bodyPart: [String]? = nil,
        method: HTTPMethod? = .get,
        statusCode: Int32 = 200,
        headers: [AnyHashable : Any]? = ["Content-Type": "application/json"],
        from fileName: String? = nil
        ) {

        stub(
            condition: { (request: URLRequest) -> Bool in
                //Method
                if let method = method?.rawValue.uppercased(), 
                        request.httpMethod?.uppercased() != method 
                        { return false }

                //URL, query
                if !(request.url?.absoluteString.contains(urlPart) ?? false) { return false }
                if let part = queryParamPart, 
                        !(request.url?.absoluteString.contains(part) ?? false) 
                        { return false }

                //Body
                if let parts = bodyPart,
                    let data = request.httpBody,
                    let body = String(data: data, encoding: .utf8)
                {
                    let bodyPartsPresent = parts
                        .map { body.contains($0) }
                        .reduce(true) { $0 && $1 }

                    if !bodyPartsPresent { return false }
                }

                //Headers
                let headers = request.allHTTPHeaderFields ?? [:]
                let headersOk = requiredHeaderValues
                    .compactMap { $0 }
                    .map { (headerValue) in headers.contains { $0.value == headerValue } }
                    .reduce(true) { $0 && $1 }

                if !headersOk { return false }

                return true
            },
            response: { _ -> OHHTTPStubsResponse in
                guard let _fileName = fileName else {
                    return OHHTTPStubsResponse(data: Data(), statusCode: statusCode, headers: nil)
                }

                let path = Bundle.main.path(forResource: _fileName, ofType: "json") ?? ""

                return fixture(filePath: path, status: statusCode, headers: headers)
            }
        )
    }

}

This method is basically what you've been waiting for. It calls OHTTPStubs's stub() method which does the API call mocking using a condition closure and a response closure. Each of them plays a major part in mocking your calls, so let's see what they're all about.

Condition closure

You can think of the condition closure as a robust version of filtering. If it returns true, the response closure will be executed, the API call will be mocked and it will never reach the real server. Otherwise, if the condition closure results in false, the API call will be executed, just like before.

In our implementation, we go through several major pillars of every API call in order to determine whether we should mock it:

  • API call URL – the only required parameter in our stubResponse() implementation, and it should match the one in the call itself. We're using contains() to provide more flexibility here, but you can implement it using letter-to-letter precision or regexes if you wish to.
  • queryParamPart – an optional parameter that defines whether the specific query parameter should appear in the API call in order to be mocked.
  • requiredHeaderValues – optional array of header values that are required to be in the call, otherwise it shouldn't be mocked.
  • bodyPart  –  optional array of request body values, perfect for cases where your calls are only differentiated by their body parameters.
  • method – specifies the request method, GET is set by default since it's the most used one.

If all our checks are successful, our API call is filtered as "good for mocking" and the response closure is then finally called to mock the API call. Otherwise, if the condition closure fails at any of the the aforementioned conditions, the call is forwarded to the server – business as usual.

Response closure

The response closure is used to create the mocked response, and it uses three parameters passed into the stubResponse()statusCode, method and fileName. We load our mocked response from the file in our bundle using the specified file name, set the headers to the provided values and create the mocked response using the status code. 

This allows us to test our implementation with several responses rather quickly. Want to know how your UI behaves if the array of products you're expecting is empty? What's going to happen if the API starts coughing up and returns 4xx or 5xx?

Using the flexibility of call mocking shown here, you can run through all your test scenarios in a breeze, leaving you more time to focus on more important parts of your app.

One more thing

One thing not mentioned above is the usefulness of OHTTPStubs for writing unit tests (which all of us are doing, right?)

It comes as a real time-saver when testing your business logic that depends on API call results, and all you need to make it happen is the stubResponse() implementation inside your testing environment.

You can do that in no time with the knowledge you've accrued. Doing that will not only help you with testing, but also make your apps more stable, resulting in happier customers and better app ratings. 

Wrapping it up

You've just witnessed that the demo manager really is a jack of all trades. It has a wide variety of uses, from showing everything your application has to offer without firing a single "real" API call, to making development faster while waiting for the API team to do their part by mocking the responses based on the agreed specification.

The best part is that it's not difficult to implement, so give it a go and reap the benefits of the demo mode in your project.

Until next time, happy coding!

Yo listen up, here's the story about a little guy that lives in a blue world... and wears an orange outfit. Courtesy of designer Marijana Šimag, da ba dee da ba daa.

Greetings from our lovely team!
1/4
Achievement unlocked
Resize Master