チュートリアルから一歩踏み出したSwiftUIのCustom Viewの作り方ーその3（ViewModifier編）

#はじめに

**付箋のView生成部分のコードがわかりずらかったので全面的に書き直しました。(7/Nov/2019)**
前回のPreferenceKeyの[記事](https://qiita.com/takaf51/items/de86221493f313ebfc3c)が重かったので、今回は軽めの内容です。
SwiftUIには色々な`Modifier`が準備されています。下記の例ですと`font`, `lineLimit`, `frame`, `foregroundColor`がそれにあたります。簡単にUIの変更が出来てとても便利ですね。

```swift:Modifier.swift
Text("Hello World")
    .font(.headline)
    .lineLimit(5)
    .frame(width: UIScreen.main.bounds.width/2)
    .foregroundColor(.white)
```


#今回作るもの
SwiftUIではさらに`ViewModifier`を利用してカスタムのModifierを作ることが出来ます。
今回は下記のように`Text`を付箋のように表示する`Modifier`を作ってみたいと思います。

```swift:Modifier.swift
Text("If today were the last day of my life, would I want to do what I am about to do today")
    .StickyNote( color: .green, ceaseSize: 50, cornerRadius: 20, shadowLength: 10))
```
<img width="248" alt="Screen Shot 2019-07-25 at 4.51.43 pm.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24726/284cfac8-918b-8a6d-b7bb-ec12de213278.png">

#ViewModifier
[`ViewModifier`](https://developer.apple.com/documentation/swiftui/viewmodifier)プロトコルを継承してカスタマイズViewModifierを作ります。そのためには`func body(content: Self.Content) -> Self.Body`を実装しなければなりません。
`content`はモディファイする対象のViewへのProxyになります。前述の例でいうと`Text("If today were the last day of my life, would I want to do what I am about to do today")`になります。下の例では対象のViewのbackgroundとして`StickyNote`という名のViewが表示されますので、今回の付箋の例では`StickyNote`のViewで付箋の画像を生成すれば良いという訳です。非常にわかりやすく、簡単です。
ひとつ注意点としては`background`を設定する前に`padding`を使って付箋のサイズを前もって設定しなければならない点です。なぜなら`StickyNote`側の`GeometryReader`で`content`のサイズ（付箋の矩形のサイズ）を取得する必要があるからです。

```swift:Modifier.swift
    func body(content:Content) -> some View {
        ZStack(content: {
            content
                .padding(.horizontal, 30)    //左右の余白
                .padding(.vertical, 40)    //上下の余白
                .background(StickyNote())    //対象のView(Text)の背景として付箋のViewを設定する
                .shadow(radius: shadowLength)    //影
        })
    }
```

#付箋描写部分の実装
次に付箋の画像生成部分です。`GeometryReader`を使って対象Viewのサイズを取得し`Path`を使って画像を生成して返しています。GeometryReaderに関しては過去記事([チュートリアルから一歩踏み出したSwiftUIのCustom Viewの作り方ーその１（GeometryReader編)](https://qiita.com/takaf51/items/a67db8bbc42a4c82b1f0))を参考にしてください。

```swift:StickyNote.swift

    func StickyNote() -> some View {
        return  GeometryReader { geometry in
            ZStack {
                //付箋の部分
                Path { path in
                    let w = geometry.size.width
                    let h = geometry.size.height
                    let d = min(w/self.divide, h/self.divide)
                    let m = min(d, self.ceaseSize)
                    let r = min(self.cornerRadius, m)

                    path.move(to: CGPoint(x: 0, y: r))
                    path.addLine(to: CGPoint(x: 0, y: h-r))
                    path.addArc(center: CGPoint(x: r, y: h-r), radius: r, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 90), clockwise: true)
                    path.addLine(to: CGPoint(x: r, y: h))
                    path.addLine(to: CGPoint(x: w-m, y: h))
                    path.addLine(to: CGPoint(x: w, y: h-m))
                    path.addLine(to: CGPoint(x: w, y: r))
                    path.addArc(center: CGPoint(x: w-r, y: r), radius: r, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 270), clockwise: true)
                    path.addLine(to: CGPoint(x: r, y: 0))
                    path.addArc(center: CGPoint(x: r, y: r), radius: r, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 180), clockwise: true)
                }
                .fill(self.color)

                //付箋の折り返し部分
                Path { path in
                    let w = geometry.size.width
                    let h = geometry.size.height
                    let d = min(w/self.divide, h/self.divide)
                    let m = min(d, self.ceaseSize)
                    let r = min(self.cornerRadius, m)

                    path.move(to: CGPoint(x: w-m, y: h))
                    path.addLine(to: CGPoint(x: w-m, y: h-m+r))
                    path.addArc(center: CGPoint(x: w-m+r, y: h-m+r), radius: r, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
                    path.addLine(to: CGPoint(x: w, y: h-m))
                    path.addLine(to: CGPoint(x: w-m, y: h))
                }
                .fill(Color.black).opacity(0.4)
            }
        }
    }
}
```

#可読性の向上
最後に`Modifier`をViewのメソッドにして可読性を高めます。
これにより`.modifier(StickyNoteModifier()`ではなく`.StickyNote()`で使えるようになります。

```swift:Modifier.swift
extension View {
    func stickyNote(color: Color, ceaseSize: CGFloat, cornerRadius: CGFloat, shadowLength: CGFloat)->some View {
        self.modifier(StickyNoteModifier(color: color, ceaseSize: ceaseSize, cornerRadius: cornerRadius, shadowLength: shadowLength))
    }
}
```

#まとめ
**`Modifier`を使うと、非プログラマーが関与する必要がない部分を隠蔽し、デザイナーなど非プログラマーがLiveViewを見ながらUIを細かに調整することを、簡単なパラメーターの変更のみで可能にします。**
今回の例ですと、色、コーナーの大きさ、付箋の折り返し部分の大きさなどを調整することができます。もちろん文字長が変わっても、複数行になっても自動で調整されます。

```swift:Modifier.swift
Text("If today were the last day of my life, would I want to do what I am about to do today")
    .StickyNote( color: .green, ceaseSize: 50, cornerRadius: 20, shadowLength: 10))
```
 - **color**
    - 付箋の色
 - **ceaseSize**
    - 付箋の折り返し部分の大きさ
 - **cornerRadius**
    - 角丸の大きさ
 - **shadowLength**
    - 影の大きさ（０で影なし）


#コード全文

```swift:StickyNoteModifier.swift
import SwiftUI

struct StickyNoteView: View {
    var body: some View {
        Text("If today were the last day of my life, would I want to do what I am about to do today")
        .font(.headline)
        .lineLimit(5)
        .frame(width: UIScreen.main.bounds.width/2)
        .stickyNote(color: .green, ceaseSize: 50, cornerRadius: 20, shadowLength: 10)
    }
}

extension View {
    func stickyNote(color: Color, ceaseSize: CGFloat, cornerRadius: CGFloat, shadowLength: CGFloat)->some View {
        self.modifier(StickyNoteModifier(color: color, ceaseSize: ceaseSize, cornerRadius: cornerRadius, shadowLength: shadowLength))
    }
}

struct StickyNoteModifier: ViewModifier {
    var color: Color = .green
    var ceaseSize: CGFloat = 50.0
    var cornerRadius:CGFloat = 20.0
    var shadowLength: CGFloat = 10.0
    private let divide: CGFloat = 2
    private let paddingHorizontal: CGFloat = 30.0
    private let paddingVertical: CGFloat = 40.0

    func body(content:Content) -> some View {
        ZStack(content: {
            content
                .padding(.horizontal, 30)
                .padding(.vertical, 40)
                .background(StickyNote())
                .shadow(radius: shadowLength)
        })
    }
    
    // StickyNote drawing
    func StickyNote() -> some View {
        return  GeometryReader { geometry in
            ZStack {
                Path { path in
                    let w = geometry.size.width
                    let h = geometry.size.height
                    let d = min(w/self.divide, h/self.divide)
                    let m = min(d, self.ceaseSize)
                    let r = min(self.cornerRadius, m)

                    path.move(to: CGPoint(x: 0, y: r))
                    path.addLine(to: CGPoint(x: 0, y: h-r))
                    path.addArc(center: CGPoint(x: r, y: h-r), radius: r, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 90), clockwise: true)
                    path.addLine(to: CGPoint(x: r, y: h))
                    path.addLine(to: CGPoint(x: w-m, y: h))
                    path.addLine(to: CGPoint(x: w, y: h-m))
                    path.addLine(to: CGPoint(x: w, y: r))
                    path.addArc(center: CGPoint(x: w-r, y: r), radius: r, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 270), clockwise: true)
                    path.addLine(to: CGPoint(x: r, y: 0))
                    path.addArc(center: CGPoint(x: r, y: r), radius: r, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 180), clockwise: true)
                }
                .fill(self.color)

                Path { path in
                    let w = geometry.size.width
                    let h = geometry.size.height
                    let d = min(w/self.divide, h/self.divide)
                    let m = min(d, self.ceaseSize)
                    let r = min(self.cornerRadius, m)

                    path.move(to: CGPoint(x: w-m, y: h))
                    path.addLine(to: CGPoint(x: w-m, y: h-m+r))
                    path.addArc(center: CGPoint(x: w-m+r, y: h-m+r), radius: r, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
                    path.addLine(to: CGPoint(x: w, y: h-m))
                    path.addLine(to: CGPoint(x: w-m, y: h))
                }
                .fill(Color.black).opacity(0.4)
            }
        }
    }
}

struct StickyNoteView_Previews: PreviewProvider {
    static var previews: some View {
        StickyNoteView()
    }
}

```