[Swift] OptionSetについてまとめてみます

2018.12.17

はじめに

モバイルアプリサービス部の中安です。

今回は、使うと非常に便利。Swift標準ライブラリにある OptionSet についてまとめようかと思います。

OptionSet

OptionSet は Swift標準ライブラリに搭載されているプロトコルのひとつです。 かつては OptionSetType という名前で定義されていました。

リファレンスの説明にはこのように記載されています。

You use the OptionSet protocol to represent bitset types, where individual bits represent members of a set.
ビット集合型を表現するためOptionSetを使用します。個々のビットは集合のメンバーを表現します。

英訳は少し怪しいですが(苦)、ビットの集合を表すという役割が OptionSet に準拠した型には求められます。 これをもう少し噛み砕くと、プログラミングではお馴染みの「ビット演算」を容易に扱うことができるのが OptionSet になります。

ビット演算の詳しいところは他記事に委ねるとして「1つの値で複数のフラグが扱えるもの」という解釈で大丈夫かと思います。 では、iOSアプリ開発において「1つの値で複数のフラグが扱える」というのが具体的にはどういうシーンで使われているかを見てみたいと思います。

例: UIControl.State

iOSアプリ開発で頻出するOptionSetの例としては、UIControlの内部で定義されているUIControl.Stateがあると思います。 UIコンポーネントの「通常時」「選択時」「使用不可時」などを表現する型です。

そのUIControl.Stateは、下記のようにOptionSetに準拠する形で定義がなされています。

extension UIControl {

	public struct State : OptionSet {

	    public init(rawValue: UInt)
	    
	    public static var normal: UIControl.State { get }
	    public static var highlighted: UIControl.State { get }
	    public static var disabled: UIControl.State { get }
	    public static var selected: UIControl.State { get }
	    public static var focused: UIControl.State { get }
	    public static var application: UIControl.State { get }
	    public static var reserved: UIControl.State { get }
	}
}

たとえばUIButtonUIControlの継承クラスなので、 ボタンをソースコードから生成する場合、そのボタンの文言を設定する際は下記のように書きます。

let button = UIButton(type: .custom)
button.setTitle("ボタン", for: .normal)

setTitle の第2引数は、UIControl.Stateです。ここに「通常時の文言」という意味で、.normal を突っ込んでいるわけですが、 下記のようにも書くことができます。

let button = UIButton(type: .custom)
button.setTitle("ボタン", for: [.normal, .highlighted])

こうすることで「通常時の文言」と「ハイライト時の文言」を同時に設定ができます。 配列の形で渡しているので2つの値を渡しているように見えますが、OptionSetの機能により「複数のフラグを有した1つの値」として扱われます。

どういうことなのかを実際に試してみます。

let state: UIControl.State = [.normal, .highlighted, .disabled]

変数stateの型はUIControl.Stateですが、渡しているのは配列の形式です。 しかしこれはコンパイルエラーにはなりません。 normalhighlighteddisabledのビット値が合計された状態で変数に代入されるというイメージです。

当然ですが、下記のようなソースコードを書くとコンパイルエラーになります。String に対して [String] を渡しているからですね。

let state: String = [".normal", ".highlighted", ".disabled"] // エラー

OptionSet が少し特別な型だというのがわかると思います。

例: UIControl.Event

同じUIControl内にあるUIControl.Event型もまたOptionSetになります。 定義は以下のような感じです。数が多いので一部省略しています。

extension UIControl {
    
    public struct Event : OptionSet {

        public init(rawValue: UInt)

        public static var touchDown: UIControl.Event { get }
        public static var touchDownRepeat: UIControl.Event { get }
        public static var touchDragInside: UIControl.Event { get }
        public static var touchDragOutside: UIControl.Event { get }
        public static var touchDragEnter: UIControl.Event { get }
        public static var touchDragExit: UIControl.Event { get }
        public static var touchUpInside: UIControl.Event { get }
        public static var touchUpOutside: UIControl.Event { get }
        public static var touchCancel: UIControl.Event { get }
        public static var valueChanged: UIControl.Event { get }
        
        // (中略)

        public static var allTouchEvents: UIControl.Event { get }
        public static var allEditingEvents: UIControl.Event { get }
        
        // (中略)
        
        public static var allEvents: UIControl.Event { get }
    }
}

ここではOptionSetの便利な使い方である「フラグの合成」の仕組みが使われています。

allTouchEventsallEventsなどがそれに当たります。 allTouchEventsでは touch〜 のすべてのイベントを表現します。 しかし、これはフラグが再定義されているわけではなく、touch〜型のすべてのビットフラグを立てた状態で定義されています。

これについては後述しますが、ビット演算の恩恵として良い例かと思います。

例: UNAuthorizationOptions

プッシュ通知の承認処理で使用する UNAuthorizationOptions もまた OptionSet の仲間です。

public struct UNAuthorizationOptions : OptionSet {

    public init(rawValue: UInt)

    public static var badge: UNAuthorizationOptions { get }
    public static var sound: UNAuthorizationOptions { get }
    public static var alert: UNAuthorizationOptions { get }
    public static var carPlay: UNAuthorizationOptions { get }

    @available(iOS 12.0, *)
    // 省略
}

通知センターにバッジ、サウンド、アラートを個別な値で渡しているのではなく、ビットのフラグとして渡しているのです。

自分で定義してみる

ここまでは既に提供されているOptionSetの仲間を見てきましたが、自分で定義することも簡単にできます。 再びリファレンスの説明を見てみます。

When creating an option set, include a rawValue property in your type declaration.
OptionSetを作成するとき、型の定義にrawValueプロパティを含めます。

For your type to automatically receive default implementations for set-related operations, the rawValue property must be of a type that conforms to the FixedWidthInteger protocol, such as Int or UInt8.
型がデフォルトの集合関連操作の実装を自動的に受けるには、rawValueプロパティがIntやUInt8のようなFixedWidthIntegerプロトコルに準拠した型でなければなりません。

Next, create unique options as static properties of your custom type using unique powers of two (1, 2, 4, 8, 16, and so forth) for each individual property’s raw value so that each property can be represented by a single bit of the type’s raw value.
その次に、個々のプロパティに一意である2の累乗(1, 2, 4, 8, 16等々)の元値を使用したカスタムな型の静的プロパティを一意なオプションとして作ってください。そうすることで、それぞれのプロパティは型の単一ビットの元値を表現することができます。

これまた英訳怪しいですが(苦)、要約すると下記のようなことです。

  • カスタムなOptionSetの型を定義する時はrawValueプロパティを定義する
  • rawValueの型はIntUIntを使用すること
  • 静的なプロパティを定義して、それぞれに一意なrawValueを定義する
  • rawValueは2の累乗である必要がある
  • そうすることで自動的にOptionSetの機能が付与される

例: パーミッション

では実際に上の要件に合わせて作ってみるのですが、 イメージしやすいのは Linuxコマンドのchmodでもおなじみなパーミッションかなと思ったのでそれでやってみます。

struct Permission : OptionSet { // (1)
    let rawValue: UInt // (2)
    
    static let executable = Permission(rawValue: 1 << 0) // (3)
    static let writable   = Permission(rawValue: 1 << 1)
    static let readable   = Permission(rawValue: 1 << 2)
}

(1) OptionSetを実装する

書いたとおり、Permissionという構造体を定義してOptionSetに準拠します。 ちなみにOptionSetである型はそれ自体が機能を有するものではない(自動的に付与されるものを除いて)ので、クラスよりは構造体で定義したほうが良いかと思います。

(2) rawValueプロパティを定義する

リファレンスにある通りrawValueプロパティを定義します。これを定義しないと、OptionSetプロトコルの準拠に適していないというエラーが出てきます。

ここでは UInt としていますが、項目が少なければキャストの面倒臭さを避けるために Int でもいいと思います。

(3) 静的プロパティで項目を定義する

パーミッションの項目を静的プロパティで定義します。 この例では、Linux でxwrに相当する「実行権限」「書き込み権限」「読み取り権限」を作成しています。

rawValueには2の累乗を渡すとされているので、1, 2, 4 と直接な値を渡してもいいのですが、 ここは「左ビットシフト演算子(<<)」を使うほうが可読性やメンテナンス性的にはよさそうです。

使ってみる

実際に使ってみた例がこちらになります。rawValueを出力してみると 1 が表示されると思います。

let permission: Permission = .executable
print(permission.rawValue)

単一ではなく複数のフラグを立てた状態にするには、前述したように配列形式で渡します。

let permission: Permission = [.executable, .readable]
print(permission.rawValue)

ここでの出力は executable = 1readable = 4 で、5が出力されます。 大事なのは出力された数値ではなく、実態として2進数によって 最初の例は 001 となり、次の例では 101 となっていることです。

ちなみに、配列の形式になっているからといって変数の定義をこのようにしないでください。意味が変わってきてしまいます。

let permission: [Permission] = [.executable, .readable]

使い方

最後にOptionSetの使い方についてです。

フラグが立っているかどうか

指定したフラグが立っているかどうかは、containsメソッドで判定できます。

let permission: Permission = [.executable, .readable] // 5 = 101

permission.contains(.executable) // true
permission.contains(.writable) // false
permission.contains(.readable) // true

2進数の値は、101なのでwritableフラグは立っていないという結果になります。

let permission: Permission = [.executable, .readable] // 5 = 101

permission.contains([.executable, .readable]) // true
permission.contains([.executable, .writable]) // false

このように複数のフラグを一度に判定することもできます。 この際の判定は AND で判定されることも結果からわかると思います。

フラグの更新

すでに値がセットされたOptionSetのフラグ値を変更することができます。 構造体で定義されている場合は変数宣言をvarでしておく必要があります。

フラグを立てる

フラグを立てるには insert メソッドで行います。下記の例でフラグが足されていることが確認できると思います。

var permission: Permission = .executable
permission.rawValue // 1 = 001

permission.insert(.readable)
permission.rawValue // 5 = 101

permission.insert(.writable)
permission.rawValue // 7 = 111

permission.insert(.readable)
permission.rawValue // 7 = 111

当然のことながら、すでに立っているフラグに対して更にinsertしてもrawValueは変わりません。

フラグを降ろす

フラグを降ろすには remove メソッドで行います。下記の例でフラグが減っていることが確認できると思います。

var permission: Permission = [.executable, .writable, .readable]
permission.rawValue // 7 = 111

permission.remove(.readable)
permission.rawValue // 3 = 011

permission.remove(.writable)
permission.rawValue // 1 = 001

permission.remove(.readable)
permission.rawValue // 1 = 001

permission.remove(.executable)
permission.rawValue // 0 = 000

当然のことながら、すでに降りているフラグに対して更にremoveしてもrawValueは変わりません。 また、すべてのフラグが降りると0になります。

ビットセット(集合)同士の計算

OptionSetがビット演算を扱う性質であるゆえに集合の演算も機能として用意されています。

和集合

100001というビットセットがある場合、和集合(OR)で演算すると101となります。 この和集合を表現するのがunionメソッドです。

let permission1: Permission = .executable // 1 = 001
let permission2: Permission = .readable // 4 = 100

permission1.union(permission2).rawValue // 5 = 101

積集合

100101というビットセットがある場合、積集合(AND)で演算すると100となります。 この積集合を表現するのがintersectionメソッドです。

let permission1: Permission = .readable // 4 = 100
let permission2: Permission = [.executable, .readable] // 5 = 101

permission1.intersection(permission2).rawValue // 4 = 100

差集合

111101というビットセットがある場合の差集合は010となります。 この差集合を表現するのがsubtractingメソッドです。

let permission1: Permission = [.executable, .readable, .writable] // 7 = 111
let permission2: Permission = [.executable, .readable] // 5 = 101

permission1.subtracting(permission2).rawValue // 2 = 010

合成の定義

UIControl.Eventの項で allTouchEventsallEventsについて書きました。 実装上その合成されたOptionSetの値に意味を付けたいときは、定義として作成しておくほうが良いかと思います。

struct Permission : OptionSet {
    let rawValue: UInt
    
    static let executable = Permission(rawValue: 1 << 0)
    static let writable   = Permission(rawValue: 1 << 1)
    static let readable   = Permission(rawValue: 1 << 2)
    
    static let full: Permission = [.executable, .readable, .writable]
}

このように定義しておくことで、「権限をすべて持ち合わせている」という意味がわかりやすくなるのではないでしょうか。

最後に

いくつものフラグを管理することも多いかと思いますが、 OptionSetの仕組みを上手く使えば、1つの整数値でいくつものフラグを管理することができます。

例えばRealmなどのデータベースにいくつもプロパティ(カラム)を作るのではなく、1つのInt値で管理することもできます。

もちろん、なんでもかんでも適しているというわけではなく、使える条件は見極める必要はありますが 設計する上で選択肢の引き出しの一つとして持っておくことも大事かと思います。

何かの参考になれば幸いです。