この記事は、ユニファ Advent Calendar 2021の20日目の記事です。 adventar.org
こんにちは。プロダクトエンジニアリング部の伊東です。主にサーバーサイドはRails、フロントエンドはVueを書いています。
今回の記事では、ios15から使えるようになったSwiftUIの新しいViewであるCanvasを扱ってみます。 ios13から使えていたGeometryReaderと比較してその使用感を書いていきます。
アプリ上で表現すること
まず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) } } } }
この結果によると、
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)) } } } } }
補足)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)) } } }
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)) } } } } }
GeometryReaderでは、RoundedRectangleやpaddingが使えたので、余白についてそこまで配慮せずかけましたが、 Canvasでは、しっかり自分で余白を考慮して順にboxをしきつめる必要がありました。
GeometryReaderとCanvasをつかってタイルの列を増減させたときのCPUとメモリ使用率の比較
最後に、列を増減させてみて、どれくらいCPUやメモリが使われるか見てみます。
最初は、GeometryReader
GeometryReaderは、列を増やしていくと、CPU使用率が、20%まで到達しました。 メモリは安定して25MB程度。
次は、Canvas
Canvasは、列を増やしていくと、CPU使用率が、4%まで到達しました。 メモリは安定して17MB程度。
この結果から、やはりCanvasは2Dのグラフィックを表示する上では、効率が良いと言えます。 GeometryReaderは、RoundedRectangleというViewを増やしていくのでオブジェクトが増え、CPUの使用率が高まったのだと思われますが、 一方、Canvasは、オブジェクトが増加するということはなく、描画するものが変わるだけなのでこの違いが出ると考えられます。
まとめ
- swiftUIのCanvasを使うと、CPUリソースなどを効率的に使って、2Dグラフィックスな描画が可能。
- 現時点では最新のios15のみサポートするという強気なアプリは出しづらいかもしれませんが、今後、ios、ipadOSなどにおいて有力な表現手段になるかもしれません。
--
ユニファでは新たな仲間を積極採用中です。
詳細についてはこちらからご確認ください。