SwiftUI トグル式 角丸ボタン

# はじめに

iOSデバイスでPCコントローラーを作る《[**こちらの記事**](https://qiita.com/nak435/items/37efeef74b8a97e65c6c)》で使っている、トグル式の角丸ボタンの紹介です。

[『swiftui 角丸ボタン』でググる](
https://www.google.com/search?client=safari&rls=en&q=swiftui+%E8%A7%92%E4%B8%B8%E3%83%9C%E3%82%BF%E3%83%B3&ie=UTF-8&oe=UTF-8)と、角丸ボタンの作り方がいくつもヒットしますが、今回作成するボタンは『**トグル式**』です。

# 『トグル式』とは

下記のGIFアニメの通り、ボタンを押すと凹んだ状態を維持し、もう一度押すと元に戻るボタンのことです。（「プッシュプル式」が正しい？）

<img width="300" alt="toggle_button.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0fd211ca-bc49-e800-ca5b-0724d00d02a6.gif" />


# 「凹んだ状態」を制御

角丸ボタンに「凹んだ状態」であることを意味するプロパティ**isSelected**を追加して、ボタンの背景色と文字色を入れ替えるだけです。これに加えて、このプロパティを親Viewから変更することで、ボタンを押す操作以外でも状態を変えることが必要でした。


# 角丸にして背景を塗る

基本形は次のコードとなります。

```swift
struct ContentView: View {
    let foreColor = Color.accentColor
    let backgroundColor = Color.white
    @State var isSelected: Bool = false
    var body: some View {
        VStack {
// ↓↓↓
            Button(action: {
                print("Button Pushed!")
            }) {
                Text("Button")
                    .bold()
                    .padding()
                    .frame(width: 100, height: 50)
                    .foregroundColor(isSelected ? backgroundColor: foreColor)
                    .overlay(
                        RoundedRectangle(cornerRadius: 10)
                            .stroke(Color.black, lineWidth: 3)
                    )
                    .background(isSelected ? foreColor : backgroundColor)
            }
// ↑↑↑
            Button("toggle") {
                isSelected.toggle()
            }
        }
    }
}
```

`Button`ボタンの下の`toggle`ボタンを押すと、角丸ボタンの外観を変更します。

![b1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/3387d580-7139-d5e5-55a8-741252780107.png) ![b2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/53f8448f-81ed-4248-3bfb-8bc7b1723657.png)

角丸ボタンのアクション内で`isSelected.toggle()`を実行すれば、『ボタンを押すと凹んだ状態を維持し、もう一度押すと元に戻るボタン』の完成。

次に、このボタンを汎用化（関数化）します。

# RoundedRectangleSelectableButton

⭐️印で示した`isSelected`が`true`の場合に凹んだ状態を示し、`false`の場合に、元に戻します。



```swift:RoundedRectangleSelectableButton.swift
struct RoundedRectangleSelectableButton: View {
    let text: String
    let textColor: Color
    let backgroundColor: Color
    let width: CGFloat
    let height: CGFloat
    let cornerRadius: CGFloat
    let borderColor: Color
    let borderWidth: CGFloat
    let action: (_ selected: Bool)->Void
    private var autoSelect: Bool? = nil
    @Binding private var isSelected: Bool            //⭐️
    init(_ text: String, textColor: Color = Color(uiColor: .tintColor), backgroundColor: Color = Color(uiColor: .systemBackground), width: CGFloat, height: CGFloat, cornerRadius: CGFloat = 8, borderColor: Color = Color(uiColor: .label), borderWidth: CGFloat = 2, isSelect: Binding<Bool>, action: @escaping (_ selected: Bool)->Void) {
        self.text = text
        self.textColor = textColor
        self.backgroundColor = backgroundColor
        self.width = width - 4
        self.height = height
        self.cornerRadius = cornerRadius
        self.borderColor = borderColor
        self.borderWidth = borderWidth
        self.action = action
        _isSelected = isSelect
    }
    var body: some View {
        Button(action: {
            if let autoSelect = self.autoSelect, autoSelect {
                isSelected.toggle()
            }
            action(isSelected)
        },label:  {
            Text(text)
                .foregroundColor(isSelected ? Color.white : textColor)
                .frame(width: width, height: height)
                .background(isSelected ? Color(uiColor: .tintColor) : backgroundColor)
                .cornerRadius(cornerRadius)
                .overlay(
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .stroke(borderColor, lineWidth: borderWidth)
                )
        })
    }
    func autoSelect(_ autoSelect: Bool) -> Self {
        var view = self
        view.autoSelect = autoSelect
        return view
    }
}
```

## 引数の説明

|引数|意味|型|省略値|
|-|-|-|-|
|text             |ボタンのラベル| String   | -（省略不可）|        
|textColor        |ラベルの色| Color  | UIColor.tintColor|
|backgroundColor  |背景色| Color     | UIColor.systemBackground|
|width            |幅| CGFloat       |-（省略不可）|
|height           |高さ| CGFloat     |-（省略不可）| 
|cornerRadius     |角丸のサイズ| CGFloat| 8|
|borderColor      |枠線の色| Color     | UIColor.label|
|borderWidth      |枠線の太さ| CGFloat  | 2|
|isSelect         |凹み状態の変数| Binding&lt;Bool&gt;|-（省略不可）|
|action           |ボタンが押された時に実行するアクション| (_ selected: Bool)->Void   |-（省略不可）|

色の省略値にシステム色を使用することで、ダークモード時もいい感じにしてくれます。


## オプション（View Modifier）

|名称|引数の型|意味|オプション<br>省略値|
|-|-|-|-|
|autoSelect  | Bool   | 自動でトグル切り替えするかどうか。<br>自動にする：**true**、しない：**false**|false|        


## 使い方

次のコードで、3つの使い方を説明します。


```swift
struct ContentView: View {
    let foreColor = Color.accentColor
    let backgroundColor = Color.white
    @State var isSelect1: Bool = false
    @State var isSelect2: Bool = false
    @State var isSelect3: Bool = false
    @State var isSelect4: Bool = false
    var body: some View {
        VStack {
            VStack {
                Text("パターン1")
                RoundedRectangleSelectableButton("Normal", width: 100, height: 40, isSelect: $isSelect1, action: { _ in
                    //action: todo
                })
                Button("Toggle") {
                    isSelect1.toggle()
                }
            }
            .padding(.bottom, 50)
            VStack {
                Text("パターン2")
                RoundedRectangleSelectableButton("Toggle", width: 100, height: 40, isSelect: $isSelect2, action: { _ in
                    //action: todo
                })
                .autoSelect(true)
            }
            .padding(.bottom, 50)
            VStack {
                Text("パターン3")
                HStack {
                    RoundedRectangleSelectableButton("Option A", width: 100, height: 40, isSelect: $isSelect3, action: { _ in
                        isSelect3.toggle()
                        if isSelect3 { isSelect4 = false }
                        //action: todo
                    })
                    Text("or")
                    RoundedRectangleSelectableButton("Option B", width: 100, height: 40, isSelect: $isSelect4, action: { _ in
                        isSelect4.toggle()
                        if isSelect4 { isSelect3 = false }
                        //action: todo
                    })
                }
            }

        }
    }
}
```

![light.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/6359cd6a-c5fe-1d0e-b584-23a135783d37.png) ![dark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/7172b2e2-bf2a-ec96-9877-8d9cdc32e516.png)



## パターン1

角丸の普通のボタン。引数isSelectの設定で親ビューから外見を変更可能です。
`Toggle`をタップすると外観が変化します。



## パターン2

角丸の普通のトグル式ボタン。`.autoSelect(true)`を指定することで、トグル切り替えを自動化しています。


## パターン3

「オプションを選択する」とかで使用するボタンです。「Option A」か「Option B」か両方無しの組み合わせだけ許可します。`.autoSelect`は使わずに、引数isSelectで指定した変数を制御することで実現しています（_グループ化の機能があれば、もっと楽できますね_）。



![toggle_button_full.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283293/0f27e3e1-8d50-b797-1bba-d181ff4d36d4.gif)


# おわりに

グループ化の機能は、今後追加しようと思います。
よかったら使ってみてください。

以上です。