cockscomblog?

cockscomb on hatena blog

UIControlにOptionSetTypeで状態を追加する

新成人のみなさん、おめでとうございます。大人になるのは難しいことだけど、自由に自分の人生を歩んでほしいと思います。

UIControlState

UIControlにはvar state: UIControlState { get }というプロパティがある。これはすべてのUIControlの状態を示す。UIControlStateは同時に複数の状態を保持できるbit-maskであり、SwiftにおいてはOptionSetTypeとして表現される。つまり例えば、.Disabledでありかつ.Selectedであることが起こり得る。

struct UIControlState : OptionSetType {
    init(rawValue rawValue: UInt)
    static var Normal: UIControlState { get }
    static var Highlighted: UIControlState { get }
    static var Disabled: UIControlState { get }
    static var Selected: UIControlState { get }
    static var Focused: UIControlState { get }
    static var Application: UIControlState { get }
    static var Reserved: UIControlState { get }
}

UIControlStateの様々な状態

このvar state: UIControlState { get }は、読み込みしかできない。ヘッダには以下のようにある。

could be more than one state (e.g. disabled|selected). synthesized from other flags.

すなわち、var enabled: Boolvar selected: Boolvar highlighted: Boolといったプロパティから合成されることになっている。

新しく状態を作る

ここでUIControlState.Applicationというものがあることに気付いただろうか。ドキュメントには以下のように書かれている。

Additional control-state flags available for application use.

つまりこの.Applicationは、私たちのアプリケーションで自由に使ってよいことになっている。Objective-Cのヘッダを見ると以下のように定義されている。

UIControlStateApplication = 0x00FF0000

Objective-Cで見たUIControlStateApplicationの値

Swiftでは隠蔽されてしまっているが、実際の値は0x00FF0000であり、bit-maskにおいて8 bit分の幅が用意されていることがわかる(いうまでもないが、16進数で二桁分であるから、2進数で表現すれば8桁分となる)。この範囲を自由に割り当てて、新しく状態を増やしてみよう。

一度でも押されたことが記録されるボタンを作る

実際の例を示す。

extension UIControlState {
    static var Visited = UIControlState(rawValue: 1 << 16)
}

class VisitableButton: UIButton {

    override var state: UIControlState {
        if visited {
            return super.state.union(.Visited)
        } else {
            return super.state
        }
    }

    var visited: Bool = false {
        didSet {
            setNeedsDisplay()
            titleLabel?.setNeedsDisplay()
            imageView?.setNeedsDisplay()
        }
    }

}

VisitableButton

このUIButtonを継承したVisitableButtonには、新しくvar visited: Bool というプロパティを追加してある。そしてこのvisitedを元に、var state: UIControlState { get }をオーバーライドして新しい状態であるUIControlState.Visitedを追加する。(visitedが設定された後に表示を更新する必要があるだろうから、didSetでなるべく自身を更新するようにしている。)

UIControlState.Visited1 << 16であり、これは0x00FF0000UIControlState.Applicationの範囲に入っている(これを確かめるにはprint(0x00FF0000 as UInt & (1 << 16) as UInt)とするか、print(UIControlState.Application.contains(.Visited))とする)。オーバーライドしているstateプロパティでは、OptionSetTypeunionメソッドを利用して、元の値との論理和を作っている。

class ViewController: UIViewController {

    @IBOutlet weak var button: VisitableButton! {
        didSet {
            button.setTitleColor(UIColor(red: 0x00 / 0xFF, green: 0x00 / 0xFF, blue: 0xEE / 0xFF, alpha: 1),
                forState: .Normal)
            button.setTitleColor(UIColor(red: 0x55 / 0xFF, green: 0x1A / 0xFF, blue: 0x8B / 0xFF, alpha: 1),
                forState: .Visited)
        }
    }

    @IBAction func visit(sender: VisitableButton) {
        sender.visited = true
    }

}

VisitableButtonを利用するコード例

利用する側は、単にvisitedプロパティを操作するだけでよい。この場合は事前に色を設定している。Webのリンクのように、一度でも訪れたページへのリンクが青から紫になる、というのを模している。

UIButtonsetXxx(_:forState:)といったメソッドは、このようにUIControlState.Applicationの範囲に定義された独自の状態に対しても機能する。すなわち、事前に状態毎に設定しておくだけで、自動的にそれが反映されるのである。

まとめ

UIControlvar state: UIControlState { get }プロパティを、新たな状態を作って拡張する方法を示した。この方法によって、UIControlの状態を統一的に表すことができる。

もし既存のコードで、UIControlやそのサブクラスに何らかの状態を増やしたというつもりで、何度setXxx(_:forState:)のようなメソッドを呼び出していたら、それはおおよそ間違いであるとみられる。

同様にUIControlEventsにも.ApplicationReservedという値があり、独自のイベントを作ることができる。

UIControlEventApplicationReserved = 0x0F000000

UIControlEvents.ApplicationReservedは4 bitの範囲を持つ

参考


詳解 Swift 改訂版

詳解 Swift 改訂版