I've been working with RxSwift for a few years now, and am starting to explore Combine with SwiftUI and am having some trouble trying to replicate some functionality from RxSwift in Combine.
On the RxSwift GitHub there is an example in a file called ActivityIndicator.swift.
Basic usage is as follows:
class Foo {
let activityIndicator = ActivityIndicator()
lazy var activity = activityIndicator.asDriver()
var disposeBag = DisposeBag()
func doSomething() {
Observable
.just("this is something")
.trackActivity(activityIndicator)
.subscribe()
.disposed(by: disposeBag)
}
}
What this does is allow you to then drive off of the activity driver and it will emit boolean values every time something subscribes or a subscription completes.
You can then directly drive something like a UIActivityIndicatorView's isAnimating property using RxCocoa.
I've been trying to figure out how to create something similar to this in Combine but am not having any luck.
Say I have a viewModel that looks like this:
class ViewModel: ObservableObject {
#Published var isActive = false
func doSomething() -> AnyPublisher<Void, Never> {
Just(())
.delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
What I would like to do is create an operator for a Publisher that will function similarly to how the Rx operator worked where I can forward the events from the subscription through the chain, but change the isActive value every time something subscribes/completes/cancels.
In the SwiftUI View I would initiate the doSomething function and sink to it, while also being able to use the published isActive property to show/hide a ProgressView
Something similar to this:
struct SomeView: View {
let viewModel = ViewModel()
var body: some View {
var cancelBag = Set<AnyCancellable>()
VStack {
Text("This is text")
if viewModel.isActive {
ProgressView()
}
}
.onAppear(perform: {
viewModel
.doSomething()
.sink()
.store(in: &cancelBag)
})
}
}
Is there something that works like this already that I am just completely missing?
If not, how can I go about replicating the RxSwift functionality in Combine?
Thank you in advance for the help.
Looks like someone created a Combine version. I don't know if it has the same issue as discussed by #Daniel T. but it looks promising.
https://github.com/duyquang91/ActivityIndicator
Hmm... The key to the ActivityIndicator class is the Observable.using(_:observableFactory:) operator. Unfortunately, I don't believe there is an equivalent operator in Combine.
The using operator creates a resource when the Observable is subscribed to, and then disposes the resource when the Observable sends a stop event (complete or error.) This insures the resource's lifetime. In this particular case, the resource just increments an Int value on creation and decrements it on disposal.
I think you could kind of mimic the behavior with something like this:
extension Publisher {
func trackActivity(_ activityIndicator: CombineActivityIndicator) -> some Publisher {
return activityIndicator.trackActivity(of: self)
}
}
final class CombineActivityIndicator {
var counter = CurrentValueSubject<Int, Never>(0)
var cancelables = Set<AnyCancellable>()
func trackActivity<Source: Publisher>(of source: Source) -> some Publisher {
let sharedSource = source.share()
counter.value += 1
sharedSource
.sink(
receiveCompletion: { [unowned self] _ in
self.counter.value -= 1
},
receiveValue: { _ in }
)
.store(in: &cancelables)
return sharedSource
}
var asPublisher: AnyPublisher<Bool, Never> {
counter
.map { $0 > 0 }
.eraseToAnyPublisher()
}
}
However, the above class will heat up the Publisher and you might miss emitted values because of it. Use at your own risk, I do not recommend the above unless you are desperate.
Maybe someone has written a using operator for Publisher and will be willing to share.
Related
So I'm getting started with Combine, and I want to have a component which publishes events like a simple event emitter.
So in other words, I want to have a model like this (pseudocode):
class MyModel {
var onNewEvent: EventPublisher
func foo(bar: Bar) {
onNewEvent.publish(Event(bar))
}
}
let model: MyModel...
model.onNewEvent.sink(
receiveValue: { event in print(event) }
)
I can achieve something like this using a notification center publisher, but I want to avoid this unnecessary step if possible.
Is there such a thing as a simple publisher which just publishes values on command, or else what would be the idiomatic way to handle this in combine?
you can use a private(outside world cannot change/emit values) subject inside your model. and then expose it using a lazy publisher to let the outside world to subscribe.
class MyModel {
private var myPassthroughSubject = PassthroughSubject<Bar, Never>() // make this subject `private`, so only inside methods can send values.
lazy var myPublisher = myPassthroughSubject.eraseToAnyPublisher() // expose this publisher to the outside world.
func foo(bar: Bar) {
myPassthroughSubject.send(bar)
}
}
struct Bar {
}
let model = MyModel()
model.myPublisher.sink(
receiveValue: { event in print(event) }
)
model.foo(bar: .init())
model.foo(bar: .init())
When I have a Binding in SwiftUI and I want to save whenever the binding changes I do (e.g. on a TextField)
var myText: String { /* value is derived */ }
func save(_ text: String) { /* doing the save stuff */ }
TextFiel(text: .init(get: { myText }, set: { save($0) })
Doing this, save() gets called whenever the binding changes. In some cases this might not be ideal, e.g. when save() makes a server call or some expensive computations. So what I'm looking for, is to get notified whenever the binding changes for the last time.
Maybe some kind of delayed observer that fires x seconds after the final change and get's invalidated if another change happens earlier than that threshold. Does Combine offer something like this?
Disclaimer: This question is about bindings in general and not just about TextFields in particular. The Textfield is only a coding example , so .onCommit is not the solution I'm looking for ;)
The debounce operator in Combine does this. It waits until a new value hasn't been pushed through the pipeline for X amount of time and then sends a signal.
In the case of a TextField, you'll still want a "normal" binding, because you'd want the user to see the characters appearing in real-time even if the data doesn't get sent to the server (or whatever other expensive operation) immediately.
import Combine
import SwiftUI
class ViewModel : ObservableObject {
#Published var text : String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$text
.debounce(for: .seconds(2), scheduler: RunLoop.main)
.sink { (newValue) in
//do expensive operation
print(newValue)
}
.store(in: &cancellables)
}
}
struct ContentView : View {
#StateObject var vm = ViewModel()
var body: some View {
TextField("", text: $vm.text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
Depending on what your operation does, you may also want to add a .receive(on:) operator. To update the UI, you'd want to receive on the main thread. But, for other expensive operations, you can send things to a background queue this way.
I have an ObservableObject that publishes some values using #Published property wrappers. This object also holds a timer.
The question is, how can I fire an event as soon as the timer is executed and handle that event in a view in SwiftUI (I'd prefer using something like onReceive)?
Using the Combine framework for publishing changing values already, I'd like to implement this event triggering / handling properly. But all that I've read so far about Combine is always about handling value changes. But in my case it's rather a single simple event (without any values).
I know that I could simply use a closure and call that when the timer expires, and I will do that if there's no better, combine-like solution.
This is a conceptual question for a very simple problem so I think it's self explaining without me coming up with a code example?
The way SwiftUI works with Combine is via .onReceive, which expects a publisher. An object can expose a publisher - whether Timer or something else - as a property.
Combine publishers work by emitting values, and if you just need to signal that an event has happened, you can emit () aka Void values.
This object, by the way, need not be an ObservableObject, but it could be.
class Foo: ObservableObject {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { _ in } // map to Void
.eraseToAnyPublisher() // optional, but a good practice
}
Now, you can use .onReceive to subscribe to the timer event:
struct ContentView: View {
#StateObject var foo = Foo()
#State var int: Int = 0
var body: some View {
Text("\(int)")
.onReceive(timer) {
self.int += 1
}
}
}
Of course, you're not restricted to a TimerPublisher. For example, if some random event happens, you can use a PassthroughSubject to publish a value:
class Foo {
let eventA: AnyPublisher<Void, Never>
private let subject = PassthroughSubject<Void, Never>()
init() {
eventA = subject.eraseToAnyPublisher()
let delay = Double.random(in: 10...100)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// something random happened, notify
self?.subject.send()
}
}
}
Just an idea, but you could dedicate a specific thread to that Object? And then listen and fire things on that Queue specifically. I've never done this before but it seems like the right sort of idea.
https://developer.apple.com/documentation/dispatch/dispatchqueue
https://www.freecodecamp.org/news/ios-concurrency/
I'm trying to learn SwiftUI and Combine syntax and am trying to understand how to create a reusable publisher that will check if a String is empty.
I've got a SwiftUI with 5 TextFields which using #Binding to connect them to my data model object.
class DataWhatIsLoanPayment: ObservableObject {
// Input
#Published var pv = ""
#Published var iyr = ""
// a bunch more fields...
// Output
#Published var isvalidform = false
}
I want to enable the Calculate button once all of the fields are filled in (isEmpty == false).
I'm following along with https://peterfriese.dev/swift-combine-love/, and I was able to get my SwiftUI to properly enable/disable my Calculate button by creating an isValidPVPublisher and an isValidIYRPublisher and combing them in an isValidFormPublisher, like so:
private var isValidPVPublisher: AnyPublisher<Bool, Never> {
$pv
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
private var isValidIYRPublisher: AnyPublisher<Bool, Never> {
$iyr
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
private var isValidFormPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest(isValidPVPublisher, isValidIYRPublisher)
.map { pvIsValid, iyrIsValid in
return pvIsValid && iyrIsValid
}
.eraseToAnyPublisher()
}
init() {
isValidFormPublisher
.receive(on: RunLoop.main)
.assign(to: \.isValidForm, on: self)
.store(in: &cancellableSet)
}
However, I'm going to have a lot more than 2 fields, and I'm going to have a lot of other forms in my app in which I will want to check if my fields are empty. And repeating .debounce(for: 0.8, scheduler: RunLoop.main).removeDuplicates().map { input in return input.isEmpty == false }.eraseToAnyPublisher() over and over again is a bad idea.
I want to create a reusable NotEmptyPublisher, or something like that, which takes a field binding, like my $pv and sets up the chain as show in the isValidPVPublisher above. So I can have something like:
// Something like this, but I'm not sure of the syntax...
private var isValidPVPublisher = NotEmptyPublisher(field:$pv)
// instead of ...
private var isValidPVPublisher: AnyPublisher<Bool, Never> {
$pv
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
But I'm having a trouble parsing a lot of Swift syntax that I'm not familiar with and I can't seem to figure out how to do it, and every example I find on the web is just defining the publisher chain inline instead of in a reusable fashion.
How can I create a reusable publisher so that I don't have to repeat these inline publishers which all do the same thing?
Here you are!
extension Publisher where Output == String {
func isStringInhabited() -> Publishers.Map<Self, Bool> {
map { !$0.isEmpty }
}
}
$0 is shorthand for the first argument to the closure, $1 means the second, and so on and so forth.
! is the Bool inversion operator, prefixing ! is shorthand for suffixing == false.
Now, as to your question about reuse, you don't need to overkill things that hard, you can just create a function.
private func isValidTransform<P: Publisher>(input: P) -> some Publisher where P.Output == String {
input
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.isStringInhabited()
}
P is a generic, which means it could be any type whatsoever as long as that type conforms to Publisher. The where clause allows us to constrain this conformance further, denoting that we can only operate on Publishers when their Output is String. some Publisher gives us an opaque return type to save us from having to write the type signature of a Publisher that has been transformed multiple times, you can change this to AnyPublisher<Bool, Never> and use .eraseToAnyPublisher() if you like but I recommend only using that erasure at the time of need.
I'm trying to come up with a simple observable object in Swift and thought to use RxSwift. I couldn't find a simple example to do something like this:
protocol PropertyObservable {
typealias PropertyType
var propertyChanged: Event<(PropertyType, Any)> { get }
}
class Car: PropertyObservable {
typealias PropertyType = CarProperty
let propertyChanged = Event<(CarProperty, Any)>()
dynamic var miles: Int = 0 {
didSet {
propertyChanged.raise(.Miles, oldValue as Any)
}
}
dynamic var name: String = "Turbo" {
didSet {
propertyChanged.raise(.Name, oldValue as Any)
}
}
}
The above is pure Swift solution for observables from this blog post; I really like how it's a protocol-based solution and not invasive. In my case, I have an object in my project where each property is set asynchronously under the hood (bluetooth device). So I need to observe/subscribe to the changes instead of getting/setting the properties in real-time.
I keep hearing RxSwift will do just that and more. However, I can't find a simple example to match above and beginning to think RxSwift is overkill for my need? Thanks for any help.
Easiest way to quickly make this observable with RxSwift would probably be to use the RxSwift class Variable (all code here is untested off the top of my head):
import RxSwift
class Car {
var miles = Variable<Int>(0)
var name = Variable<String>("Turbo")
}
This enables you to observe the values by subscribing to them:
let disposeBag = DisposeBag()
let car = Car
car.name.asObservable()
.subscribeNext { name in print("Car name changed to \(name)") }
.addToDisposeBag(disposeBag) // Make sure the subscription disappears at some point.
Now you've lost the old value in each event. There are of course numerous ways to solve this, the RxSwifty way would probably be to add a scan operation to your element sequence, which works a lot like reduce does on a normal Array:
car.name.asObservable()
.scan(seed: ("", car.name.value)) { (lastEvent, newElement) in
let (_, oldElement) = lastEvent
return (oldElement, newElement)
}
.subscribeNext { (old, new) in print("Car name changed from \(old) to \(new)") }
.addToDisposeBag(disposeBag)