结合@Published 属性:在更新期间从其他地方获取当前值

Posted

技术标签:

【中文标题】结合@Published 属性:在更新期间从其他地方获取当前值【英文标题】:Combine @Published property: get current value during update, from elsewhere 【发布时间】:2020-04-02 04:17:46 【问题描述】:

我的主要问题是我正在尝试解决(未记录的)事实,即@Published 属性在通知订阅者更改之前不会更新属性的值。我似乎无法解决它。

考虑以下人为组合的 Subject@Published 属性。首先,一个简单的类:

class StringPager 
    @Published var page = 1
    @Published var string = ""

let pager = StringPager()

然后是一个简单的直通主题:

let stringSubject = PassthroughSubject<String, Never>()

为了调试,我们订阅字符串属性并打印出来:

pager.$string.sink  print($0) 

到目前为止一切顺利。接下来,让我们订阅主题并根据其值更改寻呼机:

stringSubject.sink  string in
  if pager.page == 1 
    pager.string = string
   else 
    pager.string = string.uppercased()
  

希望这个逻辑允许我们在不在第一页时将寻呼机字符串设为大写。

现在让我们在页面更新时通过 stringSubject 发送值:

pager.$page.sink  
  $0 == 1 ? stringSubject.send("lowercase") : stringSubject.send("uppercase") 

如果我们的逻辑是正确的,那么小写字母总是小写,而大写字母总是大写。不幸的是,这根本不是发生的事情。这是一个示例输出:

pager.page = 1 // lowercase
pager.page = 2 // uppercase
pager.page = 3 // UPPERCASE
pager.page = 4 // UPPERCASE
pager.page = 1 // LOWERCASE
pager.page = 1 // lowercase

原因是当我们订阅主题时,我们检查pager.page的值...但是更新pager.page是触发主题关闭的原因,所以pager.page没有更新的值然而,主题执行了错误的分支。

我已经尝试在沉没之前通过 ziping pager.$page 和主题来解决这个问题:

stringSubject.zip(pager.$page).eraseToAnyPublisher().sink  ...same code... 

以及combineLatesting它:

stringSubject.combineLatest(pager.$page).eraseToAnyPublisher().sink  ...same code... 

但这会导致观察到的完全相同的行为(在前一种情况下)或同样不受欢迎的行为,除了更多的行为(在后一种情况下)。

我怎样才能获得当前页面主题sink闭包?

【问题讨论】:

保持简单,使用 $page publisher 和 map 操作符然后下沉它来更新字符串的值。合并不是关于突变,而是使用运算符转换值 【参考方案1】:

问题在于您实际上并没有使用 Combine 框架的强大功能。您不应该使用sink 闭包来查看任何值。这颠覆了Combine的全部观点!相反,您应该让该值流入您的sink,作为您构建的组合管道的一部分。

让我们从您的 StringPager 开始:

class StringPager 
    @Published var page = 1
    @Published var string = "this is a test"

让我们有一个具有 StringPager 的视图控制器类,以及一些设置其字符串或页码的测试按钮:

class ViewController: UIViewController 
    let pager = StringPager()
    var storage = Set<AnyCancellable>()
    override func viewDidLoad() 
        // ...
    
    @IBAction func doButton(_ sender: Any) 
        let i = Int.random(in: 1...4)
        print("setting page to \(i)")
        self.pager.page = i
    
    @IBAction func doButton2(_ sender: Any) 
        let s = ["Manny", "Moe", "Jack"].randomElement()!
        print("setting string to \(s)")
        self.pager.string = s
    

你知道这个测试台的想法是什么。我们单击第一个按钮或第二个按钮,然后将寻呼机的页面或字符串随机设置为一个新值,在控制台中报告它是什么。

我们现在的目标是创建一个组合管道,该管道将在每次字符串或页面更改时根据寻呼机的页面是否为 1 输出大写或非大写版本的寻呼机字符串。我会在viewDidLoad 中这样做。监视多个发布者中的任何一个更改并在其中任何一个更改时报告其当前值的 Combine 运算符是 combineLatest,所以这就是我将使用的:

    pager.$page.combineLatest(pager.$string)
        .map p,s in p > 1 ? s.uppercased() : s
        .sink print($0).store(in:&storage)

就是这样!现在我将点击几次按钮,看看我们得到了什么:

this is a test
setting page to 3
THIS IS A TEST
setting page to 3
THIS IS A TEST
setting page to 1
this is a test
setting string to Jack
Jack
setting string to Manny
Manny
setting page to 1
Manny
setting page to 1
Manny
setting page to 4
MANNY
setting string to Manny
MANNY
setting string to Moe
MOE
setting string to Jack
JACK

如您所见,每次我们更改页面或字符串时,我们都会从管道末端获得一个新值,并且它是正确的值!如果页面大于 1,我们得到字符串的大写版本;如果页面为 1,我们将按原样获取字符串。任务完成!

【讨论】:

最后一个在 SwiftUI 之外的经典 MVC 设置中利用 Combine 的小例子。这真的是我找到的第一个。 @Ruiz 如果你不存储从sink 返回的cancellable,它将在某个时候被释放 @halil_g 我明白为什么 sink 本身的返回值会被释放,但它们是否还没有存储在 pagestring StringPager 值中?【参考方案2】:

我从您的意图中了解到,我认为您希望控制 lowercaseUPPERCASE 的页码。但是您在一定程度上将您的逻辑应用于Combine 框架的预期工作。正如@user1046037 提出的问题中的一位 cmets 所说:

Combine 与突变无关。相反,您应该使用它来随着时间的推移改变您的价值观。

所以不应该是page 订阅者触发了string 发布者的值突变。相反,您将故意更改string 的值。然后您可以将该值转换为绑定到page 的所需逻辑。这些逻辑应该进入对象本身。让我们看看我的意思:

class StringPager 
    @Published var page = 0
    @Published var string = "lorem ipsum"

    private var cancellableBag = Set<AnyCancellable>()

    init() 

        let publisher = $page
            .map  [unowned self] in
                return $0 == 1 ? self.string.lowercased() : self.string.uppercased()
        

        publisher
            .eraseToAnyPublisher()
            .assign(to: \.string, on: self)
            .store(in: &cancellableBag) // must store the subscriber to get the events
    

然后,当您更改页面值时,您将获得当时字符串将保留的字符串值的预期大小写版本。

let pager = StringPager()
pager.$string.sink  print($0) 
pager.page = 1 // lorem ipsum
pager.page = 2 // LOREM IPSUM
pager.page = 3 // LOREM IPSUM
pager.page = 4 // LOREM IPSUM
pager.page = 1 // lorem ipsum
pager.page = 1 // lorem ipsum

当您需要将字符串更新为之前设置值以外的任何值时,它将独立于页面转换。这是什么意思?

pager.string = "new value" // new value

直到你故意重新设置你的页面:

pager.page = 3 // NEW VALUE

【讨论】:

以上是关于结合@Published 属性:在更新期间从其他地方获取当前值的主要内容,如果未能解决你的问题,请参考以下文章

Swift Combine - @Published 属性数组

来自 @Published 属性的 SwiftUI 动画从视图外部更改

相当于在 Swift Combine 中使用 @Published 计算的属性?

如何在 Combine 中最好地创建 @Published 值的发布者聚合?

无法从 Class 更新 Published var

EFCore:简单属性更新期间的实体关联错误