Kotlin Multiplatform을 Swift에서 사용할 때 유의할 점

JhoonP·2023년 7월 28일
0
post-thumbnail

Kotlin Multiplatform 도입 계기

최근 회사에서 Android, iOS 앱을 native로 구현하면서 같은 기능을 플랫폼마다 매 번 구현하거나, 팀원은 iOS를, 나는 Android로 같은 기능 A를 각자 구현하다가 나중에 통합해보니 구조가 달라서 문제가 생겨 피로감이 생기고 있었다.

물론 PR 때 생각하지 못하고 넘겨버린 내 잘못이 크다...

이러한 문제를 방지하기 위해 Kotlin Multiplatform을 사용해서 순수 Kotlin으로 구현할 수 있는 부분은 구현까지, 그것이 안되면 적어도 인터페이스라도 맞춰 구조 통일성을 가져가고자 했다.


위 이미지와 같이, 마음만 먹으면 native API만 제외하면 business logic부터 UI까지 모든 구성을 Kotlin으로 작성할 수 있으나, 우리는 공통 모듈 및 인터페이스만 공유 코드로 가져가기로 했다.

Kotlin으로 작성한 공유 코드를 Android의 Kotlin에서 사용하는 것은 큰 문제가 없었지만, 역시나 Swift에서 많은 시행착오가 있었고, 이를 정리해두면 필요할 때 다시 찾아볼 수 있을 것 같아 블로그에 남겨두고자 한다.

문서 정독 전 참고하면 좋은 자료(사실 문서 내용의 대부분은 여기 담겨 있다...)

Kotlin Multiplatform in Swift

Kotlin Multiplatform의 공유 코드 언어는 물론 Kotlin으로, 일반적으로 shared 폴더에 작성하여 관리한다.
물론 공유 코드를 Android Kotlin에서 사용하는 것은 거의 문제가 없으나, Swift의 경우 Kotlin과 완전히 매핑되지 않기 때문에 여러 제약사항이 존재한다.

가장 큰 점은 공유 코드에서 작성한 Kotlin code는 Swift가 아닌 objective-c로 변환된다.
따라서 objective-c와 Swift간 호환되지 않는 기능은 사용에 제약이 있다.

따라서 여러가지 경우를 테스트 해보며 가능/불가능 여부를 정리해두고자 한다.

Swift의 protocol 사용 불가

objective-c의 @protocol은 Swift의 class에는 사용 가능하나 struct에는 사용 불가능하다.

따라서 공통 코드에서 UI 컴포넌트의 인터페이스를 Kotlin interface으로 작성해 사용하고자 하는 경우, Swift에서는 UI 컴포넌트를 강제로 class로 구현해야 한다.

현재 우리 팀은 struct의 여러가지 이점을 챙기고자 struct로 SwiftUI 컴포넌트를 구현하고 있다.

따라서 UI 컴포넌트 인터페이스를 공통 코드로 구현하는 것은 좀 더 논의가 필요하다(23.07.27 기준)

Enum

아래와 같이 Kotlin의 enum class에 함수, 프로퍼티를 선언하면,

enum class PlatformType {
    iOS {
        override fun toUpperCaseTest(): String {
            return this.toStringTest().toUpperCase()
        }
        override val platformNum: Int = 0
    },
    Android {
        override fun toUpperCaseTest(): String {
            return this.toStringTest().toUpperCase()
        }
        override val platformNum: Int = 1
    },
    Windows {
        override fun toUpperCaseTest(): String {
            return this.toStringTest().toUpperCase()
        }
        override val platformNum: Int = 2
    };

    fun toStringTest(): String {
        return this.toString()
    }

    abstract fun toUpperCaseTest(): String
    abstract val platformNum: Int
}

Swift에서도 사용할 수 있다.

struct ContentView: View {
	let greet = Greeting().greet()
    
    func getPlatformType() -> PlatformType {
        let type: PlatformType = PlatformType.android
        let name = type.toStringTest()
        let uppercaseName = type.toUpperCaseTest()
        let platformNum = type.platformNum
        
        return type
    }

	var body: some View {
        VStack {
            Text(getPlatformType().toStringTest())
            Text(getPlatformType().toUpperCaseTest())
            Text("\(getPlatformType().platformNum)")
        }
	}
}

UIKit 관련 변수들을 Kotlin에서 사용 가능

platform.UIKit 패키지에 UIKit 관련 코드들이 래핑돼있어 CGFloat, 등을 사용할 수 있다.
다만 SwiftUI 관련 코드는 래핑돼있지 않아 Image, Color, EdgeInsets 등은 사용이 불가능하다.

Kotlin의 suspend 함수를 Swift에서 사용

결론부터 말하면 조건하에 가능하다.

Kotlin의 suspend 함수는 objective-c의 completionHandler로 변환되며, Swift에서 async/await 로 그대로 사용 가능하다.

다만 suspend 함수는 메인 쓰레드에서만 호출 가능하다. 따라서 @MainActor로 선언해 사용하는 등의 처리가 필요하다.

예시
SentinelApiKmm.kt

class SentinelApiKmm(private val baseUrl: String) {
  /*
  ...
  */
  
  @Throws(Throwable::class)
  suspend fun userSignIn(userSignInFormat: UserSignInFormatKmm): LoginResponseKmm {
      val response = httpClient.post("$baseUrl/users/sign-in") {
          contentType(ContentType.Application.Json)
          setBody(userSignInFormat)
      }
      return response.body()
  }
}

SentinelApi.swift

@MainActor
static func userSignInKmm(userEmail: String, password: String) async -> Result<LoginResponseKmm, Error> {
    let client = SentinelApiKmm(baseUrl: url)
    do {
        let response = try await client.userSignIn(userSignInFormat: UserSignInFormatKmm(userEmail: userEmail, password: password))
        return Result.success(response)
    } catch let error {
        NSLog(error.localizedDescription)
        return Result.failure(error)
    }
}

참고자료

profile
배울게 끝이 없네 끝이 없어

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

개발자로서 성장하는 데 큰 도움이 된 글이었습니다. 감사합니다.

답글 달기