SwiftUI 新玩法,3 行代码搞定抖音同款拉伸效果

大家平时在用 App 的时候肯定都见过这种效果,在详情页顶部放一张大图,往下一拉还能跟着变大,视觉冲击力直接拉满。说实话,以前我一直以为这种效果很难搞,得写一堆乱七八糟的代码,结果最近发现 SwiftUI 里居然有个骚操作,三行代码就能实现。

网上一搜"SwiftUI 拉伸头部",十有八九都是用 onScrollGeometryChange() 监听滚动偏移,然后把 offset 写到某个 state 里,再用来算图片 frame,能用,但是真的麻烦:要多维护一个 state,代码臃肿,滚动监听和 UI 绑定一大堆,复用性差,换个页面还得重写一遍。

新姿势:visualEffect()

SwiftUI 现在有个新修饰符叫 visualEffect(),用它可以直接拿到视图的 Geometry 信息,然后在闭包里随便玩。核心思路就是:拿到当前图片的高度和滚动偏移量,算出缩放比例,然后直接 scale 就完事了。

直接上代码:

extension View {
    func stretchy() -> some View {
        visualEffect { effect, geometry in
            let currentHeight = geometry.size.height
            let scrollOffset = geometry.frame(in: .scrollView).minY
            let positiveOffset = max(0, scrollOffset)

            let newHeight = currentHeight + positiveOffset
            let scaleFactor = newHeight / currentHeight

            return effect.scaleEffect(
                x: scaleFactor, y: scaleFactor,
                anchor: .bottom
            )
        }
    }
}

代码逻辑:

1,拿到当前高度

2,算出滚动偏移(只取正值,防止图片缩小)

3,算缩放比例

4,用 .bottom 锚点 scale,让图片只往上拉伸,下面的内容不受影响

有了上面的 stretchy() 修饰符,实际用起来就一句话:

struct FlowerView: View {
    let flower: Flower

    var body: some View {
        ScrollView {
            VStack {
                Image(flower.name)
                    .resizable()
                    .scaledToFill()
                    .stretchy() // 就这一行,灵魂!

                FlowerInfo(flower: flower)
            }
        }
        .ignoresSafeArea(edges: .top)
    }
}

就是这么简单,以前要写一堆 state 和监听,现在一个修饰符全搞定。

拉一下,图片就跟着变大,视觉上很顺滑,完全没有割裂感。用在商品详情、用户头像、文章封面这些场景,效果都很炸裂。

这种写法的优点:

代码极简,维护起来不头疼

没有多余的状态管理,性能也更好

封装成修饰符,想用哪用哪,复用性拉满

逻辑全在一个闭包里,阅读体验也舒服

我的笔记