This document serves as a location to record details on specific patterns, their usage, and their implementations within the app as they currently exist. These patterns are to be considered best practices, and should be adheared to within reason for code development. These patterns are not to be considered infalible, or perfected by any means, and are open to continued adaptations, refinement, and improvement to meet the needs of the app, evolving technologies, or new understandings.
This document is also provided to aid developers and others in understanding how the app works and is to be considered living in nature. As such it should be revisited as the app evolves where and when approriate and developers should make their best efforts to keep it up to date should things change.
This document is owned by all contributors to this project, be they technical or otherwise. Updates to this document are encouraged as needed and can be submitted via PR as with any other contribution to this repository.
Table of Contents
The Coordinator Pattern also known as MVVM-C is a high level pattern which aims to provide a reliable and reproducable pattern for implementation of workflows in the app. A workflow in this case is considered as action which triggers navigation into a new view heirarchy and all the actions contained therein. The Coordinator Pattern is based on a subset of underlying patterns and best practices drawn from Software and Engineering at large. These include:
- Every reference type object has a declared interface
- Single Responsibility
- Open / Close
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
- work is assigned (delegated) via “parent” objects
- this supports Anonymous Delegation by providing lambda calculable functions within the delegated object that are defined but the delegating entity
A Coordinated workflow is made up of the following parts, each represented as a Reference type object in code
- top level object that manages delegation for any / all workflows
- an injected dependency that manages simple generation of all reference type objects
- an interface wrapper around UINavigationController
- an interface wrapper around any UIViewController
- an extension of Presentable which bridges Router functions
- manages “all other” logic which is outside of the scope of the primary players Recently I have been working to clarify this role of this player, borrowing from the VIPER definition; pushing data logic that should not inflate the view layer into the interactor
...
The app subscribes to a RESTful API
to manage remote data transactions
Remote data transactions are performed leveraging the Codable framework and Native URLSession. A transient model is provided for this process which can be used for RESTful calls as well as interaction with the UI layer, these can be identified by the naming convention ...DisplayModel
and must subscribe to the Codable protocol.
In order to provide appropriate abstraction a robust architecture is provided to develop against. This architecture is made up of the following elements...
Each API transaction is handled through a ...Service
which should carry its own interface customized to the specific RESTful transactions it is meant to provide. A pragmatic naming convention should be followed when developing these interfaces whereby the appropriate RESTful verb is used in combination with the related root path. For example, the following interface and its associated class provide connections to the /things
route(s) of our API.
protocol SomeKindOfService: class {
func postSomething(_ thing: ThingDisplayModel, _ completion: @escaping (A_Result<PostSomethingResponse>) -> Void)
func deleteSomething(_ thingID: Int, _ completion: @escaping (A_Result<Bool>) -> Void)
}
Each service will own a RequestFactory
which provides a customized interface allowing it to generate those specific requests as required by the related service. For example, the following interface and its associated class provide factory methods to generate request(s) for the /things
path(s)
protocol SomeKindOfRequestFactory: class {
func makePostSomethingRequest(thing: SomethingDisplayModel) -> Request<PostSomethingResponse>?
func makeDeleteSomethingRequest(thingID: Int) -> Request<Bool>?
}
A simple, robust, native return type: A_Result
, is provided for encapsulation purposes. This return type provides two outcomes; .success
and .failure
, and should be used for all API calls to ensure a clean and reliable pattern of execution throughout the app. The .success
case takes any optional Value?
while .failure
provides encapsulation for any optional Error?
enum A_Result<Value> {
case success(Value?)
case failure(Error?)
}
it should be noted that the A_Result type is intended to be highly flexible and has many use cases outside of the API, any failable process that requires a return type may use this for encapsulation.
A transactional Request
object is provided for encapsulation of all request parameters and management of the request/response lifecycle. This object subscribes to the related protocol RequestType
which extends the request to manage its execution. Furthermore the Request
object carries the association to the related ResponseType
which empowers the request to decode its result against the Codable framework via the internalized Parser
.
struct Request<T>: RequestType where T: Decodable {
typealias ResponseType = T
var data: RequestData
var queue: Queue
}
protocol RequestType {
associatedtype ResponseType: Decodable
var data: RequestData { get set }
}
extension RequestType {
var parser: ResponseParser ...
func execute(dispatcher: NetworkDispatcher, completion: @escaping ((A_Result<ResponseType>) -> Void)) ...
}
A value type is provided for encapsulating all of the elements required to make any request. This object belongs to the Request
and should be generated within the context of a factory method and passed to the Request
object prior to its return from the factory.
struct RequestData {
var endpoint: Endpoint
var reqType: HttpMethod
let timeOut: TimeInterval
var headers: [String: String]?
let body: Data?
}
A NetworkDispatcher
is provided to the .execute
method of a request. This dispatcher is a Struct, and is designed to support an idempotent request cycle using URLSession
. Dispatchers are not intended to be reused, hence their definition as value types. The reason behind this choice again being an investment in idempotency and a robust promise that the dispatcher object is not subject to accidental mutation during the execution of a request to the API. The following interface defines each instance of NetworkDispatcher
.
protocol NetworkDispatcher {
var urlSession: URLSession { get }
func dispatch(request: RequestData, completion: @escaping ((A_Result<Data>) -> Void))
}
... code samples detailing implementation within our product
The app uses CoreData
to manage persistent data in the app
Remote data is managed through the Codable framework
. We use the object copy pattern, familiar to the Java language to maintain abstraction of the data layer. Transactional objects, following the naming convention _DisplayModel
are used to decode and encode data as it flows to and from the remote layer (API
) allowing the data layer (CoreData
) to be the only silo where NSManagedObjects
are transacted with.
Persistent data transactions to and from the display layer of the app are handled through an intermediary called the CoreDataWorker
. This object maintains the only reference in the app to the DataStack, as the DataStack should never be referenced directly by objects outside of the context of the isolated data layer. The DataWorker object provides the following interface to allow persistent data transactions...
DataWorker takes in a collection of ManagedObjectConvertable
converts too ManagedObject
and requests an Upsert
transaction in the CoreData context.
func upsert<Entity: ManagedObjectConvertable>(entities: [Entity]?, _ completion: @escaping (Error?) -> Void)
DataWorker accepts a primary key or predicate, requests a ManagedObject
from the CoreData context, and returns a converted ManagedObjectConvertable
if found.
func fetch<Entity: ManagedObjectConvertable>(with predicate: NSPredicate?, sortBy: [NSSortDescriptor]?, _ completion: @escaping (TCResult<[Entity]>) -> Void)
DataWorker takes in a collection of ManagedObjectConvertable
converts too ManagedObject
and requests a Delete
in the CoreData context.
func delete<Entity: ManagedObjectConvertable>(entities: [Entity], _ completion: @escaping (Error?) -> Void)
In order to develop an interface with the data layer a simple pattern is provided. The following is an example of how to implement this pattern.
class SomeViewModel {
var dataWorker: CoreDataWorker
init(dataWorker: CoreDataWorker) {
self.dataWorker = dataWorker
}
func saveOrUpdate(displayModel: SomeDisplayModel) {
dataWorker.upsert(entities: [dataModel]) { error in
// handle error state etc.
}
}
func fetch(with id: Int) {
let predicate = NSPredicate(format: "id == %i", id)
dataWorker.fetch(with: predicate, sortBy: nil) {
switch result {
case .success(let response):
// do something with the response collection [ManagedObjectConvertable]
case .failure(let error):
// handle error state etc.
}
}
}
func delete(displayModel: SomeDisplayModel) {
dataWorker.delete(entities: [displayModel]) { error in
// handle error state etc.
}
}
}
Two protocols are provided to standardize the abstraction of the Data Layer from the Display Layer.
protocol ManagedObject {
associatedtype DisplayModel
func toDisplayModel() -> DisplayModel?
}
protocol ManagedObjectConvertable {
associatedtype ManagedObj: NSManagedObject, ManagedObject
func toManagedObject(in context: NSManagedObjectContext) -> ManagedObj?
}
extension SomeDataObject: ManagedObject {
func toDisplayModel() -> SomeObjectDisplayModel? {
return StoredObjectDisplayModel(id: id, name: name, age: age)
}
}
extension SomeObjectDisplayModel: ManagedObjectConvertable {
func toManagedObject(in context: NSManagedObjectContext) -> SomeDataObject? {
let someObject = SomeDataObject.getOrCreateOne(with id: id, from: context)
someObject.id = id
someObject.name = name
someObject.age = age
return someObject
}
}
Unit testing in the app is done primarily using the Result and State testing patterns. A best practice of 1:1 (Expect: Assert) is also implemented for unit testing. This is done for a couple of reasons.
- The first is to keep tests very concise and clear as to what they are testing and what the expected result should be.
- The second is based on the idea that tests should act as an extension to your code's documentation. Each test is developed in such a way as to act as a sentence documenting a line of code and the expected behavior; take the following example.
This function implements the classic Pythagorean theorem: a2 + b2 = c2
func pythagorIt(a: Int, b: Int) -> Int {
return pow(2, a) + pow(2, b)
}
To fully unit test this we would only need a single test
describe("pythagorIt") {
it("should return the correct value") {
let c = pythagorIt(a: 2, b: 2)
expect(c).to(equal(8))
}
}
In Xcode's testing results this would read as: pythagorIt__should_return_the_correct_value
This seems simple enough, however, let's consider a more complex example. Say we have a class with dependencies:
class NiftyMather {
var pythagor: PythgorDoer
func pythagorIt(a: Int, b: Int) -> Int {
return pythagor.valueFor(a, b)
}
}
In this case, we need to more carefully consider our testing.
- First we will need to mock the PythagorDoer class so that we can completely control what it returns. See Mocks below for more details on how to mock objects. But let's assume we have a mock, we can do some initialization in a before block...
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
beforeEach {
sut = NiftyMather()
mockDoer = MockPythagorDoer()
mockDoer.stubbedValueForResult = 8
sut.pythagor = mockDoer
}
now that we have a testable instance set up with our mock...
- We want to assert that the method returns an expected value, but really this could be any value. We aren't testing what the pythagor is doing in its func, instead we should be testing that whatever the value that **pythagor.valueFor(, )** returns is what actually comes out of the method. So now a test for that is added...
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
beforeEach {
sut = NiftyMather()
mockDoer = MockPythagorDoer()
mockDoer.stubbedValueForResult = 8
sut.pythagor = mockDoer
}
describe("a NiftyMather") {
describe("pythagorIt") {
it("should return the expected value") {
let c = sut.pythagorIt(2, 2,)
expect(c).to(equal(8))
}
}
}
when this is viewed in the Xcode test log it will read like: NiftyMather__pythagorIt__should_return_the_expected_value which is a very readable sentence which describes what the result of running this method should be, but we aren't done yet.
- next we should prove that the instance of PythagorDoer is actually running the method we want it to.
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
beforeEach {
sut = NiftyMather()
mockDoer = MockPythagorDoer()
mockDoer.stubbedValueForResult = 8
sut.pythagor = mockDoer
}
describe("a NiftyMather") {
describe("pythagorIt") {
it("should return the expected value") ...
it("should invoke valueFor on the PythagorDoer instance" {
_ = sut.pythagorIt(2, 2)
expect(mockPythagor.invokedValueForCount).to(equal(1))
}
}
}
this new test proves that we are triggering the method on our dependency, and it will read like this: a_NiftyMather__pythagorIt__should_invoke_valueFor_on_the_PythagorDoer_instance
- last we should prove that he values we pass into our method are in fact being passed along as expected
var sut: NiftyMather!
var mockDoer: MockPythagorDoer
beforeEach {
sut = NiftyMather()
mockDoer = MockPythagorDoer()
mockDoer.stubbedValueForResult = 8
sut.pythagor = mockDoer
}
describe("a NiftyMather") {
describe("pythagorIt") {
it("should return the expected value") ...
it("should invoke valueFor on the PythagorDoer instance" ...
it("should invoke valueFor on the PythagorDoer instance with matching values" {
_ = sut.pythagorIt(2, 2)
expect(mockPythagorDoer.invokedValueForParameters).to(equal((a: 2, b: 2))
}
}
}
with this final test we prove that our input values make it through to our dependencies method, and it reads like this: a_NiftyMather__pythagorIt__should_invoke_valueFor_on_the_PythagorDoer_instance_with_matching_values
When all the tests are passing, this may seem like a bit of overkill, however when something goes wrong this level of test driven documentation becomes priceless. If one of these tests failed while the other two passed I would have a very specific idea immediately of what is going wrong! This can be especially important when I am unfamiliar with a section of code. Furthermore, if I am reading code and I am having trouble understanding what it's supposed to do, I can always run the tests and then fall back on the test logs that Xcode outputs to find additional clarity as to what should be happening.
Unit tests are developed using the Quick and Nimble matcher framework for iOS. This is a great framework which only compiles in our testing target and provides a very human readable syntax for our tests. Feel free to read more about it if you are unfamiliar, their docs are great. The basic structure of a QuickSpec looks something like this...
import Quick
import Nimble
@testable import AppsMainTarget
class MyAwesomeClassTests: QuickSpec {
override func spec() {
var sut: MyAwesomeClass!
beforeEach {
// do any full test setup, init sut, create mocks, inject stuff etc.
}
afterEach {
// if you need to do anything to tear down after every test you can do it here
// a note here, afterEach can cause race case issues with tests, the afterEach block only runs at the top level of the first block it is nested in
}
describe("a MyAwesomeClass") {
describe("create a describe for each block you are testing") {
beforeEach {
// this beforeEach will only impact tests within the enclosing describe block
// the same would be true of an afterEach
}
afterEach {
// this afterEach will only impact test that run at this level, anything nested for deeply will need a separate afterEach, in many cases it can be easier to put clean up code at the top of the beforeEach instead
}
it("may do something outside of a context") {
...
}
context("sometimes you will have different behavior based on context or conditions, context blocks are for that") {
it("should do something in this context" {
...
}
}
}
}
}
}
Testing mocks are in MOST cases generated using the Swift Mock Generator plugin for Xcode. You should have added this plugin as a part of the getting started piece of this wiki. In most cases, especially when you have provided an interface for your object, SMG will create a very complete, and rather dumb mock object. Complete with overrides, invocation tracking, and appropriate stubs. This generator follows a pretty straight forward pattern, check out any of the objects in the Mocks folder within the testing target.
There are times when you may have to write your own mocks, this should be done in the same standardized pattern so that all mocks behave in the same way, and provide the same values within tests. The main reason you might have to create your own mock is if you need to mock one of Apples super classes such as UIViewController. SMG has a hard time finding the interface patterns for these types of classes as they live in Apples API and are a layer deeper than your main target.
an example mock...
import Foundation
@testable import SomeAppMain
class MockThing: Thing {
var invokedSomethingCoolSetter = false
var invokedSomethingCoolSetterCount = 0
var invokedSomethingCool: String?
var invokedSomethingCoolList = [String]()
var invokedSomethingCoolGetter = false
var invokedSomethingCoolGetterCount = 0
var stubbedSomethingCool: String! = ""
var somethingCool: String {
set {
invokedSomethingCoolSetter = true
invokedSomethingCoolSetterCount += 1
invokedSomethingCool = newValue
invokedSomethingCoolList.append(newValue)
}
get {
invokedSomethingCoolGetter = true
invokedSomethingCoolGetterCount += 1
return stubbedSomethingCool
}
}
var invokedDoesSomethingNeat = false
var invokedDoesSomethingNeatCount = 0
var invokedDoesSomethingNeatParameters: (a: Int, b: String)?
var invokedDoesSomethingNeatParametersList = [(a: Int, b: String)]()
var stubbedDoesSomethingNeatResult: Bool! = false
func doesSomethingNeat(a: Int, b: String) -> Bool {
invokedDoesSomethingNeat = true
invokedDoesSomethingNeatCount += 1
invokedDoesSomethingNeatParameters = (a, b)
invokedDoesSomethingNeatParametersList.append((a, b))
return stubbedDoesSomethingNeatResult
}
}
Testing stubs provide a clean and fast way to initialize objects in tests. Stubs are created using a default value pattern by directly extending the object to be stubbed within the testing target. The primary benefit to this is cleanliness of tests and easy initialization. There may be times when you don't care about any of the values within an object's params, and it would be messy to have to pass arbitrary values to an initializer within a test. Or in a similar way you may only care about a single value, for instance an ID. Default value stubs provide a set of default values to the objects initializer so that you can cherry pick what you do or don't pass in. Check out any of the objects in the Stubs folder within the testing target.
Unfortunately at this point, there is not a nifty framework for this, however with a little practice using Xcodes hot keys it gets pretty easy to make these. When providing default overrides, the values should always be the lowest, simplest or most basic needed to create the object, this makes writing assertions with default objects very predictable and easy.
an example stub...
extension BigObject {
static func stub(id: Int = 0,
name: String = "",
age: Int = 0,
favFood: String = "",
favColor: String = "",
favNum: Int = 0,
isDancer: Bool = false) {
return BigObject(id: id, name: name, age: age, favFood: favFood, favColor: favColor, favNum, isDancer: isDancer)
}
}