Over the years, I have only had casual contract based help. The help I have had was in the form of contractors and interns, who by definition aren’t long lasting employees. My Day To-Do is my startup and the stakes for it are much higher for me then them. Therefore to minimise the risks associated with their work, I established an architecture for My Day To-Do through which the contractors and interns work in a “sort of” sandboxed environment.
Also, I prefer not writing the same code twice, so if I write something it needs to be reusable. In this post, I will talk about the sort of architecture I setup with some code samples. I will also shed some light on how I isolate all the code by business functionality.

Background

My background is that of a software engineer writing Java code and as such, I have learned how to code with certain best practices. Also, I just love writing modular code. Also a lot of the coding “best practices” can apply to any programming language. In the last 4 years of coding Swift, I have naturally applied some of these to my iOS Swift code too.

When it comes to work I generally anticipate the worst case scenario. I suppose I always had this trait but it was seriously amplified at my last job working for someone as a full stack developer. Especially given the client project that they put me on i.e. a Java backend developer working with the Spring Framework. More than anything, I had to make sure, the code I wrote doesn’t make us look bad to the engineers at the client office. Hence the code had to be stateless, loosely coupled, modularised etc. That just strengthened my approach to writing code with the aforementioned traits. This approach stayed with me as I developed My Day To-Do and more so when an intern or contractor came aboard. Part of the reason I applied this approach when others came aboard was to minimise the effects of the code. Aspects of their work such as, quality of their code, how useful it is etc were always questionable.

Plan

My approach to writing Swift code to the My Day To-Do iOS app would be to make it component based. I am not good with words, I often don’t know the right name for the design pattern or buzz word for it. I simply know best practices and code a certain way to achieve maintainability.

Understand the problem

Why do we write code? We write code to solve a problem so it’s very important to first understand the problem we are solving. Once you do, you will see that all the code related to solving that problem can be isolated.

Example: A simple business case

Enough abstract/conceptual talk, let’s explore this with the code we need to solve a simple business case. Say, we have an app (iOS) and the way it generates revenue is via in-app purchases (IAP). Now let’s think about what our app needs to know for this,

  1. Available IAPs: we need some sort of service to fetch all the IAPs so the app knows them
  2. Purchase: logic to make payments, purchase or restore purchases on a new device
  3. View integration: lastly, it must integrate with our ViewController so the user can see it

At this point, we have an idea of the functionality we require. At a high level, we could have a folder called “IAP” or “Purchase” or “AppStore” or whatever you want to call it. In that folder we could have a class that fetches IAPs from the app store, handles payments etc. Let’s look at the code for that,

Code samples: IAPService

//  Created by Bhuman Soni on 8/9/19.
//  Copyright © 2019 Bhuman Soni. All rights reserved.
import Foundation
import StoreKit

class IAPService: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {

    static let shared = IAPService()

    static let IAP_PRODUCTS: Set<String> = [
        "com.mydaytodo.modular.iap.1",
        "com.mydaytodo.modular.iap.2",
    ]

    var productMapping = [String:SKProduct]()

    var isAuthorizedForPayments: Bool {
        return SKPaymentQueue.canMakePayments()
    }
    var purchaseBeingRestored = false
    public var iapTransDelegate: IAPTransDelegate?
    public var iapProdDelegate: IAPProductDelegate?

    var currentIapList = [MDTIapProduct]()

    func loadProducts() {
        let request = SKProductsRequest(productIdentifiers: IAPService.IAP_PRODUCTS)
        request.delegate = self
        request.start()
    }
    //MARK: product request delegate methods
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        currentIapList = [MDTIapProduct]()
        for product in response.products {
            productMapping[product.productIdentifier] = product
            let mdtProduct = MDTIapProduct()
            mdtProduct.desc = product.localizedDescription
            mdtProduct.price = product.price
            mdtProduct.name = product.localizedTitle
            mdtProduct.priceLocale = product.priceLocale
            mdtProduct.identifier = product.productIdentifier
            currentIapList.append(mdtProduct)
        }
        iapProdDelegate?.iapList = currentIapList
        iapProdDelegate?.iapLoaded()
    }
    func restoreIAPPurchase() {
        purchaseBeingRestored = true
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
    func purchaseProduct(identifier: String) {
        if let prod = productMapping[identifier] {
            let payment = SKPayment(product: prod)
            SKPaymentQueue.default().add(payment)
        }
    }
    func completeTransaction(transaction: SKPaymentTransaction, productIdentifier:String) {
        SKPaymentQueue.default().finishTransaction(transaction)
        if purchaseBeingRestored {
            iapTransDelegate?.purchasesRestored(identifier: transaction.payment.productIdentifier)
        } else {
            iapTransDelegate?.purchaseComplete(identifier: transaction.payment.productIdentifier)
        }
        //UserDefaultsHelper.shared.toggleIAPPurchaseState(productIdentifier: productIdentifier, state: true)
    }
    //MARK: Payment transactions delegate
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for trans in transactions {
            switch trans.transactionState {
            case .purchased:
                completeTransaction(transaction: trans, productIdentifier: trans.payment.productIdentifier)
                break
            case .failed:
                SKPaymentQueue.default().finishTransaction(trans)
                //let prodId = trans.payment.productIdentifier
            //AnalyticsHelper.logIAPFail(type: IAP_FAIL.IAP_BUY, identifier: prodId)
            case .restored:
                completeTransaction(transaction: trans, productIdentifier: trans.payment.productIdentifier)
                break
            default:
                print("product not purchased")
                break
            }
        }
    }
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        for transaction in queue.transactions {
            if transaction.transactionState == .restored {
                completeTransaction(transaction: transaction, productIdentifier: transaction.payment.productIdentifier)
            } else {
                //AnalyticsHelper.logIAPFail(type: IAP_FAIL.IAP_RESTORE, identifier: transaction.payment.productIdentifier)
            }
        }
    }
}

To let our ViewController know about the available products, the purchases etc we use protocols. There are some custom protocols that handle this for us, we have the IAPProductDelegate and IAPTransDelegate.

//transaction
protocol IAPTransDelegate {
    func purchaseComplete(identifier: String)
    func purchasesRestored(identifier: String)
}
protocol IAPProductDelegate {
    var iapList: [MDTIapProduct] {get set}
    func iapLoaded()
}

Hence, all the ViewController needs to do is conform (or implement?) these protocols and it can get the information it needs.

//  Created by Bhuman Soni on 8/9/19.
//  Copyright © 2019 Bhuman Soni. All rights reserved.

import UIKit

class IAPViewController: UIViewController, IAPProductDelegate, IAPTransDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    //code is not stateless after this
    //but I will address this in another post
    var iapList: [MDTIapProduct] = []

    // MARK: IAP protocol methods
    func iapLoaded() {
        print("IAP's have been loaded")
        for iap in iapList {
            print(iap.name)
        }
    }
    func purchaseComplete(identifier: String) {
        print("Purchase complete!")
    }

    func purchasesRestored(identifier: String) {
        print("Purchases restored")
    }
}

Ohh and the MDTIapProduct is a custom data structure, that looks like this,

struct MDTIapProduct {
    var identifier = ""
    var price: NSNumber?
    var name = ""
    var desc: String?
    var priceLocale: Locale!
    var regularAppStorePrice: String?
    //intro or offer price, when we have some
    func summary() -> String {
        return "\(name) \n\(desc ?? "") \nPrice:\(priceLocale.currencySymbol!)\(String(describing: price?.doubleValue))"
        }
}

Like the blog? Subscribe for updates

Why have a custom data structure?

This way, the ViewController class in our iOS app can show IAPs, handle payments, purchases etc without knowing about StoreKit. All this works without a single import StoreKit statement in the ViewController. Hence, all the logic to handle the IAP is completely decoupled from our ViewController, isn’t that awesome? It sure is, how? let’s look at a few scenarios,

Scenario 1: Apple changes

Worst case scenario, tomorrow Apple announces that it’s getting rid of StoreKit and won’t support it. Sure, why not? it’s totally fine with us. All we need to do is just change the logic to fetch, purchase and restore  IAPs in our AppStoreService, the rest of our app code will work just fine.

Scenario 2: Contractor or intern

If we have a contractor or an intern working on adding new IAPs or so, ideally all his changes would be in the classes in the AppStore folder.

Scenario 3: New products with IAPs

A few weeks from now, we are building a new app and we have to add IAPs for revenue. Sure it’s easy. All we need to do is take the code in the AppStore folder and change the product ids for the available IAPs and implement the protocols. That should work.

Drawbacks of this approach?

There are a few minor drawback that I have with this approach. At this stage, I am the only developer at My Day To-Do and given I code all the functionality in such a way, I often don’t remember how to do things. As long as Apple doesn’t change anything at a fundamental level, I can keep adding functionality to my new apps without ever reading about them.

Like the blog? Subscribe for updates

Conclusion

If you want to see all this in an Xcode project, you can checkout this Github repo. I have talked about that repo in my post on “Decouple iOS code and get the user Location without CoreLocation”. I hope you I have made you see the benefits of writing modularised iOS code in this post. Here we applied this to AppStore purchases and StoreKit, but we can apply this to anything. We could adopt a similar approach for CloudKit, Forms, Firebase, AdMob etc etc. Just get onboard this.

As usual, if you find any of my posts useful, support us by  buying or even trying one of our products and leave us a review on the app store.

Snap! I was there
Snap! I was there
Developer: Bhuman Soni
Price: $3.99
Simple 'N' Easy Task List
Simple 'N' Easy Task List
Captain's Personal Log
Captain's Personal Log
Developer: Bhuman Soni
Price: $4.99
My Simple Notes
My Simple Notes
Developer: Bhuman Soni
Price: Free

1 Comment

Bhuman Soni · September 9, 2019 at 12:08 am

Someone on social media asked me,
“my experience working with customers is that one: very often they change the initial specifications, & there are a lot of additional changes. How do you deal with this?” My reply was

“Sure requirements change but even with the changing requirements you can identify what business category those requirements fall into. The goal is to write code that’s loosely coupled. e.g. if the requirements change in how they deal with the IAP, great, then we are only making changes to the IAP part of the code and not the ViewController. If the UI changes then that’s fine too, we are only changing the ViewController code and not the AppStore code”

Leave a Reply

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