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

SwiftUI 自定义Segmented Control分段控件底部滑块动画实现问题

你当前的实现没有动画是因为底部条是绑定在单个选中选项上的,切换时是两个不同视图的显隐,自然无法产生位移和宽度过渡效果。你不需要废弃overlay方案,只要把底部滑块抽成独立的全局视图,通过收集每个选项的位置尺寸信息来驱动动画即可。

完整实现代码如下:

// 先定义Tab模型(如果你已定义可忽略)
struct Tab: Identifiable {
    let id = UUID()
    let title: String
}

// 定义PreferenceKey用于收集每个Tab项的位置尺寸
fileprivate struct TabItemPreferenceKey: PreferenceKey {
    static var defaultValue: [CGRect] = []
    static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
        value.append(contentsOf: nextValue())
    }
}

struct Tabs: View {
    @Binding var selectedTab: Int
    var tabs: [Tab]
    // 存储所有Tab项的frame信息
    @State private var tabFrames: [CGRect] = []

    init(_ selectedTab: Binding<Int>, tabs: [Tab]) {
        self._selectedTab = selectedTab
        self.tabs = tabs
    }

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 12) {
                ForEach(tabs.indices, id: \.self) { tabIndex in
                    let tab = tabs[tabIndex]
                    Button(action: {
                        withAnimation(.easeInOut(duration: 0.2)) {
                            selectedTab = tabIndex
                        }
                    }) {
                        Text(tab.title)
                            .font(.body.weight(.medium))
                            .foregroundColor(tabIndex == selectedTab ? .blue : .gray)
                            .padding(.bottom, 8)
                            .padding(.top, 2)
                            .padding(.horizontal, 4)
                    }
                    // 收集当前Tab的frame信息
                    .background(
                        GeometryReader { proxy in
                            Color.clear
                                .preference(key: TabItemPreferenceKey.self, value: [proxy.frame(in: .named("tabsContainer"))])
                        }
                    )
                }
            }
            .coordinateSpace(name: "tabsContainer")
            // 监听收集到的所有Tab frame
            .onPreferenceChange(TabItemPreferenceKey.self) { value in
                tabFrames = value
            }
            // 独立的底部滑块
            .overlay(alignment: .bottomLeading) {
                if tabFrames.count > selectedTab {
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: tabFrames[selectedTab].width, height: 3)
                        .offset(x: tabFrames[selectedTab].minX, y: 0)
                }
            }
        }
        .padding(.horizontal)
    }
}

核心逻辑说明

  • 给HStack加了自定义坐标空间,用来获取每个Tab在同层级下的相对位置
  • 选中切换时默认加了0.2秒的缓入缓出动画,你可以根据需要调整动画曲线和时长
  • 滑块的宽度和偏移完全跟随当前选中Tab的尺寸位置,所以不管文字长度变化多大都能自动适配
  • 示例中额外加了未选中状态的灰色文字,你可以根据设计需求调整样式

实现效果参考:
预期设计效果图

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 03:24:02