iOS development: The Woost Way™

Rutger Bresjer
Woost
Published in
8 min readNov 24, 2016

At Woost, our promise is to build a Minimum Viable Product in one month. We believe the core of (almost) each new concept can be built in that timeframe. We help our clients to find that core, define goals and hypotheses, and trim down the scope until development fits in one month.

We build for web (responsive) and for mobile (native Android, native iOS). To be able to get up to speed and deliver quickly, we have a standard set of tools we use for most projects, and a standard project setup. In this post I will introduce you to what we use for our iOS projects.

Of course, as new best-practices and new frameworks pop up daily, this is subject to change. Warning: it’s getting quite technical ;-)

CocoaPods

A lot of work has already been done by others. If you know how to value work from others*, you can save a lot of time and avoid re-inventing the wheel by making use of it.

*: This is important! Your app’s performance and stability is at stake.

CocoaPods is a package/dependency manager for Swift and Obj-C projects. You can integrate CocoaPods in your project to easily add, remove or update external packages. We mostly use CocoaPods, sometimes Carthage, but find the former easier to use.

Our more-or-less standard Podfile looks like this, which already gives you a glimpse at the tools we use:

pod 'RxSwift'
pod 'RxCocoa'
pod 'SwiftyJSON'
pod 'Kingfisher'
pod 'PureLayout'
pod 'R.swift'
pod 'Fabric'
pod 'Crashlytics'
pod 'Firebase/Analytics' # or 'GoogleAnalytics'

While you’re setting up your Pods, be sure to also check out gitignore.io to create a nice .gitignore template! We normally start with this one.

Rx

RxSwift is a version of ReactiveX for Swift (RxCocoa extends the Cocoa API with Reactive magic). ReactiveX streamlines asynchronous calls (avoiding callback hell), makes code more functional, readable and less prone to bugs.

We use it mostly for API calls and user interface bindings. For more info about the why of Rx, check out RxSwift’s nice page about it. Ray Wenderlich has a nice tutorial to get started with Rx.

SwiftyJSON

Although Swift has some basic understanding of JSON, with SwiftyJSON you get cleaner, stronger typed, shorter and better understandable code.

Kingfisher

There are a lot of libraries for caching images, but we found Kingfisher to be the easiest and most convenient to use.

PureLayout

AutoLayout works fine in storyboards, but if you want to add layout constraints in code, the vanilla API’s are quite verbose and hard to read. PureLayout is a nice API that fixes this. Especially with Swift’s short dot syntax thanks to its convenient type inference, PureLayout works quite neat and concise. A nice looking alternative is SnapKit, which we have yet to try.

R.swift

An idea borrowed from Android, R.swift introduces strong typed, autocompleted and compile-time checked resources to your Swift environment. Normally when retrieving e.g. an image, you would type something like:

UIImage(named: "imageName")

With R.swift:

R.image.imageName()

With localized strings, it works even better. Say you have this localized string:

"welcome.withName" = "Welcome %@";

Normally, you would type:

let welcomeName = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Alice")

And with R.swift:

let welcomeName = R.string.localizable.welcomeWithName("Alice")

It also works great for fonts, nibs, reuse identifiers, localized strings and more! Check their repo for more examples.

Fabric / Crashlytics

Crashlytics, part of Fabric, is a great tool for crash monitoring and reporting. We also use it for beta distribution, and we use Fabric’s Answers tool for some basic usage analytics. The Fabric iOS app is a great tool to check usage and stability on the go. It also directly notifies you when there’s something wrong!

Firebase Analytics / Google Analytics

Of course, Google Analytics is the industry standard of online analytics. Recently Google bought Firebase and launched Firebase Analytics. While Google Analytics is an extreeemely extensive tool, it was primarily created for web analytics. Firebase was built from the ground up with mobile in mind. Both have their advantages and disadvantages. For each project, we decide which tool suits best.

Be sure to also check out Firebase’s other features. Their realtime database, authentication and remote config tools are also great.

Project structure

When I worked at a large iOS project for a major Dutch bank, my colleague Alexander introduced me to the MVVM-pattern with flow controllers. I have not used another structure since. Find below the folders we have set up in our skeleton Xcode project.

Example structure

Flow

Instead of connecting view controllers to each other, we use flow controllers to manage the flow through the app. A flow controller is responsible for setting up view models, connecting them to view controllers and defines the flow between views. By doing this, views become independent of each other and can be reused way easier. We set up a FlowController protocol, a MainFlowController (instantiated by the AppDelegate) and a separate flow controller for each major flow.

Example of the protocol:

protocol FlowController {
var flowDelegate: FlowController? { get set }
func start()
}

The MainFlowController may look something like this:

class MainFlowController: FlowController {
var flowDelegate: FlowController?
var window: UIWindow?
func start() {
window = UIWindow(frame: UIScreen.main.bounds)
window!.rootViewController = preparedPreloaderVc()
window!.makeKeyAndVisible()
setupDependencies {
checkUserAuthorization()
} } func checkUserAuthorization() {
if AuthService.isAuthenticated {
showMainScreen(animated: false)
} else {
showLogin()
} }}

Model

We set up a Model protocol (with an init?(json: JSON) initializer) and create a model struct for each entity. The initializer allows to easily map a JSON response from the API to a Swift struct.

The protocol:

protocol Model {
init?(json: JSON)
}

And an example model:

struct Person: Model {
let id: Int
let firstName: String?
let lastNamePrefix: String? // Dutch surnames may have a prefix
let lastName: String?
let addresses: [Address]? var fullName: String {
return [firstName, lastNamePrefix, lastName]
.flatMap { $0 }
.joined(separator: " ")
} init?(json: JSON) {
// The id is not optional, so without it the initializer fails
guard let id = json["id"].int else { return nil }
self.id = id
self.firstName = json["firstName"].string
self.lastNamePrefix = json["lastNamePrefix"].string
self.lastName = json["lastName"].string
// Use flatmap to map the JSON array with addresses
// to Address objects
self.addresses = json["addresses"].array?
.flatMap { Address(json: $0) }
}}

ViewModel

A view model basically contains all of the state and business logic of a view. By separating this from the view controller, it is easier to reuse views. Next to that, it allows for more extended and clean unit testing, because view models do not depend on UIKit.

See below for an example of a view model for a view with a list of persons. This view model can be used both to contain the user’s favorite persons, and for the results of a search query:

struct PersonsViewModel: ViewModel {
enum Mode {
case
favorites,
searchResults(query: String)
} let mode: Mode
var items: Variable<[Person]> = Variable([])
private let disposeBag = DisposeBag() init(mode: Mode) {
self.mode = mode
refresh()
} func refresh() {
switch mode {
case .favorites:
self.items.value = FavoritesService
.favorites
.map {
Person(id: $0.id, fullName: $0.name)
} case .searchResults(let query):
APIService
.persons(filters: ["freeQuery": query])
.bindTo(items)
.addDisposableTo(disposeBag)
}}

View

View controllers and views are responsible for displaying the info contained in a view model (in a nice way), and for connecting user interface interaction with business logic in the view model. We try to keep the views as ‘dumb’ as possible. They only represent the visual representation of the user interface. No more, no less.

We also try to keep the views as generic as possible. For example, we don’t create a Search Results-view, but instead create a Person Lister-view. This lister can then be used for search results, but also for e.g. listing a person’s connections. Just inject a different view model.

Together with flow controllers (and UIKit extensions), views are the only type of objects dependent on UIKit, meaning all services and view models can be easily unit tested.

Sometimes we set up views entirely in code, sometimes we use nibs, sometimes we use storyboards. We never use segues, as flow is always managed by a flow controller.

Manager

In our case, managers are singletons responsible for –you won’t say– managing stuff. Because of memory usage of the singletons, we create as few as possible managers and try to wrap as much as possible in static functions in services.

We use them to keep global state (e.g. an authentication session), or e.g. for number formatters and date formatters if these are used spread out through the app and we don’t want to instantiate them again everytime.

Service

Services are interfaces between API’s or other data providers and the rest of the app. Our services only contain static functions and these output either plain objects or Rx-Observables with object-mapped data streams.

For example, a simple service to retrieve saved favorites from the UserDefaults:

struct Favorite {
let id: Int
let name: String
}class FavoritesService {
private static let kFavorites = "Favorites"
private static var favoritesDict: [String: Any] {
get {
return UserDefaults
.standard
.dictionary(forKey: kFavorites) ?? [:]
}
set {
UserDefaults
.standard
.set(newValue, forKey: kFavorites)
}
}
static var favorites: [Favorite] {
return favoritesDict
.flatMap { (key, val) in
guard let id = Int(key) else { return nil }
return Favorite(id: id, name: String(describing: val))
} } static func isFavorite(_ id: Int) -> Bool {
return favoritesDict[String(id)] != nil
} static func add(_ favorite: Favorite) {
favoritesDict[String(favorite.id)] = favorite.name
} static func delete(_ favorite: Favorite) {
favoritesDict
.removeValue(forKey: String(favorite.id))
} static func delete(_ id: Int) {
favoritesDict
.removeValue(forKey: String(id))
}}

I will write a more extensive post about the API service with Rx later, but for example, it may contain a function like:

static func person(id: Int) -> Observable<Person> {
return request(.person(id: id))
.map {
Person(json: $0["person"])
}.errorOnNil() // Part of RxOptional}

Where request(_ route: APIRoute) is a basic wrapper around URLSession.shared.rx.json(request: URLRequest), responsible for setting headers, mapping errors and creating the request itself.

Extension

The Extension folder just contains Swift extensions to Foundation or UIKit classes/structs.

Resource

Here we store fonts, image assets, the Info.plist file, the R.generated.swift file, the Launch Screen et cetera.

Wrapping up

I hope this gives a first insight in how we develop iOS apps. Like I said in the introduction: these things change all the time. However, we use the frameworks and structure explained in this post for quite some time now, so perhaps they are here to stay for a while.

If you have any questions or comments, just respond to this post or send me a message!

Last but not least: a big thanks to my former colleague Alexander who introduced me to MVVM, flow controllers and Rx!

--

--

Rutger Bresjer
Woost
Editor for

Product Strategist / Product Owner / Developer