/TechStacksApp

TechStacks iOS App for iPhone and iPad

Primary LanguageSwift

TechStacks iOS App

The TechStacks Native iOS App provides a fluid and responsive experience for browsing content on http://techstacks.io on iPhones and iPad devices. Get it now free on the AppStore!

TechStacks on AppStore

This repository contains the complete source code for the TechStacks App which provides a good example to illustrate the ease-of-use and utility of ServiceStack's new support for Swift and XCode for quickly building services-rich iOS Apps.

Technical Overview

All remote Service Calls used by the App are encapsulated into the AppData.swift class and only uses JsonServiceClient's non-blocking Async API's to ensure a Responsive UI is maintained throughout the App.

Some other useful features and techniquest that helped with development of this App include:

MVC and Key-Value Observables (KVO)

If you've ever had to implement INotifyPropertyChanged in .NET, you'll find the built-in model binding capabilities in iOS/OSX a refreshing alternative thanks to Objective-C's underlying NSObject which automatically generates automatic change notifications for its KV-compliant properties. UIKit and Cocoa frameworks both leverage this feature to enable its Model-View-Controller Pattern.

As keeping UI's updated with Async API callbacks can get unwieldy, we wanted to go through how we're taking advantage of NSObject's KVO support in Service Responses to simplify maintaining dynamic UI's.

Enable Key-Value Observing in Swift DTO's

Firstly to enable KVO in your Swift DTO's we'll want to have each DTO inherit from NSObject which can be done by uncommenting BaseObject option in the header comments as seen below:

/* Options:
Date: 2015-02-19 22:43:04
Version: 1
BaseUrl: http://techstacks.io

BaseClass: NSObject
...
*/

and click the Update ServiceStack Reference Menu Option to fetch the updated DTO's.

Then to enable Key-Value Observing just mark the response DTO variables with the dynamic modifier, e.g:

public dynamic var allTiers:[Option] = []
public dynamic var overview:AppOverviewResponse = AppOverviewResponse()
public dynamic var topTechnologies:[TechnologyInfo] = []
public dynamic var allTechnologies:[Technology] = []
public dynamic var allTechnologyStacks:[TechnologyStack] = []

Which is all that's needed to allow properties to be observed as they'll automatically issue change notifications when they're populated in the Service response async callbacks, e.g:

func loadOverview() -> Promise<AppOverviewResponse> {
    return client.getAsync(AppOverview())
        .then(body:{(r:AppOverviewResponse) -> AppOverviewResponse in
            self.overview = r
            self.allTiers = r.allTiers
            self.topTechnologies = r.topTechnologies
            return r
        })
}

func loadAllTechnologies() -> Promise<GetAllTechnologiesResponse> {
    return client.getAsync(GetAllTechnologies())
        .then(body:{(r:GetAllTechnologiesResponse) -> GetAllTechnologiesResponse in
            self.allTechnologies = r.results
            return r
        })
}

func loadAllTechStacks() -> Promise<GetAllTechnologyStacksResponse> {
    return client.getAsync(GetAllTechnologyStacks())
        .then(body:{(r:GetAllTechnologyStacksResponse) -> GetAllTechnologyStacksResponse in
            self.allTechnologyStacks = r.results
            return r
        })
}

Observing Data Changes

In your ViewController have the datasources for your custom views binded to the desired data (which will initially be empty):

func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    return appData.allTiers.count
}
...
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return appData.topTechnologies.count
}

Then in viewDidLoad() start observing the properties your UI Controls are bound to, e.g:

override func viewDidLoad() {
    ...
    self.appData.observe(self, properties: ["topTechnologies", "allTiers"])
    self.appData.loadOverview()
}
deinit { self.appData.unobserve(self) }

In the example code above we're using some custom KVO helpers to keep the code required to a minimum.

With the observable bindings in place, the change notifications of your observed properties can be handled by overriding observeValueForKeyPath() which passes the name of the property that's changed in the keyPath argument that can be used to determine the UI Controls to refresh, e.g:

override func observeValueForKeyPath(keyPath:String, ofObject object:AnyObject, change:[NSObject:AnyObject],
  context: UnsafeMutablePointer<Void>) {
    switch keyPath {
    case "allTiers":
        self.technologyPicker.reloadAllComponents()
    case "topTechnologies":
        self.tblView.reloadData()
    default: break
    }
}

Now that everything's configured, the observables provide an alternative to manually updating UI elements within async callbacks, instead you can now fire-and-forget your async API's and rely on the pre-configured bindings to automatically update the appropriate UI Controls when their bounded properties are updated, e.g:

self.appData.loadOverview() //Ignore response and use configured KVO Bindings

Images and Custom Binary Requests

In addition to greatly simplifying Web Service Requests, JsonServiceClient also makes it easy to fetch any custom HTTP response like Images and other Binary data using the generic getData() and getDataAsync() NSData API's. This is used in TechStacks to maintain a cache of all loaded images, reducing number of HTTP requests and load times when navigating between screens:

var imageCache:[String:UIImage] = [:]

public func loadImageAsync(url:String) -> Promise<UIImage?> {
    if let image = imageCache[url] {
        return Promise<UIImage?> { (complete, reject) in complete(image) }
    }
    
    return client.getDataAsync(url)
        .then(body: { (data:NSData) -> UIImage? in
            if let image = UIImage(data:data) {
                self.imageCache[url] = image
                return image
            }
            return nil
        })
}