Skipでハマった問題まとめ
最近「Skip」というツールを使って、SwiftだけでiOSとAndroidの両方に対応するアプリ開発に挑戦しています(アプリは出来次第公開します)。
Skipとは
SwiftでiOS / Androidアプリを開発できるツール・フレームワークです。
SwiftUIをJetpack Composeに変換したり、Pure SwiftコードをKotlinに変換(Transpiled Mode、SwiftのコードがKotlinのコードに翻訳される仕組み)したり、Pure SwiftコードをAndroid上でそのまま動かしたり(Native Mode)することができます。
今はNative Modeは使ってなくてTranspiled Modeで進めています(Kotlinコードが生成されるので問題の把握がしやすい)。
ハマったけどなんとかなった問題
単純にSkip側で対応してない機能や、Jetpack Compose側で対応してない機能が色々あります。
ただ、書き方を変えたりロジックを変えたりするとなんとかなることも色々あります。
Assetの色や画像リソースが簡単に扱えない問題
Assetのコンテンツを自動的に使えるようにしてくれる ImageResource
や ColorResource
に未対応です。
なので例えば mainText
みたいな色をxcassetsに追加した場合、 Color(.mainText)
とは書けなくて、 Color("mainText", bundle: .module)
と書く必要があります。
SwiftUIなら普通に使える系の機能も対応してなかったりすることもあります。
そういうのは一旦 #if !SKIP ... #endif
で囲ったりして対処しています。
(ジェスチャー周りが全然動かないのでAndroidでは無視したりしています)
FirestoreのlimitでLong型が要求される問題
SkipFirebaseを使って firestore
で limit
を使おうとするとビルドが通りませんでした。
エラーを見ると Long
型を求められていました。
Swiftには Long
はないよなと思って調べるとSwiftでは Int64
で対処可能でした。
(最初からGitHubみとけば問題にならなかった問題)
ということでこんな感じのコードになりました。
let query = firestore.collection("collections")
#if SKIP
.limit(to: Int64(pageSize)) // SkipではLong型が必要なのでInt64で指定
#else
.limit(to: Int(pageSize)) // 通常のSwiftコードではInt型でOK
#endif
where(in:)
の引数型が合わない問題
Firestoreで 同じくSkipFirebaseを使って firestore
で where(in:)
を使いたかったので下記のようなコードを書いていました。
func search(_ values: [String]) async -> [Result] {
Firestore.firestore().collection("collection")
.where("key", in: values)
...
}
ところがこれだとビルドが通りませんでした。
引数の型が [String]
だったのが悪いのかKotlin側でビルドができなかったので下記のように修正しました。
func search(_ values: [any Sendable]) async -> [Result] {
Firestore.firestore().collection("collection")
.where("key", in: values)
...
}
[Any]
だとSwift 6だとエラーが出ちゃうのでここでは any Sendable
にすることで問題なくビルドすることができました。
iOSとAndroidで挙動が違う問題
SkipのSlackコミュニティがあるのでそこでも今聞いてるのですが、下記のようなコードだと、iOS / Androidで挙動が違ってしまいます。
struct WelcomeView : View {
@State var heartBeating = false
@Environment(ViewModel.self) var viewModel: ViewModel
var body: some View {
@Bindable var viewModel = viewModel
ScrollView {
VStack(spacing: 0) {
Text("Hello [\(viewModel.name)](https://skip.tools)!")
.padding()
Image(systemName: "heart.fill")
.foregroundStyle(.red)
.scaleEffect(heartBeating ? 1.5 : 1.0)
.animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating)
.onAppear { heartBeating = true }
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
ForEach(0..<100) { index in
ZStack {
Color.yellow
Text(String(describing: index))
}
.frame(height: 80)
}
}
}
.font(.largeTitle)
}
}
}
iOSでは画面全体がスクロールするのですが、Androidでは LazyVGrid
の部分だけがスクロールしてしまい、画面全体がスクロールしません。
2025/03/19: 追記
SkipのSlackコミュニティにてこちらの解決方法を教えてもらいました。
ScrollView { LazyVGrid { Section { ... } header: { ... } } }
のようにSection Headerを活用すればスクロールするとのことでした。
修正後のコード
struct WelcomeView: View {
@State var heartBeating = false
@Environment(ViewModel.self) var viewModel: ViewModel
var body: some View {
@Bindable var viewModel = viewModel
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
Section {
ForEach(0..<100) { index in
ZStack {
Color.yellow
Text(String(describing: index))
}
.frame(height: 80)
}
} header: {
VStack(spacing: 0) {
Text("Hello [\(viewModel.name)](https://skip.tools)!")
.padding()
Image(systemName: "heart.fill")
.foregroundStyle(.red)
.scaleEffect(heartBeating ? 1.5 : 1.0)
.animation(
.easeInOut(duration: 1).repeatForever(), value: heartBeating
)
.onAppear { heartBeating = true }
}
.font(.largeTitle)
}
}
}
}
}
ハマって対処もできなかった問題
RemoteConfigが利用できない問題
これまたSkipFirebaseの問題なのですが、一応RemoteConfigも存在するので使えるんだろうとは思うのですが、使おうとすると全然ビルドが通らなくて一旦諦めました。
使える方法があれば知りたいです。
まとめ
この記事は若干ネガティブなことが多くて申し訳ないのですが、Swift・SwiftUIのコードだけでAndroidが動くというのはSwiftでコードを書きたいという人にとってはとても夢があるし個人的には可能性を感じています。
ドキュメントも割としっかりしているし、関連のコードもGitHubにあったりサンプルコードもあったりするのでiOSアプリの開発経験のある人なら開発はできるのではないかと思いました。
まだ課題もありますが、SwiftのコードだけでAndroidアプリを開発できるSkipは、Swiftエンジニアにとって非常に魅力的なツールです。個人的にも開発を楽しみながら、Skipコミュニティとともに成長を見守っていきたいです。Skipの話ができる仲間が増えると嬉しいですね!
(一部ChatGPTに表現を直してもらいましたが90%ぐらいは自分で書きました)
Discussion