Walidacja formularzy w SwiftUI, która sprawia wrażenie natywnej: fokus i błędy
Walidacja formularzy w SwiftUI, która wygląda natywnie: zarządzaj fokusowaniem, pokazuj błędy inline we właściwym momencie i wyświetlaj komunikaty serwera czytelnie, bez irytowania użytkowników.

Jak wygląda „natywna” walidacja w SwiftUI
Natywnie brzmiący formularz na iOS jest spokojny. Nie sprzecza się z użytkownikiem podczas pisania. Daje jasną informację zwrotną wtedy, gdy ma to znaczenie, i nie zmusza do szukania, co poszło nie tak.
Główne oczekiwanie to przewidywalność. Te same działania powinny dawać ten sam rodzaj informacji zwrotnej za każdym razem. Jeśli pole jest nieprawidłowe, formularz powinien to pokazać w konsekwentnym miejscu, w spójnym tonie i z jasnym kolejnym krokiem.
Większość formularzy potrzebuje trzech rodzajów reguł:
- Reguły pola: Czy pojedyncza wartość jest poprawna (pusta, format, długość)?
- Reguły między polami: Czy wartości do siebie pasują lub zależą od siebie (Hasło i Potwierdź hasło)?
- Reguły serwera: Czy backend to akceptuje (adres e-mail już używany, wymagana zaproszenie)?
Timing ma większe znaczenie niż wyszukane sformułowania. Dobra walidacja czeka na właściwy moment, a potem mówi raz, jasno. Praktyczny rytm wygląda tak:
- Milcz, gdy użytkownik wpisuje tekst, zwłaszcza dla reguł formatu.
- Pokaż informację po opuszczeniu pola lub po tapnięciu Wyślij.
- Trzymaj błędy widoczne, aż zostaną naprawione, potem usuń je natychmiast.
Walidacja powinna być cicha, dopóki użytkownik formułuje odpowiedź, jak przy wpisywaniu e-maila czy hasła. Pokazywanie błędu przy pierwszym znaku daje wrażenie „czepiania się”, nawet jeśli technicznie jest poprawne.
Walidacja powinna pojawić się, gdy użytkownik sygnalizuje, że skończył: fokus zmienia się na inny element albo próbuje wysłać formularz. To moment, kiedy chce wskazówek i kiedy możesz pomóc skierować go na konkretne pole wymagające uwagi.
Dobrze ustawiony timing ułatwia wszystko inne. Komunikaty inline mogą być krótsze, przesuwanie fokusu jest pomocne, a błędy po stronie serwera odbierane są jak normalna informacja, a nie kara.
Ustal prosty model stanu walidacji
Natywnie brzmiący formularz zaczyna się od jasnego rozdzielenia: tekst wpisany przez użytkownika nie jest tym samym co opinia aplikacji o tym tekście. Jeśli je wymieszasz, będziesz albo pokazywać błędy za wcześnie, albo zgubisz komunikaty serwera przy odświeżeniu UI.
Proste podejście to przypisać każdemu polu własny stan z czterema częściami: bieżąca wartość, czy użytkownik wchodził w interakcję, lokalny (na urządzeniu) błąd i błąd serwera (jeśli istnieje). UI wtedy decyduje, co pokazać na podstawie „touched” i „submitted”, zamiast reagować na każdy naciśnięty klawisz.
struct FieldState {
var value: String = ""
var touched: Bool = false
var localError: String? = nil
var serverError: String? = nil
// One source of truth for what the UI displays
func displayedError(submitted: Bool) -> String? {
guard touched || submitted else { return nil }
return localError ?? serverError
}
}
struct FormState {
var submitted: Bool = false
var email = FieldState()
var password = FieldState()
}
Kilka małych reguł trzyma to w przewidywalności:
- Trzymaj błędy lokalne i serwerowe oddzielnie. Lokalnych reguł (jak „wymagane” czy „nieprawidłowy e-mail”) nie powinny nadpisywać komunikatu serwera typu „e-mail już zajęty”.
- Wyczyść
serverError, gdy użytkownik edytuje dane pole ponownie, żeby nie utknął na starym komunikacie. - Ustaw
touched = truedopiero, gdy użytkownik opuści pole (lub gdy uznasz, że próbował wchodzić w interakcję), nie przy pierwszym wpisanym znaku.
Z tym modelem widok może swobodnie wiązać się z value. Walidacja aktualizuje localError, a warstwa API ustawia serverError, bez wzajemnego konfliktu.
Obsługa fokusu, która prowadzi, a nie irytuje
Dobra walidacja w SwiftUI powinna sprawiać wrażenie, że klawiatura systemowa pomaga użytkownikowi wykonać zadanie, a nie, że aplikacja go upomina. Fokus ma w tym dużą rolę.
Prosty wzorzec to traktować fokus jako pojedyncze źródło prawdy przy użyciu @FocusState. Zdefiniuj enum dla pól, powiąż każde pole z nim, a potem przechodź dalej, gdy użytkownik naciśnie przycisk na klawiaturze.
enum Field: Hashable { case email, password, confirm }
@FocusState private var focused: Field?
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.submitLabel(.next)
.focused($focused, equals: .email)
.onSubmit { focused = .password }
SecureField("Password", text: $password)
.submitLabel(.next)
.focused($focused, equals: .password)
.onSubmit { focused = .confirm }
To, co sprawia, że to wygląda natywnie, to powściągliwość. Przenoś fokus tylko przy jasnych akcjach użytkownika: Next, Done lub główny przycisk. Przy submitcie ustaw fokus na pierwszym nieprawidłowym polu (i przewiń do niego, jeśli trzeba). Nie odbieraj fokusu podczas pisania, nawet jeśli wartość jest aktualnie nieprawidłowa. Trzymaj też spójność etykiet klawiatury: Next dla pól pośrednich, Done dla ostatniego pola.
Częsty przykład to rejestracja. Użytkownik naciska Utwórz konto. Walidujesz raz, pokazujesz błędy, potem ustawiasz fokus na pierwszym niepoprawnym polu (często Email). Jeśli użytkownik jest w polu Hasło i wciąż pisze, nie przeskakuj nagle z powrotem do Email w trakcie wpisywania. Ta mała rzecz często decyduje, czy formularz wydaje się „dopieszczony” czy „irytujący”.
Błędy inline, które pojawiają się we właściwym czasie
Błędy inline powinny być cichą wskazówką, a nie reprymendą. Największa różnica między „natywnym” a „irytującym” to moment, w którym pokazujesz komunikat.
Reguły timingowe
Jeśli błąd pojawia się w chwili, gdy ktoś zaczyna pisać, przerywa to pracę. Lepsza reguła to: poczekaj, aż użytkownik będzie miał realną szansę dokończyć pole.
Dobre momenty na ujawnienie błędu inline:
- Po opuszczeniu pola
- Po tapnięciu Wyślij
- Po krótkiej pauzie w trakcie wpisywania (tylko dla oczywistych sprawdzeń, jak format e-maila)
Niezawodne podejście to pokazywać komunikat tylko wtedy, gdy pole zostało "touched" albo gdy próbowano wysłać. Nowy formularz pozostaje spokojny, ale użytkownik dostaje jasne wskazówki po podjęciu interakcji.
Układ i styl
Nic nie wygląda mniej po iOS-owemu niż skakanie układu, gdy pojawia się błąd. Zarezerwuj miejsce dla komunikatu, albo animuj jego pojawianie się, żeby nie przemieszczał nagle kolejnego pola.
Trzymaj tekst błędu krótki i konkretny, z jednym krokiem do naprawy na komunikat. „Hasło musi mieć co najmniej 8 znaków” jest wykonalne. „Nieprawidłowe dane” nie jest.
Jeśli chodzi o styl, celuj w subtelność i spójność. Mała czcionka pod polem (np. footnote), jeden spójny kolor błędu i delikatne podświetlenie pola zazwyczaj wyglądają lepiej niż ciężkie tła. Wyczyść komunikat od razu, gdy wartość staje się poprawna.
Realistyczny przykład: w formularzu rejestracji nie pokazuj „E-mail nieprawidłowy” podczas gdy użytkownik wpisuje name@. Pokaż go po opuszczeniu pola lub po krótkiej pauzie i usuń w momencie, gdy adres stanie się poprawny.
Lokalny przepływ walidacji: wpisywanie, opuszczenie pola, wysłanie
Dobry lokalny przepływ ma trzy prędkości: delikatne wskazówki podczas pisania, stanowcze sprawdzenia po opuszczeniu pola i pełne reguły przy wysyłaniu. Ten rytm sprawia, że walidacja wydaje się natywna.
Podczas wpisywania trzymaj walidację lekką i cichą. Myśl „to jest ewidentnie niemożliwe?” nie „czy to idealne?”. Dla pola e-mail możesz sprawdzać tylko, czy zawiera @ i nie ma spacji. Dla hasła możesz pokazać małą pomoc typu „8+ znaków”, gdy zaczynają pisać, ale unikaj czerwonych błędów przy pierwszym naciśnięciu klawisza.
Gdy użytkownik opuszcza pole, uruchom surowsze reguły pojedynczego pola i pokaż błędy inline, jeśli trzeba. Tutaj pasuje „Wymagane” i „Nieprawidłowy format”. To też dobry moment, żeby przyciąć spacje i znormalizować wejście (np. zamienić e-mail na małe litery), żeby użytkownik widział to, co zostanie wysłane.
Przy wysyłaniu zweryfikuj wszystko jeszcze raz, włącznie z regułami krzyżowymi, które wcześniej nie miały sensu. Klasyczny przykład to dopasowanie Hasła i Potwierdź hasło. Jeśli to nie przejdzie, ustaw fokus na polu, które trzeba poprawić i pokaż jeden jasny komunikat obok niego.
Uważnie obchodź się z przyciskiem zatwierdzania. Trzymaj go aktywnym, dopóki użytkownik wciąż wypełnia formularz. Wyłączaj tylko wtedy, gdy tapnięcie nic nie zrobi (np. podczas rzeczywistego wysyłania). Jeśli go wyłączasz z powodu niepoprawnych danych, pokaż obok konkretnie, co trzeba naprawić.
Podczas wysyłania pokaż czytelny stan ładowania. Zamień etykietę przycisku na ProgressView, zapobiegaj podwójnym naciśnięciom i trzymaj formularz widoczny, żeby użytkownik rozumiał, co się dzieje. Jeśli żądanie trwa dłużej niż sekundę, krótka etykieta typu „Tworzenie konta...” zmniejsza niepokój bez dodawania hałasu.
Walidacja po stronie serwera bez frustrowania użytkowników
Sprawdzenia po stronie serwera są ostatecznym źródłem prawdy, nawet jeśli lokalne reguły są silne. Hasło może przejść lokalne zasady, a jednak zostać odrzucone jako zbyt powszechne, albo e-mail może być już zajęty.
Największy zysk UX to rozdzielenie „Twoje dane są nieakceptowalne” od „nie udało się połączyć z serwerem”. Jeśli żądanie przekroczy czas lub użytkownik jest offline, nie oznaczaj pól jako nieprawidłowych. Pokaż spokojny baner lub alert typu „Nie udało się połączyć. Spróbuj ponownie.” i trzymaj formularz taki, jaki jest.
Gdy serwer zwraca, że walidacja nie przeszła, zachowaj wpisane dane i wskaż dokładne pola. Kasowanie formularza, czyszczenie hasła lub zabieranie fokusu sprawia, że użytkownicy czują się ukarani za próbę.
Prosty wzorzec to sparsować ustrukturyzowaną odpowiedź błędu na dwa koszyki: błędy pól i błędy poziomu formularza. Następnie zaktualizuj stan UI bez zmieniania powiązań tekstu.
struct ServerValidation: Decodable {
var fieldErrors: [String: String]
var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.
Co zwykle wydaje się natywne:
- Umieszczaj komunikaty pól inline, pod polem, używając słów serwera, gdy są jasne.
- Przenieś fokus do pierwszego pola z błędem tylko po próbie wysłania, nie w trakcie pisania.
- Jeśli serwer zwraca wiele problemów, pokaż pierwszą wiadomość na pole, żeby było czytelnie.
- Jeśli masz szczegóły pola, nie rezygnuj z nich na rzecz „Coś poszło nie tak.”
Przykład: użytkownik wysyła formularz rejestracji, a serwer zwraca „e-mail już używany”. Zachowaj wpisany e-mail, pokaż komunikat pod polem Email i ustaw na nim fokus. Jeśli serwer jest niedostępny, pokaż jedną wiadomość o ponowieniu i zostaw wszystkie pola bez zmian.
Jak wyświetlać komunikaty serwera we właściwym miejscu
Błędy serwera wydają się „niesprawiedliwe”, gdy pojawiają się w losowym banerze. Umieszczaj każdy komunikat jak najbliżej pola, które go spowodowało. Używaj komunikatu ogólnego tylko wtedy, gdy naprawdę nie można go przypisać do żadnego wejścia.
Zacznij od przetłumaczenia payloadu błędu serwera na identyfikatory pól SwiftUI. Backend może zwracać klucze jak email, password czy profile.phone, podczas gdy Twoje UI używa enuma jak Field.email i Field.password. Zrób mapowanie raz, zaraz po odpowiedzi, aby reszta widoku pozostała spójna.
Elastyczny model to trzymać serverFieldErrors: [Field: [String]] i serverFormErrors: [String]. Przechowuj tablice nawet jeśli zwykle pokazujesz jedną wiadomość. Gdy pokazujesz błąd inline, wybierz najprzydatniejszy komunikat jako pierwszy. Na przykład „E-mail już używany” jest bardziej pomocne niż „Nieprawidłowy e-mail”, jeśli obie wiadomości występują.
Wiele błędów dla pola jest powszechne, ale pokazanie wszystkich jest głośne. Zwykle pokaż tylko pierwszy komunikat inline i trzymaj resztę w widoku szczegółów, jeśli naprawdę jest potrzebny.
Dla błędów, których nie da się przypisać do pola (wygasła sesja, limit rate, „Spróbuj później”), umieść je blisko przycisku submit, żeby użytkownik zobaczył je dokładnie wtedy, gdy działa. Upewnij się też, że czyścisz stare błędy po sukcesie, aby UI nie wyglądało na „zablokowane”.
Na koniec: czyść błędy serwera, gdy użytkownik zmieni związane pole. W praktyce handler onChange dla email powinien usunąć serverFieldErrors[.email], żeby UI natychmiast pokazało: „Dobra, naprawiasz to.”
Dostępność i ton: małe wybory, które robią różnicę
Dobra walidacja to nie tylko logika. To także sposób, w jaki to czytamy, jak to brzmi i jak zachowuje się przy Dynamic Type, VoiceOver i różnych językach.
Ułatw czytanie błędów (nie tylko kolorem)
Zakładaj, że tekst może być duży. Używaj styli przyjaznych Dynamic Type (jak .font(.footnote) lub .font(.caption) bez stałych rozmiarów) i pozwól, aby etykiety błędów się zawijały. Trzymaj spójne odstępy, aby układ nie skakał za bardzo, gdy pojawi się błąd.
Nie polegaj tylko na czerwonym tekscie. Dodaj czytelną ikonę, prefiks „Błąd:” lub oba. To pomaga osobom z zaburzeniami widzenia kolorów i przyspiesza skanowanie.
Szybki zestaw kontroli, który zwykle się sprawdza:
- Użyj czytelnego stylu tekstu, który skaluje się z Dynamic Type.
- Pozwól na zawijanie i unikaj przycinania komunikatów błędów.
- Dodaj ikonę lub etykietę „Błąd:” razem z kolorem.
- Utrzymuj wysoki kontrast w trybie jasnym i ciemnym.
Spraw, by VoiceOver czytał właściwe rzeczy
Gdy pole jest nieprawidłowe, VoiceOver powinien przeczytać etykietę, bieżącą wartość i błąd razem. Jeśli błąd jest oddzielnym Text pod polem, może zostać pominięty lub przeczytany poza kontekstem.
Dwa wzorce pomagają:
- Połącz pole i jego błąd w jeden element dostępności, aby błąd był ogłaszany, gdy użytkownik ustawi fokus na polu.
- Ustaw accessibility hint lub value, które zawiera wiadomość o błędzie (np. "Hasło, wymagane, musi mieć co najmniej 8 znaków").
Ton również ma znaczenie. Pisz komunikaty jasne i łatwe do zlokalizowania. Unikaj slangu, żartów i niejasnych sformułowań typu „Ups”. Wybieraj konkretne wskazówki jak „Brakuje e-maila” lub „Hasło musi zawierać cyfrę”.
Przykład: formularz rejestracji z regułami lokalnymi i serwerowymi
Wyobraź sobie formularz rejestracji z trzema polami: Email, Hasło i Potwierdź hasło. Cel to formularz, który jest cichy podczas wpisywania, a potem pomocny, gdy użytkownik próbuje przejść dalej.
Kolejność fokusu (co robi Return)
Z SwiftUI FocusState każde naciśnięcie Return powinno być naturalnym krokiem:
- Return w Email: przechodzi do Hasła.
- Return w Haśle: przechodzi do Potwierdź hasło.
- Return w Potwierdź hasło: chowa klawiaturę i próbuje wysłać formularz.
- Jeśli wysyłanie się nie powiodło: fokus wraca do pierwszego pola wymagającego uwagi.
Ten ostatni krok ma znaczenie. Jeśli e-mail jest nieprawidłowy, fokus wraca do Email, a nie tylko do czerwonego komunikatu gdzieś indziej.
Kiedy pojawiają się błędy
Prosta reguła utrzymuje UI w spokoju: pokazuj komunikaty po dotknięciu pola (użytkownik je opuścił) lub po próbie wysłania.
- Email: pokaż „Wprowadź prawidłowy e-mail” po opuszczeniu pola lub przy wysyłaniu.
- Hasło: pokaż reguły (np. minimalna długość) po opuszczeniu pola lub przy wysyłaniu.
- Potwierdź hasło: pokaż „Hasła się nie zgadzają” po opuszczeniu pola lub przy wysyłaniu.
Teraz strona serwera. Załóżmy, że użytkownik wysyła formularz, a API zwraca coś takiego:
{
"errors": {
"email": "That email is already in use.",
"password": "Password is too weak. Try 10+ characters."
}
}
Co widzi użytkownik: Email pokazuje wiadomość serwera pod sobą, a Hasło pokazuje wiadomość pod sobą. Potwierdź hasło pozostaje ciche, chyba że lokalne sprawdzenie też nie przejdzie.
Co robi dalej: fokus trafia na Email (pierwszy błąd z serwera). Zmienia e-mail, naciska Return, przechodzi do Hasła, poprawia hasło i wysyła ponownie. Ponieważ komunikaty są inline, a fokus idzie zgodnie z intencją, formularz wydaje się współpracujący, a nie karzący.
Częste pułapki, które robią walidację „nie-iOSową”
Formularz może być technicznie poprawny, a mimo to źle się zachowywać. Większość problemów „nie-iOSowych” sprowadza się do timingu: kiedy pokazujesz błąd, kiedy przesuwasz fokus i jak reagujesz na serwer.
Częsty błąd to mówienie za wcześnie. Jeśli pokażesz błąd przy pierwszym znaku, ludzie poczują się poprawiani w trakcie pisania. Czekanie do opuszczenia pola lub próby wysłania zwykle to naprawia.
Asynchroniczne odpowiedzi serwera też mogą złamać przepływ. Jeśli odpowiedź zwróci i nagle przeskoczysz fokus do innego pola, to wyda się losowe. Trzymaj fokus tam, gdzie użytkownik ostatnio był, i przenoś go tylko wtedy, gdy użytkownik tapnął Next albo gdy obsługujesz próbę submitu.
Inna pułapka to czyszczenie wszystkiego przy każdej edycji. Usuwanie wszystkich błędów przy każdej zmianie znaku może ukryć rzeczywisty problem, szczególnie z komunikatami serwera. Czyść tylko błąd pola, które jest edytowane, i trzymaj pozostałe, dopóki naprawdę nie zostaną naprawione.
Unikaj „cichego” przycisku submit. Wyłączenie Submit na zawsze bez wyjaśnienia, co poprawić, zmusza użytkowników do zgadywania. Jeśli go wyłączasz, połącz to ze specyficznymi wskazówkami albo pozwól na submit, a potem poprowadź do pierwszego problemu.
Wolne żądania i podwójne tapnięcia są łatwe do przeoczenia. Jeśli nie pokazujesz postępu i nie blokujesz podwójnych submitów, użytkownicy stukną dwa razy, dostaną dwie odpowiedzi i skończą z mylącymi błędami.
Szybkie sprawdzenie sanity:
- Opóźnij błędy do blur albo submitu, nie do pierwszego znaku.
- Nie przesuwaj fokusu po odpowiedzi serwera, chyba że użytkownik tego chce.
- Czyść błędy per pole, nie wszystko naraz.
- Wyjaśnij, dlaczego submit jest zablokowany (albo pozwól wysłać i poprowadź).
- Pokaż ładowanie i ignoruj dodatkowe tapnięcia podczas oczekiwania.
Przykład: jeśli serwer mówi „e-mail już używany”, zachowaj komunikat pod Email, nie dotykaj Hasła i pozwól użytkownikowi edytować Email bez restartowania całego formularza.
Szybka lista kontrolna i kolejne kroki
Natywnie brzmiące doświadczenie walidacji to w dużej mierze kwestia timingu i powściągliwości. Możesz mieć surowe reguły i jednocześnie sprawić, by ekran był spokojny.
Przed publikacją sprawdź:
- Waliduj we właściwym czasie. Nie pokazuj błędów przy pierwszym znaku, chyba że jest to naprawdę pomocne.
- Przenoś fokus z sensem. Przy submitcie przejdź do pierwszego nieprawidłowego pola i wyraźnie wskaż, co jest nie tak.
- Trzymaj sformułowania krótkie i konkretne. Powiedz, co zrobić dalej, nie tylko co użytkownik zrobił „źle”.
- Szanuj ładowanie i ponawianie. Wyłącz przycisk submit podczas wysyłania i zachowaj wpisane wartości, jeśli żądanie się nie powiedzie.
- Traktuj błędy serwera jako informacje o polu, gdy to możliwe. Mapuj kody serwera na pola i używaj wiadomości globalnej tylko dla naprawdę ogólnych problemów.
Następnie testuj to jak prawdziwy człowiek. Trzymaj telefon jedną ręką i spróbuj wypełnić formularz kciukiem. Potem włącz VoiceOver i upewnij się, że kolejność fokusu, ogłoszenia błędów i etykiety przycisków nadal mają sens.
Dla debugowania i wsparcia warto logować kody walidacji serwera (nie surowe wiadomości) obok ekranu i nazwy pola. Gdy użytkownik powie „nie mogę się zarejestrować”, szybko sprawdzisz, czy to był email_taken, weak_password czy timeout sieci.
Aby zachować to spójne w całej aplikacji, ustandaryzuj model pola (value, touched, local error, server error), miejsce wyświetlania błędów i reguły fokusu. Jeśli chcesz budować natywne formularze iOS szybciej, bez ręcznego kodowania każdego ekranu, AppMaster (appmaster.io) może wygenerować aplikacje SwiftUI razem z usługami backendowymi, co ułatwia utrzymanie zgodności reguł walidacji po stronie klienta i serwera.


