Konstantin.Efimenko
Konstantin.Efimenko

Reputation: 1450

Make TextField wrap it content in SwiftUI

I'm trying to implement such component

enter image description here

TextField (5 000) and Text (PLN) together should be centered horizontally. On entering new digit's, Text (PLN) should be dismissed. I think I have to combine this two views in to one container and center in, something like

HStack {
   TextField()
   Text("PLN")
}
.frame(alignment: .center)

But TextField is taking all possible width.

How could I handle it, or probably another solution.

Upvotes: 4

Views: 3017

Answers (3)

Chris So
Chris So

Reputation: 833

We can also prevent TextField being excessed to the container by fixing the maxWidth like:

DemoCenterTextField.swift

struct DemoCenterTextField: View {
    @State private var leadingContentWidth = CGFloat.zero
    @State private var textFieldWidth = CGFloat.zero
    @State private var trailingContentWidth = CGFloat.zero
    @State private var componentWidth = CGFloat.zero
    @State private var value = ""

    private let minWidth = 80.0
    private let spacing = 8.0

    var body: some View {
        HStack(spacing: spacing) {
            HStack {
                Text("$")
            }
            .getWidth($leadingContentWidth) // 👈
            .background(.red.opacity(0.5))
            ZStack {
                Text(value)
                    .opacity(0)
                    .frame(maxWidth: max(minWidth, componentWidth - leadingContentWidth - trailingContentWidth - spacing * 2)) // 👈
                    .fixedSize()
                    .getWidth($textFieldWidth) // 👈

                TextField("#.#", text: $value)
                    .frame(minWidth: minWidth, idealWidth: max(minWidth, textFieldWidth)) // 👈
                    .fixedSize(horizontal: true, vertical: false)
                    .border(.red.opacity(0.5))
            }
            .font(.system(size: 40).bold())
            .printSize()

            HStack {
                Text("USD")
                Text("!!!!!!")
            }
            .getWidth($trailingContentWidth) // 👈
            .background(.red.opacity(0.5))
        }
        .frame(maxWidth: .infinity)
        .getWidth($componentWidth) // 👈
        .background(.red.opacity(0.2))
        .multilineTextAlignment(.center)
    }
}

enter image description here

Where .getWidth & .printSize() modifier are:

GetWidthModifier.Swift

import SwiftUI

public struct GetWidthModifier: ViewModifier {
    @Binding var width: CGFloat

    public func body(content: Content) -> some View {
        content
            .background(
                GeometryReader { proxy in
                    Color.clear
                        .task(id: proxy.size.width) {
                            $width.wrappedValue = max(proxy.size.width, 0)
                        }
                }
            )
    }
}

public extension View {
    func getWidth(_ width: Binding<CGFloat>) -> some View {
        modifier(GetWidthModifier(width: width))
    }
}

struct GetWidthModifierPreview: View {
    @State private var width1 = CGFloat.zero // 👈
    @State private var width2 = CGFloat.zero // 👈

    @State private var adjustableWidth = 202.0

    var body: some View {
        VStack(spacing: 16) {
            Text("Width: \(width1)")
                .background(.green)
                .getWidth($width1) // 👈

            Text("Stable Width: \(width2)")
                .frame(width: adjustableWidth)
                .background(.yellow)
                .getWidth($width2) // 👈

            Slider(value: $adjustableWidth, in: 0 ... 300)

            Text("In some cases, the values obtained through GeometryReader may cause us to fall into an endless loop, resulting in unstable views and performance losses")
        }
    }
}

#Preview {
    GetWidthModifierPreview()
}

PrintSizeModifier.swift

import SwiftUI

public struct PrintSizeModifier: ViewModifier {
    @State private var size = CGSize.zero

    private let alignment: Alignment
    init(alignment: Alignment) {
        self.alignment = alignment
    }

    public func body(content: Content) -> some View {
        content
            .getSize($size)
            .overlay(alignment: alignment) {
                Text("\(cleanFloatString(size.width))⨉\(cleanFloatString(size.height))")
                    .font(.caption)
                    .foregroundStyle(.white)
                    .background(.black.opacity(0.6))
            }
    }

    private func cleanFloatString(_ from: CGFloat) -> String {
        from.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", from) : String(format: "%.2f", from)
    }
}

public extension View {
    func printSize(_ alignment: Alignment = .topLeading) -> some View {
        modifier(PrintSizeModifier(alignment: alignment))
    }
}

struct PrintSizeModifierPreview: View {
    @State private var size = CGSize.zero // 👈

    @State private var adjustableHeight = 42.333333
    @State private var adjustableWidth = 203.0

    var body: some View {
        VStack(spacing: 16) {
            Text("Text Component")
                .padding(100)
                .background(.green)
                .printSize(.center) // 👈
                .printSize(.leading) // 👈
                .printSize(.trailing) // 👈
                .printSize(.top) // 👈
                .printSize(.bottom) // 👈
                .printSize(.topLeading) // 👈
                .printSize(.topTrailing) // 👈
                .printSize(.bottomLeading) // 👈
                .printSize(.bottomTrailing) // 👈

            Text("Text Component")
                .frame(width: adjustableWidth, height: adjustableHeight)
                .background(.yellow)
                .printSize() // 👈 default .topLeading

            Slider(value: $adjustableHeight, in: 0 ... 100)
            Slider(value: $adjustableWidth, in: 0 ... 300)
        }
        .padding(16)
    }
}

#Preview {
    PrintSizeModifierPreview()
}

Upvotes: 0

Asperi
Asperi

Reputation: 257493

Here is possible approach with dynamically sized TextField.

Note: helper rectReader is taken from this my post.

Tested with Xcode 11.4 / iOS 13.4 (black border is only for demo)

demo

struct DemoCenterTextField: View {
    @State private var value = ""
    @State private var frame = CGRect.zero

    var body: some View {
        return HStack(alignment: .bottom) {
            ZStack {
                Text(self.value).foregroundColor(Color.clear)
                    .fixedSize()
                    .background(rectReader($frame))

                TextField("#.#", text: $value)
                    .multilineTextAlignment(.trailing)
                    .frame(minWidth: 80, idealWidth: frame.width)
                    .fixedSize(horizontal: true, vertical: false)
                    .border(Color.black)      // << for demo only

            }.font(Font.system(size: 40).bold())

            Text("PLN")
        }
    }
}

Upvotes: 9

Kuhlemann
Kuhlemann

Reputation: 3396

You can make the framesize depending on your amount of characters in the TextField to achive what you want:

struct ContentView: View {
    @State private var textField = ""

    var body: some View {

        HStack {
            TextField("", text: $textField)
                .frame(width: CGFloat(textField.count+10)*5, height: nil)
                .textFieldStyle(RoundedBorderTextFieldStyle()) // Just to illustrate the field better.
                .multilineTextAlignment(.center)
            Text("PLN")
                .offset(x: -15, y:0)
        }
    }
}

Upvotes: 1

Related Questions