iOS application development

So far, we looked into requirements, discussed a high-level design, and developed a simple backend API. Now, we are going to develop an iOS application that will leverage the latter.

Configuration

We will start our application development using CocoaPods (https://cocoapods.org/). We can install it by executing the following command in the terminal:

sudo gem install cocoapods

Then, we will create a folder using Finder or simply execute the following command in the terminal:

mkdir Frontend

Next, we will create a Single View Application project in Xcode:

Configuration

We are going to name it TodoApp and provide an organization name and identifier. The programming language is going to be Swift, and Devices will be Universal. Now, we can close the project and go back to the terminal.

In the terminal, we will execute the following code:

cd Frontend/TodoApp
pod init

This will create a file named Podfile. This is where we define our dependencies.

Uncomment the first and third line so it becomes like this:

platform :ios, '8.0'
use_frameworks!

target 'TodoApp' do

end

Now, we need to define dependencies for our target. We can go to https://cocoapods.org/ and search for any dependency, copy the definition, and paste it into our Podfile:

platform :ios, '8.0'
use_frameworks!

target 'TodoApp' do
    pod 'Alamofire'
    pod 'Argo'
    pod 'Curry'
    pod 'ReactiveCocoa'
    pod 'Delta', :git => "https://github.com/thoughtbot/Delta.git"
end

Now, we can save and close our Podfile and move on to the terminal application. In the terminal application, we will execute the following command:

Pod install

This directive will create a workspace, download all dependencies, and link them as frameworks into our project. Now, we can open TodoApp.xcworkspace with Xcode.

In the workspace, we will see two projects: TodoApp and Pods. Pods will contain all the dependencies.

Next, let's create a folder hierarchy to organize our workspace. In the workspace, right-click on a folder and select Show In Finder. Here, we will create the following folders and files:

  • Actions
  • Communication
  • Controllers
  • Extensions
  • Managers
  • Models
  • Resources
  • State
  • Views

Next, we will add these folders to our project by right-clicking on the TodoApp folder and selecting Add Files to "TodoApp", as shown in the following screenshot:

Configuration

At this point, we can move ViewController to Controllers and any images to the Resources folder.

When we are done with our application, the folder and file hierarchy will be as follows:

Configuration

Since our backend does not comply with security policies enforced by Apple, we will need to set the NSAllowsArbitraryLoads key to YES under the NSAppTransportSecurity dictionary in our .plist file.

Models

Obviously, we can use the Todo model we have used in our backend example, but we want to make our frontend application as functional as possible. There is a great functional JSON parsing library named Argo that we can leverage. Let's define our Todo model with Argo:

import Argo
import Curry

enum TodoFilter: Int {
    case all
    case active
    case completed
    case notSyncedWithBackend
    case selected
}

struct Todo {
    let id: Int
    let name: String
    let description: String
    let notes: String?
    let completed: Bool
    let synced: Bool
    let selected: Bool?
}

extension Todo: Decodable {
    static func decode(json: JSON) -> Decoded<Todo> {
        return curry(Todo.init)
        <^> json <| "id"
        <*> json <| "name"
        <*> json <| "description"
        <*> json <|? "notes"
        <*> json <| "completed"
        <*> json <| "synced"
        <*> json <|? "selected"
    }
}

extension Todo: Equatable {}

func == (lhs: Todo, rhs: Todo) -> Bool {
    return lhs.id == rhs.id
}

First of all, we import two libraries: Argo and Curry. Curry provides convenient currying functionalities. Although currying is going to be removed from Swift and returning closures will be the norm, it will be safe to use the Curry library.

Our Todo model becomes a struct, and then we extend our struct by conforming to a protocol named Decodable. To conform to this protocol, we need to implement the decode function. This function takes a JSON payload and returns a decoded Todo object.

In the body of the function, we will use the currying and custom operators. According to the Argo documentation, currying allows us to partially apply the init function over the course of the decoding process. This basically means that we can build up the init function call bit by bit, adding one parameter at a time, if (and only if) Argo can successfully decode them. If any of the parameters do not meet our expectations, Argo will skip the init call and return a special failure state. Let's check the syntax of Curry:

public func curry<A, B, C, D, E, F>(function: (A, B, C, D, E) -> F) -> A
  -> B -> C -> D -> E -> F {
    return { (`a`: A) -> B -> C -> D -> E -> F in { (`b`: B) -> C -> D -> E
      -> F in { (`c`: C) -> D -> E -> F in { (`d`: D) -> E -> F in { (`e`:
      E) -> F in function(`a`, `b`, `c`, `d`, `e`) } } } } }
}

The curry function takes a function that has five parameters A to E and returns F, that is, curry returns A -> B -> C -> D -> E -> F.

This enables us to partially apply our init method.

Operators

We will discuss the different custom infix operators now:

  • <^> to map a function over a value conditionally
  • <*> to apply a function with context to a value with context
  • <| to decode a value at the specific key into the requested type
  • <|? to decode an optional value at the specific key into the requested type
  • <|| to decode an array of values at the specific key into the requested type

<^>

Our first operator in the decoding process, <^>, is used to map our curried init method over a value. The definition is as follows:

public func <^> <T, U>(@noescape f: T -> U, x: Decoded<T>) -> Decoded<U> {
    return x.map(f)
}
func map<U>(@noescape f: T -> U) -> Decoded<U> {
    switch self {
        case let .Success(value): return .Success(f(value))
        case let .Failure(error): return .Failure(error)
    }
}

<*>

The <*> operator is used to conditionally apply the other parameters to our curried init method. The definition is as follows:

public func <*> <T, U>(f: Decoded<T -> U>, x: Decoded<T>) -> Decoded<U> {
    return x.apply(f)
}
func apply<U>(f: Decoded<T -> U>) -> Decoded<U> {
    switch f {
        case let .Success(function): return self.map(function)
        case let .Failure(error): return .Failure(error)
    }
}

<|

The <| operator is used to decode a value at the specified key path into the requested type. This operator uses a function named flatReduce that reduces and flattens the sequence:

public func <| <A where A: Decodable, A == A.DecodedType>(json: JSON, keys:
  [String]) -> Decoded<A> {
    return flatReduce(keys, initial: json, combine: decodedJSON)
      >>- A.decode
}

<|?

The <|? operator is used to decode an optional value at the specified key path into the requested type:

public func <|? <A where A: Decodable, A == A.DecodedType>(json: JSON, key:
  String) -> Decoded<A?> {
    return .optional(json <| [key])
}

<||

The <|| operator is used to decode an array of values at a specific key into the requested type:

public func <|| <A where A: Decodable, A == A.DecodedType>(json: JSON,
  keys: [String]) -> Decoded<[A]> {
    return flatReduce(keys, initial: json, combine: decodedJSON) >>-
      Array<A>.decode
}

Using Argo models

Whenever we receive a JSON payload from the backend, we will be able to use the decode function to decode our JSON payload to our model:

let json: AnyObject? = try?NSJSONSerialization.JSONObjectWithData(data,
  options: [])
 
if let j: AnyObject = json {
    let todo: Todo? = decode(j)
}

We can see that Argo is a great FP library that can be leveraged as an example to master lots of FP paradigms. Using Argo, Curry, and custom operators, we are able to parse and decode JSON payloads to our model objects declaratively. Also, our models become immutable value types that we can use in our applications without being concerned about mutability.

Also, we defined an enum called TodoFilter. We will use this enum to filter items.

ViewModel

We will have two viewModel, one for each ViewController.

import ReactiveCocoa

struct TodosViewModel {
    let todos: [Todo]
 
    func todoForIndexPath(indexPath: NSIndexPath) -> Todo {
        return todos[indexPath.row]
    }
}

We will use TodosViewModel to list Todo items in our table view.

struct TodoViewModel {
    let todo: Todo?
}

We will use TodoViewModel to present each Todo item's details.

Communication

So far, we have a backend API that we can use to CRUD Todo items and we have models in our iOS application. Let's examine how we can communicate with our backend and populate our models with received payloads.

Request protocol

First, we need to define a protocol for our request models:

protocol RequestProtocol {
    subscript(key: String) -> (String?, String?) { get }
}

extension RequestProtocol {
    func getPropertyNames()-> [String] {
        return Mirror(reflecting: self).children.filter {
$0.label !=
          nil }.map
        { $0.label! }}
}

Here, we defined protocol and we extended the protocol to be able to reflect the object and get properties and their values.

Also, we added subscript to our protocol, which any struct that wants to conform to this protocol should implement.

Conforming to a request protocol

Now, let's create a request model named TodoRequest:

struct TodoRequest: RequestProtocol {
 
    let id: Int
    let name: String
    let description: String
    let notes: String
    let completed: Bool
    let synced: Bool
 
    subscript(key: String) -> (String?, String?) {
        get {
            switch key {
            case "id": return (String(id), "id")
            case "name": return (name, "name")
            case "description": return (description, "description")
            case "notes": return (notes, "notes")
            case "completed": return (String(completed), "completed")
            case "synced": return (String(synced), "synced")
            default: return ("Cookie","test=123")
            }
        }
    }
}

As shown in the preceding code, this struct conforms to RequestProtocol. You might wonder why we have done this. First of all, this is an example of POP and second we will use this request model in our post web service call.

WebServiceManager

We will create a file named WebServiceManager and add a function in it:

import Alamofire
func sendRequest(method: Alamofire.Method, request: RequestProtocol) {

    // Add Headers
    let headers = configureHeaders(request)

    // Fetch Request
    Alamofire.request(method, "http://localhost:8080/todo/",
      headers: headers, encoding: .JSON)
    .validate()
    .responseJSON { response in
        if (response.result.error == nil) {
            debugPrint("HTTP Response Body: (response.data)")
        }
        else {
            debugPrint("HTTP Request failed: (response.result.error)")
        }
    }
}

func configureHeaders(request: RequestProtocol) -> [String: String] {
    let listOfProperties = request.getPropertyNames()
    var configuredRequestHeaders = Dictionary<String, String>()
    for property in listOfProperties {
        let (propertyValue, propertyName) = request[property]
        if propertyName != nil {
            configuredRequestHeaders[propertyName!] = propertyValue
        }
    }
    return configuredRequestHeaders
}

Our sendRequest function takes two parameters. The first one is the HTTP request method and the second one is the type of RequestProtocol. Here, using the implemented protocol function called getPropertyNames, we prepare the header and send a request to our backend using Alamofire.

So far, we have a working communication layer. At this point, we need to develop managers and viewController to handle the logic and show the results to the user.

We will start by testing our communication layer in our MasterViewController and will move the respective code to our managers.

Creating a Todo item

To create a Todo item, we can call the sendRequest function in our MasterViewController viewDidLoad() method to be sure that it is working:

let newRequest = TodoRequest(id: 1,
                             name: "First request",
                             description:"description",
                             notes: "notes",
                             completed: "no")
    sendRequest(Alamofire.Method.POST, request: newRequest)

This should add a new Todo item to our backend.

Our sendRequest method is incomplete and it does not provide a call back to receive the data. Let's improve it:

func sendRequest(method: Alamofire.Method,
                 request: RequestProtocol,
                 completion:(responseData: AnyObject?, error: NSError?) -> Void) {
     
    // Add Headers 
    let headers = configureHeaders(request) 
     
    // Fetch Request
    Alamofire.request(method, "http://localhost:8080/todo/", 
      headers: headers, encoding: .JSON)
        .validate() 
        .responseJSON { response in 
        if (response.result.error == nil) { 
            debugPrint("HTTP Response Body: (response.data)") 
            completion(responseData: response.result.value, error: nil) 
        } 
        else { 
            debugPrint("HTTP Request failed: (response.result.error)") 
            completion(responseData: nil, error: response.result.error) 
        } 
    } 
}

We added a closure as the function argument and called the closure in the body of the function. To test it, we will update our call in MasterViewController:

let newRequest = TodoRequest(id: 1,
                             name: "First request",
                             description:"description",
                             notes: "notes", 
                             completed: "no")
sendRequest(Alamofire.Method.POST, request: newRequest) {
    (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: (todos)")
    } else {
        print("Error")
    }
}

Here, we pass a trailing closure in our call; once it is called, we receive the response or error. Importing and using Argo, we can map the payload to our model. We called this function only for testing and we need to move this call to the proper place. After all, none of our MasterViewController classes will be able to call this function directly and they have to go through other objects. Also, we will need to improve our sendRequest function to take the proper url:

import Alamofire

enum Urls {
    case postTodo
    case getTodos
    case getTodo
    case deleteTodo
    case deleteAll
    case update
}

extension Urls {
    func httpMethodUrl() -> (Alamofire.Method, String) {
        let baseUrl = "http://localhost:8080/"
        switch self {
        case .postTodo:
            return (.POST, "(baseUrl)postTodo")
        case .getTodos:
            return (.GET, "(baseUrl)todos")
        case .getTodo:
            return (.GET, "(baseUrl)todo")
        case .deleteTodo:
            return (.DELETE, "(baseUrl)deleteTodo")
        case .deleteAll:
            return (.DELETE, "(baseUrl)deleteAll")
        case .update:
            return (.POST, "(baseUrl)updateTodo")
        }
    }
}

Here, we define an enum and extend it. In our httpMethodUrl function, we perform pattern matching to return a tuple consisting of an HTTP request method and the full url. We need to change our sendRequest function as follows:

import Alamofire

func sendRequest(url: Urls,
             request: RequestProtocol,
          completion: (responseData: AnyObject?,
               error: NSError?) -> Void) {
    // Add headers
    let headers = configureHeaders(request)
    // Get request method and full url
    let (method, url) = url.httpMethodUrl()
 
    // Fetch request
    Alamofire.request(method, url, headers: headers, encoding: .JSON)
    .validate()
    .responseJSON { response in
        if (response.result.error == nil) {
            debugPrint("HTTP Response Body: (response.data)")
            completion(responseData: response.result.value, error: nil)
        } else {
            debugPrint("HTTP Request failed: (response.result.error)")
            completion(responseData: nil, error: response.result.error)
        }
    }
}

Our function call should be changed as follows:

let newRequest = TodoRequest(id: 1,
                           name: "First request",
                    description: "description",
                          notes: "notes",
                      completed: false)

sendRequest(Urls.postTodo, request: newRequest) { (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: (todos)")
    } else {
        print("Error")
    }
}

Listing Todo items

To retrieve all Todo items, unlike our post call, we do not need to pass any header parameters, just cookie information. So, we add the following struct to handle this scenario:

struct RequestModel: RequestProtocol {

    subscript(key: String) -> (String?, String?) {
        get {
            switch key {
                default: return ("Cookie","test=123")
            }
        }
    }
}

Then, we can retrieve the list of Todo items using the following code:

sendRequest(Urls.getTodos, request: RequestModel()) { (response, error) in
    if error == nil {
        let todos: [Todo]? = decode(response!)
        print("request was successful: (todos)")
    } else {
        print("Error: (error?.localizedDescription)")
    }
}

Although we added better error printing, we need to improve it further.

Let's extract the preceding function calls, create a Swift file named TodoManager, and put these functions in it:

import Alamofire
import Argo

func addTodo(completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    let newRequest = TodoRequest(id: 1,
                               name: "Saturday Grocery",
                        description: "Bananas, Pineapple, Beer,
                          Orange juice, ...",
                              notes: "Cehck expiry date of orange juice",
                          completed: false,
                             synced: true)
 
    sendRequest(Urls.postTodo, request: newRequest) {
        (response, error) in
        if error == nil {
            let todos: [Todo]? = decode(response!)
            completion(responseData: todos, error: nil)
            print("request was successfull: (todos)")
        } else {
            completion(responseData: nil, error: error)
            print("Error: (error?.localizedDescription)")
        }
    }
}

func listTodos(completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    sendRequest(Urls.getTodos, request: RequestModel()) {
        (response, error) in
        if error == nil {
            let todos: [Todo]? = decode(response!)
            completion(responseData: todos, error: nil)
            print("request was successfull: (todos)")
        } else {
            completion(responseData: nil, error: error)
            print("Error: (error?.localizedDescription)")
        }
    }
}

Finally, we will develop two other functions: one adds or updates a Todo item and the other only updates a specific Todo item. Deleting items will be easy to implement as well. The code is as follows:

func addOrUpdateTodo(todo: [Todo]?, completion:(responseData:[Todo]?, error: NSError?) -> Void) {
    if let todoItem = todo?.first {
        let newRequest = TodoRequest(id: todoItem.id,
                                   name: todoItem.name,
                            description: todoItem.description,
                                  notes: todoItem.notes!,
                              completed: todoItem.completed,
                                 synced: true)
 
        sendRequest(Urls.postTodo, request: newRequest) {
            (response, error) in
            if error == nil {
                let todos: [Todo]? = decode(response!)
                let newTodo = todoSyncedLens.set(true, todoItem)
                store.dispatch(UpdateTodoAction(todo: newTodo))
                completion(responseData: todos, error: nil)
                print("request was successfull: (todos)")
            } else {
                completion(responseData: nil, error: error)
                print("Error: (error?.localizedDescription)")
            }
        }
    }
}

func updateTodo(todo: [Todo]?, completion:(responseData:[Todo]?,
  error: NSError?) -> Void) {
    if let todoItem = todo?.first {
        let newRequest = TodoRequest(id: todoItem.id,
                                   name: todoItem.name,
                            description: todoItem.description,
                                  notes: todoItem.notes!,
                              completed: todoItem.completed,
                                 synced: true)
 
        sendRequest(Urls.update, request: newRequest) {
            (response, error) in
            if error == nil {
                let todos: [Todo]? = decode(response!)
                let newTodo = todoSyncedLens.set(true, todoItem)
                store.dispatch(UpdateTodoAction(todo: newTodo))
                completion(responseData: todos, error: nil)
                print("request was successfull: (todos)")
            } else {   
                completion(responseData: nil, error: error)
                print("Error: (error?.localizedDescription)")
            }
        }
    }
}

In these functions, there are concepts that we have not yet covered in detail:

  • dispatch: This function dispatches an action (here, UpdateTodoAction) by settings the state's value to the result of calling its reduce method.
  • todoSyncedLens: This is a Lens to modify the synced property of the todo item. We will define these lenses in an upcoming section.
  • UpdateTodoAction: This is a struct that conforms to ActionType, which is used when we want to make modifications to the State of the Store. All changes to the Store go through this type. We will define our actions in an upcoming section.
  • State: This is a struct that will be used to manage the State. We will define it later.
  • Store: As the name suggests, this is where we store the State. We will define it later.

Lens

We will use lenses to modify our Todo item. Each of the following lenses will be used to modify a part of the Todo item:

struct Lens<Whole, Part> {
    let get: Whole -> Part
    let set: (Part, Whole) -> Whole
}

let todoNameLens: Lens<Todo, String> = Lens(
    get: { $0.name},
    set: {
        Todo(id: $1.id,
           name: $0,
    description: $1.description,
          notes: $1.notes,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoDescriptionLens: Lens<Todo, String> = Lens(
    get: { $0.description},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $0,
          notes: $1.notes,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoNotesLens: Lens<Todo, String> = Lens(
    get: { $0.notes!},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $0,
      completed: $1.completed,
         synced: $1.synced,
       selected: $1.selected)
})

let todoCompletedLens: Lens<Todo, Bool> = Lens(
    get: { $0.completed},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $1.notes,
      completed: $0,
         synced: $1.synced,
       selected: $1.selected)
})

let todoSyncedLens: Lens<Todo, Bool> = Lens(
    get: { $0.synced},
    set: {
        Todo(id: $1.id,
           name: $1.name,
    description: $1.description,
          notes: $1.notes,
      completed: $1.completed,
         synced: $0,
       selected: $1.selected)
})

State

In our application, we need to manage states to keep the state management code as declarative as possible. We will use a library named Delta.

Delta will be used along with ReactiveCocoa to manage states and state changes reactively. The code is as follows:

import ReactiveCocoa
import Delta

extension MutableProperty: Delta.ObservablePropertyType {
    public typealias ValueType = Value
}

In the preceding code, we extend the ReactiveCocoa library's MutableProperty by conforming to Delta.ObservablePropertyType.

The ObservablePropertyType protocol must be implemented by the State that is held by Store. To use a custom State type, this protocol must be implemented on that object.

MutableProperty creates a mutable property of type value and allows observation of its changes in a thread-safe way.

Using extended MutableProperty, our State objects become the following:

import ReactiveCocoa

private let initialTodos: [Todo] = []

struct State {
    let todos = MutableProperty(initialTodos)
    let filter = MutableProperty(TodoFilter.all)
    let notSynced = MutableProperty(TodoFilter.notSyncedWithBackend)
    let selectedTodoItem = MutableProperty(TodoFilter.selected)
}

Store

We will store the state in our Store object:

import ReactiveCocoa
import Delta

struct Store: StoreType {
    var state: MutableProperty<State>

    init(state: State) {
        self.state = MutableProperty(state)
    }
}

var store = Store(state: State())

Store conforms to the StoreType protocol declared in the Delta library. The StoreType protocol defines the storage of an observable state and dispatch methods to modify it.

Here, we create a MutableProperty as state and store it in Store.

We need to define properties to access and modify our state properly, so we extend our Store as follows:

import ReactiveCocoa
import Result

// MARK: Properties
extension Store {
    var todos: MutableProperty<[Todo]> {
        return state.value.todos
    }

    var activeFilter: MutableProperty<TodoFilter> {
        return state.value.filter
    }

    var selectedTodoItem: MutableProperty<TodoFilter> {
        return state.value.selectedTodoItem
    }

}

// MARK: SignalProducers
extension Store {
    var activeTodos: SignalProducer<[Todo], NoError> {
        return activeFilter.producer.flatMap(.Latest) {
            filter -> SignalProducer<[Todo], NoError> in
                switch filter {
                case .all: return self.todos.producer
                case .active: return self.incompleteTodos
                case .completed: return self.completedTodos
                case .notSyncedWithBackend: return
                  self.notSyncedWithBackend
                case .selected: return self.selectedTodo
            }
        }
    }

    var completedTodos: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { $0.completed }
        }
    }
 
    var incompleteTodos: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { !$0.completed }
        }
    }
 
    var incompleteTodosCount: SignalProducer<Int, NoError> {
        return incompleteTodos.map { $0.count }
    }
 
    var allTodosCount: SignalProducer<Int, NoError> {
        return todos.producer.map { $0.count }
    }
 
    var todoStats: SignalProducer<(Int, Int), NoError> {
        return allTodosCount.zipWith(incompleteTodosCount)
    }
 
    var notSyncedWithBackend: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter { !$0.synced }
        }
    }
 
    var selectedTodo: SignalProducer<[Todo], NoError> {
        return todos.producer.map {
            todos in
            return todos.filter {
                todo in
                if let selected = todo.selected {
                    return selected
                } else {
                    return false
                }
            }
        }
    }
 
    func producerForTodo(todo: Todo) -> SignalProducer<Todo, NoError> {
        return store.todos.producer.map {
            todos in
            return todos.filter { $0 == todo }.first
        }.ignoreNil()
    }
}

In our store, we use ReactiveCocoa's SignalProducer to create observable signals. We will observe these signals in other objects and react to signal changes.

Action

Actions are structs that conform to the ActionType protocol from the Delta library. ActionType is used when we want to make modifications to the store's state. All changes to the Store go through this type. Let's examine one example:

import Delta

struct UpdateTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map {
            todo in
            guard todo == self.todo else { return todo }
 
            return Todo(id: todo.id,
                      name: self.todo.name,
               description: self.todo.description,
                     notes: self.todo.notes,
                 completed: self.todo.completed,
                    synced: !todo.synced,
                  selected: todo.selected)
        }
 
        return state
    }
}

In our manager, we had a call like this:

store.dispatch(UpdateTodoAction(todo: newTodo))

The dispatch method call on store with the UpdateTodoAction will call the reduce method of UpdateTodoAction. It will also make modifications on the state and return a new version of it. This is the only place where changes to State are permitted; therefore, any changes to state should go through an action.

Let's define other actions as well:

import Delta

struct ClearCompletedTodosAction: DynamicActionType {
    func call() {
        let todos = store.completedTodos.first()?.value ?? []

        todos.forEach { todo in
            store.dispatch(DeleteTodoAction(todo: todo))
        }
    }
}
 
struct CreateTodoAction: ActionType {
    let id: Int
    let name: String
    let description: String
    let notes: String

    var todo: Todo {
        return Todo(id: id,
                  name: name,
           description: description,
                 notes: notes,
             completed: false,
                synced: false,
              selected: false)
    }

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value + [todo]

        return state
    }
}

struct DeleteTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.filter { $0 != self.todo }

        return state
    }
}

struct DetailsTodoAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map { todo in
            guard todo == self.todo else {

                return Todo(id: todo.id,
                          name: todo.name,
                   description: todo.description,
                         notes: todo.notes,
                     completed: todo.completed,
                        synced: todo.synced,
                      selected: false)
            }

            return Todo(id: self.todo.id,
                      name: self.todo.name,
               description: self.todo.description,
                     notes: self.todo.notes,
                 completed: self.todo.completed,
                    synced: self.todo.synced,
                  selected: true)
        }

        return state
    }
}
 
struct LoadTodosAction: ActionType {
    let todos: [Todo]

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value + todos
        return state
    }
}

struct SetFilterAction: ActionType {
    let filter: TodoFilter

    func reduce(state: State) -> State {
        state.filter.value = filter
        return state
    }
}

struct ToggleCompletedAction: ActionType {
    let todo: Todo

    func reduce(state: State) -> State {
        state.todos.value = state.todos.value.map {
            todo in
            guard todo == self.todo else { return todo }

            return Todo(id: todo.id,
                      name: todo.name,
               description: todo.description,
                     notes: todo.notes,
                 completed: !todo.completed,
                    synced: !todo.synced,
                  selected: todo.selected)
        }

        return state
    }
}

Views

The user will be able to list Todo items from the backend, toggle to mark an item as complete, or swipe left to access functionalities such as Details and Delete.

Our application will look like this:

Views

Views

Views

Views

We can design these screens in the storyboard. We will need to implement a custom UITableViewCell as shown here to be able to show the proper data on TableView:

class TodoTableViewCell: UITableViewCell {

    var todo: Todo? {
        didSet {
            updateUI()
        }
    }

    var attributedText: NSAttributedString {
        guard let todo = todo else { return NSAttributedString() }

        let attributes: [String : AnyObject]
        if todo.completed {
            attributes = [NSStrikethroughStyleAttributeName:
              NSUnderlineStyle.StyleSingle.rawValue]
        } else {
            attributes = [:]
        }

        return NSAttributedString(string: todo.name,
          attributes: attributes)
    }

    override func setSelected(selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

    func configure(todo: Todo) {
        store.producerForTodo(todo).startWithNext { nextTodo in
            self.todo = nextTodo
        }
    }

    func updateUI() {
        guard let todo = todo else { return }

        textLabel?.attributedText = attributedText
        accessoryType = todo.completed ? .Checkmark : .None
    }

}

The only interesting piece in this class is the configure method. It will be called in our cellForRowAtIndexPath method of TableViewController to create a Signal from the producer, then to add exactly one observer to the Signal, which will invoke the given callback when next events are received.

ViewController

We will have two ViewController subclasses:

  • MasterViewController: This will list the Todo items
  • DetailViewController: This will present and modify the details of each item

MasterViewController

We will present a list of items to the user in MasterViewController:

import UIKit

class MasterViewController: UITableViewController {

    @IBOutlet weak var filterSegmentedControl: UISegmentedControl!

    var viewModel = TodosViewModel(todos: []) {
        didSet {
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        listTodos() {
            (response, error) in
            if error == nil {
                store.dispatch(LoadTodosAction(todos: response!))
            } else {
                print("Error: (error?.localizedDescription)")
            }
        }

        filterSegmentedControl.addTarget(self, action:
          #selector(ViewController.filterValueChanged),
          forControlEvents: .ValueChanged)
 
        store.activeFilter.producer.startWithNext {
            filter in
            self.filterSegmentedControl.selectedSegmentIndex =
              filter.rawValue
        }
 
        store.activeTodos.startWithNext {
            todos in
            self.viewModel = TodosViewModel(todos: todos)
        }
 
        store.notSyncedWithBackend.startWithNext {
            todos in
            addOrUpdateTodo(todos) { (response, error) in
                if error == nil {
                    print("Success")
                } else {
                    print("Error: (error?.localizedDescription)")
                }
            }
        }
    }
}

We have viewModel, which is a computed property. In viewDidLoad, we list the Todo items from our backend and we store them in State using LoadTodosAction. Then, we define observations to change our viewModel and to sync changed items with the backend.

IBActions

We will need to define two IBAction, one to add a new item to the list and the other to filter the items:

// MARK: Actions
extension MasterViewController {
    @IBAction func addTapped(sender: UIBarButtonItem) {
        let alertController = UIAlertController(
          title: "Create",
        message: "Create a new todo item",
 preferredStyle: .Alert)
 
    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Id"
    }
 
    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Name"
    }
 
    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Description"
    }
 
    alertController.addTextFieldWithConfigurationHandler() {
        textField in
        textField.placeholder = "Notes"
    }
 
    alertController.addAction(UIAlertAction(title: "Cancel",
      style: .Cancel) { _ in })
 
    alertController.addAction(UIAlertAction(title: "Create",
      style: .Default) { _ in
        guard let id = alertController.textFields?[0].text,
        name = alertController.textFields?[1].text,
        description = alertController.textFields?[2].text,
        notes = alertController.textFields?[3].text
        else { return }
 
        store.dispatch(CreateTodoAction(
          id: Int(id)!,
        name: name,
 description: description,
       notes: notes))
        })
        presentViewController(alertController, animated: false,
          completion: nil)
    }
 
    func filterValueChanged() {
        guard let newFilter = TodoFilter(rawValue:
          filterSegmentedControl.selectedSegmentIndex)
        else { return }
 
        store.dispatch(SetFilterAction(filter: newFilter))
    }
}

In the addTapped method, we use createTodoAction to add an item to the list with the completed and synced values as false. Therefore, store.notSyncedWithBackend.startWithNext in viewDidLoad will observe this item as not synced and will sync it with the backend.

TableView Delegates and DataSource

Finally, we need to implement the delegates and datasource methods for UITableViewController. The code is as follows:

// MARK: UITableViewController
extension MasterViewController {
    override func tableView(tableView: UITableView,
      numberOfRowsInSection section: Int) -> Int {
        return viewModel.todos.count
    }
 
    override func tableView(tableView: UITableView, cellForRowAtIndexPath
      indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("todoCell",
          forIndexPath: indexPath) as! TodoTableViewCell
        let todo = viewModel.todoForIndexPath(indexPath)
 
        cell.configure(todo)
 
        return cell
    }
 
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath
      indexPath: NSIndexPath) {
        let todo = viewModel.todoForIndexPath(indexPath)
        store.dispatch(ToggleCompletedAction(todo: todo))
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
 
    override func tableView(tableView: UITableView, commitEditingStyle
      editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath
      indexPath: NSIndexPath) {

    }
 
    override func tableView(tableView: UITableView,
      editActionsForRowAtIndexPath indexPath: NSIndexPath)
      -> [UITableViewRowAction]? {
        let delete = UITableViewRowAction(style: .Normal, title: "Delete")
          { action, index in
            let todo = self.viewModel.todoForIndexPath(indexPath)
            store.dispatch(DeleteTodoAction(todo: todo))
        }
        delete.backgroundColor = UIColor.redColor()
 
        let details = UITableViewRowAction(style: .Normal,
          title: "Details") { action, index in
            let todo = self.viewModel.todoForIndexPath(indexPath)
            store.dispatch(DetailsTodoAction(todo: todo))
 
            self.performSegueWithIdentifier("segueShowDetails",
              sender: self)
        }
        details.backgroundColor = UIColor.orangeColor()
 
        return [details, delete]
    }
 
    override func tableView(tableView: UITableView, canEditRowAtIndexPath
      indexPath: NSIndexPath) -> Bool {
        // the cells you would like the actions to appear need to
          be editable
        return true
    }
}

In the preceding code, we use DeleteTodoAction to delete an item by swiping to the left and selecting Delete. We use ToggleCompletedAction to mark an item as completed when we tap on any item on the list, and we use DetailsTodoAction to navigate to the details page when we swipe to the left and select Details.

DetailsViewController

We will use viewController to present the details of a Todo item and modify it. We will have three textField and a switch. We will observe the changes in the UI and modify the State and backend. The code is as follows:

import UIKit
import ReactiveCocoa

class DetailsViewController: UIViewController {
 
    @IBOutlet weak var txtFieldName: UITextField!
    @IBOutlet weak var txtFieldDescription: UITextField!
    @IBOutlet weak var txtFieldNotes: UITextField!
    @IBOutlet weak var switchCompleted: UISwitch!
 
    var viewModel = TodoViewModel(todo: nil)
 
    override func viewDidLoad() {
        super.viewDidLoad()
        store.selectedTodo.startWithNext { todos in
            let model = todos.first!
            self.txtFieldName.text = model.name
            self.txtFieldDescription.text = model.description
            self.txtFieldNotes.text = model.notes
            self.switchCompleted.on = model.completed
            self.viewModel = TodoViewModel(todo: model)
        }
        setupUpdateSignals()
    }
 
    func setupUpdateSignals() {
        txtFieldName.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newName = next as? String {
                let newTodo = todoNameLens.set(newName,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))
            }
        }
 
        txtFieldDescription.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newDescription = next as? String {
                let newTodo = todoDescriptionLens.set(newDescription,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))
            }
        }
 
        txtFieldNotes.rac_textSignal().subscribeNext {
            (next: AnyObject!) -> () in
            if let newNotes = next as? String {
                let newTodo = todoNotesLens.set(newNotes,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))

            }
        }
 
        switchCompleted.rac_newOnChannel().subscribeNext {
            (next: AnyObject!) -> () in
            if let newCompleted = next as? Bool {
                let newTodo = todoCompletedLens.set(newCompleted,
                  self.viewModel.todo!)
                store.dispatch(UpdateTodoAction(todo: newTodo))

            }
        }
    }
}

In our viewDidLoad method, we look for the selected item in MasterViewController before navigating to DetailsViewController. We will also set the UITextField and UISwitch initial values. We will subscribe to changes in the UI, use lenses to update the Todo item, and change the state via UpdateTodoAction. Any item change will set synced as false. Since this property is observed in MasterViewController, any changes to the UI in DetailsViewController will be synced with the backend without any extra effort.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset