iosswiftkotlinkotlin-multiplatformkotlin-multiplatform-mobile

How to use Sealed Class Data from Flow in Swift in Kotlin Mutliplatform Mobile?


I unable to access the sealed class data observed in a Flow from Swift ..

sealed class NetworkStatus<T> {

data class Loading<T>(var loading: Boolean) : NetworkStatus<T> ()

data class CustomSignal<T>(var signal: String) : NetworkStatus<T>()

data class CustomSignalDetailed<T>(var signal: ErrorCaseData) : NetworkStatus<T>()

data class Failure<T>(val e: Throwable) : NetworkStatus<T>()

data class Data<T> (val data: T ) : NetworkStatus<T>()

companion object {

    fun <T>loading(isLoading: Boolean): NetworkStatus<T> = Loading(isLoading)

    fun <T> customstatus(signal: String): NetworkStatus<T> = CustomSignal(signal)

    fun <T> customStatusDetailed(signals: ErrorCaseData): NetworkStatus<T> = CustomSignalDetailed(signals)

    fun <T> failure(e: Throwable): NetworkStatus<T> = Failure(e)

    fun <T> data(data: T): NetworkStatus<T> = Data<T>(data)
    }

}

https://gist.github.com/RageshAntony/a4fd357973485b5fb8aef0e189ee9e7e

In the above gist ....

In MainActivity.kt , I used sealed class in flow for Data

Now I need the same in Swift but it's confusing

Even I used something as CFlow wrapping.. still confusion

In Swift , I can't get object type only as nsarray and also NetworkStatus.data is not accessible

.. I tried manual typecast (in line 8 , contentview.swift )

Still data object don't have any type ...

Please help me how to implement the same flow in Swift like in MainActivity.kt


Solution

  • Start from this sealed class declared in the shared code:

    sealed class KMMResult<out Value>
    
    data class SuccessKMMResult<Value>(
        val value: Value
    ): KMMResult<Value>()
    
    data class ErrorKMMResult<Value>(
        val throwable: Throwable
    ): KMMResult<Value>()
    

    and this method that randomly returns you a KMMResult with Success or Failure:

    fun getRandomIntWrappedInResult(): KMMResult<Int> {
        val isSuccess = Random.nextBoolean()
        return if(isSuccess) {
          SuccessKMMResult(Random.nextInt(until = 10))
        } else {
            ErrorKMMResult(RuntimeException("There was an error, Int not generated"))
        }
    }
    

    You can use it like this on Android:

    val randomInt: KMMResult<Int> = getRandomIntWrappedInResult()
    val randomIntText: String = when (randomInt) {
        is KMMResult.ErrorKMMResult -> {
            "Error: ${randomInt.throwable.message}"
        }
        is KMMResult.SuccessKMMResult -> {
            "Success: ${randomInt.value}"
        }
    }
    

    and like this on iOS:

    let randomInt: KMMResult<KotlinInt> = RandomNumberGeneratorKt.getRandomIntWrappedInIntResult()
    let randomIntText: String
    
    switch randomInt {
    case let error as KMMResultErrorKMMResult<KotlinInt>:
        randomIntText = "Error: \(error.throwable.message ?? error.throwable.description())"
    case let success as KMMResultSuccessKMMResult<KotlinInt>:
        randomIntText = "Success: \(success.value)"
    default:
        randomIntText = "This never happens"
    }
    

    This is not the best solution, I suggest you to create a Swift enum like:

    enum SwiftResult<Value> {
      case error(String)
      case success(Value)
    }
    

    and convert KMMResult to SwiftResult using:

    func toSwiftResult<Value>(kmmResult: KMMResult<Value>) -> SwiftResult<Value> {
      if let successResult = kmmResult as? KMMResultSuccessKMMResult<Value> {
        return SwiftResult.success(successResult.value!)
      }
      if let errorResult = kmmResult as? KMMResultErrorKMMResult {
        return SwiftResult.error(errorResult.throwable.message ?? errorResult.throwable.description())
      }
      return SwiftResult.error("Unexpected error converting to SwiftResult")
    }
    

    Unfortunately, you need to do this manually for each sealed class, so I suggest you to use the library moko-kswift that does this job for you.

    P.S. Avoid use List<Something> as a generic type because Something will be erased and when seeing the type from Swift you will just see NSArray. You can use two classes, one for single item and one for List, still generic on Something, or you can create a wrapper data class around your list, say ListWrapper, so the type won't be erased, because your original List<Something> would be inside ListWrapper which will be the type for NetworkStatus. So use NetworkStatus<ListWrapper<Something>> instead of NetworkStatus<List<Something>>

    Another P.S. Your function getAllCategories() in the gist is returning a flow, so it shouldn't be a suspend function.