iOS 14中matchedGeometryEffect的替代实现方案咨询
iOS 14下TabBar异常的替代实现方案
我开发的自定义TabBar在iOS 15.x系统运行正常,但在iOS 14中表现异常,需要适配iOS 14的替代实现方法。
效果对比:
- iOS 14异常效果:

- iOS 15正常效果:

原实现代码
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) } }
关键修改点
- 移除
matchedGeometryEffect:改用GeometryReader测量每个Tab文本的宽度,通过控制下划线的width属性实现平滑过渡动画,iOS 14下表现更稳定。 - 调整动画触发方式:将动画从视图修饰符改为用
withAnimation包裹状态变更,避免iOS 14中动画作用范围混乱的问题。 - 优化禁用状态反馈:添加
opacity修饰符,让禁用的Tab视觉上更直观,解决iOS 14中.disabled与.plain样式组合的交互问题。 - 调整HStack间距:增加
spacing:16提升Tab之间的可读性,原0间距在iOS 14下容易导致点击区域重叠。
内容的提问来源于stack exchange,提问作者Aaron Bratcher
相关产品推荐
相关产品推荐

