Learn how to code libraries that your iOS teammates will not frown upon using them. In this chapter: enums and sealed classes.
Androids enumerating apples: green, orange, and red apples — DALLE-2
I love Swift enums, even though I am a Kotlin developer. And iOS devs certainly do too. Let me show you why with one example:
// SWIFT CLIENT CODE enum NetworkResult { case success(data: Data) case failure(code: Int, error: Error) case notConnected } func networkCall() -> NetworkResult { .... } func retrieveSomething() { let result = networkCall() switch result { case let .success(data): print(data) case let .failure(code, error) where (400..<500).contains(code): print("Client error: \(error)") case let .failure(_, error): print("Server error: \(error)") case .notConnected: print("Not connected") } }
This article is part of a series, see the other articles here
Like any other regular enum type, the Swift enum allows you to define a finite set of values. But unlike the Kotlin one, those values are not immutable. Swift enums can have associated values, i.e., they can carry data that is defined at instantiation time. See the example above. Not only the enum can represent all the possible types of response that we can get from a network call, but if the type is “success” it will contain the response data. Similarly, if the type is “error” it will contain the error code, and so on. Also, note how switch
(the Swift equivalent to Kotlin when
) works nicely with enums in a pattern-match style. If we really want to entice Swift developers to use our Kotlin Multiplatform APIs we need to create code that can be used as conveniently and efficiently as Swift enums. Are we able to do so? I know what you are thinking but hold that thought for a moment.
Kotlin Enums
Our obvious first candidate is the Kotlin enum. Let us see how they are exported:
// KOTLIN API CODE enum class MatterState(val temp: Int) { SOLID(0), LIQUID(25), GAS(100), PLASMA(10_000) }
// EXPORTED OBJ-C HEADER __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("MatterState"))) @interface SharedMatterState : SharedKotlinEnum<SharedMatterState *> + (instancetype)alloc __attribute__((unavailable)); + (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); - (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable)); @property (class, readonly) SharedMatterState *solid __attribute__((swift_name("solid"))); @property (class, readonly) SharedMatterState *liquid __attribute__((swift_name("liquid"))); @property (class, readonly) SharedMatterState *gas __attribute__((swift_name("gas"))); @property (class, readonly) SharedMatterState *plasma __attribute__((swift_name("plasma"))); + (SharedKotlinArray<SharedMatterState *> *)values __attribute__((swift_name("values()"))); @property (readonly) int32_t temp __attribute__((swift_name("temp"))); @end
// HEADER "TRANSLATED" TO SWIFT public class MatterState : KotlinEnum<MatterState> { open class var solid: MatterState { get } open class var liquid: MatterState { get } open class var gas: MatterState { get } open class var plasma: MatterState { get } open class func values() -> KotlinArray<MatterState> open var temp: Int32 { get } }
// SWIFT CLIENT CODE let state: MatterState = .gas switch state { case .gas: print("gas") case .liquid: print("liquid") case .solid: print("solid at \(state.temp)") case .plasma: print("plasma") default: break }
The problem
Objective-C has only C-Style enums. In C the values can only be associated with a unique integer. Therefore, a Kotlin enum cannot be perfectly translated to Objective-C. If you are not familiar with either Objective-C or Swift, here is a rough translation of how the Kotlin enum is perceived by them:
class MatterState(val temp: Int): Enum<MatterState> { companion object { @JvmField val solid: MatterState = MatterState(0) @JvmField val liquid: MatterState = MatterState(25) @JvmField val gas: MatterState = MatterState(100) @JvmField val plasma: MatterState = MatterState(10_000) @JvmStatic fun values(): Array<MatterState> = arrayOf(solid, liquid, gas, plasma) } }
Thus in Swift, each one of values of the enum becomes a static immutable instance of that enum type. As we saw in the example above, that enables a pretty similar syntax to Swift enums. However, Swift has no way of knowing that those are the only possible values. Consequently, the switch
will require a default
case, because, like its Kotlin counterpart when
, it must be exhaustive, even though the default case will never be hit.
Sealed Classes
I bet that sealed classes were your first thought after I introduced Swift enums to you. Let us see how well they can do :
Job Offers
// KOTLIN API CODE sealed class ReturnValue { object Loading : ReturnValue() class Success(val data: String) : ReturnValue() class Error(val errorCode: Int) : ReturnValue() }
// EXPORTED OBJ-C HEADER __attribute__((swift_name("ReturnValue"))) @interface SharedReturnValue : SharedBase @end __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("ReturnValue.Error"))) @interface SharedReturnValueError : SharedReturnValue - (instancetype)initWithErrorCode:(int32_t)errorCode __attribute__((swift_name("init(errorCode:)"))) __attribute__((objc_designated_initializer)); @property (readonly) int32_t errorCode __attribute__((swift_name("errorCode"))); @end __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("ReturnValue.Loading"))) @interface SharedReturnValueLoading : SharedReturnValue + (instancetype)alloc __attribute__((unavailable)); + (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); + (instancetype)loading __attribute__((swift_name("init()"))); @property (class, readonly, getter=shared) SharedReturnValueLoading *shared __attribute__((swift_name("shared"))); @end __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("ReturnValue.Success"))) @interface SharedReturnValueSuccess : SharedReturnValue - (instancetype)initWithData:(NSString *)data __attribute__((swift_name("init(data:)"))) __attribute__((objc_designated_initializer)); @property (readonly) NSString *data __attribute__((swift_name("data"))); @end
// HEADER "TRANSLATED" TO SWIFT open class ReturnValue : KotlinBase {} extension ReturnValue { public class Loading : ReturnValue { public convenience init() open class var shared: ReturnValue.Loading { get } } public class Error : ReturnValue { public init(errorCode: Int32) open var errorCode: Int32 { get } } public class Success : ReturnValue { public init(data: String) open var data: String { get } } }
// SWIFT CLIENT CODE let result: ReturnValue = ReturnValue.Loading.shared switch result { case is ReturnValue.Loading: print("loading") case let success as ReturnValue.Success: print("success \(success.data)") case let error as ReturnValue.Error: print("error \(error.errorCode)") default: break }
The problem
Objective-C and Swift will see your sealed classes as a simple and plain class hierarchy, in view of the fact that the concept of sealed classes does not exist for them. Writing a switch statement becomes a cumbersome task because all you can do is attempt to downcast to the correct subclass. And again, a useless default
case will be needed to satisfy the exhaustive requirement.
The solution
As we saw in Part IV, we may have to resort to Swift wrappers in order to improve the API. Thus we define a matching Swift enum, and provide a nullable constructor extension (yes, that is possible in Swift!) to wrap our Kotlin sealed class:
// SWIFT CLIENT CODE enum ReturnValueSwift { case loading case success(data: String) case error(errorCode: Int) } extension ReturnValueSwift { init?(_ value: ReturnValue) { switch value { case is ReturnValue.Loading: self = .loading case let success as ReturnValue.Success: self = .success(data: success.data) case let error as ReturnValue.Error: self = .error(errorCode: Int(error.errorCode)) default: return nil } } } func returnValueTest() { if let result = ReturnValueSwift(ReturnValue.Loading.shared) { switch result { case .loading: print("loading") case let .success(data): print("success \(data)") case let .error(errorCode): print("error \(errorCode)") } } }
Writing wrappers for every enum and sealed class hierarchy will be a tedious process, especially if you are not fluent in Swift. Fortunately, some folks already wrote compiler plugins that can automate the process for you:
In this installment, we learned how Kotlin enum and sealed classes are translated to Swift. We also saw how we can use Swift extensions again to improve the exported APIs. See you again next week when I am gonna cover coroutines and flows. Meanwhile, do not forget to check the other episodes of this series.
References
- The Swift Programming Language (5.9 beta) — Enumerations — Associated Values — https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations/#Associated-Values
- Swift Tip: Enum Initializers — https://www.objc.io/blog/2018/03/13/mutable-self/
- MIKHAILOV, Aleksey — How to implement Swift-friendly API with Kotlin Multiplatform Mobile — https://medium.com/icerock/how-to-implement-swift-friendly-api-with-kotlin-multiplatform-mobile-e68521a63b6d
This article was previously published on proandroiddev.com