LoginSignup
26
16

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-26

はじめに

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

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

今回作るもの

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

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))

Screen Shot 2019-07-25 at 4.51.43 pm.png

ViewModifier

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側のGeometryReadercontentのサイズ(付箋の矩形のサイズ)を取得する必要があるからです。

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の作り方ーその1(GeometryReader編))を参考にしてください。

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()で使えるようになります。

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を細かに調整することを、簡単なパラメーターの変更のみで可能にします。
今回の例ですと、色、コーナーの大きさ、付箋の折り返し部分の大きさなどを調整することができます。もちろん文字長が変わっても、複数行になっても自動で調整されます。

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
    • 影の大きさ(0で影なし)

コード全文

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()
    }
}

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