（iOS開発）UIControlでレイアウト状態に応じて姿を変えるボタンを作ってみた！

# はじめに：UIButtonからUIControlへ
:::note warn
Objective-cで書いたUIButtonのコードをSwift化せよ！UIControlでね。
:::

iOS開発を始めて3週間ほど経った頃、UIButtonの
`contentEdgeInsets, imageEdgeInsets, titleEdgeInsets`
が非推奨メソッドだよというXcodeのワーニング解決任務を頂いた。
その対応の際にぶち当たった一部の備忘録。

:::note info
条件：
- Objective-Cで書かれた既存のUIButtonをSwiftで実装
- UIControlを継承したカスタムボタンで対応
- もちろん、非推奨のメソッド等はNG
:::
`UIButton`はiOS標準のボタンであり、UIの一貫性を保ちやすいというメリットがあります。しかし、今回はより柔軟なカスタマイズを目指し、`UIButton`の基底クラスである`UIControl`を直接継承する方法を選びました。
（当時の私は`UIButton`？ UIKitって何？というレベルのピヨピヨ🐣）

# 現状：UIButtonでの状態管理 (Objective-C)

まず現状。Objective-Cで`UIButton`を使った場合の一般的な実装を利用していた。
```objective-c
//例：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`を継承してカスタムボタンを作るための基本的な方針は以下の通りです。

1.  **カスタムクラス作成**
`UIControl`を継承した独自のボタンクラス（例: `TestButton`）を作成します。
2.  **状態プロパティのオーバーライド**
親クラス`UIControl`が持つ状態を表すプロパティ（`isEnabled`, `isHighlighted`など）を`override`し、`didSet`を使って状態変化を検知します。
3.  **ダークモード対応**
`registerForTraitChanges`メソッドを利用して、ライトモード/ダークモードの切り替えを検知できるようにします。(`traitCollectionDidChange`でも実装可能ですがこちらは非推奨メソッド)
4.  **レイアウト更新処理の実装**
上記2, 3で検知した状態変化に応じて、ボタンの見た目（背景色、文字色など）を更新する処理を実装します。


## 実装コード例
**※注意:** 
状態変化時にコンソールにログを出力する基本的な構造を示しています。
（状態に応じた具体的なレイアウト更新）については、`didSet`内や`registerForTraitChanges`のハンドラ内に追記する必要があります。
### ボタンを使う側 (ViewController.swift)

`TestButton`（作成したカスタムボタン）を画面に表示し、`UISwitch`でボタンの有効/無効状態を切り替えられるようにします。

```swift
// 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`を継承し、状態変化を検知する基本的な仕組みを実装します。

```swift
//  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のプロパティ・状態について**
      * [UIButtonの使い方まとめ - iPhoneアプリ開発の虎の巻]([無効な%20URL%20を削除しました])
  * **ダークモード等の変更検知 (iOS 17〜)**
      * [【iOS 17】 registerForTraitChanges(\_:handler:) / unregisterForTraitChanges(\_:) - Zenn]([無効な%20URL%20を削除しました])
  * **モダンなUIButtonのカスタマイズ**
      * [iOS 15 UIButton の使い方まとめ - Zenn]([無効な%20URL%20を削除しました])