Gurupad Mamadapur [FH]
02/23/2021, 4:22 PMclass AppState: ObservableObject {
private var closeables = Array<Closeable>()
private let viewmodel = AppViewModel()
@Published private(set) var data:AppData? = nil
init() {
let close:Closeable = UtilsKt.watch(viewmodel.data) { any in. // watch is an extension function on flow that returns a closeable
self.data = any as? AppData
}
closeables.append(close)
viewmodel.load() // some api call
}
deinit {
closeables.forEach { it in
it.onClose()
}
}
}
Then use the object of AppState (ignore the naming) on SwiftUI view, marked with @ObservedObject.
This is a bit tedious as I'll have to copy and mark (@published) for each exposed property from my shared viewmodel.
Could directly use the viewmodel in a view but there is no lifecycle I know of where I can call close.Lammert Westerhoff
02/24/2021, 2:39 PMCFlow
from the KotlinConf app (https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt) to have a generic Flow in iOS.
I also have the following utility which creates a publisher from the flow:
func asPublisher<T>(_ flow: CFlow<T>) -> AnyPublisher<T, Never> {
return Deferred<Publishers.HandleEvents<PassthroughSubject<T, Never>>> {
let subject = PassthroughSubject<T, Never>()
let closable = flow.watch { next in
if let next = next {
subject.send(next)
}
}
return subject.handleEvents(receiveCancel: {
closable.close()
})
}.eraseToAnyPublisher()
}
Now I could subscribe to that in an ObservableObject similar as you. However I created a wrapper view with a content builder in which I just pass the publisher:
public struct ObservingView<Observed, Content>: View where Content: View {
@State private var model: Observed? = nil
private let publisher: AnyPublisher<Observed, Never>
private let content: (Observed) -> Content
public init(publisher: AnyPublisher<Observed, Never>, @ViewBuilder content: @escaping (Observed) -> Content) {
self.publisher = publisher
self.content = content
}
public var body: some View {
let view: AnyView
if let model = self.model {
view = AnyView(content(model))
} else {
view = AnyView(EmptyView())
}
return view.onReceive(publisher) { self.model = $0 }
}
}
And then I use it all as following:
private struct ViewWithObserver: View {
let viewModel: ViewModel // has output wich is a CFlow of ViewModelOutput
var body: some View {
ObservingView(publisher: asPublisher(viewModel.output))
}
}
private struct InnerView: View {
let output: ViewModelOutput // is a data class with all properties used in the view
var body: some View {
Text(output.title)
}
}
Here the InnerView automatically gets updated by the ObservingView
whenever something from the ViewModelOutput
changes.leandro
02/27/2021, 5:13 PMLammert Westerhoff
02/27/2021, 7:29 PMleandro
02/27/2021, 9:29 PMGurupad Mamadapur [FH]
03/01/2021, 5:38 AMGurupad Mamadapur [FH]
03/01/2021, 5:40 AMLammert Westerhoff
03/02/2021, 1:12 PMrusshwolf
03/03/2021, 4:30 AMasPublisher()
and it’s neat how compact your implementation is.
Here’s a couple questions I came away with if it’s helpful feedback, though maybe they get beyond your intended scope:
1. What does typical error-handling look like at the Swift level? My Kotlin instincts get uncomfortable with eraseToAnyPublisher()
because it sounds like it would discard error information, but from what I can tell it still throws if an error occurs, so maybe it’s fine. What’s the standard best practice for such things in Combine/SwiftUI?
2. Does it make sense to combine asPublisher()
and ObservingView
so your View could just call something like ObservingView(cflow: viewModel.output)
? Or do the two live in different places in a more complete app?
3. Are there any useful patterns for passing interactions back to the viewmodel?leandro
03/03/2021, 5:06 PMSendChannel<UiEvent>
and have the click listeners send typed events to the view modelrusshwolf
03/07/2021, 6:00 PMrusshwolf
03/07/2021, 6:01 PMLammert Westerhoff
03/08/2021, 6:12 PMeraseToAnyPublisher()
does not erase error/failing cases as the name might suggest. It just erases the concrete type. All publishers are concrete types of the Publisher
protocol. But since protocols in Swift have associated types instead of generic types using Publisher
is not very useful. You can’t for example do something like Publisher<String, Never>
. AnyPublisher
is used instead. It’s a concrete type that you can convert any other publisher to so you can use AnyPublisher
everywhere as if you would a protocol with a generic type. It’s a bit out of scope of the article since it’s more of a Swift thing, commonly misunderstood especially by those new to Swift. More related to this article: I’m using Never
in AnyPublisher<T, Never>
since I prefer to already do the error handing in my shared viewmodel and then for example add things like val errorMessage: String?
to the view model output.
2. You could indeed create ObservingView
with a cflow directly but since it’s only one additional function asPublisher
I like to keep them separate to avoid adding initializers for all kind of different ways of constructing publishers. And like you mentioned you will soon find situations in which you need them to be separate things.
3. there are and I’ve used a variety of different ways now. some work better than others with SwiftUI.
I’m planning to write follow up articles for error handling and for 3. the interactions between the view model.
And I’ve just published the post on Medium: https://lwesterhoff.medium.com/using-swiftui-with-view-model-written-in-kotlin-multiplatform-mobile-67cf7b6da551russhwolf
03/08/2021, 6:13 PMrusshwolf
03/08/2021, 6:16 PMeraseToAnyPublisher()
isn't really the source of my error-handling confusion. But it still seemed like SwiftUI wants a Publisher<String, Never>
so I'm not clear on what the standard Swift-level error-handling looks like.russhwolf
03/08/2021, 6:16 PMLammert Westerhoff
03/08/2021, 6:38 PMrusshwolf
03/08/2021, 6:42 PMLammert Westerhoff
03/08/2021, 6:45 PMasDriver
and asSignal
which essentially do the same as asPublisher
Lammert Westerhoff
03/08/2021, 6:46 PMpublic func asDriver<T>(_ flow: CFlow<T>) -> Driver<T?> {
return asObservable(flow).asDriver(onErrorDriveWith: .empty())
}
public func asSignal<T>(_ flow: CFlow<T>) -> Signal<T?> {
return asObservable(flow).asSignal(onErrorSignalWith: .empty())
}
public func asObservable<T>(_ flow: CFlow<T>) -> Observable<T?> {
return Observable.create { observer in
let closable = flow.watch(block: { object in
observer.onNext(object)
})
return Disposables.create {
closable.close()
}
}
}
russhwolf
03/08/2021, 6:48 PMLammert Westerhoff
03/08/2021, 7:02 PMclass PublishedFlow<T> : ObservableObject {
@Published
var output: Result<T, KotlinError>
init(_ publisher: AnyPublisher<T, KotlinError>, defaultValue: T) {
output = .success(defaultValue)
publisher
.map { Result.success($0) }
.catch { Result.Publisher(.failure($0)) }
.replaceError(with: nil)
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.assign(to: &$output)
}
}
russhwolf
03/08/2021, 8:39 PM