https://kotlinlang.org logo
#multiplatform
Title
# multiplatform
c

Charles Prado

06/02/2021, 1:27 PM
Hey guys. I'm trying to consume a sealed class on the iOS side. In iOS the method return a completion handler with
SessionManagerCheckInResult<UserInfo>?
being
SessionManagerCheckInResult
is the sealed class:
Copy code
sealed class CheckInResult<out T : Any> {
        data class Success(val data: UserInfo) : CheckInResult<UserInfo>()
        data class Error(val exception: Throwable) : CheckInResult<Nothing>()
    }
I'm trying to get the error when I receive one, but I'm not able to get the result or error separated here. What I get if I print the object is:
Copy code
▿ Optional<SharedSessionManagerCheckInResult>
  - some : Error(exception=ApiError(code=400112, message=Invalid or missing members in payload
...
How can I get this error in iOS side? Some way I can convert the data to a
SharedSessionManagerCheckInResultError
here?
s

Sam

06/02/2021, 3:50 PM
Check your bridging header for the full name of your class. It’ll be something like
SessionManagerCheckInResultError
then just try to cast it.
Copy code
if let err = result as? SessionManagerCheckInResultError {
    //Do error handling
}
// Or
guard let userInfo = result as? SessionManagerCheckInResultSuccess<UserInfo> {
    //Error handling here
}
c

Charles Prado

06/02/2021, 3:51 PM
it seems that there's something wrong with these headers
what I have here is:
Copy code
// RESULT
__attribute__((swift_name("SessionManagerCheckInResult")))
@interface SharedSessionManagerCheckInResult<__covariant T> : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end;

// ERROR
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("SessionManagerCheckInResultError")))
@interface SharedSessionManagerCheckInResultError : SharedSessionManagerCheckInResult<SharedKotlinNothing *>
- (instancetype)initWithException:(SharedKotlinThrowable *)exception __attribute__((swift_name("init(exception:)"))) __attribute__((objc_designated_initializer));
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
- (SharedKotlinThrowable *)component1 __attribute__((swift_name("component1()")));
- (SharedSessionManagerCheckInResultError *)doCopyException:(SharedKotlinThrowable *)exception __attribute__((swift_name("doCopy(exception:)")));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
@property (readonly) SharedKotlinThrowable *exception __attribute__((swift_name("exception")));
@end;
s

Sam

06/02/2021, 4:07 PM
I have something similar in my project but I never try to switch over the type.
Copy code
sealed class RailResult<out V, out E> {
    /**
     * Represents a successful [RailResult], containing a [value].
     */
    data class Success<out V>(val value: V) : RailResult<V, Nothing>()

    /**
     * Represents a failed [RailResult], containing an [error].
     */
    data class Failure<out E>(val error: E) : RailResult<Nothing, E>()

    fun valueOrNull(): V? = when (this) {
        is Success -> this.value
        else -> null
    }

    val isSuccess: Boolean
        get() = when (this) {
            is Success -> true
            else -> false
        }

    fun failureOrNull(): E? = when (this) {
        is Failure -> this.error
        else -> null
    }

    val isFailure: Boolean
        get() = when (this) {
            is Failure -> true
            else -> false
        }
}
Instead I just use the
valueOrNull
and
failureOrNull
methods with
guard
and
if let
statements.
c

Charles Prado

06/02/2021, 4:22 PM
Thanks @Sam ! I think this can help me improving legibility here. However, I still wasn't able to figure out why I'm not able to cast my data to
SessionManagerCheckInResultSuccess
or
SessionManagerCheckInResultError
, do you see anything that could be wrong in my sealed class ?
s

Sam

06/02/2021, 4:34 PM
I wrote this code about 2 years ago and probably ran into the same issue that you did. I tried using a switch on my result class and get the same compiler warnings. It’s likely a limitation in the ObjC <-> Swift bridging. ObjC generics are very limited.
Here’s a trick you can do that will make the code a little more idiomatic Swift. Usually you see
switch
statements done over enums with associated values rather than trying to cast it into something. Here’s an enum that wraps the result object I’m using.
Copy code
enum RailResultEnum<T: AnyObject, E: KotlinError> {
    case Success(result: T)
    case Failure(error: Error)
    static func fromResult(rail: RailResult<T, E>) -> RailResultEnum {
        if let r = rail.valueOrNull(), rail.isSuccess {
            return .Success(result: r)
        } else {
            return .Failure(error: rail.nserrorOrNull() ?? NSError(domain: "Kotlin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Uknown error in converting to Cocoa Error"]))
        }
    }
}
You can then switch over it like this:
Copy code
switch RailResultEnum.fromResult(rail: result) {
case .Success(let value):
    print("success \(value)")
case .Failure(let error):
   print("failed \(error)")
}
It has some limitations. A null in a nullable result will fall through to the error condition. T can’t be a primitive, it has to be a class type.
c

Charles Prado

06/03/2021, 1:35 PM
Hi @Sam! Today I tested your suggestion and it worked for me 🎉🎉🎉 I'm still only using the base solution, just replaced my
CheckInResult
with your
RailResult
class. Now I'm able to intercept the error:
Copy code
static func signIn(idToken: String, serverAuthCode: String) -> Future<UserInfo, Error> {
        return Future() { promise in
            signIn(idToken: idToken, serverAuthCode: serverAuthCode) { (data, _) in
                if let userInfo = data?.valueOrNull() {
                    promise(Result.success(userInfo))
                } else if let _ = data?.failureOrNull() {
                    // TODO: map the error
                    promise(Result.failure(AppError.unknown))
                }
            }
        }
    }
Thanks so much for this I'm still a little bit curious why the other solution didn't work. Not so sure, I had seen some similar implementations in some other GitHub repos. My bg is in iOS programming, so there's some stuff that still doesn't make so much sense to me hehe.
s

Sam

06/03/2021, 1:45 PM
Same here, my bg is more iOS focused. I’m glad the solution worked for you.