ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

swiftUIのCanvasを使ってみる

この記事は、ユニファ Advent Calendar 2021の20日目の記事です。 adventar.org

こんにちは。プロダクトエンジニアリング部の伊東です。主にサーバーサイドはRails、フロントエンドはVueを書いています。

今回の記事では、ios15から使えるようになったSwiftUIの新しいViewであるCanvasを扱ってみます。 ios13から使えていたGeometryReaderと比較してその使用感を書いていきます。

f:id:unifa_tech:20211214121004p:plain

アプリ上で表現すること

まずCanvasとGeometryReaderを使う上で、簡単なユースケースで見ていきたいと思います。 今回は、下記を行ってみます。

  • 画面上に四角のタイルをしきつめる

iphone、ipadそしてmacなど、画面の縦横サイズはたくさんあります。その画面サイズに応じて四角いタイルをしきつめる方法を考えてみます。

GeometryReaderを使った場合

iOS13以降では、GeometryReaderというViewを使う方法があります。

GeometryReaderはViewの1つで、コンテンツを独自のサイズと座標空間の関数として定義できます。

例えば、下記コードは、デバイスのセーフエリア(黄色部分)の幅と高さをテキストで表示しています。

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            let width = geometry.size.width
            let hight = geometry.size.height
            ZStack{
                Rectangle().foregroundColor(Color.yellow)
                
                VStack{
                    Text("width: \(String(format: "%.1f", width))")
                    Text("hight: \(String(format: "%.1f", hight))")
                }.font(.largeTitle)
            }
        }
    }
}

f:id:unifa_tech:20211214112410j:plain

この結果によると、

  • iphone8のセーフエリアは、横幅375px、高さ647pxです。

  • ipad Air (第4世代)のセーフエリアは、横幅820px、高さ1136pxです。

デバイスごとのセーフエリアが得られれば、正方形のタイルを画面上にしきつめるのは簡単そうですね。

今回は5列でしきつめてみます。 セーフエリア幅÷5をすれば、タイルの一辺のピクセルサイズが出せます。 行数は、画面いっぱいに表示したいので、セーフエリア高さ÷タイルの一辺で出せますね。

そのコードが下記となります。

import SwiftUI

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            let width = geometry.size.width
            let hight = geometry.size.height
            let colNum = 5
            let boxWidth = width / CGFloat(colNum)
            let rowNum = hight / CGFloat(boxWidth)

            ForEach(0..<Int(rowNum)+1){ r in
                ForEach(0..<Int(colNum)){ c in
                    RoundedRectangle(cornerRadius: 20)
                        .foregroundColor(Color.yellow)
                        .padding(5)
                        .frame(width: boxWidth, height: boxWidth)
                        .offset(x: boxWidth*CGFloat(c), y: boxWidth*CGFloat(r))
                }
            }
        }
    }
}

f:id:unifa_tech:20211214112602j:plain

補足)RoundedRectangleという角丸四角のViewに対してpaddingは余白を設定し、frameで表示する縦横のサイズを設定し、offsetで画面左上を(0,0)としてx、y軸にどの位置に配置するか設定しています。

canvasを使った場合

iOS15から新しくcanvasが使えるようになりました。これは、描画をサポートするViewの1つです。 SwiftUIビュー内にリッチでダイナミックな2Dグラフィックスを描画します。

さっそくGeometryReaderと同じことをしてみます。 まずは、下記コードで、セーフエリア(黄色部分)の幅と高さをテキストで表示しました。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Canvas { context, size in
            let safeAreaWidth = size.width
            let safeAreahight = size.height

            context.fill(
                Path(CGRect(x: 0, y: 0, width: safeAreaWidth, height: safeAreahight)),
                with: .color(.yellow))
            context.draw(
                Text("width: \(String(format: "%.1f", safeAreaWidth))")
                    .font(.largeTitle),
                at: CGPoint(x: safeAreaWidth/2, y: 200))
            context.draw(
                Text("hight: \(String(format: "%.1f", safeAreahight))")
                    .font(.largeTitle),
                at: CGPoint(x: safeAreaWidth/2, y: 250))
        }
    }
}

f:id:unifa_tech:20211214112910j:plain

Canvasは、GraphicsContextというcontextやsizeを使えます。 context.fillやcontext.drawとして、グラフィックを描画します。 今回の場合はTextや背景の黄色を描画しています。 GeometryReaderのときと違って、Canvas内部にZStackやVStackは使えないので、CGPointで、x,y座標で描画する位置を決める必要があります。

では、いよいよcanvasで5列のタイルを画面いっぱいにしきつめてみましょう。 下記がそのコードになります。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Canvas { context, size in
            let safeAreaWidth = size.width
            let safeAreahight = size.height
            let colNum = 5
            let paddingWidth = 5
            let paddingNum = colNum + 1
            let paddingTotalWidth = paddingWidth * paddingNum
            let boxWidth = (safeAreaWidth - CGFloat(paddingTotalWidth)) / CGFloat(colNum)
            let rowNum = safeAreahight / CGFloat(boxWidth)
            
            for r in 0..<Int(rowNum)+1 {
                for c in 0..<Int(colNum) {
                    context.fill(
                        Path(roundedRect: CGRect(x: CGFloat(c)*boxWidth+CGFloat(c+1)*CGFloat(paddingWidth),
                                                 y: CGFloat(r)*boxWidth+CGFloat(r+1)*CGFloat(paddingWidth),
                                                 width: boxWidth,
                                                 height: boxWidth),
                             cornerRadius: 20),
                        with: .color(.yellow))
                }
            }
        }
    }
}

f:id:unifa_tech:20211214113049j:plain

GeometryReaderでは、RoundedRectangleやpaddingが使えたので、余白についてそこまで配慮せずかけましたが、 Canvasでは、しっかり自分で余白を考慮して順にboxをしきつめる必要がありました。

GeometryReaderとCanvasをつかってタイルの列を増減させたときのCPUとメモリ使用率の比較

最後に、列を増減させてみて、どれくらいCPUやメモリが使われるか見てみます。

最初は、GeometryReader

f:id:unifa_tech:20211214113140g:plain

GeometryReaderは、列を増やしていくと、CPU使用率が、20%まで到達しました。 メモリは安定して25MB程度。

次は、Canvas f:id:unifa_tech:20211214113215g:plain

Canvasは、列を増やしていくと、CPU使用率が、4%まで到達しました。 メモリは安定して17MB程度。

この結果から、やはりCanvasは2Dのグラフィックを表示する上では、効率が良いと言えます。 GeometryReaderは、RoundedRectangleというViewを増やしていくのでオブジェクトが増え、CPUの使用率が高まったのだと思われますが、 一方、Canvasは、オブジェクトが増加するということはなく、描画するものが変わるだけなのでこの違いが出ると考えられます。

まとめ

  • swiftUIのCanvasを使うと、CPUリソースなどを効率的に使って、2Dグラフィックスな描画が可能。
  • 現時点では最新のios15のみサポートするという強気なアプリは出しづらいかもしれませんが、今後、ios、ipadOSなどにおいて有力な表現手段になるかもしれません。

--

ユニファでは新たな仲間を積極採用中です。

詳細についてはこちらからご確認ください。

unifa-e.com