Left and right padding (not leading and trailing) in SwiftUI
Asked Answered
K

4

21

A padding modifier in SwiftUI takes an EdgeInsets, eg

.padding(.leading, 8)

However, there are only leading and trailing EdgeInsets, and not left and right. This presents a problem because not everything in RTL languages should necessarily be reversed from the way it's displayed in LTR languages.

Is there a way to achieve the same or similar effect as the padding modifier, which forces the padding to be on left or right side, regardless of directionality of the language?

Keffiyeh answered 11/9, 2021 at 14:5 Comment(0)
S
11

Use @Environment(\.layoutDirection) to get the current layout direction (LTR or RTL) and use it to flip .leading and .trailing as needed.

Here’s a ViewModifier that wraps all that conveniently:

enum NoFlipEdge {
    case left, right
}

struct NoFlipPadding: ViewModifier {
    let edge: NoFlipEdge
    let length: CGFloat?
    @Environment(\.layoutDirection) var layoutDirection
    
    private var computedEdge: Edge.Set {
        if layoutDirection == .rightToLeft {
            return edge == .left ? .trailing : .leading
        } else {
            return edge == .left ? .leading : .trailing
        }
    }
    
    func body(content: Content) -> some View {
        content
            .padding(computedEdge, length)
    }
}

extension View {
    func padding(_ edge: NoFlipEdge, _ length: CGFloat? = nil) -> some View {
        self.modifier(NoFlipPadding(edge: edge, length: length))
    }
}

Use it like you would the standard padding modifiers:

Text("Text")
    .padding(.left, 10)
Sequestrate answered 11/9, 2021 at 17:24 Comment(4)
The layoutDirection is read-write env value, so let's imagine what happens if someone in parent view will need to change it intentionally to value opposite to current system one.Haemostasis
@Haemostasis If you’re concerned the layoutDirection will be overridden, you could check this global setting instead of the environment value: UIApplication.shared.userInterfaceLayoutDirection == .rightToLeftSequestrate
@Haemostasis actually padding(.leading) depends on @Environment(\.layoutDirection), so if you change it - this modifier still gonna give the correct padding.Estovers
@Philip You’re right, I misread @Asperi’s question. I’m solution should work regardless.Sequestrate
H
14

Here is a simplified demo of possible approach - use extension with injected fixed-sized Spacer at each side.

Prepared & tested with Xcode 13 / iOS 15

enum Side: Equatable, Hashable {
    case left
    case right
}

extension View {
    func padding(sides: [Side], value: CGFloat = 8) -> some View {
        HStack(spacing: 0) {
            if sides.contains(.left) {
                Spacer().frame(width: value)
            }
            self
            if sides.contains(.right) {
                Spacer().frame(width: value)
            }
        }
    }
}

demo of usage

var body: some View {
  TextField("Last Name", text: $nameLast)
    .textFieldStyle(.roundedBorder)
    .padding(sides: [.left], value: 20)
}

demo

Haemostasis answered 11/9, 2021 at 14:25 Comment(1)
In fact, your solution does not work at all. Have you tried it on the RTL? See this example. The order of the HStack children swapped with RTL.Estovers
S
13

For me it was

VStack(alignment: .center) {
                    Text("Car")
                        .font(.body)
                        .foregroundColor(.black)
                }
                .padding(.trailing, 5)
Selfconfidence answered 8/2, 2022 at 20:50 Comment(0)
S
11

Use @Environment(\.layoutDirection) to get the current layout direction (LTR or RTL) and use it to flip .leading and .trailing as needed.

Here’s a ViewModifier that wraps all that conveniently:

enum NoFlipEdge {
    case left, right
}

struct NoFlipPadding: ViewModifier {
    let edge: NoFlipEdge
    let length: CGFloat?
    @Environment(\.layoutDirection) var layoutDirection
    
    private var computedEdge: Edge.Set {
        if layoutDirection == .rightToLeft {
            return edge == .left ? .trailing : .leading
        } else {
            return edge == .left ? .leading : .trailing
        }
    }
    
    func body(content: Content) -> some View {
        content
            .padding(computedEdge, length)
    }
}

extension View {
    func padding(_ edge: NoFlipEdge, _ length: CGFloat? = nil) -> some View {
        self.modifier(NoFlipPadding(edge: edge, length: length))
    }
}

Use it like you would the standard padding modifiers:

Text("Text")
    .padding(.left, 10)
Sequestrate answered 11/9, 2021 at 17:24 Comment(4)
The layoutDirection is read-write env value, so let's imagine what happens if someone in parent view will need to change it intentionally to value opposite to current system one.Haemostasis
@Haemostasis If you’re concerned the layoutDirection will be overridden, you could check this global setting instead of the environment value: UIApplication.shared.userInterfaceLayoutDirection == .rightToLeftSequestrate
@Haemostasis actually padding(.leading) depends on @Environment(\.layoutDirection), so if you change it - this modifier still gonna give the correct padding.Estovers
@Philip You’re right, I misread @Asperi’s question. I’m solution should work regardless.Sequestrate
N
10
struct ContentView: View {
var body: some View {
    Text("Hello, SwiftUI!")
        .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)) // Apply custom padding values
}

}

Nevernever answered 26/6, 2023 at 8:15 Comment(1)
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?Hochheimer

© 2022 - 2024 — McMap. All rights reserved.