This final section will depicts an implementation on the iOS platform for the introduced architecture.
The Word
type only contains some read-only properties and a constructor, just like the Android conterpart.
struct Word {
let id: Int
let word: String
let day: Int
let month: Int
let year: Int
let imageUrl: String
init(id: Int, word: String, day: Int, month: Int,
year: Int, imageUrl: String){
self.id = id
self.word = word
self.day = day
self.month = month
self.year = year
self.imageUrl = imageUrl
}
}
The WordResponse
struct only wraps an array of Word
, and also the logic for failed requests (that is omitted for the sake of concisness).
struct WordResponse {
let words: [Word]
init(values: [Word]) {
words = values
}
}
The WordService
class only expose a public method that returns an SignalProducer<WordResponse>
. Also in this case, the real implementation of this method is not important for the purpose of this thesis, and it's omitted.
class WordService {
init() {}
func getWords(month: Int, year: Int)
-> SignalProducer<WordResponse, NSError> {
...
}
}
The additional data binding layer introduced in Android, in iOS and RAC is already in place, with the MutableProperty
and ConstantProperty
types that are already depicted in the section about RAC.
Also the viewmodel layer is pretty the same as its Android counterpart. All the abstraction used are similar, and even the name are pretty much the same, so there's no need to repeat the description of the code.
The only things that are relevant in this implementation are the types used and the chain of operators.
class WordListViewModel {
let isLoading = MutableProperty<Bool>(false)
let words = MutableProperty<[WordViewModel]>([WordViewModel]())
private let wordService: WordService
init(wordService: WordService) {
self.wordService = wordService
// retrieve the words, create a view model for each words, and update
// the overall view model
self.wordService.getWords(1, year: 2015)
|> on(started: { self.isLoading.put(true) })
|> flatMap(FlattenStrategy.Latest,
{ SignalProducer(values: $0.words) })
|> map({ WordViewModel(word: $0)})
|> collect
|> observeOn(QueueScheduler.mainQueueScheduler)
|> start(next: {
wordViewModelList in
self.isLoading.put(false)
self.words.put(wordViewModelList)
}, error: {
self.isLoading.put(false)
println("Error \($0)")
})
}
}
The same arguments are valid for the WordViewModel
class.
class WordViewModel {
let wordTitle: ConstantProperty<String>
let day: ConstantProperty<Int>
let month: ConstantProperty<Int>
let year: ConstantProperty<Int>
let imageUrl: ConstantProperty<String>
init (word: Word) {
self.wordTitle = ConstantProperty(word.word)
self.day = ConstantProperty(word.day)
self.month = ConstantProperty(word.month)
self.year = ConstantProperty(word.year)
self.imageUrl = ConstantProperty(word.imageUrl)
}
}
The view layer is represented by three main components:
UIViewController
, named WordsViewController
, that contains a UITableView
;UITableViewCell
, named WordCellView
;TableViewBindingHelper
, that make it easier to bind a table view with a viewmodel;The entry point of the application is the WordsViewController
class, that is shown at app launch. The implementation is pretty similar to the implementation of the MainActivityFragment
of the previous section.
The view controller creates a WordService
, that then is passed to a WordListViewModel
.
After these operation, it binds the viewmodel's properties:
The last things that the view controller performs is to register its interest in the item selection events from the table view. In this simple use case, the view controller just shows a message in an alert view, but in more real use case scenario this can be the entry point for a presentation of another view controller or other actions.
class WordsViewController: UIViewController {
@IBOutlet weak var loadActivityIndicator: UIActivityIndicatorView!
@IBOutlet weak var wordsTable: UITableView!
private var bindingHelper: TableViewBindingHelper<WordViewModel>!
override func viewDidLoad() {
super.viewDidLoad()
let wordService = WordService()
let viewModel = WordListViewModel(wordService: wordService)
// bind ui to current loading status
loadActivityIndicator.rac_hidden
<~ viewModel.isLoading.producer |> map { !$0 }
wordsTable.rac_alpha
<~ viewModel.isLoading.producer |> map { $0 ? CGFloat(0.5) : CGFloat(1.0) }
// bind the table view with the view model
bindingHelper = TableViewBindingHelper(tableView: wordsTable,
sourceSignal: viewModel.words.producer, nibName: "WordCell")
// observe selection event and show another view with the selected content
bindingHelper.getTableViewSelectedItemSignal()
|> observeOn(UIScheduler())
|> observe(next: { self.showWordDetail($0) })
}
func showWordDetail(wordViewModel: WordViewModel) {
// simply showing an alert..
let alert = UIAlertView(title: "Selection",
message: "You selected: \(wordViewModel.wordTitle.value)",
delegate: nil,
cancelButtonTitle: nil, otherButtonTitles: "Ok")
alert.show()
}
}
The WordCellView
class represents a single cell in the table view, and use the property of the viewmodel to which is bind to show the values of the current word.
class WordCellView: UITableViewCell, ReactiveView {
@IBOutlet weak var yearLabel: UILabel!
... // other ui stuff
func bindViewModel(viewModel: AnyObject) {
let triggerSignal = self.rac_prepareForReuseSignal.asSignal() |> toVoidSignal
if let wordViewModel = viewModel as? WordViewModel {
// bind the text of the labels to its value
yearLabel.rac_text <~ wordViewModel.year.producer |> map { "\($0)" }
monthLabel.rac_text <~ wordViewModel.month.producer |> map { "\($0)" }
dayLabel.rac_text <~ wordViewModel.day.producer |> map { "\($0)" }
wordLabel.rac_text <~ wordViewModel.wordTitle.producer
// download the image
picImageSignalProducer(wordViewModel.imageUrl.value)
|> startOn(scheduler)
|> takeUntil(triggerSignal)
|> observeOn(QueueScheduler.mainQueueScheduler)
|> on(started: { self.wordImageView.image = nil })
|> start(next: {
self.wordImageView.image = $0
})
}
}
private func picImageSignalProducer(imageUrl: String)
-> SignalProducer<UIImage, NoError> {
...
// download and returns the image
}
}
The TableViewBindingHelper
is an helper class, that save the developer a lot of boilerplate code. The relevant part of the code is the following, which illustrates an initializer that takes a SignalProducer
of items (viewmodels for the cells of the table view) and a public method that returns a Signal of items, representing the items selected by the user.
// a helper that makes it easier to bind to UITableView instances
// initial implementation: http://www.scottlogic.com/blog/2014/05/11/reactivecocoa-tableview-binding.html
class TableViewBindingHelper<T: AnyObject> : NSObject, UITableViewDelegate {
//MARK: Properties
...
//MARK: Public API
init(tableView: UITableView,
sourceSignal: SignalProducer<[T], NoError>, nibName: String) {
...
sourceSignal.start(next: {
data in
// reload the table view each time a new array of viewmodels is set
self.dataSource.data = data.map { $0 as AnyObject }
self.tableView.reloadData()
})
}
// returns a Signal which emits the items selected by the user
func getTableViewSelectedItemSignal() -> Signal<T, NoError> {
...
}
}