ObjecTips

Swift & Objective-C で iOS とか macOS とか

UIAction と UIControlEvent

少し調査部分が長くなってしまったのでざっと読みたい人は「まとめ」の段をどうぞ


UIButton でメソッドの実行を設定するには親クラスの UIControl で定義されている以下のメソッドを使用する。

func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event)

https://developer.apple.com/documentation/uikit/uicontrol/1618259-addtarget

実装例

let button = UIButton(type: .system)
button.addTarget(self, action: #selector(someMethod), for: .touchUpInside)

iOS 14 では UIAction を使った代替メソッドが UIControl に追加されている。

func addAction(_ action: UIAction, for controlEvents: UIControl.Event)

実装例

let button = UIButton(type: .system)
let action = UIAction(title: "title") { action in
    // do something
}
button.addAction(action, for: .touchUpInside)

暗黙的なトリガータイミングの指定

UIControl には addAction メソッドの他に以下のイニシャライザが追加されている。

convenience init(frame: CGRect, primaryAction: UIAction?)

https://developer.apple.com/documentation/uikit/uicontrol/3600494-init

このイニシャライザを使用した場合 UIControlEvent の指定がされていない。
ではどのタイミングで処理が発生するのか。
以下のコードでイニシャライザを使って UIAction を指定した時の UIControlEvent を確認してみる。

let action = UIAction(title: "title") { action in
    // do something
}
let button = UIButton(frame: frame, primaryAction: action)
print(button.allControlEvents) // UIControlEvents(rawValue: 8192)

結果は UIControlEvents(rawValue: 8192) となった。
これは UIControlEventprimaryActionTriggered の値と一致する。

@available(iOS 9.0, *)
public static var primaryActionTriggered: UIControl.Event { get }

https://developer.apple.com/documentation/uikit/uicontrol/event/1618222-primaryactiontriggered

ドキュメントには以下のように書かれているが説明が短い。

A semantic action triggered by buttons.

調べてみたところ以下のメソッドのヘッダコメントにもう少し説明があった。

@available(iOS 14.0, *)
public convenience init(frame: CGRect, primaryAction: UIAction?)

Initializes the control and adds primaryAction for the UIControlEventPrimaryActionTriggered control event. Subclasses of UIControl may alter or add behaviors around the usage of primaryAction, see subclass documentation of this initializer for additional information.

ちゃんと UIControlEventPrimaryActionTriggered を使うという記載があったが、じゃあそのトリガーのタイミングがいつかという具体的な説明はされていない。
サブクラスのドキュメントを見ろと書いてあるので見てみる。

UIButton

convenience init(frame: CGRect, primaryAction: UIAction?)

https://developer.apple.com/documentation/uikit/uibutton/3600349-init

The action to perform when the button is selected. The button registers this action for the primaryActionTriggered control event and sets the title and image properties to the action’s title and image.

という事で、ボタンが選択された時に実行すると書かれている。

UISegmentedControl

convenience init(frame: CGRect, actions: [UIAction])

https://developer.apple.com/documentation/uikit/uisegmentedcontrol/3600580-init

No overview available.

ドキュメント無し。
ヘッダコメントの方に記述があった。

Initializes the segmented control with the given frame and segments constructed from the given UIActions. Segments will prefer images over titles when both are provided. Selecting a segment calls UIAction.actionHandler as well as handlers for the ValueChanged and PrimaryActionTriggered control events.

選択すると valueChangedPrimaryActionTriggered の両方がトリガーされるらしい。

調査結果

結局 UIControlEventPrimaryActionTriggered の詳細な説明は見つからなかった。
コードを書いて試してみたところ実際の動作としては UIButton は選択時、おそらく touchUpInside に相当。
UISegmentedControl UISwitch UIStepper UIDatePicker も選択時で valueChanged に相当。
UISlider はスライダー操作時にトリガーで、挙動を見た感じでは valueChanged 以外に touchDown touchUpInside touchUpOutside あたりも入っていそうな動きをしていた。
最後に UITextField はキーボードのリターンキーを押したタイミングでトリガーされ、テキスト入力のフォーカスは外れないという挙動だった。UITextField + editingDidEnd の場合はフォーカスが外れた時にトリガーされ、editingDidEndOnExit はフォーカスが外れただけではトリガーされずにリターンキーを押すとトリガーされつつフォーカスが外れるという挙動になるので primaryActionTriggered はこれらとは異なった挙動になる事が分かった。

まとめ

iOS 14.5で確認

primaryAction を使ったイニシャライザで UIAction を設定すると暗黙的に UIControlPrimaryActionTriggered が設定される。

Implicitly using UIControlEventPrimaryActionTrigg…

UIControlPrimaryActionTriggered はクラスによって挙動が異なる。

Class UIControlPrimaryActionTriggered と同じような挙動の UIControlEvent
UIButton touchUpInside
UISegmentedControl, UISwitch, UIStepper, UIDatePicker valueChanged
UISlider [valueChanged, touchDown, touchUpInside, touchUpOutside]
UITextField None

UITextField の場合 UIControlPrimaryActionTriggered と同じ挙動になる UIControlEvent は無い。

UITextField の UIControlEvent リターンキーを押した時の挙動 トリガーのタイミング
editingDidEnd フォーカスが外れない フォーカスが外れた時
editingDidEndOnExit フォーカスが外れる リターンキーを押してフォーカスが外れた時(リターンキー以外でフォーカスが外れた場合はトリガーされない)
primaryActionTriggered フォーカスが外れない リターンキーを押した時

UISliderUITextFieldUIControlPrimaryActionTriggered を使用する時は単純に単一の 'UIControlEvent' を用いた時とは挙動が異なる事を頭の片隅に置いておいた方が良いかも知れない。
また、これらの挙動の違いを理解した上で初期化済みのインスタンスに対して UIControlPrimaryActionTriggered でアクションを設定したいという場合は以下の様に実装する事も可能。

let action = UIAction(title: "title") { action in
    // do something
}
let textField = UITextField(frame: frame)
textField.addAction(action, for: .primaryActionTriggered)

以上