SwiftではバージョンはStructにして演算子オーバーロードで比較したらどうでしょう

  • 27
    いいね
  • 1
    コメント

はじめに

iOSアプリでは現在実行中のバージョンを前回起動した際のバージョンと比較したり、特定のバージョンと比較したりということがあります。そういう時、バージョン表記としてStringで取得できる"1.0.1"のようなカンマが2つある文字列はそのままでは数値として成立しないので比較することが出来ません。その解決方法として、簡単なのはStringのcompareメソッドで比較したりしますよね。ただ、compareするにしても演算子(オペレータ)オーバーロードを使えば表現が分かりやすくなりますよという話を書いておきます。

3行くらいでやりたいことをまとめると

  • iOSで取得できるバージョンの文字列は大抵カンマが2つありそのまま数値にして比較できない
  • Stringの文字列比較で大小を比べられるため大抵はそれをメソッド化する
  • メソッド化するにしてもVersionという型を演算子で比較できるようにしとけば分かりやすい表現になる

struct でバージョンを表現する

まずstructでVersionを定義する

struct Version {
    let rawString: String    
}

次に演算子オーバーロードを定義します。

func < (lhs: Version, rhs: Version) -> Bool {
    if lhs.rawString.compare(rhs.rawString, options: .numeric) == .orderedAscending {
        // バージョンの文字列を数値として比較してます。"1.0.1" < "2.0" なら .orderedAscending
        return true
    }

    return false
}

compareの戻り値は enum ComparisonResult なので .orderedAscending などが返ってくるんですが、そういうのを隠蔽化したいという話ですね。

利用例は

Version(rawString: "1.0.0") < Version(rawString: "2.0") // true
Version(rawString: "1.0.0") < Version(rawString: "0.2") // false

このままで何のバージョンにでも使えるようなVersionが作れました。しかし、現在のアプリのバージョンが一番使うじゃないですか。というかそのために作ってるので次のように取得できるようにします

extension Version {
    static let currentApp = Version(rawString: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)
}

利用例は次のようになります。

print(Version.currentApp.rawString) // 1.0
Version(rawString: "0.1") < Version.currentApp // true
Version(rawString: "1.1") < Version.currentApp // false
Version(rawString: "10.1") < Version.currentApp // false

ちなみにXcode8.3.2のPlaygroundでは CFBundleShortVersionString で取得するバージョンは1.0になっていましたのでそれとの比較になります

ただ、Versionのextensionでアプリのバージョンを返すのが少し嫌な感じです。バージョンという型がアプリのことを知っているのってやりすぎかもしれませんね。

なので、struct CurrentApp というアプリ情報を返す型が、バージョンを返すことを考えてみましょうか。

struct CurrentApp {
    static let version = Version(rawString: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)
}

// 利用例もあわせて書くと
Version(rawString: "10.1") < CurrentApp.version

CurrentAppはバージョンの他にもビルド用の何かの情報を出したりするのに良いでしょうね。

その他

struct Versionではinitを省略したため自動で暗黙的にイニシャライザが作られたので、 rawString という外部引数名が出てきましたが、このrawStringを外部引数名として露出したくない時は、もちろんinitで外部引数名を省略すれば良いのでこちらのほうが短くて好みの人は多そうです。

struct Version {
    let rawString: String

    init(_ rawString: String) {
        self.rawString = rawString
    }
}

Version("0.1") < Version("1.1") // true

おわりに

このような比較の他にも、struct Versionのメリットとして現在のバージョンが 1..<2の間かどうか、などの際も演算子をオーバーロードすれば、範囲内かどうかの表現を演算子で表現できて、読みやすいコードになるかもしれないですね(そんなふうな事をするアプリにならないのが一番ですが)。