Structuring Your iOS App for Split Testing

The goal of this article is to provide a simple way of structuring and organizing your application in order to achieve clean and scalable iOS code when using split testing.

Practical tips and examples are provided in order for this article to stay as a guideline for real-world app scenarios.

General Problems

Using split testing (also known as an A/B testing), we have endless possibilities for what to test. But in general, we can group the changes needed regarding a split test in the following order:

  1. Content Changes: Changing only specific parts in a given view or adding/removing specific content based on the given test.
  2. Design Changes: Testing how changes like colors, typography, or layout can affect our users’ behaviors.
  3. Behavioral Changes: Changing the behavior of a button action or a screen presentation depending on the split group

The problem is that in all of these categories, huge code duplications can arise.

We need a way to create an easily maintainable code structure for the tests—this is because we’ll have continuous requirements for adding new tests and dropping or modifying old ones. Therefore, we should design with scalability in mind.

Creating a Split Testing Manager

We’ll try to create a generic reusable solution that we could use for our change categories described above.

We’ll first create a protocol that defines the rules to which an object has to conform in order to represent a split test:

protocol SplitTestProtocol {
    associatedtype ValueType: Any
    static var identifier: String {get}
    var value: ValueType {get}
    init(group: String)
}

The value field will represent a generic value that’ll be implemented by a concrete Split Testing Object. It will correspond to a color, font, or any attribute that we’re testing for our target goal.

Our identifier will simply be a unique identifier for our test.

Our group will represent which value of the test is currently being tested. It can be a and b or red and green. This all depends on the naming of the values that are decided for a given test.

We’ll also create a manager that’s responsible for getting the value of our split test based on the group stored in our database related to the testing identifier:

class SplitTestingManager {
    static func getSplitValue<Value: SplitTestProtocol>(for split: Value.Type) -> Value.ValueType {
        let group = UserDefaults.standard.value(forKey: split.self.identifier) as! String
        return Value(group: group).value
    }
}

Content Changes

Suppose that we’re working on a reading application and we decide to give users a free e-book.

Our marketing team has decided to create a split test for giving the book by first asking the users to either:

Share our app on social media

or

Subscribe to our newsletter

Both of these two cases use the same view controller, but a part of the design changes based on the given case. In our view controller, we’ll create a content view area where we’ll be adding different content views.

In this case, we need to create two different views: one for social share and one for the newsletter and add them respectively to our view controller’s content defined area.

Let’s first create an object that holds our view controller style and pass it in the view controller’s initializer:

struct PromotionViewControllerStyle {
    let contentView: String
}
init(style: PromotionViewControllerStyle) {
    self.style = style
    super.init(nibName: nil, bundle: nil)
}

Basically, the style object currently holds the xib name for the content view of our PromotionViewController.

We can create our Test Object that conforms to our SplitTestProtocol:

class EBookPromotionSplitTest: SplitTestProtocol {
    typealias ValueType = PromotionViewControllerStyle
    static var identifier: String = "ebookPromotionTest"
    var value: PromotionViewControllerStyle
    
    
    required init(group: String) {
        self.value =
            group == "social" ?
                PromotionViewControllerStyle.init(contentView: "(TwitterView.self)")
            :   PromotionViewControllerStyle.init(contentView: "(NewsLetterView.self)")
    }
}

We can now easily present our view controller with either newsletter or social share content based on our split test:

@IBAction func presentNextVc(_ sender: UIButton) {
    let style = SplitTestManager.getSplitValue(for: EBookPromotionSplitTest.self)
    let vc = PromotionViewController(style: style)
    self.present(vc, animated: true)
}
func addContentView() {
    let nib = UINib(nibName: style.contentView, bundle: nil)
    let view = nib.instantiate(withOwner: nil, options: nil)[0] as! UIView
    contentView.addSubview(view)
    view.bindFrameToSuperviewBounds()
}

Design Changes

Usually, in e-commerce-based applications, it’s popular to change the call to action button design, i.e an add to cart button or purchase button to make it more appealing to the users so it gets more clicks.

We can always use any object of our need for our splitting manager. In this case, suppose we need an object that holds our purchase button color as a value:

class PurchaseButtonColorSplitTest: SplitTestProtocol {
    typealias ValueType = UIColor
    
    static var identifier: String = "purchase_button_color"
    var value: ValueType
    
    required init(group: String) {
        if group == "a" {
            self.value = UIColor.red
        } else {
            self.value = UIColor.green
        }
    }
}

and we can use it simply from our view as follows:

let color = SplitTestManager.getSplitValue(for: PurchaseButtonColorSplitTest.self)
purchaseButton.backgroundColor = color

Similarly, any other attributes can be tested, like fonts and margins or any other attribute that needs to change based on our testing.

Behavioral changes

Let’s suppose that we decide to split users into two groups regarding subscriptions in our app:

We want to either

Present a discount dialog when opening the IAP view

or

Present a default view without any dialog

We’ll be using the Strategy Pattern for this example to handle the discount presentation base in our strategy.

Since our SplitTestProtocol contains a generic value, we can create the split testing object that will hold the strategy as its value:

class DiscountSplitTest: SplitTestProtocol {
    typealias ValueType = DisountStrategy
    static var identifier: String = "iap_discount_type"
    var value: DisountStrategy
    

    required init(group: String) {
        if group == "offer" {
            value = DefaultDiscountStrategy()
        }
        value = NoDiscountStrategy()
    }
}

We can then initialize and present our view controller depending on the specific strategy:

init(discountStrategy: DisountStrategy) {
    self.discountStrategy = discountStrategy
    super.init(nibName: nil, bundle: nil)
}
func presentDiscoutViewController() {
    let strategy = SplitTestManager.getSplitValue(for: DiscountSplitTest.self)
    let viewController = DiscountViewController(discountStrategy: strategy)
    self.present(viewController, animated: true)
}

We now can easily pass our discount responsibility to the DiscountStrategy object and scale it based on our needs without having to change anything in our view controller’s code:

protocol DisountStrategy {
    func presentDiscountMessage()
}

struct NoDiscountStrategy: DisountStrategy {
    //Provides handling for non discount case
}

struct DefaultDiscountStrategy: DisountStrategy {
    //Provides handling for discount case
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(true)
    discountStrategy.presentDiscountMessage()
}

General Tips

While doing split testing, it’s important to always be careful regarding the following points:

  1. Always using caching for the test values in order for the app to stay consistent for the user.
  2. Clean up the code after one specific test is finished. Remove views, fonts, images, and whatever resources you added in the project for the split test.
  3. Make sure that if something goes wrong you have control over and can disable the A/B test.

In Conclusion

Split testing (also known as A/B testing) is a powerful and effective tool for our apps, but it can easily create messy code if we’re not careful with our code design.

In this article, we’ve created a generic solution that can manage our split testing logic. We gave some real-world app examples and practical tips in order for this article to work as a guide when implementing split testing for your iOS applications.

If you enjoyed this article make sure to clap to show your support.

Follow me to view many more free articles that can take your iOS skills to the next level.

If you have any questions or comments feel free to leave a note here or email me at [email protected].

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *