如何解决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
相关产品推荐
相关产品推荐

