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

SwiftUI中实现类似Stripe的旗帜风格渐变背景动画

解决SwiftUI渐变背景无法持续动画的问题

问题描述

我希望在用户个人资料视图上方创建类似Stripe官网的旗帜风格渐变动画。当前编写的SwiftUI代码仅在切换包含该视图的页面时会切换渐变外观,页面打开后无法持续播放动画,请问该如何调整代码以实现所需的动画效果?

原代码:

import SwiftUI

struct UserProfileHeaderView: View {
    let plan: String
    let headerTitle: String
    let greeting: String
    let planDescription: String
    var isTrailingTitle: Bool
    let description: String
    
    @State private var animateGradient = false
    
    var body: some View {
        let startColor = plan == "Classic" ? Color.purple : plan == "Plus" ? Color.red : Color(hex: "#636363")
        let endColor = plan == "Classic" ? Color.blue : plan == "Plus" ? Color.orange : plan == "Ultimate" ? Color(hex: "#010057") : Color(hex: "#636363")
        
        let gradient = LinearGradient(
            gradient: Gradient(colors: [startColor, endColor]),
            startPoint: animateGradient ? .topLeading : .bottomTrailing,
            endPoint: animateGradient ? .bottomTrailing : .topLeading
        )

        VStack(alignment: .leading, spacing: 8) {
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: headerTitle)
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                } else {
                    Spacer()
                    TextInfinity(text: headerTitle)
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .trailing)
                }
            }
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: greeting)
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.leading)
                } else {
                    Spacer()
                    TextInfinity(text: greeting)
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.leading)
                        .padding(.trailing)
                }
            }
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: description)
                        .font(.title3)
                        .foregroundColor(.white)
                        .padding(.leading)
                } else {
                    Spacer()
                    TextInfinity(text: description)
                        .font(.title3)
                        .foregroundColor(.white)
                        .padding(.leading)
                        .padding(.trailing)
                }
            }
            HStack {
                Spacer()
                TextInfinity(text: plan == "none" ? "NOT SUBSCRIBED" : planDescription.uppercased())
                    .font(.headline)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding([.trailing, .bottom], 10)
            }
        }
        .padding(.top, 35)
        .frame(maxWidth: .infinity)
        .background(gradient)
        .padding(.horizontal, 0)
        .padding(.vertical, 20)
        .ignoresSafeArea()
        .onAppear {
            withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
                animateGradient.toggle()
            }
        }
    }
}

// Preview Provider
struct UserProfileHeaderView_Previews: PreviewProvider {
    static var previews: some View {
        UserProfileHeaderView(
            plan: "Classic",
            headerTitle: "Your Profile",
            greeting: "Welcome!",
            planDescription: "Classic Plan",
            isTrailingTitle: false,
            description: "\( DeveloperPreview.shared.user.name ?? "" )\(( DeveloperPreview.shared.user.name != nil &&  DeveloperPreview.shared.user.name != "" ) ? " | " : "" )\( DeveloperPreview.shared.user.email )"
        )
    }
}

解决方案

问题出在你用布尔值切换渐变起点/终点,且仅触发一次状态变更——即便设置了repeatForever,SwiftUI也无法基于单次状态变化维持无限循环动画。改用浮点型状态变量控制渐变位置的平滑过渡,能实现类似Stripe的流畅动画效果:

修改后的完整代码

import SwiftUI

struct UserProfileHeaderView: View {
    let plan: String
    let headerTitle: String
    let greeting: String
    let planDescription: String
    var isTrailingTitle: Bool
    let description: String
    
    // 改用浮点型变量控制渐变位置,实现平滑过渡
    @State private var animateGradient = 0.0
    
    var body: some View {
        let startColor = plan == "Classic" ? Color.purple : plan == "Plus" ? Color.red : Color(hex: "#636363")
        let endColor = plan == "Classic" ? Color.blue : plan == "Plus" ? Color.orange : plan == "Ultimate" ? Color(hex: "#010057") : Color(hex: "#636363")
        
        let gradient = LinearGradient(
            gradient: Gradient(colors: [startColor, endColor]),
            // 根据浮点值计算渐变起点和终点,实现平滑移动
            startPoint: UnitPoint(x: animateGradient, y: 1 - animateGradient),
            endPoint: UnitPoint(x: 1 - animateGradient, y: animateGradient)
        )

        VStack(alignment: .leading, spacing: 8) {
            // 原有布局代码保持不变
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: headerTitle)
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                } else {
                    Spacer()
                    TextInfinity(text: headerTitle)
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .fontWeight(.bold)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .trailing)
                }
            }
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: greeting)
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.leading)
                } else {
                    Spacer()
                    TextInfinity(text: greeting)
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.leading)
                        .padding(.trailing)
                }
            }
            HStack {
                if !isTrailingTitle {
                    TextInfinity(text: description)
                        .font(.title3)
                        .foregroundColor(.white)
                        .padding(.leading)
                } else {
                    Spacer()
                    TextInfinity(text: description)
                        .font(.title3)
                        .foregroundColor(.white)
                        .padding(.leading)
                        .padding(.trailing)
                }
            }
            HStack {
                Spacer()
                TextInfinity(text: plan == "none" ? "NOT SUBSCRIBED" : planDescription.uppercased())
                    .font(.headline)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding([.trailing, .bottom], 10)
            }
        }
        .padding(.top, 35)
        .frame(maxWidth: .infinity)
        .background(gradient)
        .padding(.horizontal, 0)
        .padding(.vertical, 20)
        .ignoresSafeArea()
        .onAppear {
            // 启动无限循环动画,让浮点值在0到1之间来回平滑变化
            withAnimation(Animation.easeInOut(duration: 4).repeatForever(autoreverses: true)) {
                animateGradient = 1.0
            }
        }
    }
}

// Preview Provider
struct UserProfileHeaderView_Previews: PreviewProvider {
    static var previews: some View {
        UserProfileHeaderView(
            plan: "Classic",
            headerTitle: "Your Profile",
            greeting: "Welcome!",
            planDescription: "Classic Plan",
            isTrailingTitle: false,
            description: "\( DeveloperPreview.shared.user.name ?? "" )\(( DeveloperPreview.shared.user.name != nil &&  DeveloperPreview.shared.user.name != "" ) ? " | " : "" )\( DeveloperPreview.shared.user.email )"
        )
    }
}

关键修改说明

  1. 状态变量类型调整:将@State private var animateGradient从布尔型改为浮点型,用于精确控制渐变位置的过渡。
  2. 渐变位置计算:通过UnitPoint根据浮点值动态计算渐变的起点和终点,实现平滑的渐变流动效果,而非生硬的切换。
  3. 动画逻辑优化:在onAppear中启动动画时,直接将浮点值从0过渡到1,结合repeatForever(autoreverses: true)实现无限循环的来回动画,持续时间设为4秒(可根据需求调整),更接近Stripe官网的动画节奏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 15:49:51