I develop iOS applications using the Model View ViewModel paradigm to structure my view controllers and to represent their data. That in conjunction with ReactiveCocoa is a powerful tool; view controllers become less bloated, view models are easier to test, and there's a clear separation of concerns.
The one problem I have with this particular architecture is that like MVC, there's still not a clear place or way to structure networking code. Take the following trivial example:
class HomepageViewModel {
var posts: MutableProperty<[Post]> = MutableProperty([])
func fetchPosts() -> SignalProducer<[Post], NSError> {
return SignalProducer { observer, disposable in
// do some networking stuff
let posts = ....
observer.sendNext(posts)
observer.sendCompleted()
}
}
}
Then in my view controller somewhere I can do:
self.viewModel.posts <~ self.viewModel.fetchPosts().on(next: { _ in self.collectionView.reloadData() })
To me it feels like the whole point of using MVVM was to not expose the views and view controller (what I call the view presentation layer) to any networking code whatsoever but I still need a way to be able to observe that new content has been fetched without knowing the specifics, just that a successful fetch occurred. I'd imagine that would look something like:
self.viewModel.contentUpdatedSignal.observeNext { _ in self.collectionView.reloadData() }
At the same time, I also don't want to lose the ability to bind signals and signal producers to mutable properties on my using <~.
class ViewModel {
let someProperty = MutableProperty<[SomeModel]>([])
var (contentUpdatedSignal, observer) = Signal.pipe()
init() {
self.someProperty <~ self.fetchContent().on(next: { _ in observer.sendNext() }
}
func fetchContent() -> SignalProducer<[SomeModel], NSError> {
// do some fun stuff
}
}
This method of doing this is a little better but it still uses side effects to send a next event on the signal observer and if you're using a common ViewModel base class, you have to expose that observer so that subclasses can use it.
I'm looking for any improvements that can be made to the MVVM architecture, either changes to the architecture itself so that it's no longer MVVM and facilitates networking in a better and more generic way or even some sort of generic base protocol for view models that abstracts the whole process away.
For me, being generic as possible while exposing as little info about the view model as possible is key. Ideally, I'd like to have each view controller interact with the view model in exactly the same way in regards to how networking works.
EDIT:
Per at @lonut's suggestion, I moved some of the networking code to my model classes but only as static methods:
import Foundation
import ReactiveCocoa
import Moya
protocol RESTModel {
typealias Model
static func create(parameters: [NSObject: AnyObject]?) -> SignalProducer<Model, Moya.Error>
static func find() -> SignalProducer<Model, Moya.Error>
static func get(id: String) -> SignalProducer<Model, Moya.Error>
static func update(id: String) -> SignalProducer<Model, Moya.Error>
static func remove(id: String) -> SignalProducer<Model, Moya.Error>
}
extension RESTModel {
static func create(parameters: [NSObject: AnyObject]? = nil) -> SignalProducer<Self.Model, Moya.Error> {
return SignalProducer.empty
}
static func find() -> SignalProducer<Self.Model, Moya.Error> {
return SignalProducer.empty
}
static func get(id: String) -> SignalProducer<Self.Model, Moya.Error> {
return SignalProducer.empty
}
static func update(id: String) -> SignalProducer<Self.Model, Moya.Error> {
return SignalProducer.empty
}
static func remove(id: String) -> SignalProducer<Self.Model, Moya.Error> {
return SignalProducer.empty
}
}
This way models can implement network calls at will which has the benefit of abstracting away the implementation details such as specific Moya network calls, mapping of response objects, etc.
Imagine you have a User model:
User.get("myUserID")
It doesn't entirely solve the problem of how the view controller and the view model should interact with each other but it definitely moves the network code to a single point of failure.