SwiftUI with complex MVVM (Repository + Nested ObservedObject) - mvvm

Explanation
I am still in the process of learning to utilize SwiftUI patterns in the most optimal way. But most SwiftUI MVVM implementation examples I find are very simplistic. They usually have one database class and then 1-2 viewmodels that take data from there and then you have views.
In my app, I have a SQLite DB, Firebase and different areas of content. So I have a few separate model-vm-view paths. In the Android equivalent of my app, I used a pattern like this:
View - ViewModel - Repository - Database
This way I can separate DB logic like all SQL queries in the repository classes and have the VM handle only view related logic. So the whole thing looks something like this:
In Android this works fine, because I just pass through the LiveData object to the view. But when trying this pattern in SwiftUI, I kind of hit a wall:
It doesn't work / I don't know how to correctly connect the Published objects of both
The idea of "chaining" or nesting ObservableObjects seems to be frowned upon:
This article about Nested Observable Objects in SwiftUI:
I’ve seen this pattern described as “nested observable objects”, and it’s a subtle quirk of SwiftUI and how the Combine ObservableObject protocol works that can be surprising. You can work around this, and get your view updating with some tweaks to the top level object, but I’m not sure that I’d suggest this as a good practice. When you hit this pattern, it’s a good time to step back and look at the bigger picture.
So it seems like one is being pushed towards using the simpler pattern of:
View - ViewModel - Database Repository
Without the repository in-between. But this seems annoying to me, it would make my viewmodel classes bloated and would mix UI/business code with SQL queries.
My Code
So this is a simplified version of my code to demonstrate the problem:
Repository:
class SA_Repository: ObservableObject {
#Published var selfAffirmations: [SelfAffirmation]?
private var dbQueue: DatabaseQueue?
init() {
do {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
// Etc. other SQL code
} catch {
print(error.localizedDescription)
}
}
private func fetchSelfAffirmations() {
let saObservation = ValueObservation.tracking { db in
try SelfAffirmation.fetchAll(db)
}
if let unwrappedDbQueue = dbQueue {
let _ = saObservation.start(
in: unwrappedDbQueue,
scheduling: .immediate,
onError: {error in print(error.localizedDescription)},
onChange: {selfAffirmations in
print("change in SA table noticed")
self.selfAffirmations = selfAffirmations
})
}
}
public func updateSA() {...}
public func insertSA() {...}
// Etc.
}
ViewModel:
class SA_ViewModel: ObservableObject {
#ObservedObject private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
init() {
selfAffirmations = saRepository.selfAffirmations ?? []
}
public func updateSA() {...}
public func insertSA() {...}
// + all the Firebase stuff later on
}
View:
struct SA_View: View {
#ObservedObject var saViewModel = SA_ViewModel()
var body: some View {
NavigationView {
List(saViewModel.selfAffirmations, id: \.id) { selfAffirmation in
SA_ListitemView(content: selfAffirmation.content,
editedValueCallback: { newString in
saViewModel.updateSA(id: selfAffirmation.id, newContent: newString)
})
}
}
}
}
Attempts
Obviously the way I did it here is wrong, because it clones the data from repo to vm once with selfAffirmations = saRepository.selfAffirmations ?? [] but then it never updates when I edit the entries from the view, only on app restart.
I tried $selfAffirmations = saRepository.$selfAffirmations to just transfer the binding. But the repo one is an optional, so I'd need to make the vm selfAffirmations an optional too, which would then mean handling unnecessary logic in the view code. And not sure if it would even work at all.
I tried to do it manually with Combine but this way seemed to not be recommended and fragile. Plus it also didn't work:
selfAffirmations = saRepository.selfAffirmations ?? []
cancellable = saRepository.$selfAffirmations.sink(
receiveValue: { [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
)
Question
Overall I would just need some way to pass through the data from the repo to the view, but have the vm be in the middle as a separator. I read about the PassthroughSubject in Combine, which sounds like it would be fitting, but I'm not sure if I am just misunderstanding some concepts here.
Now I am not sure if my architecture concepts are wrong/unfitting, or if I just don't understand enough about Combine publishers yet to make this work.
Any advice would be appreciated.

After getting some input from the comments, I figured out a clean way.
The problem for me was understanding how to make a property of a class publish its values. Because the comments suggested that property wrappers like #ObservedObject was a frontend/SwiftUI only thing, making me assume that everything related was limited to that too, like #Published.
So I was looking for something like selfAffirmations.makePublisher {...}, something that would make my property a subscribable value emitter. I found that arrays naturally come with a .publisher property, but this one seems to only emit the values once and never again.
Eventually I figured out that #Published can be used without #ObservableObject and still work properly! It turns any property into a published property.
So now my setup looks like this:
Repository (using GRDB.swift btw):
class SA_Repository {
private var dbQueue: DatabaseQueue?
#Published var selfAffirmations: [SelfAffirmation]?
// Set of cancellables so they live as long as needed and get deinitialiazed with the class end
var subscriptions = Array<DatabaseCancellable>()
init() {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
}
private func fetchSelfAffirmations() {
// DB code....
}
}
And viewmodel:
class SA_ViewModel: ObservableObject {
private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
// Set of cancellables to keep them running
var subscriptions = Set<AnyCancellable>()
init() {
saRepository.$selfAffirmations
.sink{ [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
.store(in: &subscriptions)
}
}

Related

Access and modify a #EnvironmentObject on global functions

I have an ObservableObject declared on my main view (ContentView.swift).
final class DataModel: ObservableObject {
#AppStorage("stuff") public var notes: [NoteItem] = []
}
Then I declare it in the main entry of the app as (removed extra code not needed for this example):
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
In the ContentView.swift, I can use it on the different views I declared there:
struct NoteView: View {
#EnvironmentObject private var data: DataModel
// more code follows...
}
Now, I have a collection of global functions saved on FileFunctions.swift, which essentially are functions that interact with files on disk. One of them is to load those files and their content into my app.
Now, I'm trying to use #EnvironmentObject private var data: DataModel in those functions so at loading time, I can populate the data model with the actual data from the files. And when I declare that either as a global declaration in FileFunctions.swift or inside each function separately, I get two behaviors.
With the first one I get an error:
Global 'var' declaration requires an initializer expression or an explicitly stated getter`,
and
Property wrappers are not yet supported in top-level code
I tried to initialize it in any way, but it goes nowhere. With the second one, adding them to each function, Xcode craps on me with a segfault. Even if I remove the private and try to declare it in different ways, I get nowhere.
I tried the solution in Access environment variable inside global function - SwiftUI + CoreData, but the more I move things around the worse it gets.
So, how would I access this ObservableObject, and how would I be able to modify it within global functions?
Below is an example of a global function and how it's being called.
In FileFunctions.swift I have:
func loadFiles() {
var text: String = ""
var title: String = ""
var date: Date
do {
let directoryURL = try resolveURL(for: "savedDirectory")
if directoryURL.startAccessingSecurityScopedResource() {
let contents = try FileManager.default.contentsOfDirectory(at: directoryURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles])
for file in contents {
text = readFile(filename: file.path)
date = getModifiedDate(filename: file.absoluteURL)
title = text.components(separatedBy: NSCharacterSet.newlines).first!
// I need to save this info to the DataModel here
}
directoryURL.stopAccessingSecurityScopedResource()
} else {
Alert(title: Text("Couldn't load notes"),
message: Text("Make sure the directory where the notes are stored is accessible."),
dismissButton: .default(Text("OK")))
}
} catch let error as ResolveError {
print("Resolve error:", error)
return
} catch {
print(error)
return
}
}
And I call this function from here:
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles()
}
}
}
You could change the signature of the global functions to allow receiving the model:
func loadFiles(dataModel: DataModel) { ... }
This way, you have access to the model instance within the function, what's left to do is to pass it at the call site:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles(dataModel: self.dataModel)
}
You can do the same if the global functions calls originate from the views.
I would do something like this :
final class DataModel: ObservableObject {
public static let shared = DataModel()
#AppStorage("stuff") public var notes: [NoteItem] = []
}
#main struct The_NoteApp: App {
private let dataModel = DataModel.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
now in your viewModel you can access it like this
class AnyClass {
init (){
print(DataModel.shared.notes)
}
// or
func printNotes(){
print(DataModel.shared.notes)
}
}
As discussed in the comments, here a basic approach which makes some changes to the structure by defining dedicated "components" which have a certain role and which are decoupled as far as necessary.
I usually define a namespace for a "feature" where I put every "component" which is related to it. This offers a couple of advantages which you might recognise soon later:
enum FilesInfo {}
Using a "DataModel" or a "ViewModel" to separate your "Data" from the View
makes sense. A ViewModel - as opposed to DataModel - just obeys the rules from the MVVM pattern. A ViewModel should expose a "binding". I call this "ViewState", which completely describes what the view should render:
extension FilesInfo {
enum ViewState {
struct FileInfo {
var date: Date
var title: String
}
case undefined
case idle([FileInfo])
init() { self = .undefined } // note that!
}
}
Why ViewState is an enum?
Because you might want to represent also a loading state when your load function is asynchronous (almost always the case!) and an error state later. As you can see, you start with a state that's "undefined". You can name it also "zero" or "start", or however you like. It just means: "no data loaded yet".
A view model basically looks like this:
extension FilesInfo {
final class ViewModel: ObservableObject {
#Published private(set) var viewState: ViewState = .init()
...
}
}
Note, that there is a default initialiser for ViewState.
It also may have public functions where you can send "events" to it, which may originate in the view, or elsewhere:
extension FilesInfo.ViewModel {
// gets the view model started.
func load() -> Void {
...
}
// func someAction(with parameter: Param) -> Void
}
Here, the View Model implements load() - possibly in a similar fashion you implemented your loadFiles.
Almost always, a ViewModel operates (like an Actor) on an internal "State", which is not always the same as the ViewState. But your ViewState is a function of the State:
extension FilesInfo.ViewModel {
private struct State {
...
}
private func view(_ state: State) -> ViewState {
//should be a pure function (only depend on state variable)
// Here, you likely just transform the FilesInfo to
// something which is more appropriate to get rendered.
// You call this function whenever the internal state
// changes, and assign the result to the published
// property.
}
}
Now you can define your FileInfosView:
extension FilesInfo {
struct ContentView: View {
let state: ViewState
let action: () -> Void // an "event" function
let requireData: () -> Void // a "require data" event
var body: some View {
...
.onAppear {
if case .undefined = state {
requireData()
}
}
}
}
}
When you look more closely on the ContentView, it has no knowledge from a ViewModel, neither from loadFiles. It only knows about the "ViewState" and it just renders this. It also has no knowledge when the view model is ready, or provides data. But it knows when it should render data but has none and then calls requireData().
Note, it does not take a ViewModel as parameter. Those kind of setups are better done in some dedicated parent view:
extension FilesInfo {
struct CoordinatorView: View {
#ObservedObject viewModel: ViewModel
var body: some View {
ContentView(
state: viewModel.viewState,
action: {},
requireData: viewModel.load
)
}
}
}
Your "coordinator view" deals with separating ViewModel from your specific content view. This is not strictly necessary, but it increases decoupling and you can reuse your ContentView elsewhere with a different ViewModel.
Your CoordinatorView may also be responsible for creating the ViewModel and creating target views for navigation. This depends on what convention you establish.
IMHO, it may make sense, to restrict the access to environment variables to views with a certain role, because this creates a dependency from the view to the environment. We should avoid such coupling.
Also, I would consider mutating environment variables from within Views a "smell". Environment variables should be kind of a configuration which you setup in a certain place in your app (also called "CompositionRoot"). You may end up with an uncontrollable net of variables if you allow that everyone can change any environment variable at any time. When you have "ViewModels" in your environment, these of course get not "mutated" when they change their state - these are classes - for a reason.
Basically, that's it for a very basic but functional MVVM pattern.

Swift memory conflict where it should not happen

I am working on a SwiftUI project, where I use the MVVM-architecture.
When changing a View-model object property from the SwiftUI view, it causes a memory conflict crash in the view-model object.
The error is of the type: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
In steps, here is what happens:
View-model property is changed from view
View-model property changes model property
Model property notifies about changes
View-model receives change notification
View-model access model object
Crash occur due to memory conflict
Relevant code snippets are seen below. Xcode project is a standard SwiftUI project.
The error will happen, after first clicking the add button, and then the modify button.
If the "update" code is moved into the "receiveValue" closure, the error will not occur. Likewise, the error will not occur, if the View-model class is made non-generic.
To my best knowledge, the code is all-right, so I suspect it is a compiler problem. But I am not sure.
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var item: ViewModel<Model> = ViewModel<Model>()
var body: some View {
VStack {
Button("Add", action: { item.add(model:Model()) })
Button("Modify", action: { item.selected.toggle() })
}
}
}
protocol ModelType {
var objectDidChange: ObservableObjectPublisher { get }
var selected: Bool { get set }
}
class Model: ModelType {
let objectDidChange = ObservableObjectPublisher()
var selected = false {
didSet {
objectDidChange.send()
}
}
}
class ViewModel<Model:ModelType>: ObservableObject {
var selected = false {
didSet {
model.selected = selected
}
}
func add(model: Model) {
self.model = model
cancellable = model.objectDidChange.sink(receiveValue: { _ in
self.update()
})
}
private var model: Model! = nil
private var cancellable: AnyCancellable? = nil
func update() {
// Crash log: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
print("update \(model.selected)")
}
}
Short version: require AnyObject for ModelType.
Long version:
You're trying to read from self.model while you're in the middle of setting self.model. When you say "If the "update" code is moved into the "receiveValue" closure, the error will not occur," this isn't quite correct. I expect what you mean is you wrote this:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(model.selected)")
})
And that worked, but that's completely different code. model in this case is the local variable, not the property self.model. You'll get the same crash if you write it this way:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(self.model.selected)")
})
The path that gets you here is:
ViewModel.selected.didSet
WRITE to Model.selected <---
Model.selected.didSet
(observer-closure)
ViewModel.update
READ from ViewModel.model <---
This is a read and write to the same value, and that violates exclusive access. Note that the "value" in question is "the entire ViewModel value," not ViewModel.selected. You can show this by changing the update function to:
print("update \(model!)")
You'll get the same crash.
So why does this work when you take out the generic? Because this particularly strict version of exclusivity only applies to value types (like structs). It doesn't apply to classes. So when this is concrete, Swift knows viewModel is a class, and that's ok. (The why behind this difference a bit complex, so I suggest reading the proposal that explains it.)
When you make this generic, Swift has to be very cautious. It doesn't know that Model is a class, so it applies stricter rules. You can fix this by promising that it's a class:
protocol ModelType: AnyObject { ... }

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....

Is there an alternative to Combine's #Published that signals a value change after it has taken place instead of before?

I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.

Observe a string and get from API with RxSwift

I have a MVVM test project to experiment RxSwift. I have a UItextfield a button. User write a food name, click on the button and a get from an API is triggered to get all recipes with that food.
View model
struct FoodViewModel
var foodIdentifier: Variable<String> = Variable<String>("")
init() {
foodIdentifier.asObservable().subscribe(onNext: { (identifier) in
self.getRecipes() // Get from API
})
}
}
ViewController
class FoodViewController: UIViewController {
#IBOutlet weak var foodTextField: UITextField!
#IBAction func setCurrentRace(_ sender: Any) {
viewModel.foodIdentifier.value = foodTextField.text!
}
}
After compile I got an error
Closure cannot implicitly capture a mutating self parameter
What I'm doing wrong ? I think it's because of struct of FoodViewModel. If yes, how can I achieve that using struct ?
-- EDIT
I wrote all of the below but forgot to answer your explicit question... The reason you are getting the error is because you are trying to capture self in a closure where self is a struct. If this were allowed, you would be capturing a copy of the view model that you haven't even finished constructing. Switching your view model to a class alleviates the problem because you are no longer capturing a copy, but the object itself for later use.
Here is a better way to set up a view model. You didn't give all the necessary information so I took some liberties...
First we need a model. I don't know exactly what should be in a Recipe so you will have to fill it in.
struct Recipe { }
Next we have our view model. Note that it doesn't directly connect with anything in the UI or the server. This makes testing very easy.
protocol API {
func getRecipies(withFood: String) -> Observable<[Recipe]>
}
protocol FoodSource {
var foodText: Observable<String> { get }
}
struct FoodViewModel {
let recipes: Observable<[Recipe]>
init(api: API, source: FoodSource) {
recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
}
}
In real code, you aren't going to want to make a new server call every time the user types a letter. There are a lot of examples on the web that explain how to build in a delay that waits until the user stops typing before making the call.
Then you have the actual view controller. You didn't mention what you wanted to do with the results of the server call. Maybe you want to bind the result to a table view? I'm just printing the results here.
class FoodViewController: UIViewController, FoodSource {
#IBOutlet weak var foodTextField: UITextField!
var api: API!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = FoodViewModel(api: api, source: self)
viewModel.recipes.subscribe(onNext: {
print($0)
}).disposed(by: bag)
}
var foodText: Observable<String> {
return foodTextField.rx.text.map { $0 ?? "" }.asObservable()
}
let bag = DisposeBag()
}
Notice how we avoid having to make an IBAction. when you are coding up a view controller with Rx, you will find that almost all the code ends up in the viewDidLoad method. This is because with Rx, you are mainly just worried about wiring everything up. Once the observables are wired up, user action will cause things to happen. It's more like programming a spreadsheet. You just put in the formulas and link the observables together. User's data entry takes care of the actual action.
The above is just one way of setting everything up. This method matches closely with Srdan Rasic's model: http://rasic.info/a-different-take-on-mvvm-with-swift/
You could also turn the food view model into a pure function like this:
struct FoodSink {
let recipes: Observable<[Recipe]>
}
func foodViewModel(api: API, source: FoodSource) -> FoodSink {
let recipes = source.foodText
.flatMapLatest({ api.getRecipies(withFood: $0) })
return FoodSink(recipes: recipes)
}
One takeaway from this... Try to avoid using Subjects or Variables. Here's a great article that helps determine when using a Subject or Variable is appropriate: http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx