Swiftで同じ名前のメソッドでオーバーロードしたい!
この記事はAppify Advent Calendar 2021 の12/16の記事です。
こんにちは Appify Technologies で業務委託で携わっている bannzai です。 Appify では初期の頃からフルSwiftUIの導入をおこなっています。この記事ではSwiftUIでiOS 15対応中に見つけた実装方法を書いていきます
Swiftで同じ名前のメソッドでオーバーロードしたい!
例えば下の二つの宣言があったとしましょう
func someFunc() { print("void") }
func someFunc(x: Int = 2) { print("x: \(x)") }
この場合以下の呼び出しだとどちらの定義の関数がよばれるかが分かりません
someFunc() // void or x: 2
呼び出し側で同じ形のメソッドシグネチャになる場合に優先度を決めることができる属性がSwiftにはあります。その属性である @_disfavoredOverload
の紹介です
@_disfavoredOverload
使い方は簡単です。呼ばれたくない 方に @_disfavoredOverload
を付与します。
func someFunc() { print("void") }
@_disfavoredOverload func someFunc(x: Int = 2) { print("x: \(x)") }
someFunc() // void #=> someFunc() が呼ばれる
自身のPlayground環境で試してみてください。@_disfavoredOverload
をつける前に呼ばれていた方に @_disfavoredOverload
をつけると変化がわかります
使い所
前述したような単純なメソッドだと伝わりにくいと思うので例を変えましょう。SwiftUIの場合を考えます。
SwiftUIでは public func modifier<T>(_ modifier: T) -> SwiftUI.ModifiedContent<Self, T>
を使用したメソッドチェインでViewの見た目を変えたり、機能を追加していきます。コードで書くと下のような具合です。 .background
も .font
も .modifier
を内部で実行しています。
TabBarItem()
.font(width: 44, height: 44)
.background(Color.red)
iOS 15からTabBarのItemに対して badge
をつけられるようになりました。Document#badge 早速対応していきたいと思います。 想定しているアプリはiOS 14以降をサポートしているものと考えます。なので、 iOS >= 15
の場合は .badge
を適用。 iOS < 15
の場合は何もしないようにすればよさそうですね。
と思いましたが、うまくいかないことに気づきます。 if #avilable(iOS 15, *)
をつけるのが新API対応の定石でしたが、メソッドチェインの途中に if
を書くことができません。
TabBarItem()
.frame(width: 44, height: 44)
.background(Color.red)
if #available(iOS 15, *) { // こうは書けない
.badge(1)
}
もちろん、新たに modifier を使って iOS 15の時にだけ対応するように書くように拡張した機能を作ることも可能です。が、今回はAPIの命名をそのままに、メソッドチェインの形もフラットにして対応するとしましょう。この場合に @_disfavoredOverload
を使えば実現できます。
extension View {
@_disfavoredOverload
func badge(_ count: Int) -> some View {
Group {
if #available(iOS 15.0, *) {
badge(count)
} else {
self
}
}
}
}
解説です。公式APIと同じ形でメソッドを生やします。ただし @_disfavoredOverload
もつけます。内部の if
に注目します。iOS 15以降であれば badge(count)
の結果を返す。iOS 14未満であれば自分自身を返すようにしています。 @_disfavoredOverload
をつけていなければiOS 15以降では独自定義した badge
を無限に呼ぶことになります。 @_disfavoredOverload
により自分で定義した badge
が呼ばれる優先度が下がり、SwiftUIで提供されている badge
が呼ばれるようになります。
さて、実際にbadgeを呼ぶコードを書いてみます。元々実現したかった APIの命名をそのままに、メソッドチェインの形もフラット の形になっています。これで対応完了。めでたしめでたし
TabBarItem()
.frame(width: 44, height: 44)
.background(Color.red)
.badge(1)
後書き
この記事で紹介している手法をおすすめしている訳ではないです。_
付きのAttributesはSwift Evolutionが通っていないのでこれから変わる可能性があります。使うときは理解して使いましょう
ちなみに @_disfavoredOverload
は SwiftUI 自身で使われています。Textのinitializerとかが該当します。
各 modifier
の実装内容も .swiftinterface
で見ることができるので興味のある方は覗くのも楽しいかもしれないですね。例えば僕の環境であれば下のパスに .swiftinterface
があるので各々の環境に合わせて少し書き換えてエディタで見てみてください。intel macだとファイル名微妙に違うかも
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64e-apple-ios.swiftinterface
Discussion