KMM - Casting a sealed class/interface in swift not possible

3.6k views Asked by At

Within my KMM library I use sealed interfaces/classes to represent certain states/errors. I decided to use sealed interfaces/classes because these states must have different associated objects.

In the Android code, this also works as expected without any problems.

But in the iOS part, I'm not able to detect the specific state because the cast is not possible.

The error is independent of whether it is a sealed interface or a sealed class, here as an example:

sealed class SyncState() {
    object Loading : SyncState()
    data class Active(val syncNumber: String) : SyncState()
    data class Error(val throwable: Throwable) : SyncState()
}

In the repository within the KMM library, a corresponding SyncState is now returned depending on the state.

fun currentSyncState(): SyncState {
    if … {
        return SyncState.Error(Throwable("…"))
    } else if … {
        return SyncState.Active("…")
    } else {
        return SyncState.Loading
    }
}

In iOS, I can also call up this function without any problems. The only problem is that I can't tell which state it is from the object that is returned, because the casting doesn't work at any point.

let state = repo.currentSyncState()
…

(lldb) po state
SyncState.Loading@2fe538

(lldb) po state is SyncState
true

(lldb) po state is SyncState.Loading
false

(lldb) po type(of: state)
SyncStateLoading

(lldb) po state as? SyncState.Loading
nil
let state = repo.currentSyncState()
…

(lldb) po state
Active(syncNumber=syncNumber 123)

(lldb) po state is SyncState
true

(lldb) po state is SyncState.Active
false

(lldb) po type(of: state)
SyncStateActive

(lldb) po state as? SyncState.Active
nil

A possible solution might be to add an additional Type enum case in KMM for every State, but this still does not allow me to process the associated value of the corresponding state.

Has anyone had similar problems & found a possible solution for them? I am grateful for every little advice.

I'm using Kotlin 1.6.10 and Xcode 13.2 with Swift 5.5.2.

2

There are 2 answers

0
Brady On

I'd recommend not using your generated sealed classes from Swift. You don't get anything helpful from it, because it's awkward and there is no exhaustive when statement. It's much easier to use a callback that should execute when you get that sealed class. Here are a couple ways:

Mutually exclusive Callbacks

sealed class SyncState() {
    object Loading : SyncState()
    data class Active(val syncNumber: String) : SyncState()
    data class Error(val throwable: Throwable) : SyncState()
}

...

class NativeViewModel (
    onLoading: () -> Unit,
    onActive: (String) -> Unit,
    onError: (Throwable) -> Unit
)

And in Swift:

let mainViewModel = NativeViewModel(
    onLoading: { ... },
    onActive: { ... },
    onError: { ... }
)

1 Callback for all cases

Alternatively, you could encompass all cases in 1 callback on a data class with nullable properties:

data class SyncState() {
    val loading: Boolean?,
    val active: String?,
    val error: Throwable?
}

...

class NativeViewModel (
    onSyncState: (SyncState) -> Unit
)

And in Swift:

let mainViewModel = NativeViewModel(
    onSyncState: { ... }
)
1
Tørk Egeberg On

I used a library at https://github.com/icerockdev/moko-kswift for the same problem. It gives you autogenerated swift enums with constructors that take your Obj C type generated from the sealed class in Kotlin, and also mapping back to the Obj C type.