1

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 ?

Ludovic
  • 1,992
  • 1
  • 21
  • 44
  • where do you instantiate your VM? – Ocunidee Mar 01 '17 at 08:52
  • also , you may want to add [unowned self] before (identifier) in the subscription. Why are you not passing identifier to getRecipes()? – Ocunidee Mar 01 '17 at 08:54
  • @Ocunidee The VM is instantiate in the ViewController. Identifer is not pass in the getRecipes because I forget to add it in my question but it's on my code. – Ludovic Mar 01 '17 at 09:00
  • 1
    I don't think you should put your subscription straight in the init of the viewModel. You should have a method listenToFoodIdentifier in VM with the subscription and in your VC after you instantiate the VM, you call viewModel.listenToFoodIdentifier() – Ocunidee Mar 01 '17 at 09:04
  • I guess you should use a class then – Ocunidee Mar 01 '17 at 09:26
  • @Ocunidee But what I'm trying to do is basic RxSwift right ? Listen to a Observable String – Ludovic Mar 01 '17 at 09:27
  • 1
    the reason is that structs are value types and instances of the closure will get their own, independent copy of the captured value that it, and only it, can alter. The value is captured in the time of executing the closure. – Ocunidee Mar 01 '17 at 09:36
  • 1
    This answer will help you: http://stackoverflow.com/questions/41940994/closure-cannot-implicitly-capture-a-mutating-self-paramter – Ocunidee Mar 01 '17 at 09:40
  • Thanks for that but How should I do what I need without class ? I mean It's (agin) basic stuff : exemple research from GitHub get from API when search text field is updated. – Ludovic Mar 01 '17 at 09:47
  • 1
    why do you want to avoid classes so much? to work with Rx closures you need to get the reference of the object in which you have the subscription (usually you avoid a strong ref cycle with [unowned self]) and not the value of that object hence classes not structs – Ocunidee Mar 01 '17 at 09:50
  • Because Im' trying to learn another way to achieve what I did with classes every days :) But maybe it's impossible you are right – Ludovic Mar 01 '17 at 09:51

1 Answers1

1

-- 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

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Wow! Thanks that answer is what I need :) Juste a little question, I don't understand the purpose of FoodSource it's a TableView datasource ? If yes why a string ? About the button, I want the string to be sent to the API only when user clicks on the button. – Ludovic Mar 06 '17 at 11:17
  • `FoodSource` is a protocol that represents user input. The view controller fulfills that protocol by tying `foodText` to the `foodTextField`. – Daniel T. Mar 06 '17 at 16:29
  • 1
    To connect to a button, you would need to add an `Observable` to the `FoodSource` protocol to represent the button, tie the observable to a specific button in the view controller and then use the `sample` operator in the view model's `init` method to make sure the text only goes through the pipe if the button is tapped. – Daniel T. Mar 06 '17 at 16:32
  • @DanielT. I am following the above approach , but how to get the current value (I need it in making a API request) for the `Observable`s? Don't I need `Variable` then instead of `Observable` ? – SandeepAggarwal Nov 12 '17 at 10:35
  • Use a flatMap like I did above. – Daniel T. Nov 12 '17 at 13:03