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

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()
}

关键改进点

  1. 用scaleEffect替代frame修改:缩放属于渲染层变换,不会触发布局重计算,彻底解决卡顿问题
  2. clipShape结合动态圆角:通过调整圆角半径,让视图从矩形平滑过渡为圆形
  3. 简化尺寸获取逻辑:直接使用UIScreen.main.bounds获取屏幕尺寸,更简洁可靠
  4. 增加拖拽阈值判断:拖拽距离超过阈值时直接完成关闭,否则回弹恢复初始状态
  5. 优化动画逻辑:结束拖拽时的过渡动画更自然,符合用户交互预期

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 22:40:20