SwiftUI

Canvas 뷰를 통한 성능 향상

Phililip
728x90

안녕하세요.

 

이번에는 "Canvas 뷰를 썼을 때 정말 성능이 좋아질까??"에 대해 알아볼게요.

 

전체 프로젝트는 github에 올려두었습니다.

 


 

SwiftUI에는 Canvas 뷰라는 것이 있어요.

 

Canvas 뷰를 사용하면 복잡한 도형을 그려야 할 경우에 더 좋은 성능을 보여준다고 해요.

 

However, it might provide better performance for a complex drawing that involves dynamic data. Use a canvas to improve performance for a drawing that doesn’t primarily involve text or require interactive elements.

 

 

정말 그런지 확인하기 위해 저희는 

 

 

위 화면을 3가지 방법으로 만들어볼 거예요.

 

  1. Shape를 이용해서
  2. Canvas를 이용해서
  3. 이미지를 이용해서

 

 

# 0. 다이아몬드 만들기

Shape로 만들든, Canvas로 만들든 다이아몬드 모양은 똑같이 재사용할 것이기 때문에 다이아몬드 모양을 먼저 만들어줄게요.

 

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.width > rect.height) ? 0.0 : (rect.height - rect.width) / 2.0
        
        func offsetPoint(p: CGPoint) -> CGPoint {
            return CGPoint(x: p.x + xOffset + rect.origin.x, y: p.y + yOffset + rect.origin.y)
        }
        
        return Path { path in
            path.move(to: offsetPoint(p: CGPoint(x: 0, y: size * 0.5)))
            path.addQuadCurve(to: offsetPoint(p: CGPoint(x: size * 0.5, y: 0)),
                              control: offsetPoint(p: CGPoint(x: size * 0.4, y: size * 0.4)))
            path.addQuadCurve(to: offsetPoint(p: CGPoint(x: size, y: size * 0.5)),
                              control: offsetPoint(p: CGPoint(x: size * 0.6, y: size * 0.4)))
            path.addQuadCurve(to: offsetPoint(p: CGPoint(x: size * 0.5, y: size)),
                              control: offsetPoint(p: CGPoint(x: size * 0.6, y: size * 0.6)))
            path.addQuadCurve(to: offsetPoint(p: CGPoint(x: 0, y: size * 0.5)),
                              control: offsetPoint(p: CGPoint(x: size * 0.4, y: size * 0.6)))
            path.closeSubpath()
        }
    }
}

 

 

 

# 1. Shape로 화면 구성

Shape 기반의 카드를 만들어볼게요.

struct CardShapeView: View {
    var rows: Int
    var cols: Int
    
    var body: some View {
        GeometryReader { proxy in
            let width = proxy.size.width / CGFloat(cols)
            let height = proxy.size.height / CGFloat(rows)
            
            VStack(spacing: 0) {
                ForEach(0..<rows) { i in
                    let rowCols = (i % 2 == 0) ? cols: cols - 1
                    HStack(spacing: 0) {
                        ForEach(0..<rowCols) { _ in
                            DiamondShape()
                                .fill(Color(red: 250/255, green: 100/255, blue: 90/255))
                                .frame(width: width, height: height)
                        }
                    }
                }
            }
        }
        .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))
    }
}

 

 

그리고 50개의 CardShapeView가 들어간 ScrollView를 만들어줄게요.

 

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(rows: 7, cols: 21)
                }
            }
            .navigationTitle("50 Shape Cards")
        }
    }
}

 

일단 화면만 만들어두고 구체적인 성능 비교는 본문 맨 아래에서 할게요.

 

다음엔 Canvas 기반의 화면을 만들어봅시다.

 

 

 

# 2. Canvas로 화면 구성

Canvas 기반의 카드를 만들고,

 

struct CardCanvasView: View {
    var rows: Int
    var cols: Int
    var body: some View {
        Canvas { context, size in
            for r in 0..<rows {
                let rowCols = (r % 2 == 0) ? cols: cols - 1
                for c in 0..<rowCols {
                    let w = size.width / CGFloat(cols)
                    let h = size.height / CGFloat(rows)
                    var x = w * CGFloat(c)
                    var y = h * CGFloat(r)
                    if r % 2 == 1 {
                        x = w * CGFloat(c) + w * 0.5
                        y = h * CGFloat(r)
                    }
                    context.fill(DiamondShape().path(in: CGRect(x: x, y: y, width: w, height: h)), 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: 300, height: 200)
        .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))
    }
}

 

 

그리고 50개의 CardCanvasView가 들어간 ScrollView를 만들어줄게요.

 

struct MultiCanvasView: 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(rows: 7, cols: 21)
                }
            }
            .navigationTitle("50 Canvas Card")
        }
    }
}

 

 

# 3. 이미지로 화면 구성

아래 이미지를 Assets에 추가하고

 

card-background.png

 

 

이미지 기반의 카드를 만듭니다.

 

struct CardImageView: View {
    var body: some View {
        Image("card-background")
            .resizable()
            .scaledToFill()
            .clipShape(RoundedRectangle(cornerRadius: 40))
            .frame(width: 300, height: 200)
            .cornerRadius(30)
            .overlay(RoundedRectangle(cornerRadius: 30)
                        .stroke(.black, lineWidth: 2)
                        .cornerRadius(30))
    }
}

 

 

그리고 50개의 CardImageView가 들어간 ScrollView를 만들어줄게요.

 

struct MultiImageView: 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
                    CardImageView()
                }
            }
            .navigationTitle("50 Image Card")
        }
    }
}

 

 

# 4. 성능 비교

## 4.1 Memory

우선 메모리 사용량부터 살펴볼게요.

 

 

 

Shape로 구성된 화면을 출력할 때 상당한 메모리를 사용하는 것으로 확인됩니다.

 

Canvas로 구성된 화면은 Shape로 구성된 화면과 비교했을 때 상대적으로 메모리를 적게 사용한 것을 볼 수 있습니다.

 

Image로 구성된 화면은 Canvas로 구성된 화면보다 메모리를 미세하게 적게 사용했어요.

 

 

 

## 4.2 CPU

이번엔 CPU 사용량을 볼까요?

 

 

 

Shape로 구성된 화면을 스크롤 했을 땐, CPU 사용량이 매우 높았어요.

 

Canvas로 구성된 화면을 스크롤 했을 땐, 화면 첫 로딩 때 CPU 사용량이 증가했다가 이후 안정적인 성능을 보였습니다.

 

Image로 구성된 화면을 스크롤 했을 땐, 화면 첫 로딩 때부터 안정적인 성능을 보였습니다.

 

 

 

## 4.3 결론

Memory, CPU 사용량을 봤을 때, Canvas 기반 화면이 Shape 기반 화면보다 더 좋은 성능을 보여줬어요.

 

그리고 Image 기반 화면의 성능도 매우 좋았는데요,

 

 

만약 "화면에 출력할 모양이 동적으로 변할 필요 없이 항상 동일한 모양이다!" 하면 Canvas보다 Image를 사용하는 것이 성능이 더 좋을 것이고,

 

만약 "화면에 출력할 모양이 동적으로 변하는데 상당히 복잡한 모양이다!" 하면 Canvas를 사용하는 것이 성능 향상에 도움이 될 것 같아요.

 

간단한 모양이면 Shape를 쓰든, Canvas를 쓰든 큰 차이는 없겠죠?? 😁 😁

 

 

 

## 참고

- https://swdevnotes.com/swift/2022/better-performance-with-canvas-in-swiftui/

 

Better performance with canvas in SwiftUI

Better performance with canvas in SwiftUI

swdevnotes.com

 

 


 

이번 글은 여기서 마무리.

 

 

 

반응형

'SwiftUI' 카테고리의 다른 글

__printChanges  (0) 2022.03.23
SceneStorage  (0) 2022.03.20
cornerRadius  (0) 2022.03.13
SwiftUI에서 testable 한 코드 만들기  (0) 2022.03.05
animatableData  (0) 2022.03.03