How to create observables in iOS views?
I'm not sure how to create Observables, Singles etc. in iOS views.
Let's say this is in my shared module:
interface SampleView {
val reloadTrigger: Observable<Unit>
val sortTrigger: Observable<Boolean>
}
And this would be the android implementation:
class SampleFragment : Fragment, SampleView {
override val reloadTrigger: Observable<Unit>
get() = binding.swipeRefreshLayout.refreshes().asReaktiveObservable()
override val sortTrigger: Observable<Boolean>
get() = binding.sortingCheckbox.checkedChanges().asReaktiveObservable()
}
But how to create fields like reloadTrigger or sortTrigger in an iOS view using Swift? Is it even possible, or does the communication between the iOS layer and shared layer need to be non-reaktive? If it's possible, is there any sample with this approach I could check out?
The best way is to create subjects (e.g. PublishSubject) in Kotlin. The main problem is that subjects are interfaces with type parameters, which makes it impossible to use them from Swift (see #368 and #538 for more information).
One approach is to create an abstract view class in Kotlin which would hold subjects privately, and expose dispatch methods to be used by platforms.
Another possible way is to create subject wrapper classes in Kotlin. They can be used in Swift, either directly or for interop with RxSwift.
class PublishSubjectWrapper<T : Any> private constructor(
subject: PublishSubject<T>
) : ObservableWrapper<T>(subject), ObservableCallbacks<T> by subject {
constructor() : this(PublishSubject())
}
Then your View interface would look something like:
interface SampleView {
val reloadTrigger: ObservableWrapper<Unit>
val sortTrigger: ObservableWrapper<Boolean>
}
Your Android code:
class SampleFragment : Fragment, SampleView {
override val reloadTrigger: ObservableWrapper<Unit>
get() = binding.swipeRefreshLayout.refreshes().asReaktiveObservable().wrap()
override val sortTrigger: ObservableWrapper<Boolean>
get() = binding.sortingCheckbox.checkedChanges().asReaktiveObservable().wrap()
}
And in Swift you can actually use PublishSubjectWrapper.
We gave it a try, but our main problem that blocked us in the end is that we don't know how to "map" RxSwift observables to fields of type ObservableWrapper which are defined in SampleView interface:
final class SampleViewController: SampleView {
var reloadTrigger: ObservableWrapper<KotlinUnit> {
refreshControl.rx.controlEvent(.valueChanged) // How to transform this to ObservableWrapper?
}
var sortTrigger: ObservableWrapper<Bool> {
sortingToggle.rx.isOn // How to transform this to ObservableWrapper?
}
}
How to transform observables exposed by RxSwift to ObservableWrappers on the Swift side? Is it even possible? We couldn’t manage to do that because asReaktiveObservable().wrap() construction is not visible in Swift.
This could be done in multiple ways, assuming you are using RxSwift.
- You can use
PublishSubjectWrapperfor it. In this case subscriptions to UI will be hot and you will need to collect and dispose all disposables when yourViewControlleris destroyed.
// This is code is approximate, I didn't have a chance to try it
extension RxSwift.Observable where Element : AnyObject {
func wrap(_ bag: DisposeBag) -> ObservableWrapper<Element> {
let subj = PublishSubjectWrapper<Element>()
self.subscribe(onNext: subj.onNext).addDisposableTo(bag)
return subj
}
}
- Another approach is to directly convert RxSwift
Observableto ReaktiveObservableWrapper. In this case you don't needPublishSubjectWrapperat all. Observables will be cold, and no disposable collection is required.
// In Kotlin
class MyObservableWrapper<T: Any>(onSubscribe: (ObservableEmitterWrapper<T>) -> Unit) : ObservableWrapper<T>(
observable { emitter ->
onSubscribe(ObservableEmitterWrapper(emitter))
}
)
class ObservableEmitterWrapper<T>(emitter: ObservableEmitter<T>) : ObservableEmitter<T> by emitter
// In Swift. This is code is approximate, I didn't have a chance to try it.
extension RxSwift.Observable where Element : AnyObject {
func wrap() -> ObservableWrapper<Element> {
return MyObservableWrapper { emitter in
let disposable = self.subscribe(onNext: emitter.onNext, onCompleted: emitter.onComplete)
emitter.setDisposable(...) // TODO: convert RxSwift Disposable to Reaktive Disposable
}
}
}
If the second approach is more preferable, we could add the first (Kotlin) snippet to the library, so only the second (Swift) would be required on client side.
There is also another way of creating ObservableWrapper in Swift.
Add the following Kotlin class:
class CallbackObservable<out T : Any>(
onSubscribe: (
onNext: (T) -> Unit,
onComplete: () -> Unit,
onError: (Throwable) -> Unit,
setDisposable: (Disposable) -> Unit,
) -> Unit
) : ObservableWrapper<T>(
observable { emitter ->
onSubscribe(
emitter::onNext,
emitter::onComplete,
emitter::onError,
emitter::setDisposable,
)
}
)
This class can be used in Swift to create ObservableWrapper and emit events manually. It can also be used to convert Swift reactive streams (e.g. RxSwift observable) to Reaktive.
For example:
class SampleViewController : SampleView {
var reloadTrigger: ObservableWrapper<KotlinUnit> {
CallbackObservable { onNext, onComplete, onError, setDisposable in
// Susbcribe to a Swift stream here
}
}
}
Hi @arkivanov
thank you for your job in KMM.
I try to convert RxSwift Observable in Reaktive ObservableWrapper with this method you suggested:
extension RxSwift.Observable where Element : AnyObject {
func wrap() -> ObservableWrapper<Element> {
return MyObservableWrapper { emitter in
let disposable = self.subscribe(onNext: emitter.onNext, onCompleted: emitter.onComplete)
emitter.setDisposable(...) // TODO: convert RxSwift Disposable to Reaktive Disposable
}
}
}
But I cannot understand how to convert RxSwift Disposable to Reaktive Disposable. I try with following code:
let disposableScope = shared.DisposableScopeBuilderKt.disposableScope { _ in
disposable.dispose()
}
emitter.setDisposable(disposable: disposableScope)
But as soon as I subscribe to ObservableWrapper, disposable.dispose() is called and the emitter is disposed.
Can you help me?
@flaviosuardi Actually there could be a better way, you can try using Emitter.setCancellable extension function, instead of emitter.setDisposable.
This is an example from the top of my head:
EmitterExtKt.setCancellable(emitter, disposable.dispose)