26
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

iOS15からのUIButton.Configuration

Last updated at Posted at 2023-02-24

はじめに

UIButton.Configuration は、UIButtonの見た目や動作などを指定する構成で、iOS 15.0以上で利用可能です。
UIButton.Configuration の導入により、画像の配置位置を変更したり、ボタン内にindicatiorを導入したり、サブタイトルを追加したりなどが簡単にできるようになり、よりリッチなボタンの実装を行いやすくなりました。
それに伴いiOS 15.0以上で今までのUIButtonで利用していたプロパティやメソッドがDeprecatedとなっており、Deployment Targetを15.0以上に変更する際には警告を消すためには UIButton.Configuration への置き換えやSwiftUIへの書き換えが必要になります。

UIButton.Configuration の記事が少なく思ったような実装を行うのに苦労したので、わかったこともわからなかったことも併せて記載していきます。

Environment

  • macOS Ventura 13.1
  • Xcode 14.2
    • Simulator: iPhone 14 Pro(iOS 16.2)

基本

背景なしのシンプルなボタン

今までと異なり、アイコンがタイトルの右にあるボタンやサブタイトルを持つボタンが実装しやすくなりました。

/// Only title
final class TitleButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        var config = UIButton.Configuration.plain()
        config.title = "Button"
        config.baseForegroundColor = .black // title, imageの色の設定
        configuration = config
    }
}

/// Title + Icon
final class TitleIconButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        var config = UIButton.Configuration.plain()
        config.title = "Button"
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8 // titleとimageのpaddingを設定
        config.imagePlacement = .trailing // titleに対するimageの位置を設定
        config.baseForegroundColor = .black
        configuration = config
    }
}

/// Title + Subtitle
final class TitleSubtitleButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        var config = UIButton.Configuration.plain()
        config.title = "Button"
        config.subtitle = "subtitle"
        config.baseForegroundColor = .black
        config.titlePadding = 12 // titleとsubtitleのpaddingを設定
        configuration = config
    }
}
Only title Title + Icon Title + Subtitle

背景色あり

configuration を生成するメソッドを適切なものに変更すると背景色をつけることができるようになります。
.plain() のままだと背景色は変更されません。 .tinted() filled() .gray() などから適切なものを設定します。
baseBackgroundColor を設定する場合、 filled() .gray() には差がなさそう?)

/// tinted
final class TintedButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        // .tinted()で背景色が半透明になる
        var config = UIButton.Configuration.tinted()
        config.title = "Button"
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.baseForegroundColor = .black
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16) // button自体のpaddingを設定
        config.baseBackgroundColor = .red // 背景色を設定
        configuration = config
    }
}

/// filled
final class FilledButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        // .filled()で背景色が不透明になる
        var config = UIButton.Configuration.filled()
        config.title = "Button"
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.baseForegroundColor = .black
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
        config.baseBackgroundColor = .red
        configuration = config
    }
}
tinted filled

枠線をつける

今までUIButtonで枠線をつける場合は layer.borderColor layer.borderWidth で設定していましたが、 UIButton.Configuration を利用する場合、設定した UIBackgroundConfiguration をUIButton.Configurationにセットすることで設定可能になります。
UIButton.Configuration.borderd() がありますが、これを設定しても枠線はつきませんでした。背景色の有無に応じて .plain() を使うか .filled() を使うかなどを決めると良さそうです :thinking:

UIBackgroundConfiguration を生成するためのstatic funcはそのほとんどがUICollectionViewやUITableViewのためのものなので、よほどの事情がなければ .clear() のみを利用するのが良さそうです。

/// plain + border
final class BorderedPlainButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        var config = UIButton.Configuration.plain()
        config.title = "Button"
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.baseForegroundColor = .black
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)

        // 背景の設定を行う
        var backgroundConfig = UIBackgroundConfiguration.clear()
        backgroundConfig.strokeColor = .black // 枠線の色を設定
        backgroundConfig.strokeWidth = 2 // 枠線の太さを設定
        config.background = backgroundConfig // UIBackgroundConfigurationをセット

        configuration = config
    }
}

/// filled + border + capsule
final class CapsuleBorderedFilledButton: UIButton {
    convenience init() {
        self.init(frame: .zero)

        var config = UIButton.Configuration.filled()
        config.title = "Button"
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.baseForegroundColor = .black
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
        config.baseBackgroundColor = .red
        config.cornerStyle = .capsule // cornerRadiusを常にカプセル状になるように設定する

        var backgroundConfig = UIBackgroundConfiguration.clear()
        backgroundConfig.strokeColor = .black
        backgroundConfig.strokeWidth = 2
        config.background = backgroundConfig

        configuration = config
    }
}

background を設定した時に cornerStyle をデフォルトのままにしていると何故か角丸でなくなります :thinking:
UIButton.ConfigurationcornerStyle を設定するか、UIBackgroundConfigの cornerRadius を設定してあげると背景と枠線に角丸が適用されます。

plain + border filled + border + capsule

思い通りのUIを実装する

フォントサイズを変える

UIButton.Configuration には buttonSize というプロパティが用意されていますが、指定できるのはenumで定義された4種類のみしか無く、自在にサイズを指定できるわけではありません。

直接フォントサイズを指定することもできませんが、 attributedTitle でAttributedStringを指定することでラベルの装飾を細かく指定できます。
attributedSubtitle も存在します。

final class FixedFontSizeButton: UIButton {
    convenience init(
        title: String,
        fontSize: CGFloat
    ) {
        self.init(frame: .zero)

        var config = UIButton.Configuration.tinted()
        // attributedTitleを設定する
        // titleは不要なので削除
        let container = AttributeContainer([
            .font: UIFont.systemFont(ofSize: fontSize)
        ])
        config.attributedTitle = AttributedString(title, attributes: container)
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.baseForegroundColor = .black
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
        config.baseBackgroundColor = .red

        configuration = config
    }
}

Stateに合わせて見た目を変える

上記に記した実装でもボタンの状態に応じてOS側で自動的に色を変えてくれるようになっていますが、業務でUIを実装する際にはボタンの状態ごとに色が明確に決められている場合もあるかと思います。
UIButton.ConfigurationtitleTextAttributesTransformerimageColorTransformerUIBackgroundConfigurationbackgroundColorTransformerstrokeColorTransformer などを使うことでボタンの状態が変化したときに思い通りの見た目に更新することができます。
(枠線をつける場合、 strokeColorTransformer のみ実装しても色がつかず、 strokeColor を追加する必要がありました。バグなのかな…… :thinking:

final class ColorTransformerButton: UIButton {
    convenience init(
        title: String,
        isEnabled: Bool
    ) {
        self.init(frame: .zero)

        var config = UIButton.Configuration.filled()
        config.attributedTitle = AttributedString(title)
        config.image = UIImage(systemName: "plus")
        config.imagePadding = 8
        config.imagePlacement = .trailing
        config.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
        // attributedTitleのattributesを設定する
        // `baseForegroundColor` は不要なので削除
        config.titleTextAttributesTransformer = .init { [weak self] _ in
            guard let self = self else { return .init([:]) }
            let color: UIColor = {
                switch self.state {
                case .disabled:     return .darkGray
                case .highlighted:  return .white
                default:            return .black
                }
            }()
            return .init([
                .font: UIFont.systemFont(ofSize: 14),
                .foregroundColor: color
            ])
        }
        // imageColorを設定する
        config.imageColorTransformer = .init { [weak self] _ in
            guard let self = self else { return .clear }
            switch self.state {
            case .disabled:     return .darkGray
            case .highlighted:  return .white
            default:            return .black
            }
        }

        var backgroundConfig = UIBackgroundConfiguration.clear()
        backgroundConfig.strokeWidth = 1
        backgroundConfig.cornerRadius = 4
        // backgroundColorを設定
        // `config.baseBackgroundColor` は不要なので削除
        backgroundConfig.backgroundColorTransformer = .init { [weak self] _ in
            guard let self = self else { return .clear }
            switch self.state {
            case .disabled:     return .lightGray
            case .highlighted:  return .magenta
            default:            return .systemPink
            }
        }
        // strokeColorを設定
        backgroundConfig.strokeColor = .black // 初期値を入れないとstrokeColorが反映されない
        backgroundConfig.strokeColorTransformer = .init { [weak self] _ in
            guard let self = self else { return .clear }
            switch self.state {
            case .disabled:     return .darkGray
            case .highlighted:  return .white
            default:            return .black
            }
        }

        config.background = backgroundConfig
        configuration = config

        self.isEnabled = isEnabled
    }
}

UIButton.Configuration で実現できなかったこと

UIButton.Configuration を使用しない従来の方法でUIButtonを実装する際にラベルの行数を制限したい時、 titleLabel?.numberOfLines = 1 を追加するとラベルの行数を1行に制限することができました。
しかしながら、 UIButton.Configuration を使用して実装した場合にはUIButtonが持つ titleLabel に対して設定を追加しても反映されないため、 UIButton.Configuration に代替となるプロパティが存在しない numberOfLines textAlignment adjustsFontSizeToFitWidth などの設定はできませんでした。
タイトルの長さによってボタンのサイズは自動的に変わってくれますが、サイズに制約を追加している状態だとボタンからタイトルがはみ出してしまうことがあります。
よって、ボタンのサイズに対して長いタイトルが挿入される場合や、任意のタイトルが挿入されるようなボタンを実装する場合には注意が必要です。

幅の制約のみ 幅・高さの制約あり

おわりに

UIButton.Configuration の登場によってindicatorを追加するなど柔軟なボタンの実装はできるものの、従来の方法では出来ていたことができなくなってしまうなど痒い所に手が届かない印象を受けました。
現状の UIButton.Configuration で思い通りのボタンを実装するのは難しい場合もあり、warningが出るのを承知で従来の方法を使い続けて UIButton.Configuration が改善されるのを待つか、SwiftUIのButtonに置き換えるかした方がいいかもしれません :thinking:

時代はやはりSwiftUIということなんでしょうか……。
とはいえ、UIKitで実装された画面上のボタンを実装する際に、タイトルを動的に変更する必要があったり、タイトルがボタンのサイズに対して小さい分には実用に足るものかと思ったので、今後の改善を待ちたいところです。

参考

26
13
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
26
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?