Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
- iOS 14.2
マルチプラットフォーム対応の定義
ここでは、以下を指して マルチプラットフォーム対応と書いてます。
- 同一のコードで、複数プラットフォームをターゲットとした時に コンパイルエラーにならない
- 特定のプラットフォームにのみ存在する機能を、そのプラットフォーム上で利用できる
特定のプラットフォームにのみ存在する機能を別プラットフォームにも実装していくことは、想定していません。
背景
SwiftUI になって、Apple の複数のプラットフォームへの対応が容易になってきました。
基本的な View は、複数プラットフォーム対応ですし、個別 OS への対応用のコードは、以下のように #if – #endif で囲うことで、対応できます。
もちろん、#else を使って、プラットフォーム毎に機能を実装することもできます。
#if os(iOS)
何らかの処理
#endif
上記のコードでは、”何らかの処理”の部分は、iOS 向けにのみコンパイル対象となるので、iOS にのみ存在する関数等を使用しても他ターゲットでコンパイルエラーが発生する等もありません。
[Swift] マルチプラットフォームで使えるコードを記述する方法
#if / ViewModifier の問題点
OS の違いは、用意されている API の違いだけではなく、View Modifier の違いもあります。
付与する View Modifier を使い分けるために、#if – #endif を使おうとして以下のようなコードを書くと、問題が発生します。
struct ContentView: View {
@State private var data = ["Item1", "Item2", "Item3", "Item4"]
var body: some View {
VStack {
#if os(iOS)
EditButton()
#endif
List {
ForEach(data, id:\.self) { title in
HStack {
Text("Title: \(title)")
}
}
.onDelete(perform: { indexSet in
data.remove(at: indexSet.first!)
})
.onMove(perform: { indices, newOffset in
withAnimation {
data.move(fromOffsets: indices, toOffset: newOffset)
}
})
}
#if os(iOS)
.environment(\.editMode, .constant(.active))
#endif // - エラー Unexpected platform condition(expected 'ios', 'arch', or 'swift')
}
}
}
エラーは、#if – #endif で囲う範囲は、”Statement” である必要があることから来ています。
# ちなみに、#if – #endif を削除すると iOS ターゲットでは問題なく動きます。macOS ターゲットでは、’editMode’ is unavailable in macOS というコンパイルエラーとなります。
View Modifier 内部でのマルチプラットフォーム対応
#if – #endif で囲う範囲が “Statement” であることが必要であれば、そのようにすれば良いということになります。
以下のような ViewModifier を作って、その内部で、#if – #endif を使います。
struct onlyiOSModifier: ViewModifier {
func body(content: Content) -> some View {
#if os(iOS)
return content.environment(\.editMode, .constant(.active))
#else
return content
#endif
}
}
先ほどのコードは以下のようになります。
List {
// .. snip
}
.modifier(onlyiOSModifier())
こうすることで、macOS ターゲットでもコンパイルエラーとならず、iOS ターゲットでは 指定したかった View Modifier が付与される状態となります。
すこし拡張
一つのビューに、OS 毎に指定したい View Modifier が異なるケースを想定して、少し拡張してみます。
まずは、OS を定義
以下のように OS を定義しました。(システムのどこかに定義されているかも・・・)
enum OperatingSystem {
case macOS
case iOS
case tvOS
case watchOS
#if os(macOS)
static let current = macOS
#elseif os(iOS)
static let current = iOS
#elseif os(tvOS)
static let current = tvOS
#elseif os(watchOS)
static let current = watchOS
#else
#error("Unsupported platform")
#endif
}
OS を判別して、適切な modifier を適用する
OS を判定して、modifier を起用するようにします。
extension View {
@ViewBuilder
func applyOSModifier() -> some View {
switch OperatingSystem.current {
case .iOS:
self.modifier(iOSOnlyModifier())
default:
self
}
}
}
上記では、iOS のみ実装していますが、macOS, tvOS 等も同様です。
使用例は以下のようになります。
List {
// .. snip
}
.applyOSModifier()
ここまでくると、マルチプラットフォーム対応のコードを書けた気になります。
注意点
当初、closure として外部から modifier を渡すことを検討していました。
しかし、closure で渡すとすると プラットフォーム依存のコードは、その内部でも #if – #endif を使うことになってしまい、コードが複雑化してしまいます。
ここまで実装してみて分かったのは、現時点では 標準的なコードで マルチプラットフォームを実現することは容易ですが、すこしでもその枠からはみ出ると 必要な手間が大幅に増えるということでした。
まとめ:マルチプラットフォーム対応の View Modifier
- view modifier には、#if os(XX) – #endif は使えない
- view modifier を作って、その内部で #if os(XX) – #endif を使う
- view extension を作ると、modifier 的に使えて便利
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link