【SwiftUI】iOS 用のスクラッチ カード UI の実装

<h2> はじめに </h2>
はじめまして、Hridoy Chandra Das(リド)（@ihridoydas）です。


#### SwiftUI で、Google Pay 特典のスクラッチ カードのようなスクラッチ カードを作成します。

スクラッチカードは競技用に設計されたカードで、多くの場合、PIN を隠すために薄いカードストックまたはプラスチックで作られており、1 つまたは複数の領域に隠された情報が含まれており、不透明なカバーを剥がすことで明らかになります。

ユーザーはデバイスの画面上でカードを仮想的に「スクラッチ」「指で擦る」し、その下に隠されたコンテンツを明らかにすることができます。 このコンテンツは、クーポン コード、割引、その他の特典を紹介するためによく使用されます。

<img width="400" alt="s1.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/8be07b11-1c97-3c0f-e81c-438d8e19542c.jpeg">


SwiftUI の.mask Modifier(Instance Property)を使用すると、あるビューを別のビューでマスクできます。 簡単な例から始めましょう。

<h2> Mask </h2>
maskは、レンダリング システムが指定されたビューにアルファを適用するビュー。

こちらで.mask Modifierを使用して二つ方法でスクラッチビュー作成のついて共有したいと思います。（Path、Canvas）

<h2> 方法：１（Path）</h2>

```PathView.swift
    // MARK: Scratchable overlay view
       RoundedRectangle(cornerRadius: 20)
          .frame(width: 250, height: 250)
             Image("bg", bundle: nil)
             .resizable()
             .frame(width: 250, height: 250)
             .cornerRadius(20)
               

    // MARK: Hidden content view
            Image("bg", bundle: nil)
             .resizable()
             .frame(width: 250, height: 250)
             .cornerRadius(20)
             .overlay {
                Image("won", bundle: nil)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200)
                }
                 .mask(
                    Path { path in
                     path.addLines(currentPathState)
                 }.stroke(style: StrokeStyle(lineWidth: 50, lineCap: .round, lineJoin: .round))
                        )
                    .gesture(
                        DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onChanged({ value in
                        currentPathState.append(value.location)
                    })
                )
```


<h2> 完全な作成ビュー(Path) : ScratchCardView </h2>

```ScratchCardView.swift
import SwiftUI

struct ScratchCardView: View {
    @State var currentPathState = [CGPoint]()
    //navigationBarTitle
    init() {
           UINavigationBar.appearance().titleTextAttributes = [.font : UIFont(name: "Georgia-Bold", size: 20)!]
       }
    
    var body: some View {
        NavigationView {
            VStack {
                ZStack{
                    // MARK: Scratchable overlay view
                    RoundedRectangle(cornerRadius: 20)
                        .frame(width: 250, height: 250)
                    Image("bg", bundle: nil)
                        .resizable()
                        .frame(width: 250, height: 250)
                        .cornerRadius(20)
               

                    // MARK: Hidden content view
                    Image("bg", bundle: nil)
                        .resizable()
                        .frame(width: 250, height: 250)
                        .cornerRadius(20)
                        .overlay {
                            Image("won", bundle: nil)
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200)
                        }
                        .mask(
                            Path { path in
                                path.addLines(currentPathState)
                            }.stroke(style: StrokeStyle(lineWidth: 50, lineCap: .round, lineJoin: .round))
                        )
                        .gesture(
                            DragGesture(minimumDistance: 0, coordinateSpace: .local)
                                .onChanged({ value in
                                    currentPathState.append(value.location)
                                })
                        )
                }
                
            }
            .navigationBarTitle (Text("Scratch Card"), displayMode: .automatic)
            .toolbar{
                ToolbarItemGroup(placement: .navigationBarTrailing){
                    Button{
                        currentPathState.removeAll()
                    } label: {
                        Label("Clear",systemImage: "clear.fill")
                    }
                }
            }
            .accentColor(.red)
        }
        
    }
}

struct ScratchCardViewPreview: PreviewProvider {
    static var previews: some View {
        ScratchCardView()
    }
}

```

<h2> 方法：2（Canvas）</h2>

```Canvas.swift
   // MARK: Scratchable overlay view
       RoundedRectangle(cornerRadius: 20)
          .frame(width: 250, height: 250)
             Image("bg", bundle: nil)
                  .resizable()
                  .frame(width: 250, height: 250)
                  .cornerRadius(20)

    // MARK: Hidden content view
             Image("bg", bundle: nil)
                  .resizable()
                  .frame(width: 250, height: 250)
                  .cornerRadius(20)
                  .overlay {
             Image("won", bundle: nil)
                  .resizable()
                  .scaledToFit()
                  .frame(width: 200)
                }
                // Mask the Canvas
                .mask(
                    Canvas { context, _ in
                        for line in lines {
                            var path = Path()
                            path.addLines(line.points)
                            context.stroke(path,
                                 with: .color(.white),
                                style: StrokeStyle(lineWidth:line.lineWidth,
                                        lineCap: .round,
                                        lineJoin: .round)
                     )}
                   }
                )
                // User Can gesture and change the point value
                 .gesture(
                     DragGesture(minimumDistance: 0, coordinateSpace: .local)
                 .onChanged({ value in
                          let newPoint = value.location
                            currentLine.points.append(newPoint)
                            lines.append(currentLine)
                }))
```

<h2> 完全な作成ビュー(Canvas) : ScratchCardCanvasView </h2>


```ScratchCardCanvasView.swift
import SwiftUI

struct Line {
    var points = [CGPoint]()
    var lineWidth: Double = 50.0
}

struct ScratchCardCanvasView: View {
    @State private var currentLine = Line()
    @State private var lines = [Line]()
    
    init() {
           UINavigationBar.appearance().titleTextAttributes = [.font : UIFont(name: "Georgia-Bold", size: 20)!]
       }
    
    var body: some View {
        
        NavigationView {
            VStack {
                ZStack {
                    // MARK: Scratchable overlay view
                    RoundedRectangle(cornerRadius: 20)
                        .frame(width: 250, height: 250)
                    Image("bg", bundle: nil)
                        .resizable()
                        .frame(width: 250, height: 250)
                        .cornerRadius(20)

                    // MARK: Hidden content view
                    Image("bg", bundle: nil)
                        .resizable()
                        .frame(width: 250, height: 250)
                        .cornerRadius(20)
                        .overlay {
                            Image("won", bundle: nil)
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200)
                        }
                        .mask(
                            Canvas { context, _ in
                                for line in lines {
                                    var path = Path()
                                    path.addLines(line.points)
                                    context.stroke(path,
                                                   with: .color(.white),
                                                   style: StrokeStyle(lineWidth: line.lineWidth,
                                                                      lineCap: .round,
                                                                      lineJoin: .round)
                                    )
                                }
                            }
                        )
                        .gesture(
                            DragGesture(minimumDistance: 0, coordinateSpace: .local)
                                .onChanged({ value in
                                    let newPoint = value.location
                                    currentLine.points.append(newPoint)
                                    lines.append(currentLine)
                                })
                        )
                }
            }
            .navigationBarTitle (Text("Scratch Card Canvas"), displayMode: .automatic)
            .toolbar{
                ToolbarItemGroup(placement: .navigationBarTrailing){
                    Button{
                        currentLine.points.removeAll()
                        lines.removeAll()
                    } label: {
                        Label("Clear",systemImage: "clear.fill")
                    }
                }
            }
            .accentColor(.red)
        }
  
    }
}

struct ScratchCardCanvasViewPreview: PreviewProvider {
    static var previews: some View {
        ScratchCardCanvasView()
    }
}
```

<h2> 結果:</h2> 

[GitHub サンプル](https://github.com/ihridoydas/ScratchCard_iOS)

以上、最後までお読みいただきありがとうございました。

<img width="400" alt="Scratch_view_ios.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/ba48a727-73c4-f2e6-0977-9856b0ed2983.gif">