👌
[SwiftUI] #availableチェックを楽にする
SwiftUIのmodifierに@available
がついているとき、ありますよね。たとえばiOS15で追加されたsubmitLabel
は次のような定義になっています。
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {
/// Sets the submit label for this view.
///
/// Form {
/// TextField("Username", $viewModel.username)
/// .submitLabel(.continue)
/// SecureField("Password", $viewModel.password)
/// .submitLabel(.done)
/// }
///
/// - Parameter submitLabel: One of the cases specified in ``SubmitLabel``.
public func submitLabel(_ submitLabel: SubmitLabel) -> some View
}
今、iOS13を最低要件としたプロジェクトで、次のようなview
にsubmitLabel
を追加することを考えます。
var body: some View {
TextField("Text", $text)
.someLongMethodChain()
}
このmodifierを使うにはif #available(...)
チェックが必要になります。従って素直に書くとこうなります。
var body: some View {
if #available(iOS 15.0, *) {
TextField("Text", $text)
.someLongMethodChain()
.submitLabel(.done)
} else {
TextField("Text", $text)
.someLongMethodChain()
}
}
変更自体はシンプルですが、嫌な点として同じ構造を2度書くことになってしまいました。これは変更するときに厄介になります。
そこで、切り出しを行います。
var textField: some View {
TextField("Text", $text)
.someLongMethodChain()
}
var body: some View {
if #available(iOS 15.0, *) {
textField
.submitLabel(.done)
} else {
textField
}
}
先程よりはマシになりました。
しかし、今度はiOS14から追加されたhogeModifier
をくっつけたくなったとします。以下のようになります。
var body: some View {
if #available(iOS 15.0, *) {
textField
.hogeModifier()
.submitLabel(.done)
} else if #available(iOS 14.0, *) {
textField
.hogeModifier()
} else {
textField
}
}
再び共通の構造が生じてしまったので、切り出すことにします。
var textField: some View {
TextField("Text", $text)
.someLongMethodChain()
}
@available(iOS 14.0, *)
var iOS14textField: some View {
textField
.hogeModifier()
}
var body: some View {
if #available(iOS 15.0, *) {
iOS14textField
.submitLabel(.done)
} else if #available(iOS 14.0, *) {
iOS14textField
} else {
textField
}
}
滅多に使わないmodifierならこれでもいいにはいいのですが、submitLabel
のような頻繁に使うmodifierではさすがにやっていられない気がします。
解決法
submitLabel
を定義し直します。
// SubmitLabel
enum _Available_SubmitLabel {
case `continue`, done, go, join, next, `return`, route, search, send
@available(iOS 15, *)
var label: SubmitLabel {
switch self {
case .continue: return .continue
case .done: return .done
case .go: return .go
case .join: return .join
case .next: return .next
case .return: return .return
case .route: return .route
case .search: return .search
case .send: return .send
}
}
}
extension View {
@ViewBuilder
func iOS15_submitLabel(_ label: _Available_SubmitLabel) -> some View {
if #available(iOS 15, *) {
self.submitLabel(label.label)
} else {
self
}
}
}
var body: some View {
TextField("Text", $text)
.someLongMethodChain()
.iOS14_hogeModifier()
.iOS15_submitLabel(.done)
}
やや力技と感じるかもしれませんが、複数の利点があります。
- prefixがついていれば動作の推測が十分につきます。
- availabilityの違うmodifierが複数あってもチェックを分ける必要がありません。
- プロジェクトのOS要件が変更になってavailabilityチェックが要らなくなった場合、
iOS15_submitLabel
を一括でsubmitLabel
にrenameした上でextension
を消すだけでそのまま動きます。
一括renameの利点は失われますが、modifierによっては次のようなelse
ブロックを用意しておくとフォールバックもかけるようになります。
extension View {
@ViewBuilder
func iOS15_submitLabel<ElseView: View>(_ label: _Available_SubmitLabel, `else`: (Self) -> ElseView) -> some View {
if #available(iOS 15, *) {
self.submitLabel(label.label)
} else {
else(self)
}
}
}
Discussion