You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决SwiftUI中ScrollView导致Canvas频繁重绘的性能瓶颈?

解决SwiftUI中ScrollView与大型Canvas滚动时频繁重绘的性能问题

在SwiftUI里嵌套ScrollView和大型Canvas时,每次滚动都会触发Canvas重绘,像绘制100×100网格这类复杂场景会明显卡顿——但换成多个小型Canvas组合,就不会出现这种频繁重绘的问题。你提供的示例代码也验证了这一点:

import SwiftUI
var i = 0
struct ContentView: View {
    var body: some View {
        ScrollView([.horizontal, .vertical]) {       
            Canvas { _, _ in
                i = i+1
                print(i)
                
            }.background(.red).frame(width: 2000, height: 2000, alignment: .center)
        }
    }
}

以下是兼容iOS 15的两种有效解决方案:

方案一:用Equatable约束控制Canvas重绘时机

利用SwiftUI的Equatable协议,让Canvas仅在绘制数据真正变化时才重绘,滚动时因为数据未改变,不会触发重复绘制。

import SwiftUI

// 封装Canvas绘制所需数据,遵循Equatable
struct CanvasDrawData: Equatable {
    let rows: Int
    let columns: Int
}

struct ContentView: View {
    @State private var drawData = CanvasDrawData(rows: 100, columns: 100)
    private var drawCount = 0
    
    var body: some View {
        ScrollView([.horizontal, .vertical]) {
            Canvas { context, size in
                drawCount += 1
                print("绘制次数:\(drawCount)")
                
                // 执行网格绘制逻辑
                let cellWidth = size.width / CGFloat(drawData.columns)
                let cellHeight = size.height / CGFloat(drawData.rows)
                
                for row in 0..<drawData.rows {
                    for column in 0..<drawData.columns {
                        let rect = CGRect(
                            x: CGFloat(column) * cellWidth,
                            y: CGFloat(row) * cellHeight,
                            width: cellWidth,
                            height: cellHeight
                        )
                        context.stroke(Path(rect), with: .color(.gray), lineWidth: 0.5)
                    }
                }
            }
            .frame(width: 2000, height: 2000)
            .background(.red)
            .equatable() // 关键:根据drawData的Equatable判断是否重绘
        }
    }
}

// 让ContentView遵循Equatable,依赖drawData的变化
extension ContentView: Equatable {
    static func == (lhs: ContentView, rhs: ContentView) -> Bool {
        lhs.drawData == rhs.drawData
    }
}

方案二:用UIViewRepresentable封装UIKit绘制逻辑

UIKit的视图重绘机制更可控,只有手动调用setNeedsDisplay()时才会触发重绘,滚动时不会自动执行绘制逻辑,完全解决频繁重绘问题。

import SwiftUI

// 自定义UIView负责网格绘制
class GridDrawView: UIView {
    var rows: Int = 100
    var columns: Int = 100
    private var drawCount = 0
    
    override func draw(_ rect: CGRect) {
        drawCount += 1
        print("绘制次数:\(drawCount)")
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        let cellWidth = bounds.width / CGFloat(columns)
        let cellHeight = bounds.height / CGFloat(rows)
        
        context.setStrokeColor(UIColor.gray.cgColor)
        context.setLineWidth(0.5)
        
        for row in 0..<rows {
            for column in 0..<columns {
                let cellRect = CGRect(
                    x: CGFloat(column) * cellWidth,
                    y: CGFloat(row) * cellHeight,
                    width: cellWidth,
                    height: cellHeight
                )
                context.stroke(cellRect)
            }
        }
    }
    
    // 仅当数据变化时触发重绘
    func updateDrawData(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        setNeedsDisplay()
    }
}

// 包装成SwiftUI可用的View
struct GridCanvas: UIViewRepresentable {
    var rows: Int
    var columns: Int
    
    func makeUIView(context: Context) -> GridDrawView {
        let view = GridDrawView()
        view.rows = rows
        view.columns = columns
        return view
    }
    
    func updateUIView(_ uiView: GridDrawView, context: Context) {
        if uiView.rows != rows || uiView.columns != columns {
            uiView.updateDrawData(rows: rows, columns: columns)
        }
    }
}

struct ContentView: View {
    var body: some View {
        ScrollView([.horizontal, .vertical]) {
            GridCanvas(rows: 100, columns: 100)
                .frame(width: 2000, height: 2000)
                .background(.red)
        }
    }
}

方案选择建议

  • 如果更倾向SwiftUI原生写法,优先用Equatable方案,逻辑简洁且能有效控制重绘;
  • 如果追求最稳定的性能表现,选择UIViewRepresentable方案,完全规避滚动触发的重绘,适合复杂绘制场景。

内容的提问来源于stack exchange,提问作者1ctinus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 10:07:44