SwiftUI拖拽视图时实现圆形裁剪Dismiss效果的最优方案
问题描述
向下拖拽视图(图片)关闭时,需要实现将其裁剪为圆形的过渡效果。当前方案通过修改frame实现缩放,导致拖拽时出现卡顿,求优化方案复现目标效果。
原代码
import SwiftUI struct uyjrbertbr: View { @State var offset: CGSize = .zero @State var cornerRad: CGFloat = 0.0 @State var widthStory = 0.0 @State var heightStory = 0.0 var body: some View { ZStack { RoundedRectangle(cornerRadius: cornerRad) .foregroundStyle(.blue) .ignoresSafeArea() .frame(width: widthStory == 0.0 ? widthOrHeight(width: true) : widthStory) .frame(height: heightStory == 0.0 ? widthOrHeight(width: false) : heightStory) .offset(x: offset.width, y: offset.height) .gesture(DragGesture() .onChanged({ value in if value.translation.height > 0 { self.offset = value.translation clip() } }) .onEnded({ value in withAnimation(.easeInOut(duration: 0.2)) { offset = .zero cornerRad = 0.0 widthStory = widthOrHeight(width: true) heightStory = widthOrHeight(width: false) } }) ) } .onAppear(perform: { widthStory = widthOrHeight(width: true) heightStory = widthOrHeight(width: false) }) } func clip() { var ratio = abs(self.offset.height) / 200.0 ratio = min(1.0, ratio) self.cornerRad = ratio * 300.0 let min = 80.0 let maxWidth = widthOrHeight(width: true) let maxHeight = widthOrHeight(width: false) widthStory = maxWidth - ((maxWidth - min) * ratio) heightStory = maxHeight - ((maxHeight - min) * ratio) } } #Preview { uyjrbertbr() } func widthOrHeight(width: Bool) -> CGFloat { let scenes = UIApplication.shared.connectedScenes let windowScene = scenes.first as? UIWindowScene let window = windowScene?.windows.first if width { return window?.screen.bounds.width ?? 0 } else { return window?.screen.bounds.height ?? 0 } }
优化方案
卡顿的核心原因是频繁修改frame会触发SwiftUI的布局重计算,属于高开销操作。改用scale渲染层变换(不触发布局)结合cornerRadius和clipShape,可实现流畅的过渡效果:
优化后代码
import SwiftUI struct DragToDismissView: View { // 拖拽位移 @State private var dragOffset: CGSize = .zero // 缩放比例 @State private var scale: CGFloat = 1.0 // 圆角半径 @State private var cornerRadius: CGFloat = 0.0 // 屏幕尺寸 private var screenSize: CGSize { UIScreen.main.bounds.size } // 拖拽关闭阈值 private let dragThreshold: CGFloat = 200.0 var body: some View { ZStack { // 替换为你的图片视图即可 Color.blue .ignoresSafeArea() // 先裁剪再缩放,保证圆形裁剪逻辑正确 .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) .scaleEffect(scale) .offset(x: dragOffset.width, y: dragOffset.height) .gesture( DragGesture() .onChanged { value in guard value.translation.height > 0 else { return } dragOffset = value.translation updateTransitionRatio(ratio: value.translation.height / dragThreshold) } .onEnded { value in let shouldDismiss = value.translation.height > dragThreshold withAnimation(.easeInOut(duration: 0.2)) { if shouldDismiss { // 此处添加关闭后的业务逻辑,如导航返回、移除视图等 scale = 0.0 } else { // 回弹恢复初始状态 dragOffset = .zero scale = 1.0 cornerRadius = 0.0 } } } ) } } // 根据拖拽比例更新缩放和圆角参数 private func updateTransitionRatio(ratio: CGFloat) { let clampedRatio = min(max(ratio, 0.0), 1.0) // 缩放从1降到0.3,对应视频中的最小圆形大小 scale = 1.0 - (0.7 * clampedRatio) // 圆角从0增加到屏幕短边的一半,实现矩形到圆形的过渡 cornerRadius = clampedRatio * min(screenSize.width, screenSize.height) / 2 } } #Preview { DragToDismissView() }
关键改进点
- 用
scaleEffect替代frame修改:缩放属于渲染层变换,不会触发布局重计算,彻底解决卡顿问题 clipShape结合动态圆角:通过调整圆角半径,让视图从矩形平滑过渡为圆形- 简化尺寸获取逻辑:直接使用
UIScreen.main.bounds获取屏幕尺寸,更简洁可靠 - 增加拖拽阈值判断:拖拽距离超过阈值时直接完成关闭,否则回弹恢复初始状态
- 优化动画逻辑:结束拖拽时的过渡动画更自然,符合用户交互预期
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

