Use Combine with a SwiftUI TextField to react to text changes

This article condenses three small SwiftUI patterns: watch a TextField with Combine, trim user input to a maximum length, and drive live password-rule feedback from the current text value.

SwiftUI password field demo with live validation indicators

The central trick is simple: publish the current text value back into the view and react every time it changes.

This article uses Combine together with SwiftUI's TextField to solve a practical problem: run extra logic as the user types. Once that hook is in place, the same pattern can power length limits, live counters, and lightweight validation rules.

The article focuses on three examples:

Covered Here Watching a TextField, trimming the text to a maximum length, and showing live password requirement state from the same input stream.

Attach .onReceive(Just(messageText)) to the field so code runs every time the bound text changes.

The smallest example in the article imports both SwiftUI and Combine, binds the field to a @State string, then attaches an .onReceive modifier that republishes the current value through Just.

import SwiftUI
import Combine

struct ContentView: View {
    @State var messageText: String = ""

    var body: some View {
        Form {
            TextField("Message", text: $messageText)
                .onReceive(Just(messageText)) { _ in
                    // Put your text-change logic here.
                }
                .padding()
        }
    }
}

That is the whole technique. Once the closure is firing on each edit, the rest of the article just swaps different validation logic into that same location.

A character limit is just one extra branch: if the text is too long, replace it with its allowed prefix.

The second example uses the same .onReceive hook to cap the input at 30 characters and show a simple count underneath. When the user types past the limit, the value is reassigned to a truncated prefix.

import SwiftUI
import Combine

struct ContentView: View {
    @State var messageText: String = ""

    var body: some View {
        Form {
            TextField("Message", text: $messageText)
                .onReceive(Just(messageText)) { _ in
                    if messageText.count > 30 {
                        messageText = String(messageText.prefix(30))
                    }
                }
                .padding()

            Text(String(messageText.count))
        }
    }
}

It is a small pattern, but it is often enough for tweet-like fields, short titles, invite codes, or other bounded text inputs.

The same text stream can also drive live password rules with booleans, regex checks, and a small status row view.

The final section extends the same idea into a password-style form. The text change handler updates booleans for uppercase letters, symbols, and a length warning, then the UI renders those conditions with a tiny helper view.

import SwiftUI
import Combine

struct ContentView: View {
    @State var messageText: String = ""

    @State var hasCapitalLetter: Bool = false
    @State var hasSymbol: Bool = false
    @State var lengthExceeds16: Bool = false

    var body: some View {
        Form {
            TextField("Message", text: $messageText)
                .onReceive(Just(messageText)) { _ in
                    self.lengthExceeds16 = (messageText.count > 16)

                    let capitalLetterExpression = ".*[A-Z]+.*"
                    let capitalLetterCondition = NSPredicate(
                        format: "SELF MATCHES %@",
                        capitalLetterExpression
                    )
                    self.hasCapitalLetter = capitalLetterCondition.evaluate(with: messageText)

                    let symbolExpression = ".*[!&^%$#@()/]+.*"
                    let symbolCondition = NSPredicate(
                        format: "SELF MATCHES %@",
                        symbolExpression
                    )
                    self.hasSymbol = symbolCondition.evaluate(with: messageText)
                }
                .padding()

            Text(String(messageText.count))
                .keyboardType(.asciiCapable)
                .autocapitalization(.none)
                .disableAutocorrection(true)

            PasswordConditionDisplay(
                isConditionMet: $hasCapitalLetter,
                conditionName: "Capital letter"
            )
            PasswordConditionDisplay(
                isConditionMet: $hasSymbol,
                conditionName: "Symbol"
            )
            PasswordConditionDisplay(
                isConditionMet: $lengthExceeds16,
                conditionName: "Password exceeds 16 characters"
            )
        }
    }
}

struct PasswordConditionDisplay: View {
    @Binding var isConditionMet: Bool
    var conditionName: String

    var body: some View {
        HStack {
            Image(systemName: isConditionMet ? "circle.fill" : "xmark")
                .padding(.horizontal, 5)
            Text(conditionName)
        }
    }
}

This article also mentions checking for digits. In the posted sample, the digit regex only appears as a comment, so the fully implemented conditions are uppercase letters, symbols, and the 16-character threshold state.

Live password validation animation from this article
The demo shows the same input stream driving live rule indicators while the user types.

This is a useful early SwiftUI pattern, even though newer apps may reach for .onChange first.

The article was written in early 2021, and it reflects that moment in the SwiftUI API surface. The Combine-based approach is still valid, especially if the rest of the screen already leans on publishers, but it is best understood as a practical pattern from that era rather than the only way to observe text changes in modern SwiftUI.

Once the field can publish edits back into the view, validation logic becomes ordinary state updates.

That is the real point of the post. .onReceive(Just(text)) is not only a way to observe typing. It gives you a place to normalize, trim, validate, and annotate input while keeping the surrounding SwiftUI code small and direct.