👁️‍🗨️

Swiftで同じ名前のメソッドでオーバーロードしたい!

2021/12/22に公開

この記事は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が通っていないのでこれから変わる可能性があります。使うときは理解して使いましょう

https://github.com/apple/swift/blob/cce3e8a7f533e8484592113b2b7bf9b417bb89ca/docs/ReferenceGuides/UnderscoredAttributes.md#underscored-attributes-reference

ちなみに @_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