はじめに:UIButtonからUIControlへ
Objective-cで書いたUIButtonのコードをSwift化せよ!UIControlでね。
iOS開発を始めて3週間ほど経った頃、UIButtonの
contentEdgeInsets, imageEdgeInsets, titleEdgeInsets
が非推奨メソッドだよというXcodeのワーニング解決任務を頂いた。
その対応の際にぶち当たった一部の備忘録。
条件:
- Objective-Cで書かれた既存のUIButtonをSwiftで実装
- UIControlを継承したカスタムボタンで対応
- もちろん、非推奨のメソッド等はNG
UIButton
はiOS標準のボタンであり、UIの一貫性を保ちやすいというメリットがあります。しかし、今回はより柔軟なカスタマイズを目指し、UIButton
の基底クラスであるUIControl
を直接継承する方法を選びました。
(当時の私はUIButton
? UIKitって何?というレベルのピヨピヨ🐣)
現状:UIButtonでの状態管理 (Objective-C)
まず現状。Objective-CでUIButton
を使った場合の一般的な実装を利用していた。
//例:objective-c
[button setTitle:"hoge" forState:UIControlStateNormal];
[button setTitle:"fuga" forState:UIControlStateDisabled];
[button setTitle:"foo" forState:UIControlStateHighlighted];
// ボタンの状態 (UIControlState)
// - UIControlStateNormal : 通常(有効)
// - UIControlStateHighlighted : タップ中(ハイライト)
// - UIControlStateDisabled : 無効
UIButton
にはsetTitle(_:for:)
のような便利なメソッドが用意されており、これらを使うことでボタンの状態(UIControlState
)に応じて表示を簡単に切り替えられます。
ただし、デザインによってはUIButton
の標準機能だけでは対応しきれず、余白調整などで工夫が必要になることもあります。UIButton
を利用する場合は、iOS 15から導入されたUIButton.Configuration
を使うことで、非推奨メソッドを避けて実装が可能かと思います。
(今回はUIControl
を使うため詳細は割愛します。)
実装方針
UIButton
に頼らず、UIControl
を継承してカスタムボタンを作るための基本的な方針は以下の通りです。
-
カスタムクラス作成
UIControl
を継承した独自のボタンクラス(例:TestButton
)を作成します。 -
状態プロパティのオーバーライド
親クラスUIControl
が持つ状態を表すプロパティ(isEnabled
,isHighlighted
など)をoverride
し、didSet
を使って状態変化を検知します。 -
ダークモード対応
registerForTraitChanges
メソッドを利用して、ライトモード/ダークモードの切り替えを検知できるようにします。(traitCollectionDidChange
でも実装可能ですがこちらは非推奨メソッド) -
レイアウト更新処理の実装
上記2, 3で検知した状態変化に応じて、ボタンの見た目(背景色、文字色など)を更新する処理を実装します。
実装コード例
※注意:
状態変化時にコンソールにログを出力する基本的な構造を示しています。
(状態に応じた具体的なレイアウト更新)については、didSet
内やregisterForTraitChanges
のハンドラ内に追記する必要があります。
ボタンを使う側 (ViewController.swift)
TestButton
(作成したカスタムボタン)を画面に表示し、UISwitch
でボタンの有効/無効状態を切り替えられるようにします。
// ViewController.swift
import UIKit
class ViewController: UIViewController {
// buttonをプロパティとして保持できるように変更
var button: TestButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.tertiarySystemBackground
/**
* TestButton(今回作成したボタン)
* レイアウトは任意。お好みで
* ※注: 以下のframe指定だと画面外に表示される可能性があります。
* 表示位置は適宜調整してください。
*/
button = TestButton(frame:CGRect(x: 1200, y: 100, width: CGFloat(112), height: CGFloat(41)))
/// ボタンの初期設定(見た目)
button.makeButton()
/// ボタンをビューに追加
view.addSubview(button)
/**
* ボタンのenabled動作確認用のスイッチ
* レイアウトはテキトー。
* ※注: 以下のframe指定だと画面外に表示される可能性があります。
* 表示位置は適宜調整してください。
*/
let toggleSwitch = UISwitch()
toggleSwitch.frame = CGRect(x: 1256, y: 145, width: CGFloat(30), height: CGFloat(10))
toggleSwitch.addAction(UIAction { [weak button] _ in
guard let button = button else { return }
button.isEnabled.toggle()
}, for: .valueChanged)
toggleSwitch.isOn = button.isEnabled
view.addSubview(toggleSwitch)
}
}
カスタムボタン本体 (TestButton.swift)
UIControl
を継承し、状態変化を検知する基本的な仕組みを実装します。
// testButton.swift
// Created by yussan on 2025/04/04.
import UIKit
class TestButton: UIControl {
var label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
// ダークモード/ライトモードの変更監視を開始
self.handleRegisterForTraitChanges()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// ダークモード/ライトモードの変更監視を開始
self.handleRegisterForTraitChanges()
}
/*
isEnabled プロパティの変更を監視
*/
override var isEnabled: Bool {
didSet {
if(isEnabled){
print("有効状態")
// <<< ここに有効状態の見た目を設定するコードを追加 >>>
}else{
print("無効状態")
// <<< ここに無効状態の見た目を設定するコードを追加 >>>
}
}
}
/*
isHighlighted プロパティの変更を監視
*/
override var isHighlighted: Bool {
didSet {
// isEnabled が true の場合のみハイライト処理を行うなどの考慮が必要
if(self.isHighlighted){
print("ハイライトオン")
// <<< ここにハイライト状態の見た目を設定するコードを追加 >>>
}else{
print("ハイライトオフ")
// <<< ここに非ハイライト状態の見た目を設定するコードを追加 >>>
}
}
}
/**
* ダーク/ライトモードの変化を検知 (iOS 17以降推奨)
* init() から呼び出して監視を開始します。
*/
private func handleRegisterForTraitChanges(){
// userInterfaceStyle の変化のみを監視対象とする
registerForTraitChanges([UITraitUserInterfaceStyle.self], handler: { (self: Self, previousTraitCollection: UITraitCollection) in
if self.traitCollection.userInterfaceStyle == .light {
print("LightMode")
// <<< ここにライトモード時の見た目を設定するコードを追加 >>>
} else {
print("DarkMode")
// <<< ここにダークモード時の見た目を設定するコードを追加 >>>
}
})
}
/**
* ボタンの初期設定(初期レイアウト)
* 今回はViewControllerから呼び出される。
*/
func makeButton() {
self.layer.cornerRadius = 12
self.layer.backgroundColor = UIColor(red: 37/255, green: 99/255, blue: 235/255, alpha: 1).cgColor
// ラベルの設定
self.label.font = UIFont.boldSystemFont(ofSize:20)
self.label.adjustsFontSizeToFitWidth = true // サイズに合わせて文字縮小
self.label.frame = self.bounds // ボタンと同じサイズに
self.label.textAlignment = .center
self.label.textColor = UIColor.white // 初期状態の文字色(例)
self.label.text = "Button"
addSubview(label)
}
}
UIControl VS UIButton
個人的にUIControl
を直接使ってみて、基本的には、標準のUIButton
を使う方が良い選択だと感じました。
理由は以下の通りです。
-
学習コストと保守性:
-
UIButton
はiOS標準のボタンのため情報も沢山。使い方を学びやすく、他の開発者も理解しやすいコードになる。 - 独自実装の
UIControl
は、その内部仕様を理解する必要があり、開発・保守のコストが高くなる可能性がある。
-
-
UIの一貫性:
-
UIButton
を使えば、iOSユーザーにとって馴染みのあるボタンになる。(ごりごりレイアウトを変えなければ) - タップ時のハイライト表示やその解除タイミングなど、
UIButton
には自然な動作が標準で組み込まれているように感じた。これらを自前で再現するのは意外と手間がかかるし、開発者によって微妙に変わってしまう可能性もある。
-
もちろん、UIButton
のカスタマイズ範囲を超える凝ったデザインや特殊なインタラクションを実現したい場合には、UIControl
を継承して自作するメリットがあります。
そうでなければ、まずはUIButton
で要件を満たせないか検討することが良さそうに思いました。
(レイアウトはiOS 15以降推奨のUIButton.Configuration
を利用して)
最後に
この記事が、同じようにカスタムボタンの実装を検討されている方の参考になれば幸いです。
「もっと良い方法がある」「ここが違うのでは?」といったご意見があれば、ぜひコメントよろしくお願いします!
参考記事
- UIButtonのプロパティ・状態について
- ダークモード等の変更検知 (iOS 17〜)
- モダンなUIButtonのカスタマイズ