We're considering staring to share some more code ...
# apollo-kotlin
s
We're considering staring to share some more code with our iOS friends, and want to share the ApolloClient with them too so that we can share some networking logic. One problem arose from trying to do this, which is that we want to be able to append the right authorization tokens which are stored in Datastore for Android, but they are handled separately on the iOS side. Particularly, in iOS it is stored in the keychain, fetched from an
func foo() async throws -> Token?
. Not sure if I can call a Swift async function from Kotlin, we typically only do it the other way around. Is there any possible way you can imagine how I can reach that from inside my Apollo interceptor, or will I have to do something different altogether for this?
e
I don't use Swift or Apple/iOS, but https://github.com/swiftlang/swift-evolution/blob/main/proposals/0297-concurrency-objc.md#defining-asynchronous-objc-methods-in-swift seems to indicate that Swift
async
can be exposed to Objective-C as a callback-based
completionHandler:
, which should be natural to use with
suspendCoroutine
in Kotlin. not sure what you'd do from a non-
suspend
context though
s
I gotta admit, I tried looking into this but I really didn't understand how to put everything together. It's probably my lack of knowledge on doing KMP stuff in general, but even with potentially getting the callback API from Swift I didn't figure out a way to call that from my Kotlin code. What I did which does look to work, albeit looks a bit hacky is this: I made an interface in the common code which looks like this
Copy code
interface AccessTokenFetcher {
  fun fetch(): String
}
which I expect iOS to provide an implementation for, and the ApolloInterceptor uses in order to get the header during the interception. Then on iOS I added this (where
foo()
is the aforementioned async function)
Copy code
class KeychainAccessTokenFetcher : AccessTokenFetcher {
    func fetch() -> String? {
        final class TokenBox: @unchecked Sendable {
            var token: String?
        }
        let semaphore = DispatchSemaphore(value: 0)
        let box = TokenBox()
        Task {
            do {
                box.token = try await foo().accessToken
            } catch {
                box.token = nil
            }
            semaphore.signal()
        }
        semaphore.wait()
        return box.token
    }
}
Which does seem to work, albeit looks nothing like what I'd do in Kotlin normally. I will have to figure out a better way I think, but at least I am unblocking myself for now.
d
Is there a reason why your
fetch
function in the interface is not a suspend function?
s
It doesn't seem to get exposed to objc at all when I do that, the interface gets generated as an empty interface. So I don't see a way to then call it from Swift
d
That's weird - I do have a couple interfaces defined in commonMain in my project that have suspend functions. Like ephemient has mentioned they should be translated to functions with completionHandlers... Some links to the docs https://kotlinlang.org/docs/native-objc-interop.html#mappings - see suspend section and https://kotlinlang.org/docs/native-objc-interop.html#suspending-functions
s
Yeah... you do seem to be correct. I am so confused right now, I need to test this out again. Can you confirm that you've made this work with a suspend function specifically being present inside an interface specifically too? I can try test some scenarios to see for myself too
d
Yep I have pretty much the same case in our code - an interface to do some auth token manipulations that. My interface has 1 suspend, 1 non-suspend and 1 val and it's implemented in Swift and the implementation passed back to my shared code DI.
s
Do you mind sharing how this interface is exposed in objc in the end? Also are you by any chance using SKIE here, or not?
d
I do use SKIE in general on my project yes, I'm unsure how much SKIE does and what Kotlin does out of the box but this is what I get in my objc header I've created a new example interface in common code:
Copy code
public interface TokenFetcher {
    public val token: String

    public suspend fun fetch(): String

    public fun setToken(token: String)
}
Copy code
__attribute__((swift_name("TokenFetcher")))
@protocol SharedTokenFetcher
@required

/**
 * @note This method converts instances of CancellationException to errors.
 * Other uncaught Kotlin exceptions are fatal.
*/
- (void)fetchWithCompletionHandler:(void (^)(NSString * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("fetch(completionHandler:)")));
- (void)setTokenToken:(NSString *)token __attribute__((swift_name("setToken(token:)")));
@property (readonly) NSString *token __attribute__((swift_name("token")));
@end
s
Well skie might be interfering here, since that's what it does basically. If you turn skie off do you get the same objc header?
You do seem to be right, I am also getting the completionHandler generated now. I really don't know what I was doing wrong before. That means I can get away without any of these weird semaphore tricks