2

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.

Harshad Kale
  • 17,446
  • 7
  • 30
  • 44

3 Answers3

3

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
  • 128
  • 5
  • 2
    This works perfectly fine me but I ended up using `@Environment(\.layoutDirection)` instead of a little helper method. – Harshad Kale Nov 19 '21 at 20:42
0

@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)
                    }
                }
            }
        }
Dharman
  • 30,962
  • 25
  • 85
  • 135
  • Not sure why but for me, in RTL, I must set the scrollTo to id of the last item in the data source, what @MatM suggested. So calling scrollTo conditionally only for RTL works perfect. – Harshad Kale Nov 19 '21 at 20:39
  • Also I think anchor in this case is anchor of the element itself, not the anchor of ScrollView. Meaning, for both LTR and RTL, the anchor should be leading. Note that for RTL, leading is right edge and for LTR leading is left edge. – Harshad Kale Nov 19 '21 at 20:41
  • no worries! . glad @MatM answer can help ! in my case it will automatically goes to the RTL leading btw, I think it depends the id that we set too. anyway good if already solved – Michael Abadi Nov 20 '21 at 09:01
0

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
    }
  }
}
Metin Atalay
  • 1,375
  • 18
  • 28