[SwiftUI] カスタムビューにTextのメソッドを適用する

[Swift Advent Calendar 2020](https://qiita.com/advent-calendar/2020/swift)の8日目の記事です
7日目の記事はこちらです→[SwiftのArraySliceのおもしろい話](https://zenn.dev/koher/articles/swift-array-slice)

***
記事を開いてくれてありがとうございます。みんくると言います٩(🌀❛O❛🌀)۶
本記事では**カスタムビューに Text や Image などのビューモディファイアにないメソッドを適用する方法**についてお話します！

**環境**
Xcode: Ver. 11.6
Swift: Ver. 5.1
では上手くいくことを確認しています(Swift 5.0 以上なら大丈夫だと思います！)

# 事前知識
簡単なざっくりとした説明です。知ってるよ！って方は飛ばしてください。

<details><summary>SwiftUI について</summary><div>
SwiftUI はアプリやソフトウェア開発ができるフレームワークで、コードを書いてデザインできます。

```Swift:SwiftUI的&#x20;Hello&#x20;World
import SwiftUI  // SwiftUI のフレームワークを使うときは必要(以降では省略します)

struct HelloWorld: View {
    var body: some View {
        Text("Hello World") // 文字を表示する
    }
}
```

こんな感じで``var body: some View { }``のカッコ内で書いたようにデザインできます。文字を表示する``Text()``の他にも、円を表示する``Circle()``や画像を表示する``Image()``などのたくさんの部品を組み合わせてデザインしていきます。こうして作ったものや``Text()``などの部品のことをビューと呼んでいます。
↓上のコードの例(``Text()``で文字を表示している)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/08b8be95-bc99-628e-add3-8c89a70edb3f.png" alt="SwiftUI Hello World" width=400>
</div></details>

<details><summary>カスタムビューについて</summary><div>
カスタムビューとは``Text()``や``Circle()``などを組み合わせて作ったビューを繰り返し使えるようにしたものです。

↓カスタムビューの例

```Swift:角丸四角なテキストボックスを作るカスタムビュー
struct CustomView: View {
    let text: String  // 表示させる文字のプロパティ
    
    init(_ text: String) {
        self.text  = text
    }

    var body: some View {
        Text(self.text)   // 文字を表示する
            .padding()    // 余白をとる
            .background(  // 文字のビューの後ろに↓を表示する
                RoundedRectangle(cornerRadius: 16)  // 角丸四角の図形を生成する
                    .fill(Color.orange)  // 図形をオレンジ色にする
            )
    }
}
```

このカスタムビューは、オレンジ色の角丸四角な図形の上に入力した文字を表示させます。

```Swift:カスタムビューの使用例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {  // ←ビューを縦に並べる
            CustomView("Hello World")  // カスタムビューを使う
            CustomView("Custom View")
            CustomView("Natto Gohan")
        }
    }
}
```

この(↑)ように使用したいビューの``body``内でカスタムビューを宣言することで繰り返し使うことができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/35952a3d-650d-3042-d8b7-a46378630715.png" alt="カスタムビューの使用例" width=400>
</div></details>


<details><summary>ビューモディファイアについて</summary><div>
``Text()``という文字を表示するビューがありますが、文字を表示するといっても『大きい文字を表示する』や『赤い文字を表示する』など様々なパターンがあります。
SwiftUI ではビューモディファイアという機能でそれらの様々なパターンを実現しています。

```Swift:ビューモディファイアの例
struct HelloWorld: View {
    var body: some View {
        Text("Hello World")
            .font(.largeTitle)      //文字を大きくする
            .foregroundColor(.red)  //文字を赤色にする
    }
}
```

この(↑)ように"大きい"文字という文字の修飾を、``Text().font()``のような``Text()``を``.font()``で修飾することで表現しています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/f135a263-0ee9-efd1-1955-c3b3cbf71e2e.png" alt="View Modifierの例" width=400>
</div></details>



# 説明に使うカスタムビュー
本題に入る前に説明に使うカスタムビューを紹介します。

今回説明に使うのは、``CustomView("表示したい文章")``と宣言すると "表示したい文章" を角丸四角な図形の上に表示してくれるカスタムビューです。

```Swift:角丸四角なテキストボックスを作るカスタムビュー
struct CustomView: View {
    let text: String  // 表示させる文字のプロパティ
    
    init(_ text: String) {
        self.text  = text
    }

    var body: some View {
        Text(self.text)   // 文字を表示する
            .padding()    // 余白をとる
            .background(  // 文字のビューの背面に↓を表示する
                RoundedRectangle(cornerRadius: 16)  // 角丸四角の図形を生成する
                    .fill(Color.orange)  // 図形をオレンジ色にする
            )
    }
}
```

デフォルトではオレンジ色の角丸四角な図形の上に入力した文字が表示されます。

```Swift:カスタムビューの使用例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {  // ←ビューを縦に並べる
            CustomView("Hello World")  // カスタムビューを使う
            CustomView("Custom View")
            CustomView("Natto Gohan")
        }
    }
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/35952a3d-650d-3042-d8b7-a46378630715.png" alt="説明に使うカスタムビュー" width=400>



# 導入: 使う用途に合わせてカスタムビューを変化させたい！
実際にカスタムビューを使うとき、使う用途に合わせて変化させたいときがあります。
例えばテキストボックスのカスタムビューなら、文字の大きさを変えたりボックスの色を変えたりしたいみたいな感じです。

こういうときビューモディファイア使って変化させることができます。

```Swift:カスタムビューをビューモディファイアで変化させる
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            CustomView("Hello World")
            CustomView("Custom View")
                .font(.largeTitle)        // 文字を大きくする
            CustomView("Natto Gohan")
                .foregroundColor(.white)  // 文字を白色にする
        }
    }
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/4ef188bc-d42d-aa10-bed5-773b54d518b6.png" alt="Custom Viewに View Modifierを使う" width=400>

しかし、加えたい変化がビューモディファイアに存在しない場合この方法は使えません。ビューモディファイアにない「Text ビューのメソッドで変化を加えたい」みたいなときは、最初にカスタムビューを定義する時点でそのことを想定した設計にする必要があります。

本記事では Text, Image, Circle などのメソッドをカスタムビューに使用する方法について、**文字の太さ**と**図形の色**を変化させれるように拡張することを通して解説します。

ちなみに文字の太さを変更するのは Text ビューの "fontWeight" メソッドで、図形の色を変更するのは Shape プロトコルの "fill" メソッドです。


# 方法1: イニシャライザでデザインを指定する
一番単純な方法は、デザインの状態を管理するプロパティをビューに持たせ、宣言時にデザインを指定させます。そしてその値を元にデザインを変更する間接的な方法です。

例えばカスタムビューに、
・ 文字の太さ → fontWeight
・ 図形の色  → boxColor
とそれぞれプロパティを作り、カスタムビュー宣言時にデザインを指定できるようにします。

```Swift:イニシャライザでデザインを指定する
struct CustomViewEx1: View {
    let text: String
    let fontWeight: Font.Weight?  // 文字の太さのプロパティ
    let boxColor: Color           // 図形の色のプロパティ
    
    //  変更点: イニシャライザでデザインを指定する
    // * 指定しない場合はデフォルトの値が使われる
    init(_ text: String, fontWeight: Font.Weight? = .regular, boxColor: Color = .orange) {
        self.text       = text
        self.fontWeight = fontWeight  // 指定された値 or デフォルトの値
        self.boxColor   = boxColor    // 指定された値 or デフォルトの値
    }

    var body: some View {
        Text(self.text)
            .fontWeight(self.fontWeight)  // 文字の太さを指定する
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 16)
                    .fill(self.boxColor)  // 図形の色を指定する
            )
    }
}
```
イニシャライザに引数を増やしカスタムビューの宣言時にデザインを指定できるようにします(指定しない場合はデフォルトの値が使われ同じデザインになります)。

```Swift:イニシャライザでデザインを指定する方法の使用例
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            CustomViewEx1("Hello World")
            CustomViewEx1("Custom View", boxColor: .blue)
                .font(.largeTitle)
            CustomViewEx1("Natto Gohan", fontWeight: .black, boxColor: .black)
                .foregroundColor(.white)
        }
    }
}
```
宣言時に引数で一気に指定するので変えたいデザインが多くなると煩雑になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/501d60ce-d98b-3fc8-fc57-87724608519d.png" alt="イニシャライザでデザインを指定する方法の使用例" width=400>



# 方法2: インスタンスメソッドでデザインを指定する
方法1ではイニシャライザの引数で一気に指定するので、変えたいデザインが多くなると引数が多くなり分かりにくくなります。また、デフォルトで使いたいたいだけの人にも多くの引数が見えてしまうのも少し嫌なところです。

そこでカスタムビューの宣言時には文章の入力だけにし、デザインの変更はインスタンスメソッドから行う方法を紹介します。こちらの方法でもデザインの状態をプロパティで管理し、それをもとにデザインを指定するところは同じです。

```Swift:インスタンスメソッドからデザインを指定する
struct CustomViewEx2: View {
    let text: String
    let fontWeight: Font.Weight?  // 文字の太さのプロパティ
    let boxColor:   Color         // 図形の色のプロパティ
    
    // 1 変更点1: デフォルトのイニシャライザ
    init(_ text: String) {
        self.text = text
        self.fontWeight = .regular  // デフォルト値を指定する
        self.boxColor   = .orange   // デフォルト値を指定する
    }

    var body: some View {
        Text(self.text)
            .fontWeight(self.fontWeight)  // 文字の太さを指定する
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 16)
                    .fill(self.boxColor)  // 図形の色を指定する
        )
    }
}
// 2 変更点2
extension CustomViewEx2 {
    // 2 変更点2: プロパティを変化させるためのイニシャライザ
    // * アクセスレベルを private にすることで宣言時に見えない
    private init(_ text: String, fontWeight: Font.Weight?, boxColor: Color) {
        self.text       = text
        self.fontWeight = fontWeight
        self.boxColor   = boxColor
    }
    
    // 2 変更点2: フォントの太さを指定するメソッド
    // 太さを引数として受け取り、それを使って再度カスタムビューを宣言する
    func fontWeight(_ weight: Font.Weight?) -> Self {
        CustomViewEx2(self.text, fontWeight: weight, boxColor: self.boxColor)
    }

    // 2 変更点2: 図形の色を指定するメソッド
    // 色を引数として受け取り、それを使って再度カスタムビューを宣言する
    func boxColor(_ color: Color) -> Self {
        CustomViewEx2(self.text, fontWeight: self.fontWeight, boxColor: color)
    }
}
```
ちょっと長いですが変更点は2箇所です。それぞれ解説します！

## デフォルトのイニシャライザ

```Swift:変更点1
    // 1 変更点1: デフォルトのイニシャライザ
    init(_ text: String) {
        self.text = text
        self.fontWeight = .regular  // デフォルト値を指定
        self.boxColor   = .orange   // デフォルト値を指定
    }
```
これはカスタムビューを宣言するときに使われるイニシャライザです。文章のみを入力として受け取り、受け取った文章と文字の太さと図形の色のデフォルトの値をそれぞれのプロパティに代入しています。これによりカスタムビューの使用者は宣言時は入力する文章のことだけ考えれば良くなります。


## プロパティを変化させる方法
ビューのレイアウトを決める``var body: some View { }``のカッコ内ではプロパティを変化させる "mutating function" は使えず、通常の方法でプロパティを変化させられません。そこで、直接プロパティを変化させるのではなく、変えたい値を使ってもう一度カスタムビューを宣言し、それを戻り値として返すということをします。こうすることで、間接的にプロパティを変化させられます。

```Swift:変更点2-1&#x20;プロパティを変化させるためのイニシャライザ
    // 2 変更点2: プロパティを変化させるためのイニシャライザ
    // * アクセスレベルを private にすることで宣言時に見えない
    private init(_ text: String, fontWeight: Font.Weight?, boxColor: Color) {
        self.text       = text
        self.fontWeight = fontWeight
        self.boxColor   = boxColor
    }
```
プロパティを変化させるためのイニシャライザです。このイニシャライザをインスタンスメソッドから呼び出して間接的にプロパティを変化させます。``private init(略...)``とすることで外部からこのイニシャライザにアクセスできなくなるのでカスタムビューの宣言時に必要ない情報を隠すことができます。

```Swift:変更点2-2&#x20;プロパティ指定用のメソッド
    // 2 変更点2: フォントの太さを指定するメソッド
    // 太さを引数として受け取り、それを使って再度カスタムビューを宣言する
    func fontWeight(_ weight: Font.Weight?) -> Self {
        CustomViewEx2(self.text, fontWeight: weight, boxColor: self.boxColor)
    }

    // 2 変更点2: 図形の色を指定するメソッド
    // 色を引数として受け取り、それを使って再度カスタムビューを宣言する
    func boxColor(_ color: Color) -> Self {
        CustomViewEx2(self.text, fontWeight: self.fontWeight, boxColor: color)
    }
}
```
プロパティを変化させるためのメソッドです。引数でデザインを指定してもらいそれを使って再度カスタムビューを宣言します。このときイニシャライザの他の引数には``self.プロパティ名``を渡し変更が伝搬していくようにします。``func メソッド名(略...) -> Self { }``と戻り値の型を Self (カスタムビューの型)にしているところも大事です。これによりカスタムビューのインスタンスメソッドを複数個連続して使用できます[^1]。

```Swift:メソッドでデザイン指定する方法の使用例
struct ContentView: View {
    var body: some View {  // ←この内側では mutating function は使えない
        VStack(spacing: 10) {
            CustomViewEx2("Hello World")
            CustomViewEx2("Custom View")
                .boxColor(.blue)
                .font(.largeTitle)
            CustomViewEx2("Natto Gohan")
                .fontWeight(.black)  // 連続して
                .boxColor(.black)    // 使用できる
                .foregroundColor(.white)
        }
    }
}
```
使用例です。宣言時の引数が1つになりビューの役割が明確化されました。また、ビューモディファイアと同じ記法でデザインを変化させているので統一感があります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/39e963a3-64d3-3b78-0467-e4999ca4970f.png" alt="メソッドでデザインを指定する方法の使用例" width=300>
(同じことをしてるので上の例と全く同じ結果になっています)



# 比較
説明した2つの方法を比較すると、イニシャライザでデザインを指定する方法は、実装は簡単ですがカスタムビューの使用時にコードが複雑化してしまう可能性があります。また、インスタンスメソッドでデザインを指定する方法は、実装が少し面倒な分使用時はすっきりしています。

|  |実装しやすさ|使いやすさ|
|:-:|:-:|:-:|
|方法1|○|✖︎|
|方法2|✖︎|○|

## コード補完の違い
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/aa0b5974-7856-f063-d645-86aa58071bd8.png" alt="コード補完の違い" width=600>
2つの方法のコード補完の違いを見てみると、方法1では2種類の候補が出ているのに対し方法2では1つの候補のみ表示されています。


## 使用方法の違いを見比べる
最後に変更前と2つの変更後の計3つのカスタムビューを並べて終わりたいと思います。

```Swift:3つのカスタムビューの使用方法の違い
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            CustomView("Merry🎄")
            CustomViewEx1("Christmas🎁", fontWeight: .black, boxColor: .green)
            CustomViewEx2("(๑•ૅㅁ•๑)۶🍗")
                .fontWeight(.black)
                .boxColor(.red)
        }
        .font(.largeTitle)
        .foregroundColor(.white)
    }
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520712/7e6bb28f-8fd7-db22-0cde-32884d96342b.png" alt="3つのカスタムビューの使用方法の違い" width=400>


# まとめ
カスタムビューにビューモディファイア以外の Text などのメソッドを間接的に使用する方法を紹介しました。もし必要なときがきたら用途に合わせて使ってもらえたら嬉しいです。

質問やおかしな点がありましたらコメントしていただけると嬉しいです。
最後まで読んでくださりありがとうございました！٩(๑❛0❛๑)۶ﾒﾘｸﾘ

[^1]: 一つのメソッドのみしか使わないのなら戻り値の型は``some View``でも構いません