RTL语言下SwiftUI InfiniteCarouselView无法滑动问题求助
SwiftUI InfiniteCarouselView RTL环境滑动异常修复方案
我开发的SwiftUI InfiniteCarouselView在LTR(左到右)语言环境(如英语)中运行正常,但切换到RTL(右到左)语言环境(如阿拉伯语)后,无法正常实现无限滑动效果。以下是相关代码及修复方案:
问题代码
InfiniteCarouselView.swift
// InfiniteCarouselView.swift // Lose Weight in 30 Days // // Created by MacBook on 08/01/2025. // import SwiftUI // Model struct Page: Identifiable, Hashable { var id: UUID = .init() var plan: Plan } // Helpers struct OffsetKey: PreferenceKey { static var defaultValue: CGRect = .zero static func reduce(value: inout CGRect, nextValue: () -> CGRect) { value = nextValue() } } // Extentions extension View { @ViewBuilder func offsetX(_ addObserver: Bool, completion: @escaping (CGRect) -> ()) -> some View { self .frame(maxWidth: .infinity) .overlay { if addObserver { GeometryReader { let rect = $0.frame(in: .global) Color.clear .preference(key: OffsetKey.self, value: rect) .onPreferenceChange(OffsetKey.self, perform: completion) } } } } } // Views struct PageControl: UIViewRepresentable { /// Page Properties var totalPages: Int var currentPage: Int func makeUIView(context: Context) -> UIPageControl { let control = UIPageControl() control.numberOfPages = totalPages control.currentPage = currentPage // Set the selected and default dot colors control.currentPageIndicatorTintColor = UIColor(Color.accent) control.backgroundStyle = .prominent control.allowsContinuousInteraction = false return control } func updateUIView(_ uiView: UIPageControl, context: Context) { uiView.numberOfPages = totalPages uiView.currentPage = currentPage } } struct InfiniteCarouselView: View { /// View Properties @State private var currentPage: String = "" @State private var listOfPages: [Page] = [] /// Infinite Carousel Properties @State private var fakedPages: [Page] = [] @State private var isNavigating: Bool = false // Added flag to handle navigation state var body: some View { VStack { GeometryReader { let size = $0.size VStack { TabView(selection: $currentPage, content: { ForEach(fakedPages) { page in ZStack { PlanMainBox(plan: page.plan) .onTapGesture { handleTap(for: page.id) } } .padding(.horizontal, 10) .frame(height: size.height) .tag(page.id.uuidString) /// Calculating Entire Page Scroll Offset .offsetX(currentPage == page.id.uuidString) { rect in let minX = rect.minX let pageOffset = minX - (size.width * CGFloat(fakedIndex(page))) /// Converting Page Offset into Progress let pageProgress = pageOffset / size.width /// Infinite Carousel Logic if -pageProgress < 1.0 { if fakedPages.indices.contains(fakedPages.count - 1) { currentPage = fakedPages[fakedPages.count - 1].id.uuidString } } if -pageProgress > CGFloat(fakedPages.count - 1) { if fakedPages.indices.contains(1) { currentPage = fakedPages[1].id.uuidString } } } } }) .tabViewStyle(.page(indexDisplayMode: .never)) } } .frame(height: 350) .onAppear { guard fakedPages.isEmpty else { return } for plan in Plan.allCases { listOfPages.append(.init(plan: plan)) } fakedPages.append(contentsOf: listOfPages) if var firstpage = listOfPages.first, var lastPage = listOfPages.last { currentPage = firstpage.id.uuidString firstpage.id = .init() lastPage.id = .init() fakedPages.append(firstpage) fakedPages.insert(lastPage, at: 0) } } PageControl(totalPages: listOfPages.count, currentPage: originalIndex(currentPage)) .disabled(true) if listOfPages.indices.contains(originalIndex(currentPage)) { NavigationLink( destination: PlanView(plan: listOfPages[originalIndex(currentPage)].plan), isActive: $isNavigating ) { EmptyView() } .hidden() } } } private func handleTap(for id: UUID) { isNavigating = true } func fakedIndex(_ of: Page) -> Int { return fakedPages.firstIndex(of: of) ?? 0 } func originalIndex(_ id: String) -> Int { return listOfPages.firstIndex { page in page.id.uuidString == id } ?? 0 } } #Preview { InfiniteCarouselView() }
PlanMainBox.swift
// PlanMainBox.swift // Plank Workout // // Created by MacBook on 26/12/2024. // import SwiftUI struct PlanMainBox: View { @Environment(\.layoutDirection) var layoutDirection // Detect layout direction var plan: Plan var body: some View { ZStack(alignment: .leading) { Image(plan.bgImage) .resizable() .scaleEffect(x: layoutDirection == .rightToLeft ? -1 : 1, y: 1) // Flip horizontally for RTL .cornerRadius(20) VStack(alignment: .leading) { _Text(plan.title, size: 25, type: .bold) .foregroundColor(.white) .padding(.top, 10) _Text(plan.subTitle, size: 18, type: .bold) .foregroundColor(Color(hex: "#808082")) .frame(width: 200, alignment: .leading) _Text("WORKOUT", size: 25, type: .bold) .foregroundColor(Color(hex: "#808082")) .frame(width: 200, alignment: .leading) HStack { Image(.fireIcon) Image(.fireIcon) .opacity(plan.level >= 2 ? 1 : 0.2) // 20% opacity Image(.fireIcon) .opacity(plan.level == 3 ? 1 : 0.2) // 20% opacity } _Text("START YOUR PLAN NOW !", size: 11, type: .medium) .foregroundColor(Color(hex: "#808082")) Spacer() Image(.playIcon) .resizable() .scaleEffect(x: layoutDirection == .rightToLeft ? -1 : 1, y: 1) // Flip horizontally for RTL .aspectRatio(contentMode: .fit) .padding(20) .background(Color.accentColor) .clipShape(Circle()) .frame(width: 60, height: 60) } .padding(15) } .frame(height: 350) } } #Preview { MainTabView() .environmentObject(ProfileViewModel()) }
问题根源
无限滚动的核心逻辑依赖于视图的minX偏移计算,但在RTL布局下,系统坐标系统方向反转,导致原本的偏移计算和边界判断完全失效:
- LTR环境中,页面从左向右滑动时
minX值增大 - RTL环境中,页面从右向左滑动时
minX值变化方向相反,原有的pageProgress计算逻辑不再适用
修复方案
修改InfiniteCarouselView中的偏移计算逻辑,适配RTL布局:
1. 更新offsetX扩展,传递布局方向
extension View { @ViewBuilder func offsetX(_ addObserver: Bool, layoutDirection: LayoutDirection, completion: @escaping (CGRect, LayoutDirection) -> ()) -> some View { self .frame(maxWidth: .infinity) .overlay { if addObserver { GeometryReader { let rect = $0.frame(in: .global) Color.clear .preference(key: OffsetKey.self, value: rect) .onPreferenceChange(OffsetKey.self) { rect in completion(rect, layoutDirection) } } } } } }
2. 调整无限滚动核心逻辑,适配RTL
struct InfiniteCarouselView: View { // 原有属性保持不变 var body: some View { VStack { GeometryReader { proxy in let size = proxy.size @Environment(\.layoutDirection) var layoutDirection VStack { TabView(selection: $currentPage, content: { ForEach(fakedPages) { page in ZStack { PlanMainBox(plan: page.plan) .onTapGesture { handleTap(for: page.id) } } .padding(.horizontal, 10) .frame(height: size.height) .tag(page.id.uuidString) .offsetX(currentPage == page.id.uuidString, layoutDirection: layoutDirection) { rect, direction in let minX = rect.minX var pageOffset: CGFloat var pageProgress: CGFloat // 根据布局方向调整偏移计算 if direction == .rightToLeft { pageOffset = (size.width * CGFloat(fakedPages.count - 1 - fakedIndex(page))) - minX pageProgress = pageOffset / size.width } else { pageOffset = minX - (size.width * CGFloat(fakedIndex(page))) pageProgress = pageOffset / size.width } // 适配RTL的无限滚动边界判断 if direction == .rightToLeft { if pageProgress < 1.0 { if fakedPages.indices.contains(1) { currentPage = fakedPages[1].id.uuidString } } if pageProgress > CGFloat(fakedPages.count - 1) { if fakedPages.indices.contains(fakedPages.count - 1) { currentPage = fakedPages[fakedPages.count - 1].id.uuidString } } } else { if -pageProgress < 1.0 { if fakedPages.indices.contains(fakedPages.count - 1) { currentPage = fakedPages[fakedPages.count - 1].id.uuidString } } if -pageProgress > CGFloat(fakedPages.count - 1) { if fakedPages.indices.contains(1) { currentPage = fakedPages[1].id.uuidString } } } } } }) .tabViewStyle(.page(indexDisplayMode: .never)) } } .frame(height: 350) // 原有onAppear、PageControl、NavigationLink逻辑保持不变 } } // 原有方法保持不变 }
额外说明
PlanMainBox中的图片翻转逻辑正确,无需修改,确保了RTL下图片内容显示方向正常PageControl使用系统UIPageControl,会自动适配RTL布局,无需额外调整
内容的提问来源于stack exchange,提问作者Abdul Rehman
相关产品推荐
相关产品推荐

