Search code examples
iosswiftuser-interfaceswiftuispacing

Incorrect Spacing in SwiftUI View


Xcode 11.2.1, Swift 5

I have a custom view in SwiftUI and I have been fiddling around with it for a while to try and get rid of the extra space being added. I cannot tell if this is a bug in SwiftUI itself or if I am doing something wrong.

View:

struct Field: View {
    @Binding var text: String
    var title: String
    var placeholderText: String
    var leftIcon: Image?
    var rightIcon: Image?
    var onEditingChanged: (Bool) -> Void = { _ in }
    var onCommit: () -> Void = { }

    private let height: CGFloat = 47
    private let iconWidth: CGFloat = 16
    private let iconHeight: CGFloat = 16


    init(text: Binding<String>, title: String, placeholder: String, leftIcon: Image? = nil, rightIcon: Image? = nil, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = { }) {
        self._text = text
        self.title = title
        self.placeholderText = placeholder
        self.leftIcon = leftIcon
        self.rightIcon = rightIcon
        self.onEditingChanged = onEditingChanged
        self.onCommit = onCommit
    }

    var body: some View {

        VStack(alignment: .leading, spacing: 3) {
            Text(title.uppercased())
                .font(.caption)
                .fontWeight(.medium)
                .foregroundColor(.primary)
                .blendMode(.overlay)

            Rectangle()
                .fill(Color(red: 0.173, green: 0.173, blue: 0.180))
                .blendMode(.overlay)
                .cornerRadius(9)
                .frame(height: height)
                .overlay(
                    HStack(spacing: 16) {
                        if leftIcon != nil {
                            leftIcon!
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(width: iconWidth, height: iconHeight)
                                .foregroundColor(.secondary)
                        }
                        ZStack(alignment: .leading) {
                            if text.isEmpty {
                                Text(placeholderText)
                                    .foregroundColor(.secondary)
                                    .lineLimit(1)
                                    .truncationMode(.tail)
                            }
                            TextField("", text: $text, onEditingChanged: onEditingChanged, onCommit: onCommit)
                                .foregroundColor(.white)
                                .lineLimit(1)
                                .truncationMode(.tail)
                        }

                        Spacer()

                        if rightIcon != nil {
                            rightIcon!
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(width: iconWidth, height: iconHeight)
                                .foregroundColor(.secondary)
                        }


                    }
                    .padding(.horizontal, 16)
            )
        }
    }
}

When I create an instance of it like so:

struct ContentView: View {
    @State var text = ""

    let backgroundGradient = Gradient(colors: [
        Color(red: 0.082, green: 0.133, blue: 0.255),
        Color(red: 0.227, green: 0.110, blue: 0.357)
    ])

    var body: some View {
        ZStack {
            LinearGradient(gradient: backgroundGradient, startPoint: .top, endPoint: .bottom)
                .edgesIgnoringSafeArea(.all)
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)

            Field(text: $text, title: "field", placeholder: "Required", leftIcon: Image(systemName: "square.and.pencil"), rightIcon: Image(systemName: "info.circle"))
                .padding(.horizontal, 30)

        }
        .onAppear {
            UIApplication.shared.windows.forEach { window in
                window.overrideUserInterfaceStyle = .dark
            }
        }
    }
}

At first glance, it seems to behave as expected: Empty Field

The problem appears after the user tries typing in the field. There is too much spacing between the right icon and the text, causing it to be truncated earlier than necessary.

Field with Truncated Text

As you can see, there is a much larger amount of spacing between the text field and the right icon (excess spacing) than there is between the text field and the left icon (correct spacing).

All elements of Field should have a spacing of 16 on all sides. For some reason, the TextField is not taking up enough space and thus its spacing from the right icon is less than the desired 16.

How can I fix this spacing?


Solution

  • You have the following:

    Spacer()
    

    before the following section of code:

    if rightIcon != nil {
        rightIcon!
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: iconWidth, height: iconHeight)
            .foregroundColor(.secondary)
    }
    

    Removing the Spacer() will give you the same pixels between the left icon and the right icon.

    enter image description here