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,
- Available IAPs: we need some sort of service to fetch all the IAPs so the app knows them
- Purchase: logic to make payments, purchase or restore purchases on a new device
- 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))"
}
}
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.
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.
[appbox appstore 1020072048]
[appbox appstore 1367294518]
[appbox googleplay com.mydaytodo.android.numbersgame]
[appbox appstore 1468826652]
[appbox appstore 1470834613]
[appbox googleplay com.mydaytodo.simplenotes]
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”