0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-08

はじめに: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を継承してカスタムボタンを作るための基本的な方針は以下の通りです。

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


最後に

この記事が、同じようにカスタムボタンの実装を検討されている方の参考になれば幸いです。
「もっと良い方法がある」「ここが違うのでは?」といったご意見があれば、ぜひコメントよろしくお願いします!

参考記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?