Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
iOSにKMMを導入するtips
Taiki Suzuki
February 24, 2021
Programming
1
1.4k
iOSにKMMを導入するtips
Flutter × Kotlin Multiplatform by CyberAgent #2
https://cyberagent.connpass.com/event/204014/
Taiki Suzuki
February 24, 2021
Tweet
Share
More Decks by Taiki Suzuki
See All by Taiki Suzuki
既存のプロジェクトにKMMを導入するための対応策
martysuzuki
2
370
iOSアプリエンジニアが2-3週間でそれなりのAndroidアプリを開発できるようになるまでの考え方
martysuzuki
6
790
Unioを開発してプロダクトに導入してから2年が経った
martysuzuki
0
730
iOSDC Japan 2020 Day 1 Track B 10:50
martysuzuki
5
2.1k
Combineを利用したSwiftUI・UIKitのどちらにも対応するUnidirectionalな設計を実現するには
martysuzuki
0
1.2k
MVVMの実装を縛るFrameworkを開発・導入し、チームでばらつきがあった実装を統一する
martysuzuki
2
760
ログの発火テストをXCUITestで自動化しようとしたがUnitテストで実装した話
martysuzuki
7
760
今更聞けないMVPとMVVMの違い
martysuzuki
10
3.2k
MVC→MVP→MVVM→Fluxの実装の違いを比較してみる
martysuzuki
39
15k
Other Decks in Programming
See All in Programming
Embracing commonMain for Android Development - Droidcon SF 2022
handstandsam
4
230
Android Tools & Performance
takahirom
1
410
Voiceflowではじめる音声アプリ・チャットボット開発〜2022年版〜 / Introduction to Developing Voice Apps & Chatbots with Voiceflow
kun432
0
170
Seleniumでイキってたらサーバを絞め落としかけてた話
kenfujita
0
360
Mobile Product Engineering
championswimmer
0
290
言語処理ライブラリ開発における失敗談 / NLPHacks
taishii
1
420
RFC 9111: HTTP Caching
jxck
0
150
GitHub Actions を導入した経緯
tamago3keran
1
420
Why Airflow? & What's new in Airflow 2.3?
kaxil
0
110
Keeping your team in top shape with the Gradle Enterprise API
runningcode
3
120
GitHubのユーザー名を変更した後のあれこれ
tahia910
0
120
io22 extended What's new in app performance
veronikapj
0
330
Featured
See All Featured
The Straight Up "How To Draw Better" Workshop
denniskardys
225
120k
WebSockets: Embracing the real-time Web
robhawkes
57
5.1k
Music & Morning Musume
bryan
35
4.2k
GraphQLとの向き合い方2022年版
quramy
16
8.2k
Typedesign – Prime Four
hannesfritz
33
1.3k
The Art of Programming - Codeland 2020
erikaheidi
32
9.8k
VelocityConf: Rendering Performance Case Studies
addyosmani
316
22k
A Philosophy of Restraint
colly
192
15k
How GitHub Uses GitHub to Build GitHub
holman
465
280k
Ruby is Unlike a Banana
tanoku
91
9.2k
Documentation Writing (for coders)
carmenhchung
48
2.5k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
181
15k
Transcript
Flutter × Kotlin Multiplatform by 2021/02/24 #2 #ca_flutter_kmm by Taiki
Suzuki iOSΞϓϦʹKMMΛಋೖ͢Δtips
ࣗݾհ #ca_flutter_kmm marty_suzuki marty-suzuki Taiki Suzuki
ΞδΣϯμ #ca_flutter_kmm • Suspend functionΛiOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ • ςετͷͱ͖͚ͩҙͷϞδϡʔϧΛimport͍ͨ͠ • KMMͷFrameworkΛϓϥΠϕʔτϦϙδτϦͷReleasesʹ Ξοϓϩʔυͯ͠CocoaPodsͰDLͰ͖ΔΑ͏ʹ͢Δ
• KotlinͰ࣮͞ΕͨNSObjectͷαϒΫϥεΛSwizzle͢Δͱ Ϋϥογϡ͢Δ
Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm
KotlinͰͷInterface #ca_flutter_kmm public interface VideoRankingApi { public suspend fun getGenreRanking(
type: GenreRankingType, limit: Int? ): GenreRankingResponse }
SwiftͰݺͼग़ͤΔϝιου #ca_flutter_kmm videoRankingApi.getGenreRanking( type: .free, limit: nil completionHandler: { (response:
GenreRankingResponse?, error: Error?) in } ) ※ϝιουͷฦΓ͕VoidͳͷͰAPI௨৴ͳͲΛΩϟϯηϧ͢Δज़͕Swift͔ΒΞΫηεͰ͖Δੈքʹͳ͍
SwiftͰҎԼͷΑ͏ʹݺͼग़ͤΔΑ͏ʹ͢Δ #ca_flutter_kmm let job = videoRankingApi.getGenreRanking(type: .free, limit: nil) .subscribe(
onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil)
Working with Kotlin Coroutines and RxSwift #ca_flutter_kmm https://touchlab.co/kotlin-coroutines-rxswift/
Suspend functionΛϥοϓ͢Δ #ca_flutter_kmm class SuspendWrapper<T>(private val suspender: suspend () ->
T) { suspend fun suspend(): T = suspender() fun subscribe( onSuccess: (item: T) -> Unit, onThrow: (error: Throwable) -> Unit ) : Job = CoroutineScope(SupervisorJob() + Dispatchers.Main).launch { try { onSuccess(suspender()) } catch (error: Throwable) { onThrow(error) } } }
Suspend functionΛϥοϓ͢Δ #ca_flutter_kmm public interface VideoRankingApi { public suspend fun
getGenreRanking( type: GenreRankingType, limit: Int? ): SuspendWrapper<GenreRankingResponse> } ˞%FGFSSFE5Ͱฦ͢ํ๏͋Δ͕ɺ%FGFSSFEJOUFSGBDFͰ͋ΔͨΊ0CKFDUJWF$ʹม͢ΔͱQSPUPDPMʹͳΔ ɹ0CKFDUJWF$ͷQSPUPDPM(FOFSJD"SHVNFOUΛ࣋ͨͳ͍ͨΊɺ5ͱఆ͍ٛͯͯ͠"OZʹͳͬͯ͠·͏
SwiftͰҎԼͷΑ͏ʹݺͼग़ͤΔ #ca_flutter_kmm let job = videoRankingApi.getGenreRanking(type: .free, limit: nil) .subscribe(
onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil) ˞σϝϦοτͱͯ͠J04Ͱ্هͷݺͼग़͠ΛͰ͖ΔΑ͏ʹ͢ΔͨΊʹɺ"OESPJEଆͰຊདྷෆཁͳTVTQFOE Λ ɹܦ༝ͯ͠TVTQFOEGVODUJPOΛݺͼग़͢Α͏ʹͳͬͯ͠·͏
Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm SuspendWrapperΛͬͱ׆༻͍ͯ͘͠
Koru #ca_flutter_kmm https://github.com/FutureMind/koru
KoruͰࣗಈੜ͞ΕΔΫϥε #ca_flutter_kmm public class VideoRankingApiNative( private val wrapped: VideoRankingApi )
{ public fun getGenreRanking( type: GenreRankingType, limit: Int? ): SuspendWrapper<GenreRankingResponse> = SuspendWrapper(exportedScopeProvider_mainScopeProvider) { wrapped.getGenreRanking(type, limit) } }
KoruΛར༻͢Δ #ca_flutter_kmm @ToNativeClass(launchOnScope = MainScopeProvider::class) public interface VideoRankingApi { public
suspend fun getGenreRanking( type: GenreRankingType, limit: Int? ): GenreRankingResponse }
KoruΛར༻͢Δ #ca_flutter_kmm @ExportedScopeProvider public class MainScopeProvider : ScopeProvider { override
val scope: CoroutineScope = MainScope() }
Suspend functionͷؔΛͦͷ··ݺΜͩ߹ #ca_flutter_kmm videoRankingApi.getGenreRanking( type: .free, limit: nil completionHandler: {
(response: GenreRankingResponse?, error: Error?) in } )
KoruͰࣗಈੜ͞ΕͨΫϥεͰϥοϓͯ͠ݺͼग़͢ #ca_flutter_kmm let videoRankingApi: VideoRankingApi let api = VideoRankingApiNative(wrapped: videoRankingApi)
let job = api.getGenreRanking(type: .free, limit: nil) .subscribe( onSuccess: { (response: GenreRankingResponse?) in }, onThrow: { (throwable: KotlinThrowable) in } ) job.cancel(cause_: nil)
SuspendWrapperΛRxSwiftͰදݱ͢ΔͱSingleͱͯ͠ѻ͑Δ #ca_flutter_kmm enum SuspendWrapperError: Error { case invalidResponse case throwable(KotlinThrowable)
} extension Single where Element: AnyObject { static func create(_ suspendWrapper: SuspendWrapper<Element>) -> Single<Element> { Single.create { observer in let job = suspendWrapper.subscribe( onSuccess: { value in guard let value = value else { observer(.failure(SuspendWrapperError.invalidResponse)) return } observer(.success(value)) }, onThrow: { throwable in observer(.failure(SuspendWrapperError.throwable(throwable))) } ) return Disposables.create { job.cancel(cause_: nil) } } } }
ҙͷΫϥεͰར༻͢Δ߹ #ca_flutter_kmm final class VideoRankingRepository { private let videoRankingApi: VideoRankingApiNative
init(videoRankingApi: VideoRankingApi) { self.videoRankingApi = VideoRankingApiNative(wrapped: videoRankingApi) } func getGenreRanking() -> Single<[GenreRanking]> { Single.create(videoRankingApi.getGenreRanking(type: .free, limit: nil)) .map { $0.genres.map(GenreRanking.init) } } }
Suspend functionΛ iOSͰΩϟϯηϧͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm SuspendWrapperΛར༻ͨ͠ΫϥεΛςετ͢Δ
iOSͰͷςετ༻ʹImmediateScopeΛՃ #ca_flutter_kmm @Suppress("FunctionName") public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() +
Dispatchers.Main) https://github.com/Kotlin/kotlinx.coroutines/blob/1.4.2/kotlinx-coroutines-core/common/src/CoroutineScope.kt#L84-L104
MainScopeΛར༻͍ͯ͠ΔͷͰඇಉظͳςετΛॻ͘ඞཁ͕͋Δ #ca_flutter_kmm class SampleProjectTests: XCTestCase { func testGenreRanking() throws {
let fakeApi = FakeVideoRankingApi() let suspender = MockSuspendFunction<GenreRankingResponse>() fakeApi.genreRankingResponse = suspender.asSuspendWrapper() let repository = VideoRankingRepository(videoRankingApi: fakeApi) let expect = expectation(description: "Wait") var response: [GenreRanking]? let disposable = repository.getVideoGenreRanking() .subscribe(onSuccess: { (value: [GenreRanking]) in response = value expect.fulfill() }) defer { disposable.dispose() } DispatchQueue.main.async { suspender.respond(.success(RankingVideoSeriesResponse.Companion().fake())) } wait(for: [expect], timeout: 0.1) XCTAssertNotNil(response) } } ᶃ ᶄ ᶅ ᶆ ᶇ
Dispatchers.Main.immediate͕Main Thread͔ΒͰdispatch͞ΕΔ #ca_flutter_kmm https://github.com/Kotlin/kotlinx.coroutines/issues/2283
iOSͰͷςετ༻ʹImmediateScopeΛՃ #ca_flutter_kmm @Suppress("FunctionName") public fun ImmediateScope() = CoroutineScope( SupervisorJob() +
object : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { block.run() } } )
iOSͰͷςετ༻ʹImmediateScopeΛՃ #ca_flutter_kmm @ExportedScopeProvider public class MainScopeProvider : ScopeProvider { override
val scope: CoroutineScope get() = scopeHolder.access { it.scope } private class ScopeHolder(var scope: CoroutineScope) private val scopeHolder = IsolateState<ScopeHolder> { ScopeHolder(MainScope()) } fun setScope(scope: CoroutineScope) { scopeHolder.access { it.scope = scope } } }
iOSͰͷςετ༻ʹImmediateScopeΛՃ #ca_flutter_kmm MainScopeProviderContainerKt .exportedScopeProvider_mainScopeProvider .setScope(scope: ImmediateScopeKt.ImmediateScope())
ςετͷSuspendFunction͚ͷScopeProviderΛՃ #ca_flutter_kmm final class ImmediateScopeProvider: ScopeProvider { let scope: CoroutineScope
= ImmediateScopeKt.ImmediateScope() }
ςετ༻ͷKotlinSuspendFunctionͷϞοΫΛՃ #ca_flutter_kmm final class MockSuspendFunction<T: AnyObject>: KotlinSuspendFunction0 { private var
handler: ((Result<T, Error>) -> Void)? func invoke(completionHandler: @escaping (Any?, Error?) -> Void) { handler = { result in do { completionHandler(try result.get(), nil) } catch { completionHandler(nil, error) } } } func respond(_ result: Result<T, Error>) { handler?(result) } func asSuspendWrapper() -> SuspendWrapper<T> { SuspendWrapper(scopeProvider: ImmediateScopeProvider(), suspender: self) } }
ImmediateScopeΛར༻ͯ͠ಉظతʹϢχοτςετΛ࣮ߦ͢Δ #ca_flutter_kmm class SampleProjectTests: XCTestCase { func testGenreRanking() throws {
MainScopeProviderContainerKt.exportedScopeProvider_mainScopeProvider .setOverrideScope(scope: ImmediateScopeKt.ImmediateScope()) let fakeApi = FakeVideoRankingApi() let suspender = MockSuspendFunction<GenreRankingResponse>() fakeApi.genreRankingResponse = suspender.asSuspendWrapper() let repository = VideoRankingRepository(videoRankingApi: fakeApi) let response = BehaviorRelay<[GenreRanking]?>(value: nil) let disposable = repository.getGenreRanking().asObservable().bind(to: response) defer { disposable.dispose() } suspender.respond(.success(GenreRankingResponse.Companion().fake())) XCTAssertNotNil(response.value) } }
ςετͷͱ͖͚ͩ ҙͷϞδϡʔϧΛimport͍ͨ͠ #ca_flutter_kmm
KMMͷߏ #ca_flutter_kmm module A iOSKMM.framwork
KMMͷߏ #ca_flutter_kmm ios-kmm module A module B iOSKMM.framwork
KMMͷߏ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMM.framwork iOSKMMMock.framwork
KMMͷߏ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMM.framwork iOSKMMMock.framwork
❌
KMMͷߏ #ca_flutter_kmm ios-kmm module A module B ios-kmm-mock iOSKMMMock.framwork ios-kmm
module A module B iOSKMM.framwork
== KMMͷߏ #ca_flutter_kmm iOSKMM.framwork iOSKMMMock.framwork ❌ module A class ApiClient
module A class ApiClient
ଥڠҊ: ReleaseͱDebugͰϦϯΫ͢ΔFrameworkΛذ͢Δ #ca_flutter_kmm target ‘SampleProject’ do pod 'iOSKMM', '0.1.0', :configuration
=> 'Release' pod 'iOSKMMMock', ‘0.1.0', :configuration => 'Debug' end ※ςετλʔήοτͷϏϧυ࣌ʹϦϯΫ͢ΔFrameworkΛมߋ͢ΔεΫϦϓτΛ࣮ߦ͢Δͱ͍͏ख͋Γ·͢
KMMͷFrameworkΛϓϥΠϕʔτϦϙδτϦͷReleasesʹ Ξοϓϩʔυͯ͠CocoaPodsͰDLͰ͖ΔΑ͏ʹ͢Δ #ca_flutter_kmm
Use a Kotlin Gradle project as a CocoaPods dependency #ca_flutter_kmm
Use a Kotlin Gradle project as a CocoaPods dependency #ca_flutter_kmm
https://kotlinlang.org/docs/native-cocoapods.html#add-a-dependency-between-a- kotlin-pod-and-xcode-project-with-one-target
όΠφϦΛCocoaPodsܦ༝Ͱཧ͢Δ #ca_flutter_kmm Private Repository Releases iOSKMM.zip target ‘SampleProject’ do pod
'iOSKMM', '0.1.0' end iOSKMM.framwork iOSKMM.zip iOSKMM.zip Private Specs iOSKMM.podspec iOSKMM.podspec.json iOSKMM.framwork
௨ৗͷpodspecͷఆٛ #ca_flutter_kmm Pod::Spec.new do |spec| spec.name = 'iOSKMM' spec.version =
'0.1.0' spec.homepage = 'https://github.com/marty-suzuki/kmm' spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = { :http => 'https://github.com/marty-suzuki/kmm/releases/download/0.1.0/iOSKMM.zip' } spec.vendored_frameworks = 'iOSKMM.framework' end
Private RepositoryͷReleases #ca_flutter_kmm ˞1SJWBUF3FQPTJUPSZͳͷͰʮIUUQTHJUIVCDPNNBSUZTV[VLJLNNSFMFBTFTEPXOMPBEJ04,..[JQʯʹ ɹೝূঢ়ଶͰΞΫηεͨ͠ͱͯ͠ɺΞΫηεՄೳͳ63-BTTFUͷVSMͱͳΔͨΊΤϥʔʹͳͬͯ͠·͏
assetsͷURLͷऔಘॲཧΛؚΜͩpodspec #ca_flutter_kmm Pod::Spec.new do |spec| module PrivateRepository class Spec attr_reader
:version attr_reader :homepage attr_reader :source def initialize(owner, name, version, source) @version = version @homepage = "https://github.com/#{owner}/#{name}" @source = source end end class SpecLoader def initialize(owner, name, version, token) @owner = owner @name = name @version = version if !token.nil? && !token.empty? @authorizationToken = "Authorization: token #{token}" else @authorizationToken = nil end end def loadPrivateSpec tagResponse = loadTag assetURL = getAssetURL(tagResponse) headers = ["Accept: application/octet-stream"] if !@authorizationToken.nil? headers.push(@authorizationToken) end source = { :http => assetURL, :headers => headers, :type => "zip" } return PrivateRepository::Spec.new(@owner, @name, @version, source) end private def loadTag requestURL = "https://api.github.com/repos/#{@owner}/#{@name}/releases/tags/#{@version}" cmdElements = ["curl"] if !@authorizationToken.nil? cmdElements.push("-sL") cmdElements.push("-H \"#{@authorizationToken}\"") else cmdElements.push("-nsL") end cmdElements.push(requestURL) cmd = cmdElements.join(" ") response = %x[#{cmd}] if response.nil? puts "Failed Request: #{requestURL}" exit! else return response end end def getAssetURL(tagResponse) tag_hash = JSON.load(tagResponse) asset = tag_hash["assets"]&.first assetURL = asset && asset["url"] if assetURL.nil? puts "Can't get asset URL." exit! else return assetURL end end end end privateSpecLoader = PrivateRepository::SpecLoader.new("marty-suzuki", "kmm", "0.1.0", ENV["GITHUB_ACCESS_TOKEN"]) privateSpec = privateSpecLoader.loadPrivateSpec spec.name = 'iOSKMM' spec.version = privateSpec.version spec.homepage = privateSpec.homepage spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = privateSpec.source spec.vendored_frameworks = 'iOSKMM.framework' end
pod repo push࣌ʹpodspecͰassetsͷURLΛऔಘ͢Δ #ca_flutter_kmm iOSKMM.podspec GitHub API GET /repos/{OWNER}/{REPO}/releases/tag/{NAME} ҙͷtagͷreleasesͷใ
Ϩεϙϯεͷassets͔ΒҙͷzipϑΝΠϧ໊ͷassetΛऔಘ͢Δ asset[“url”]Λऔಘ͠ɺspecͷsourceʹೖ͢Δ
࠷ऴతͳpodspecͷ༰ #ca_flutter_kmm Pod::Spec.new do |spec| spec.name = 'iOSKMM' spec.version =
'0.1.0' spec.homepage = 'https://github.com/marty-suzuki/kmm' spec.authors = 'marty-suzuki' spec.license = 'LICENSE' spec.summary = 'SUMMARY' spec.source = { :http => 'https://api.github.com/repos/marty-suzuki/kmm/releases/assets/12345678', :headers => ["Accept: application/octet-stream"], :type => "zip" } spec.vendored_frameworks = 'iOSKMM.framework' end
Private RepositoryͷReleases༻ͷpodspec template #ca_flutter_kmm IUUQTHJTUHJUIVCDPNNBSUZTV[VLJ EGFDDCFFDGEEDBCEB
Private RepositoryͷReleasesΛར༻͢Δ߹ͷҙ #ca_flutter_kmm • v1.9.xͷCocoaPodsͰɺ࣮ࡍʹpod installՄೳͳpodspecͰ͋ͬͯpod lib lint࣌ʹ vendored_frameworksͷvalidationͰΤϥʔʹͳͬͯ͠·͏ͷͰɺlintΛ࣮ߦ͠ͳ͍ ͱ͍͏ରԠํ๏͕ݱঢ়ͷճආࡦʢ࠷৽൛Ͱͬͯͳͦ͞͏ʣ
• GITHUB_ACESS_TOKENΛར༻ͭͭ͠pod repo push࣌ʹ—use-jsonΛࢦఆ͢Δͱ Private SpecsͷjsonͷheadersʹGITHUB_ACESS_TOKEN͕هࡌ͞Εͯ͠·͏ͷͰ .netrcΛར༻͢Δ͜ͱΛਪ • xcframeworkΛར༻͍ͯ͠Δͱ࣮ࡍʹϦϯΫՄೳͰpod repo push࣌ʹlinkerͷΤϥʔ ʹͳΔ߹͕͋ΔͷͰɺ—skip-import-validationͱ—skip-testsΛࢦఆ͢Δ͜ͱͰճආ • pod install࣌ʹ.netrc·ͨGITHUB_ACCESS_TOKEN͕ඞཁʢpod repo pushΛ ࣮ߦͨ͠ͱ͖ʹར༻ͨ͠ํʹ߹ΘͤΔʣ
KotlinͰ࣮͞ΕͨNSObjectͷαϒΫϥεΛ Swizzle͢ΔͱΫϥογϡ͢Δ #ca_flutter_kmm
Fixed since kotlin-native v1.4.30 #ca_flutter_kmm https://github.com/JetBrains/kotlin-native/pull/4569
e.g. ktor IosResponseReader #ca_flutter_kmm https://github.com/ktorio/ktor/blob/1.5.1/ktor-client/ktor-client-ios/darwin/ src/io/ktor/client/engine/ios/IosResponseReader.kt
e.g. ktor IosResponseReader #ca_flutter_kmm
e.g. ktor IosResponseReader #ca_flutter_kmm https://github.com/ktorio/ktor/blob/master/ktor-client/ktor-client-ios/ darwin/src/io/ktor/client/engine/ios/IosClientEngine.kt ~ ~
e.g. ktor IosResponseReader #ca_flutter_kmm internalͳͷͰKMMଆͰରԠ͢Δ͜ͱ͕Ͱ͖ͳ͍ forkͯ͠मਖ਼ରԠ͢Δ͔͠ͳ͍?
SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm extension URLSession { @objc dynamic fileprivate class func
kotlinWorkaroundInitializer( configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue? ) -> URLSession { kotlinWorkaroundInitializer( configuration: configuration, delegate: delegate.map { // xxxio.ktor.client.engine.ios.IosResponseReader0 guard NSStringFromClass(type(of: $0)).contains("ktor") else { return $0 } return WorkaroundDelegateProxy(delegate: $0) }, delegateQueue: queue ) } }
SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm fileprivate final class KotlinWorkaroundDelegateProxy: NSObject, URLSessionDataDelegate { private
let delegate: URLSessionDataDelegate? init(delegate: URLSessionDelegate) { self.delegate = delegate as? URLSessionDataDelegate } … func urlSession( _ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data ) { delegate?.urlSession?(session, dataTask: dataTask, didReceive: data) } }
SwiftଆͰURLSessionͷinitializerΛswizzleͯ͠delegateΛೖΕସ͑Δ #ca_flutter_kmm extension ApiClient { private static var _once: ()
= { typealias FunctionType = (URLSessionConfiguration, URLSessionDelegate?, OperationQueue?) -> URLSession let from = #selector(URLSession.init(configuration:delegate:delegateQueue:) as FunctionType) let to = #selector(URLSession.kotlinWorkaroundInitializer(configuration:delegate:delegateQueue:)) guard let fromMethod = class_getClassMethod(URLSession.self, from), let toMethod = class_getClassMethod(URLSession.self, to) else { return } method_exchangeImplementations(fromMethod, toMethod) }() static func swizzleURLSessionInitializer() { _ = _once } }
One more thing… #ca_flutter_kmm
Flutter × Kotlin Multiplatform by 2021/xx/xx #3 #ca_flutter_kmm KMMશମʹؔ͢ΔτʔΫ UBLBIJSPN
!OFX@SVOOBCMF
#ca_flutter_kmm ͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ by Taiki Suzuki iOSΞϓϦʹKMMΛಋೖ͢Δtips