SwiftUI - how to make horizontally scrollable views RTL friendly?

Viewed 562

I have a SwiftUI app which supports both LTR and RTL languages. It is not fixated to either one of those, it just depends on what user wants.

I have a horizontally scrollable list of things inside a vertically scrollable list of things, previously achieved by UICollectionView inside a UITableViewCell inside a UITableView.

For RTL, the horizontally scrollable row needs to be on the first element by default which is the rightmost (opposite of LTR obviously). I have found no way yet to make this happen.

I tried flipsForRightToLeftLayoutDirection(_:) but the problem is it flips everything, including the Text() inside the scrolling views. So the Arabic text looks mirrored.

Here is my skeleton of the view -

ScrollView(.horizontal) {
  LazyHStack {
    ForEach(things) { thing in
      Text(thing.name)
    }
  }
}

Any ideas on how to achieve this? Thanks in advance.

3 Answers

To automatically scroll to the rightmost element you can use the scrollTo function of the ScrollViewReader view.

You should set an id to each element of the ForEach loop and use the onAppear modifier of the ForEach to scroll to the rightmost element (the last id). This should be done only if language direction is right to left (Locale.characterDirection gives this information).

The code below illustrates this:

struct LanguageScrollView: View {
    let things: [String] = ["Thing1", "Thing2", "Thing3", "Thing4", "Thing5", "Thing6", "Thing7", "Thing8", "Thing9", "Thing10"]
    
    var body: some View {
        ScrollView(.horizontal) {
            ScrollViewReader { value in
                LazyHStack {
                    ForEach(0..<things.count) { i in
                        Text(things[i])
                            .id(i)
                    }
                    .onAppear {
                        if getLangDirection() == .rightToLeft {
                            value.scrollTo(things.count - 1)
                        }
                    }
                }
            }
        }
    }
    
    func getLangDirection() -> Locale.LanguageDirection? {
        guard let language = Locale.current.languageCode else { return nil }
        let direction = Locale.characterDirection(forLanguage: language)
        return direction
    }
    
}

@MatM answer above is correct, however, I LazyHStack has a problem with sizing (on iOS 14 at least) if we don't set the frame explicitly, which I haven't had any idea why. Another thing instead uses Locale.LanguageDirection? I prefer to use layoutDirection from Environment like below. Keep in mind I wrote to use HStack because in Lazy I have a problem with the view is not properly showing for RTL somehow except we define the frame.

    @Environment(\.layoutDirection) var layoutDirection
    

ScrollView(.horizontal) {
            ScrollViewReader { value in
                HStack {
                    ForEach(0..<things.count) { i in
                        Text(things[i])
                            .id(i)
                    }
                    .onAppear {
                        // change things.first with whatever is the first identifier you set
                        proxy.scrollTo(things.first ?? 0, anchor: layoutDirection == LayoutDirection.rightToLeft ? .trailing : .leading)
                    }
                }
            }
        }

The following will be very helpful.

import SwiftUI

struct ScrollViewRTL<Content: View>: View {
  @ViewBuilder var content: Content
  @Environment(\.layoutDirection) private var layoutDirection
  var type: RowType
   
  init(type: RowType, @ViewBuilder content: () -> Content) {
    self.type = type
    self.content = content()
  }
   
  @ViewBuilder var body: some View {
    ScrollView(type.scrollAxis, showsIndicators: false) {
      content
        .rotation3DEffect(Angle(degrees: layoutDirection == .rightToLeft ? -180 : 0), axis: (
          x: CGFloat(0),
          y: CGFloat(layoutDirection == .rightToLeft ? -10 : 0),
          z: CGFloat(0)))
       
    }
    .rotation3DEffect(Angle(degrees: layoutDirection == .rightToLeft ? 180 : 0), axis: (
      x: CGFloat(0),
      y: CGFloat(layoutDirection == .rightToLeft ? 10 : 0),
      z: CGFloat(0)))
  }
}

public enum RowType {
  case hList
  case vList
   
  var scrollAxis: Axis.Set {
    switch self {
    case .hList:
      return .horizontal
       
    case .vList:
      return .vertical
    }
  }
}
Related