Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create observables in iOS views? #664

Closed
KamilSucharski opened this issue Jun 8, 2022 · 6 comments
Closed

How to create observables in iOS views? #664

KamilSucharski opened this issue Jun 8, 2022 · 6 comments

Comments

@KamilSucharski
Copy link

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?

@KamilSucharski KamilSucharski changed the title How to create observables in iOS views How to create observables in iOS views? Jun 8, 2022
@arkivanov
Copy link
Contributor

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.

@KamilSucharski
Copy link
Author

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.

@arkivanov
Copy link
Contributor

arkivanov commented Jun 9, 2022

This could be done in multiple ways, assuming you are using RxSwift.

  1. You can use PublishSubjectWrapper for it. In this case subscriptions to UI will be hot and you will need to collect and dispose all disposables when your ViewController is 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
    }
}
  1. Another approach is to directly convert RxSwift Observable to Reaktive ObservableWrapper. In this case you don't need PublishSubjectWrapper at 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.

@arkivanov
Copy link
Contributor

arkivanov commented Aug 15, 2022

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
        }
    }
}   

@flaviosuardi
Copy link

flaviosuardi commented Oct 10, 2022

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?

@arkivanov
Copy link
Contributor

@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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants