Повышение производительности при использовании canvas в SwiftUI

584
Повышенное использование памяти при использовании шаблона карты, созданного с помощью Shape
Повышенное использование памяти при использовании шаблона карты, созданного с помощью Shape

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

Повышение производительности при использовании canvas в SwiftUI
Повышение производительности при использовании 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()
            }
        }
    }
}
Определите шаблон карты с помощью Canvas
Определите шаблон карты с помощью Canvas

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))
    }
}
Шаблон нескольких карт определяется с помощью Shape
Шаблон нескольких карт определяется с помощью Shape

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))
        }
    }
}
Определение шаблона нескольких карт с помощью Canvas
Определение шаблона нескольких карт с помощью Canvas

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.

Повышенное использование памяти и процессора при использовании шаблона карты, созданного с помощью Shape
Повышенное использование памяти и процессора при использовании шаблона карты, созданного с помощью Shape
Повышенное использование памяти при использовании шаблона карты, созданного с помощью Shape
Повышенное использование памяти при использовании шаблона карты, созданного с помощью Shape

Заключение

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