Говорят, что использование Canvas для создания сложных форм может обеспечить лучшую производительность в SwiftUI. В этой статье сравнивается производительность прокрутки нескольких экземпляров одного и того же шаблона карты, созданного с помощью формы, холста или изображения.

Говорят, что использование Canvas для создания сложных форм может обеспечить лучшую производительность в SwiftUI. В этой статье сравнивается производительность прокрутки нескольких экземпляров одного и того же шаблона карты, созданного с помощью формы, холста или изображения.
Создание шаблона карты с помощью Shape
Все детали построены с использованием фигуры Diamond, созданной в разделе Создание деталей из базовых фигур в SwiftUI. DiamondShape соответствует протоколу Shape и реализует функцию path для создания ромбовидной формы внутри содержащего прямоугольника. В оригинальную форму алмаза внесена одна поправка в функции offsetPoint, чтобы скорректировать начало координат содержащего прямоугольника не в точке (0,0).
struct DiamondShape: Shape {
func path(in rect: CGRect) -> Path {
let size = min(rect.width, rect.height)
let xOffset = (rect.width > rect.height) ? (rect.width - rect.height) / 2.0 : 0.0
let yOffset = (rect.height > rect.width) ? (rect.height - rect.width) / 2.0 : 0.0
func offsetPoint(p: CGPoint) -> CGPoint {
return CGPoint(x: p.x + xOffset + rect.origin.x, y: p.y+yOffset + rect.origin.y)
}
let path = Path { path in
path.move(to: offsetPoint(p: CGPoint(x: 0, y: (size * 0.50))))
path.addQuadCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: 0)),
control: offsetPoint(p: CGPoint(x: (size * 0.40), y: (size * 0.40))))
path.addQuadCurve(to: offsetPoint(p: CGPoint(x: size, y: (size * 0.50))),
control: offsetPoint(p: CGPoint(x: (size * 0.60), y: (size * 0.40))))
path.addQuadCurve(to: offsetPoint(p: CGPoint(x: (size * 0.50), y: size)),
control: offsetPoint(p: CGPoint(x: (size * 0.60), y: (size * 0.60))))
path.addQuadCurve(to: offsetPoint(p: CGPoint(x: 0, y: (size * 0.50))),
control: offsetPoint(p: CGPoint(x: (size * 0.40), y: (size * 0.60))))
path.closeSubpath()
}
return path
}
}
DiamondCardView создается путем расположения строк и столбцов ромбов с помощью вертикальных и горизонтальных стеков.
struct DiamondCardView: View {
var rows :Int
var cols: Int
var body: some View {
GeometryReader { gr in
let width = gr.size.width / CGFloat(cols)
let height = gr.size.height / CGFloat(rows)
VStack(spacing:0) {
ForEach(0..<rows) { i in
let rowCols = (i%2==0) ? cols : cols - 1
HStack(spacing:0) {
Group {
ForEach(0..<rowCols) { _ in
DiamondShape()
.fill(Color(red: 250/255, green: 100/255, blue: 90/255))
.frame(width: width, height: height)
}
}
}
}
}
}
}
}
Узор открытки можно дополнить, добавив бордюры и закруглив углы.
struct SingleShapeView: View {
var body: some View {
ZStack {
Color(red: 214/255, green: 232/255, blue: 248/255)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Card from Shape")
.font(.title)
.fontWeight(.bold)
.padding(.vertical, 30)
Group {
DiamondCardView(rows: 21, cols: 7)
.padding(40)
.frame(width: 250, height: 430)
.background(Color(red: 30/255, green: 40/255, blue: 60/255))
.cornerRadius(30)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
.cornerRadius(30))
.overlay(RoundedRectangle(cornerRadius: 20)
.stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 20)
.cornerRadius(15)
.padding(25))
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(.black, lineWidth: 2)
.cornerRadius(30))
}
.rotationEffect(.degrees(10))
Spacer()
}
}
}
}

Создание шаблона карты с помощью Canvas
Как обсуждалось в разделе Использование canvas в SwiftUI, представление Canvas предоставляет механизм для эффективного рисования в SwiftUI. Холст использует GraphicsContext и размер, чтобы рисовать непосредственно в пределах содержащего прямоугольника. Тот же шаблон карты, что и выше, создается с использованием холста для компоновки отдельных фигур ромбов. Кроме того, тот же DiamondShape используется в холсте путем доступа к функции path фигуры и передачи содержащего прямоугольника.
struct SingleCanvasView: View {
var body: some View {
ZStack {
Color(red: 214/255, green: 232/255, blue: 248/255)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Card from Canvas")
.font(.title)
.fontWeight(.bold)
.padding(.vertical, 30)
Canvas { context, size in
let w = size.width
let h = size.height
let rows = 21
let cols = 7
for r in 0..<rows {
for c in 0..<cols {
let x = w/CGFloat(cols) * CGFloat(c)
let y = h/CGFloat(rows) * CGFloat(r)
context.fill(
DiamondShape().path(in: CGRect(
x: x,
y: y,
width: w/CGFloat(cols),
height: h/CGFloat(rows))),
with: .color(red: 250/255, green: 100/255, blue: 90/255))
}
}
}
.padding(40)
.background(Color(red: 30/255, green: 40/255, blue: 60/255))
.frame(width: 250, height: 430)
.cornerRadius(30)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
.cornerRadius(30))
.overlay(RoundedRectangle(cornerRadius: 20)
.stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 20)
.cornerRadius(15)
.padding(25))
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(.black, lineWidth: 2)
.cornerRadius(30))
.rotationEffect(.degrees(10))
Spacer()
}
}
}
}

ScrollView с карточками из Shape
По отдельности вышеупомянутые представления отображаются нормально, и, похоже, нет никаких различий в производительности. Создается ScrollView, содержащий 50 карт DiamondCardView, чтобы нагрузить систему и поискать проблемы с производительностью.
struct MultipleShapeView: View {
var body: some View {
ZStack {
Color(red: 214/255, green: 232/255, blue: 248/255)
.edgesIgnoringSafeArea(.all)
ScrollView {
ForEach(1...50, id: \.self) { _ in
CardShapeView()
}
}
.navigationTitle("50 Shape Cards")
}
}
}
struct CardShapeView: View {
var body: some View {
DiamondCardView(rows: 7, cols: 21)
.padding(30)
.frame(width: 300, height: 200)
.background(Color(red: 30/255, green: 40/255, blue: 60/255))
.cornerRadius(30)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
.cornerRadius(30))
.overlay(RoundedRectangle(cornerRadius: 20)
.stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 10)
.cornerRadius(15)
.padding(20))
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(.black, lineWidth: 2)
.cornerRadius(30))
}
}

ScrollView с карточками из Canvas
Создается аналогичный ScrollView, содержащий 50 карточек из CardCanvasView, чтобы нагрузить систему и поискать проблемы с производительностью.
struct MultipleCanvasView: View {
var body: some View {
ZStack {
Color(red: 214/255, green: 232/255, blue: 248/255)
.edgesIgnoringSafeArea(.all)
ScrollView {
ForEach(1...50, id: \.self) { i in
CardCanvasView()
}
}
.navigationTitle("50 Canvas Card")
}
}
}
struct CardCanvasView: View {
var body: some View {
VStack {
Canvas { context, size in
let w = size.width
let h = size.height
let rows = 7
let cols = 21
for r in 0..<rows {
for c in 0..<cols {
let x = w/CGFloat(cols) * CGFloat(c)
let y = h/CGFloat(rows) * CGFloat(r)
context.fill(
DiamondShape().path(in: CGRect(x: x, y: y, width: w/CGFloat(cols), height: h/CGFloat(rows))),
with: .color(red: 250/255, green: 100/255, blue: 90/255))
}
}
}
.padding(30)
.frame(width: 300, height: 200)
.background(Color(red: 30/255, green: 40/255, blue: 60/255))
.cornerRadius(30)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(Color(red: 0.93, green: 0.94, blue: 0.77), lineWidth: 50)
.cornerRadius(30))
.overlay(RoundedRectangle(cornerRadius: 20)
.stroke(Color(red: 250/255, green: 100/255, blue: 90/255), lineWidth: 10)
.cornerRadius(15)
.padding(20))
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(.black, lineWidth: 2)
.cornerRadius(30))
}
}
}

ScrollView с карточками из изображения
Наконец, создается ScrollView, содержащий 50 изображений для карточек, чтобы сравнить производительность с использованием фигур и холста. Изображение создается из скриншота одной из вышеуказанных карточек и добавляется в активы.
struct MultipleImageView: View {
var body: some View {
ZStack {
Color(red: 214/255, green: 232/255, blue: 248/255)
.edgesIgnoringSafeArea(.all)
ScrollView {
ForEach(1...50, id: \.self) { _ in
Image("card-background")
.resizable()
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: 40))
.frame(width: 310, height: 200)
}
}
.navigationTitle("50 Image Card")
}
}
}


Обзор производительности
Существует множество способов измерения производительности, включая автоматизированные тесты производительности, о которых я расскажу в другой статье. В этом приложении мы просто запустим приложение и посмотрим на использование процессора и памяти в Xcode. Видно, что ScrollView с 50 картами, созданными с использованием фигур для каждого алмаза и выкладыванием их в стопки, использует значительно больше памяти для загрузки. Он также использует значительно больше процессора при прокрутке представления вверх и вниз. Карточки, созданные с помощью canvas, используют значительно меньше памяти для загрузки и меньше процессора при прокрутке вверх и вниз. Использование изображения для карточек работает лучше всего, но лишь немного лучше, чем использование canvas.


Заключение
ScrollView, содержащий 50 экземпляров одной и той же карты, возможно, не самый реалистичный сценарий, но он иллюстрирует разницу в производительности при создании таких представлений. Использование холста для компоновки нескольких фигур, таких как алмазы, дает гораздо более высокую производительность, чем компоновка нескольких фигур в представлении. Это видно по отзывчивости прокрутки и использованию памяти и процессора. Если эти фигуры в приложении статичны, то использование изображения обеспечивает немного лучшую производительность, чем использование canvas. Использование canvas показало почти такие же результаты, как и использование изображений, поэтому если содержимое должно быть динамичным, то canvas может быть лучшим вариантом.
















