My main problem is that I'm trying to work around the (undocumented) fact that @Published
properties don't update the property's value until after subscribers have been notified of the change. I can't seem to get a good way around it.
Consider the following contrived combination of a Subject
and @Published
properties. First, a simple class:
class StringPager {
@Published var page = 1
@Published var string = ""
}
let pager = StringPager()
And then a simple passthrough subject:
let stringSubject = PassthroughSubject<String, Never>()
For debugging, let's subscribe to the string property and print it out:
pager.$string.sink { print($0) }
So far so good. Next, let's subscribe to the subject and alter the pager based on its value:
stringSubject.sink { string in
if pager.page == 1 {
pager.string = string
} else {
pager.string = string.uppercased()
}
}
Hopefully, this logic will allow us to make the pager string uppercased whenever we're not on the first page.
Now let's send values through the stringSubject when the page gets updated:
pager.$page.sink {
$0 == 1 ? stringSubject.send("lowercase") : stringSubject.send("uppercase")
}
If we've gotten this logic right, then lowercase will always be lowercased, while uppercase will always be uppercased. Unfortunately, that's not at all what happens. Here's a sample output:
pager.page = 1 // lowercase
pager.page = 2 // uppercase
pager.page = 3 // UPPERCASE
pager.page = 4 // UPPERCASE
pager.page = 1 // LOWERCASE
pager.page = 1 // lowercase
The reason for this is when we subscribe to the subject, we check the value of pager.page
... but updating pager.page
is what triggers the subject's closure, so the pager.page
doesn't have an updated value yet, so the subject executes the wrong branch.
I've tried fixing this by both zip
ing the pager.$page
with the subject before sinking:
stringSubject.zip(pager.$page).eraseToAnyPublisher().sink { ...same code... }
as well as combineLatest
ing it:
stringSubject.combineLatest(pager.$page).eraseToAnyPublisher().sink { ...same code... }
but that leads either to the exact same observed behavior (in the former case) or equally undesired behavior except more of it (in the latter case).
How can I get the current page within the subject sink
closure?
The problem is that you're not actually using the power of the Combine framework. You should not be using a sink
closure to look inside any value. That subverts the whole point of Combine! Instead, you should let that value flow into your sink
as part of the Combine pipeline that you construct.
Let's start with your StringPager:
class StringPager {
@Published var page = 1
@Published var string = "this is a test"
}
And let's have a view controller class that has a StringPager, along with some test buttons that set its string or its page number:
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
}
}
You see what the idea of this test bed is. We click the first button or the second button, and we set the pager's page or string, randomly, to a new value, reporting in the console what it is.
Our goal now is create a Combine pipeline that will spit out an uppercase or a non-uppercase version of the pager's string, depending on whether the pager's page is 1 or not, every time the string or the page changes. I'll do that in viewDidLoad
. The Combine operator that watches for any of multiple publishers to change and reports their current values when either of them changes is combineLatest
, so that's what I'll use:
pager.$page.combineLatest(pager.$string)
.map {p,s in p > 1 ? s.uppercased() : s}
.sink {print($0)}.store(in:&storage)
That's it! Now I'll tap the buttons a few times and let's see what we get:
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
As you can see, every time we change the page or the string, we get a new value out the end of the pipeline, and it is the right value! If the page is greater than 1, we get an uppercase version of the string; if the page is 1, we get the string as is. Mission accomplished!
What I understand from your intention is, I think you want to control lowercase & UPPERCASE with respect to the page number. But you are taking your logic to an extent what is not the intended job of the Combine
framework. As one of the comments in your question by @user1046037 mentions:
Combine
is not about mutation. Instead you should be using it for transforming your values over time.
So it should not be the page
subscriber which triggers the value mutation of the string
publisher. Instead you will deliberately change the value of string
. Then you can transform the value to your desired logic bound to the page
. And these logic should go into the object itself. Let's see what I meant:
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
}
}
Then when you change your page value, you will get the expected cased version of the string value that the string at that time will hold.
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
When you need to update your string to any value other than the previous set value, it will be independent from the page transformation. What does this mean?
pager.string = "new value" // new value
Until you deliberately set your page again:
pager.page = 3 // NEW VALUE
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With