SwiftUI复刻Apple Freeform画布视图的技术问题求助
解决SwiftUI复刻Freeform画布的缩放与手势问题
问题1:scaleEffect缩放导致内容裁切与混乱
根源分析
当前实现用scaleEffect后调整frame,但ScrollView基于缩放后的frame计算滚动范围,导致滚动偏移与缩放锚点不同步,进而出现内容裁切、滚动逻辑混乱的问题。正确思路是让ScrollView基于原始画布尺寸计算滚动范围,通过同步滚动偏移来适配缩放后的视图位置。
修复方案
修改CanvasView,去掉缩放后的frame调整,改用滚动偏移同步来修正视图位置:
struct CanvasView<Content: View>: View { let content: () -> Content let canvasSize: CGSize = CGSize(width: 3000, height: 3000) @State private var zoom: CGFloat = 0.5 @State private var scrollOffset: CGPoint = .zero var body: some View { ScrollView([.horizontal, .vertical], showsIndicators: true) { content() .frame(width: canvasSize.width, height: canvasSize.height) .scaleEffect(zoom) .offset(x: scrollOffset.x, y: scrollOffset.y) } .defaultScrollAnchor(.topLeading) .onScrollGeometryChange(for: CGPoint.self, of: { $0.contentOffset }) { old, new in scrollOffset = new } .overlay(alignment: .bottomTrailing) { VStack(spacing: 12) { HStack { Text("Zoom: \(zoom, specifier: "%.2f")") .frame(width: 80) Slider(value: $zoom, in: 0.1...5, onEditingChanged: handleZoomChange) .frame(width: 150) } } .padding(16) .background(.ultraThinMaterial) .cornerRadius(12) .padding() } } private func handleZoomChange(_ isEditing: Bool) { guard !isEditing else { return } // 缩放后修正滚动偏移,保持视图位置一致 let scaleRatio = zoom / (zoom * 0.99) // 近似计算缩放变化率 scrollOffset = CGPoint( x: scrollOffset.x * scaleRatio, y: scrollOffset.y * scaleRatio ) } }
关键改进
- 保持内容frame为原始画布尺寸,ScrollView基于原始尺寸计算滚动范围
- 用
offset配合滚动偏移,解决缩放后的视图位置偏移问题 - 滑块结束编辑时修正滚动偏移,确保缩放后视图位置不跳变
问题2:包装UIScrollView时子视图手势异常
根源分析
UIScrollView自带的捏合缩放手势会与SwiftUI子视图的拖拽手势冲突,且缩放后子视图的坐标系统未同步缩放比例,导致拖拽位置计算错误。
修复方案
自定义UIKitScrollViewRepresentable,处理手势优先级与坐标转换:
// 1. 自定义UIScrollView包装器 struct UIKitCanvasView<Content: View>: UIViewRepresentable { let content: Content let canvasSize: CGSize @Binding var zoom: CGFloat @Binding var scrollOffset: CGPoint func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator scrollView.minimumZoomScale = 0.1 scrollView.maximumZoomScale = 5 scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = true // 添加SwiftUI内容容器 let hostingController = UIHostingController(rootView: content) hostingController.view.frame = CGRect(origin: .zero, size: canvasSize) scrollView.addSubview(hostingController.view) scrollView.contentSize = canvasSize // 允许子视图接收手势,避免UIScrollView拦截 scrollView.panGestureRecognizer.cancelsTouchesInView = false scrollView.pinchGestureRecognizer.cancelsTouchesInView = false return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { if uiView.zoomScale != zoom { uiView.setZoomScale(zoom, animated: true) } scrollOffset = uiView.contentOffset } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UIScrollViewDelegate { var parent: UIKitCanvasView init(_ parent: UIKitCanvasView) { self.parent = parent } func viewForZooming(in scrollView: UIScrollView) -> UIView? { scrollView.subviews.first } func scrollViewDidZoom(_ scrollView: UIScrollView) { parent.zoom = scrollView.zoomScale } func scrollViewDidScroll(_ scrollView: UIScrollView) { parent.scrollOffset = scrollView.contentOffset } } } // 2. 修正DraggableSymbol的坐标计算 struct DraggableSymbol: View { @Binding var position: Symbol @Binding var zoom: CGFloat // 传入当前缩放比例 @State private var dragOffset = CGSize.zero var body: some View { Circle() .fill(Color.blue) .frame(width: 30, height: 30) .position(x: position.x + dragOffset.width, y: position.y + dragOffset.height) .gesture( DragGesture() .onChanged { value in // 拖拽偏移除以缩放比例,修正坐标系统 dragOffset = CGSize( width: value.translation.width / zoom, height: value.translation.height / zoom ) } .onEnded { value in position.x += value.translation.width / zoom position.y += value.translation.height / zoom dragOffset = .zero } ) } } // 3. 使用示例 struct SchematicView: View { @State private var symbols: [Symbol] = [ Symbol(x: 100, y: 100), Symbol(x: 200, y: 200) ] @State private var zoom: CGFloat = 0.5 @State private var scrollOffset: CGPoint = .zero var body: some View { UIKitCanvasView( content: { ForEach($symbols) { $symbol in DraggableSymbol(position: $symbol, zoom: $zoom) } }, canvasSize: CGSize(width: 3000, height: 3000), zoom: $zoom, scrollOffset: $scrollOffset ) .overlay(alignment: .bottomTrailing) { HStack { Text("Zoom: \(zoom, specifier: "%.2f")") Slider(value: $zoom, in: 0.1...5) } .padding(10) .background(.white) .cornerRadius(8) .padding() } } }
关键改进
- 设置
cancelsTouchesInView = false,允许子视图接收拖拽手势 - 拖拽偏移量除以当前缩放比例,修正缩放后的坐标计算
- 通过UIScrollViewDelegate同步缩放比例与滚动偏移到SwiftUI状态
额外优化建议
- 支持动态扩展画布尺寸:当内容超出当前画布范围时,自动增大
canvasSize - 添加双指捏合缩放手势,替代仅用滑块控制,更符合Freeform的交互逻辑
- 优化缩放锚点:以手势中心为锚点进行缩放,提升用户体验
内容的提问来源于stack exchange,提问作者devon't
相关产品推荐
相关产品推荐

