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

iOS 14中matchedGeometryEffect的替代实现方案咨询

iOS 14下TabBar异常的替代实现方案

我开发的自定义TabBar在iOS 15.x系统运行正常,但在iOS 14中表现异常,需要适配iOS 14的替代实现方法。

效果对比:

  • iOS 14异常效果:
    iOS 14下的TabBar异常效果
  • iOS 15正常效果:
    iOS 15下的TabBar正常效果

原实现代码

public struct Tab: Identifiable, Equatable, Hashable {
    public let title: String
    public let isEnabled: Bool
    public let id: Int

    var isDisabled: Bool {
        !isEnabled
    }

    public init(title: String, isEnabled: Bool = true, id: Int) {
        self.title = title
        self.isEnabled = isEnabled
        self.id = id
    }

    public static func == (lhs: Tab, rhs: Tab) -> Bool {
      lhs.id == rhs.id
    }
}

public struct TabBar: View {
    private let tabs: [Tab]
    @Binding private var selectedTab: Tab
    @State private var selectedIndex = 0

    @Namespace var namespace

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

    public var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 0) {
                ForEach(Array(zip(self.tabs.indices, self.tabs)), id: \.0) { index, tab in
                    TabBarItem(selectedIndex: self.$selectedIndex,
                               selectedTab: self.$selectedTab,
                               namespace: namespace.self,
                               tab: tab,
                               index: index)
                }
            }
            .padding(.horizontal)
        }
        .onAppear {
            selectedIndex = tabs.firstIndex(where: { $0.id == selectedTab.id }) ?? 0
        }
    }
}

struct TabBarItem: View {
    @Binding var selectedIndex: Int
    @Binding var selectedTab: Tab
    let namespace: Namespace.ID
    let tab: Tab
    let index: Int

    var isSelected: Bool {
        selectedIndex == index
    }

    var textColor: Color {
        if tab.isEnabled {
            return Color.black
        } else {
            return Color.gray
        }
    }

    var body: some View {
        Button {
            selectedIndex = index
            selectedTab = tab
        } label: {
            VStack(spacing: 0) {
                Text(tab.title)
                    .font(.body)
                    .padding(.horizontal, 4)
                    .foregroundColor(isSelected ? Color.blue : Color.black)

                if isSelected{
                    Color.red
                        .frame(height: 2)
                        .padding(.top, 2)
                        .matchedGeometryEffect(id: "underline", in: namespace, properties: .frame)
                } else {
                    Color.clear.frame(height: 2)
                }
            }
            .animation(.spring(), value: selectedIndex)
        }
        .disabled(tab.isDisabled)
        .buttonStyle(.plain)
    }
}

问题分析

原代码核心问题在于matchedGeometryEffect在iOS 14中的兼容性缺陷,该API对跨视图几何动画的支持不完善,容易出现下划线位置偏移、动画卡顿甚至不显示的问题;另外iOS 14中Button的.plain样式与.disabled修饰符的组合也可能导致交互反馈异常。

iOS 14适配方案

替换matchedGeometryEffect为基于文本宽度的自定义下划线动画,调整动画作用范围,优化Button禁用状态表现:

public struct Tab: Identifiable, Equatable, Hashable {
    public let title: String
    public let isEnabled: Bool
    public let id: Int

    var isDisabled: Bool {
        !isEnabled
    }

    public init(title: String, isEnabled: Bool = true, id: Int) {
        self.title = title
        self.isEnabled = isEnabled
        self.id = id
    }

    public static func == (lhs: Tab, rhs: Tab) -> Bool {
      lhs.id == rhs.id
    }
}

public struct TabBar: View {
    private let tabs: [Tab]
    @Binding private var selectedTab: Tab
    @State private var selectedIndex = 0
    // 存储每个Tab文本的宽度,用于下划线匹配
    @State private var textWidths: [CGFloat] = []

    public init(tabs: [Tab], selectedTab: Binding<Tab>) {
        self.tabs = tabs
        self._selectedTab = selectedTab
        self._textWidths = State(initialValue: Array(repeating: 0, count: tabs.count))
    }

    public var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 16) { // 调整间距提升可读性
                ForEach(Array(zip(tabs.indices, tabs)), id: \.0) { index, tab in
                    TabBarItem(selectedIndex: $selectedIndex,
                               selectedTab: $selectedTab,
                               tab: tab,
                               index: index,
                               textWidth: $textWidths[index])
                }
            }
            .padding(.horizontal)
        }
        .onAppear {
            selectedIndex = tabs.firstIndex(where: { $0.id == selectedTab.id }) ?? 0
        }
    }
}

struct TabBarItem: View {
    @Binding var selectedIndex: Int
    @Binding var selectedTab: Tab
    let tab: Tab
    let index: Int
    @Binding var textWidth: CGFloat

    var isSelected: Bool {
        selectedIndex == index
    }

    var body: some View {
        Button {
            withAnimation(.spring()) { // 用withAnimation包裹状态变化,iOS 14更稳定
                selectedIndex = index
                selectedTab = tab
            }
        } label: {
            VStack(spacing: 0) {
                Text(tab.title)
                    .font(.body)
                    .padding(.horizontal, 4)
                    .foregroundColor(isSelected ? .blue : (tab.isEnabled ? .black : .gray))
                    .background(
                        // 测量文本宽度
                        GeometryReader { proxy in
                            Color.clear.onAppear {
                                textWidth = proxy.size.width
                            }
                        }
                    )

                // 自定义下划线动画,替代matchedGeometryEffect
                ZStack(alignment: .leading) {
                    Color.clear.frame(height: 2)
                    Color.red
                        .frame(width: isSelected ? textWidth : 0, height: 2)
                        .padding(.top, 2)
                }
            }
        }
        .disabled(tab.isDisabled)
        .opacity(tab.isDisabled ? 0.6 : 1) // 用opacity直观体现禁用状态
        .buttonStyle(.plain)
    }
}

关键修改点

  1. 移除matchedGeometryEffect:改用GeometryReader测量每个Tab文本的宽度,通过控制下划线的width属性实现平滑过渡动画,iOS 14下表现更稳定。
  2. 调整动画触发方式:将动画从视图修饰符改为用withAnimation包裹状态变更,避免iOS 14中动画作用范围混乱的问题。
  3. 优化禁用状态反馈:添加opacity修饰符,让禁用的Tab视觉上更直观,解决iOS 14中.disabled与.plain样式组合的交互问题。
  4. 调整HStack间距:增加spacing:16提升Tab之间的可读性,原0间距在iOS 14下容易导致点击区域重叠。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 12:48:23