Blog Infos
Author
Published
Topics
Published

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

 

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Making the Big Kotlin Multiplatform Decision

Why is adopting Kotlin Multiplatform Mobile not an easy decision to make? After all, it can potentially save a business millions of dollars by cutting down duplicate iOS and Android code and saving many developer…
Watch Video

Making the Big Kotlin Multiplatform Decision

Sumayyah Ahmed
Android Tech Lead
Square

Making the Big Kotlin Multiplatform Decision

Sumayyah Ahmed
Android Tech Lead
Square

Making the Big Kotlin Multiplatform Decision

Sumayyah Ahmed
Android Tech Lead
Square

Jobs

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

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
After successfully implementing the basic Kotlin multiplatform app in our last blog, we will…
READ MORE
blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE
blog
Kotlin Multiplatform Mobile (or simply Multiplatform Mobile) is a new SDK from JetBrains that…
READ MORE
Menu