观察字符串并使用RxSwift从API获取

观察字符串并使用RxSwift从API获取,swift,mvvm,rx-swift,rx-cocoa,Swift,Mvvm,Rx Swift,Rx Cocoa,我有一个MVVM测试项目来测试RxSwift。我有一个UItextfield和一个按钮。用户输入一个食物名称,点击按钮,就会触发一个来自API的get来获取该食物的所有食谱 查看模型 struct FoodViewModel var foodIdentifier: Variable<String> = Variable<String>("") init() { foodIdentifier.asObservable().subscr

我有一个MVVM测试项目来测试RxSwift。我有一个UItextfield和一个按钮。用户输入一个食物名称,点击按钮,就会触发一个来自API的get来获取该食物的所有食谱

查看模型

struct FoodViewModel
    var foodIdentifier: Variable<String> = Variable<String>("")
    init() {    
        foodIdentifier.asObservable().subscribe(onNext: { (identifier) in
            self.getRecipes() // Get from API
        })
    }
}
编译之后,我得到了一个错误

Closure cannot implicitly capture a mutating self parameter
我做错了什么?我想这是因为FoodViewModel的结构。如果是,如何使用struct实现该功能?

--EDIT

我写了下面的所有内容,但忘了回答你明确的问题。。。出现错误的原因是,您试图在self为结构的闭包中捕获self。如果允许的话,您将捕获一个甚至还没有完成构建的视图模型的副本。将视图模型切换到类可以缓解问题,因为您不再捕获副本,而是捕获对象本身以供以后使用


下面是设置视图模型的更好方法。你没有提供所有必要的信息,所以我有点放肆

首先我们需要一个模型。我不知道食谱里应该有什么,所以你得填上

struct Recipe { }
接下来是视图模型。请注意,它不会直接连接到UI或服务器中的任何内容。这使得测试非常容易

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) })
    }
}
协议API{
func getRecipies(带食物:字符串)->可观察
}
协议食物源{
var foodText:Observable{get}
}
结构FoodViewModel{
让食谱:可观察
init(api:api,源代码:FoodSource){
recipes=source.foodText
.flatMapLatest({api.getRecipies(withFood:$0)})
}
}
在实际代码中,您不会希望每次用户键入字母时都进行新的服务器调用。网络上有很多例子解释了如何建立一个延迟,直到用户在打电话之前停止输入

然后就是实际的视图控制器。您没有提到要对服务器调用的结果做什么。也许您想将结果绑定到表视图?我只是在这里打印结果

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()
}
类FoodViewController:UIViewController,FoodSource{
@IBOutlet弱var foodTextField:UITextField!
var-api:api!
重写func viewDidLoad(){
super.viewDidLoad()
让viewModel=FoodViewModel(api:api,来源:self)
viewModel.recipes.subscribe(onNext:{
印刷品(0美元)
}).处置(由:袋)
}
var foodText:可观察{
返回foodTextField.rx.text.map{$0??“}.asObservable()
}
let bag=DisposeBag()
}
注意我们如何避免进行iAction。当您使用Rx对视图控制器进行编码时,您会发现几乎所有的代码都以viewDidLoad方法结束。这是因为使用Rx时,您主要担心的是接线问题。一旦可观察对象连接起来,用户操作将导致事情发生。这更像是编写电子表格。你只需要输入公式,把观测值联系起来。用户的数据输入负责实际操作

以上只是设置一切的一种方式。该方法与Srdan Rasic的模型非常匹配:

您还可以将food view模型转换为纯函数,如下所示:

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)
}
struct FoodSink{
让食谱:可观察
}
func foodViewModel(api:api,source:FoodSource)->FoodSink{
让recipes=source.foodText
.flatMapLatest({api.getRecipies(withFood:$0)})
返回食物链接(配方:配方)
}

一个外卖从这个。。。尽量避免使用
主题
变量
。这里有一篇很好的文章,可以帮助您确定何时使用主题或变量是合适的:

您在哪里实例化您的VM?我认为您不应该将订阅直接放在viewModel的init中。在订阅的VM中应该有一个方法listenToFoodIdentifier,在VC中实例化VM后,调用viewModel.listenToFoodIdentifier()原因是结构是值类型,闭包的实例将获得其自己的、捕获值的独立副本,只有它可以更改。这个值是在执行闭包时捕获的。这个答案将帮助您:为什么要如此避免类?要使用Rx闭包,您需要获取订阅对象的引用(通常使用[unowned self]避免强引用循环),而不是该对象的值,因此类不是structsWow!谢谢,这就是我需要的答案:)只是一个小问题,我不明白FoodSource的用途它是TableView数据源?如果是,为什么是字符串?关于按钮,我希望字符串仅在用户单击按钮时发送到API。
FoodSource
是表示用户输入的协议。视图控制器通过将
foodText
绑定到
foodTextField
来实现该协议。要连接到按钮,您需要向
FoodSource
协议添加一个
可观察的
,以表示按钮,将observable绑定到视图控制器中的特定按钮,然后使用视图模型的
init
方法中的
sample
操作符,确保文本仅在点击按钮时通过管道。@DanielT。我遵循上面的方法,但是如何获得
可观察的
s的当前值(我在发出API请求时需要它)?我不需要
变量
而不是
可观察的
?像上面那样使用平面图。
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)
}