16 thg 12, 2025·8 phút đọc

Xác thực biểu mẫu SwiftUI có cảm giác như ứng dụng gốc: focus và lỗi

Xác thực biểu mẫu SwiftUI có cảm giác như ứng dụng gốc: xử lý focus, hiển thị lỗi nội tuyến đúng lúc và trình bày thông báo từ máy chủ rõ ràng mà không làm khó chịu người dùng.

Xác thực biểu mẫu SwiftUI có cảm giác như ứng dụng gốc: focus và lỗi

Giao diện xác thực “cảm giác gốc” trông như thế nào trong SwiftUI

Một biểu mẫu iOS có cảm giác gốc thì bình tĩnh. Nó không tranh cãi với người dùng khi họ đang gõ. Nó cung cấp phản hồi rõ ràng khi cần, và không bắt bạn phải tìm xem chỗ nào sai.

Kỳ vọng chính là tính dự đoán. Những hành động giống nhau nên dẫn đến phản hồi cùng kiểu mỗi lần. Nếu một trường không hợp lệ, biểu mẫu nên hiển thị ở cùng một chỗ, với giọng điệu nhất quán và bước tiếp theo rõ ràng.

Hầu hết các biểu mẫu cần ba kiểu quy tắc:

  • Quy tắc trường: Giá trị đơn lẻ này có hợp lệ không (rỗng, định dạng, độ dài)?
  • Quy tắc chéo trường: Các giá trị có khớp hoặc phụ thuộc lẫn nhau không (Password và Confirm Password)?
  • Quy tắc máy chủ: Backend có chấp nhận không (email đã dùng, cần invite)?

Thời điểm quan trọng hơn là diễn đạt khéo léo. Xác thực tốt chờ đợi lúc có ý nghĩa, rồi nói một lần, rõ ràng. Nhịp điệu thực tế như sau:

  • Im lặng khi người dùng đang gõ, đặc biệt với các quy tắc định dạng.
  • Hiển thị phản hồi sau khi rời khỏi trường, hoặc sau khi người dùng nhấn Submit.
  • Giữ lỗi hiển thị cho đến khi được sửa, rồi loại bỏ ngay lập tức.

Xác thực nên im lặng khi người dùng vẫn đang định hình câu trả lời, như khi gõ email hoặc mật khẩu. Hiển thị lỗi ngay ký tự đầu tiên sẽ khiến người dùng thấy bị phàn nàn, dù về mặt kỹ thuật có thể đúng.

Xác thực nên hiện ra khi người dùng ra dấu họ đã xong: focus chuyển đi, hoặc họ cố gửi. Đó là khoảnh khắc họ muốn được hướng dẫn, và lúc đó bạn có thể giúp họ đến đúng trường cần sửa.

Nắm đúng thời điểm thì mọi thứ khác sẽ dễ dàng hơn. Tin nhắn nội tuyến có thể ngắn, di chuyển focus sẽ hữu ích, và lỗi phía máy chủ sẽ giống phản hồi bình thường hơn là “hình phạt”.

Thiết lập mô hình trạng thái xác thực đơn giản

Một biểu mẫu có cảm giác gốc bắt đầu bằng sự tách bạch rõ ràng: văn bản người dùng gõ không phải cùng một thứ với ý kiến của app về văn bản đó. Nếu trộn chúng, bạn sẽ hoặc hiển thị lỗi quá sớm hoặc mất thông báo máy chủ khi UI làm mới.

Cách đơn giản là cho mỗi trường trạng thái riêng gồm bốn phần: giá trị hiện tại, liệu người dùng đã tương tác chưa, lỗi cục bộ (trên thiết bị), và lỗi từ máy chủ (nếu có). UI sau đó quyết định hiển thị dựa trên “touched” và “submitted”, thay vì phản ứng với mỗi lần gõ.

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()
}

Một vài quy tắc nhỏ giữ cho điều này dự đoán được:

  • Giữ lỗi cục bộ và lỗi máy chủ tách biệt. Các quy tắc cục bộ (như “bắt buộc” hoặc “email không hợp lệ”) không nên ghi đè thông điệp máy chủ như “email đã được dùng”.
  • Xóa serverError khi người dùng chỉnh sửa lại trường đó, để họ không bị mắc kẹt nhìn vào thông báo cũ.
  • Chỉ đặt touched = true khi người dùng rời khỏi trường (hoặc khi bạn quyết họ đã cố tương tác), không phải khi gõ ký tự đầu tiên.

Với cấu trúc này, view có thể bind thẳng vào value. Xác thực cập nhật localError, và tầng API đặt serverError, mà không đấu nhau.

Xử lý focus để hướng dẫn, không càm ràm

Xác thực SwiftUI tốt nên cảm giác như bàn phím hệ thống đang giúp người dùng hoàn thành nhiệm vụ, chứ không phải app đang mắng họ. Focus đóng vai trò lớn trong đó.

Mẫu đơn giản là coi focus như nguồn chân lý duy nhất bằng @FocusState. Định nghĩa một enum cho các trường của bạn, bind mỗi trường vào nó, rồi di chuyển tiến khi người dùng nhấn nút trên bàn phím.

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 }

Điều giữ cảm giác gốc là kiềm chế. Chỉ di chuyển focus trên những hành động rõ ràng của người dùng: nhấn Next, Done, hoặc nút chính. Khi submit, focus vào trường sai đầu tiên (và cuộn tới đó nếu cần). Đừng cướp focus trong lúc người dùng đang gõ, ngay cả khi giá trị hiện tại chưa hợp lệ. Cũng giữ nhất quán với nhãn bàn phím: Next cho trường trung gian, Done cho trường cuối.

Một ví dụ phổ biến là Đăng ký. Người dùng nhấn Create Account. Bạn xác thực một lần, hiển thị lỗi, rồi đặt focus vào trường sai đầu tiên (thường là Email). Nếu họ đang ở trường Password và vẫn đang gõ, đừng nhảy họ về Email giữa lúc nhập. Chi tiết nhỏ đó thường là khác biệt giữa “biểu mẫu iOS tinh chỉnh” và “biểu mẫu khó chịu”.

Lỗi nội tuyến xuất hiện đúng lúc

Lỗi nội tuyến nên giống gợi ý nhẹ, không phải lời quở trách. Sự khác biệt lớn nhất giữa “cảm giác gốc” và “khó chịu” là thời điểm bạn hiển thị thông báo.

Quy tắc thời điểm

Nếu một lỗi hiển thị ngay khi ai đó bắt đầu gõ, nó làm gián đoạn. Quy tắc tốt hơn là: chờ đến khi người dùng có cơ hội hợp lý để hoàn thành trường.

Những khoảnh khắc phù hợp để hiện lỗi nội tuyến:

  • Sau khi trường mất focus
  • Sau khi người dùng nhấn Submit
  • Sau một khoảng dừng ngắn khi gõ (chỉ cho các kiểm tra rõ ràng, như định dạng email)

Một cách tiếp cận đáng tin cậy là chỉ hiển thị thông báo khi trường đã được touched hoặc khi đã có nỗ lực submit. Biểu mẫu mới sẽ bình tĩnh, nhưng người dùng vẫn nhận được hướng dẫn rõ ràng sau khi họ tương tác.

Bố cục và kiểu dáng

Không có gì kém iOS hơn layout nhảy khi một lỗi xuất hiện. Dành chỗ cho thông báo, hoặc animate cách nó xuất hiện để không đẩy trường tiếp theo xuống đột ngột.

Giữ văn bản lỗi ngắn và cụ thể, với một hướng sửa cho mỗi thông báo. “Mật khẩu phải có ít nhất 8 ký tự” thì hành động được. “Dữ liệu không hợp lệ” thì không.

Về kiểu, hướng tới tinh tế và nhất quán. Một font nhỏ dưới trường (như footnote), một màu lỗi nhất quán, và một highlight nhẹ trên trường thường dễ đọc hơn nền đậm. Xóa thông báo ngay khi giá trị trở nên hợp lệ.

Ví dụ thực tế: trên form signup, đừng hiển thị “Email không hợp lệ” khi người dùng vẫn đang gõ name@. Hiển thị sau khi họ rời trường, hoặc sau một khoảng dừng ngắn, và loại bỏ ngay khi địa chỉ hợp lệ.

Luồng xác thực cục bộ: gõ, rời trường, gửi

Chuẩn hóa mẫu xác thực
Tạo trạng thái lỗi và luồng gửi nhất quán mà không viết lại logic trên từng màn hình.
Xây dựng ngay

Luồng cục bộ tốt có ba tốc độ: gợi ý nhẹ khi gõ, kiểm tra chắc hơn khi rời trường, và quy tắc đầy đủ khi gửi. Nhịp điệu đó làm cho xác thực cảm giác gốc.

Khi người dùng gõ, giữ xác thực nhẹ nhàng và im lặng. Nghĩ “cái này có rõ ràng là không thể” chứ không phải “cái này hoàn hảo chưa?”. Với trường email, bạn có thể chỉ kiểm tra có @ và không có khoảng trắng. Với mật khẩu, bạn có thể hiển thị trợ giúp nhỏ như “8+ ký tự” khi họ bắt đầu gõ, nhưng tránh lỗi màu đỏ ngay ký tự đầu.

Khi người dùng rời trường, chạy các quy tắc chặt chẽ hơn chỉ cho trường đó và hiển thị lỗi nội tuyến nếu cần. Đây là nơi ghi “Bắt buộc” và “Định dạng không hợp lệ” thuộc về. Cũng là lúc tốt để trim whitespace và chuẩn hóa (ví dụ viết thường email) để người dùng thấy những gì sẽ được gửi.

Khi gửi, xác thực lại mọi thứ, bao gồm quy tắc chéo trường bạn không thể quyết trước. Ví dụ kinh điển là Password và Confirm Password phải khớp. Nếu thất bại, đặt focus vào trường cần sửa và hiển thị một thông báo rõ ràng gần đó.

Sử dụng nút submit cẩn thận. Giữ nó bật trong khi người dùng vẫn đang điền form. Chỉ vô hiệu hóa khi nhấn sẽ không làm gì (ví dụ đang gửi). Nếu bạn vô hiệu hóa vì input không hợp lệ, vẫn chỉ ra chỗ cần sửa gần đó.

Trong lúc gửi, hiển thị trạng thái loading rõ ràng. Thay nhãn nút bằng ProgressView, ngăn chặn nhấn đúp, và giữ form hiển thị để người dùng hiểu chuyện gì đang xảy ra. Nếu yêu cầu kéo dài hơn một giây, một nhãn ngắn như “Đang tạo tài khoản...” giảm lo lắng mà không thêm tiếng ồn.

Xác thực phía máy chủ mà không làm người dùng nản lòng

Kiểm tra phía máy chủ là nguồn chân lý cuối cùng, ngay cả khi kiểm tra cục bộ mạnh. Mật khẩu có thể vượt qua quy tắc của bạn nhưng bị từ chối vì quá phổ biến, hoặc email có thể đã bị dùng.

Lợi ích UX lớn nhất là tách “dữ liệu của bạn không chấp nhận được” khỏi “chúng tôi không thể kết nối tới máy chủ.” Nếu yêu cầu time out hoặc người dùng offline, đừng đánh dấu các trường là không hợp lệ. Hiển thị banner hoặc cảnh báo nhẹ như “Không thể kết nối. Thử lại.” và giữ nguyên form.

Khi máy chủ trả về lỗi xác thực, giữ nguyên dữ liệu người dùng và trỏ đến các trường chính xác. Xóa form, xóa mật khẩu, hoặc chuyển focus đi nơi khác làm người dùng cảm thấy bị phạt vì đã cố gắng.

Mẫu đơn giản là parse một phản hồi lỗi có cấu trúc thành hai rổ: lỗi theo trường và lỗi toàn form. Sau đó cập nhật trạng thái UI mà không thay đổi binding văn bản.

struct ServerValidation: Decodable {
  var fieldErrors: [String: String]
  var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.

Những thứ thường khiến cảm giác gốc:

  • Đặt thông điệp theo trường nội tuyến, ngay dưới trường, dùng cách diễn đạt của server khi rõ ràng.
  • Chuyển focus tới trường có lỗi đầu tiên chỉ sau submit, không khi đang gõ.
  • Nếu server trả nhiều vấn đề, hiển thị mỗi trường một thông báo đầu tiên để dễ đọc.
  • Nếu có chi tiết trường, đừng dùng fallback là “Có lỗi xảy ra.”

Ví dụ: người dùng gửi form signup, và server trả “email already in use.” Giữ email họ đã gõ, hiển thị thông báo dưới Email, và focus vào trường đó. Nếu server down, hiển thị một thông báo retry chung và để nguyên các trường.

Hiển thị thông báo máy chủ ở đúng chỗ

Triển khai khi bạn sẵn sàng
Triển khai backend và app được tạo lên hosting đám mây khi biểu mẫu sẵn sàng.
Ra mắt app

Lỗi máy chủ sẽ khiến người dùng thấy “không công bằng” khi xuất hiện trong một banner lạ. Đặt mỗi thông báo càng gần trường gây ra càng tốt. Dùng thông báo chung chỉ khi không thể gắn cho một input cụ thể.

Bắt đầu bằng việc chuyển payload lỗi của server sang các định danh trường SwiftUI của bạn. Backend có thể trả key như email, password, hoặc profile.phone, trong khi UI của bạn dùng enum như Field.emailField.password. Map một lần, ngay sau phản hồi, để phần còn lại của view giữ nhất quán.

Cách mô hình linh hoạt là giữ serverFieldErrors: [Field: [String]]serverFormErrors: [String]. Lưu mảng ngay cả khi bạn thường chỉ hiển thị một thông báo. Khi hiển thị lỗi nội tuyến, chọn thông báo hữu ích nhất trước. Ví dụ, “Email already in use” hữu ích hơn “Invalid email” nếu cả hai cùng xuất hiện.

Nhiều lỗi trên một trường là bình thường, nhưng hiển thị tất cả sẽ rối. Hầu hết thời gian, chỉ hiển thị thông báo đầu tiên nội tuyến và giữ phần còn lại cho view chi tiết nếu cần.

Với lỗi không gắn vào trường (session hết hạn, rate limit, “Thử lại sau”), đặt chúng gần nút submit để người dùng thấy ngay khi hành động. Cũng chắc chắn xóa lỗi cũ khi thành công để UI không trông như bị “kẹt”.

Cuối cùng, xóa lỗi máy chủ khi người dùng thay đổi trường liên quan. Thực tế, một handler onChange cho email nên xóa serverFieldErrors[.email] để UI phản ánh ngay: “Được rồi, bạn đang sửa.”

Truy cập và giọng điệu: các lựa chọn nhỏ tạo cảm giác gốc

Giữ luật ở một nơi
Sử dụng công cụ trực quan để định nghĩa quy tắc, rồi gửi chúng tới giao diện web và mobile.
Tạo app

Xác thực tốt không chỉ là logic. Nó còn là cách đọc, cách nghe, và hành vi với Dynamic Type, VoiceOver và các ngôn ngữ khác nhau.

Làm cho lỗi dễ đọc (không chỉ bằng màu)

Giả sử văn bản có thể lớn. Dùng style thân thiện Dynamic Type (như .font(.footnote) hoặc .font(.caption) mà không cố định kích thước), và cho phép nhãn lỗi xuống dòng. Giữ khoảng cách nhất quán để layout không nhảy quá nhiều khi lỗi xuất hiện.

Đừng chỉ dựa vào màu đỏ. Thêm icon rõ ràng, tiền tố “Lỗi:” hoặc cả hai. Điều này giúp người có vấn đề về phân biệt màu và làm cho việc quét nhanh hơn.

Một bộ kiểm tra nhanh thường hiệu quả:

  • Dùng style chữ dễ đọc và tỉ lệ với Dynamic Type.
  • Cho phép xuống dòng và tránh cắt chữ cho thông báo lỗi.
  • Thêm icon hoặc nhãn như “Lỗi:” cùng với màu.
  • Giữ độ tương phản cao cả Light Mode và Dark Mode.

Khi VoiceOver nên đọc đúng thứ

Khi một trường không hợp lệ, VoiceOver nên đọc nhãn, giá trị hiện tại, và lỗi cùng nhau. Nếu lỗi là một Text riêng bên dưới trường, nó có thể bị bỏ qua hoặc đọc rời ngữ cảnh.

Hai cách giúp:

  • Gom trường và lỗi vào một accessibility element, để lỗi được thông báo khi người dùng focus trường.
  • Đặt accessibility hint hoặc value bao gồm thông điệp lỗi (ví dụ: “Password, bắt buộc, phải có ít nhất 8 ký tự”).

Giọng điệu cũng quan trọng. Viết thông báo rõ ràng và dễ dịch. Tránh tiếng lóng, đùa cợt hoặc câu mơ hồ như “Ôi.” Ưu tiên hướng dẫn cụ thể như “Email bị thiếu” hoặc “Mật khẩu phải có số”.

Ví dụ: form đăng ký với cả quy tắc cục bộ và máy chủ

Hãy tưởng tượng form đăng ký gồm ba trường: Email, Password và Confirm Password. Mục tiêu là một form im lặng khi người dùng gõ, rồi hữu ích khi họ cố tiến lên.

Thứ tự focus (Return làm gì)

Với SwiftUI FocusState, mỗi lần nhấn Return nên cảm giác như một bước tự nhiên.

  • Email Return: chuyển focus tới Password.
  • Password Return: chuyển focus tới Confirm Password.
  • Confirm Password Return: ẩn bàn phím và thử Submit.
  • Nếu Submit thất bại: chuyển focus tới trường cần sửa đầu tiên.

Bước cuối cùng quan trọng. Nếu email không hợp lệ, focus phải trở về Email, không chỉ hiện một thông báo đỏ ở đâu đó.

Khi nào lỗi xuất hiện

Quy tắc đơn giản giữ UI bình tĩnh: hiển thị thông báo sau khi trường được touched (người dùng rời nó) hoặc sau khi đã cố submit.

  • Email: hiển thị “Nhập email hợp lệ” sau khi rời trường, hoặc khi Submit.
  • Password: hiển thị quy tắc (như độ dài tối thiểu) sau khi rời trường, hoặc khi Submit.
  • Confirm Password: hiển thị “Mật khẩu không khớp” sau khi rời trường, hoặc khi Submit.

Bây giờ phía server. Giả sử người dùng gửi và API trả như sau:

{
  "errors": {
    "email": "That email is already in use.",
    "password": "Password is too weak. Try 10+ characters."
  }
}

Những gì người dùng thấy: Email hiển thị thông báo server ngay dưới nó, và Password hiển thị thông báo dưới Password. Confirm Password giữ yên trừ khi nó cũng thất bại cục bộ.

Họ làm gì tiếp theo: focus nằm ở Email (lỗi server đầu tiên). Họ thay email, nhấn Return để tới Password, chỉnh mật khẩu, rồi gửi lại. Vì thông báo nội tuyến và focus di chuyển có mục đích, biểu mẫu cảm giác hợp tác chứ không quở trách.

Các bẫy phổ biến khiến xác thực trông “không phải iOS”

Tạo app và backend
Mô hình hóa người dùng và thông tin đăng nhập trong PostgreSQL, sau đó tạo app và backend cùng lúc.
Bắt đầu dự án

Một biểu mẫu có thể đúng về mặt kỹ thuật nhưng vẫn sai về cảm giác. Hầu hết vấn đề “không phải iOS” đến từ thời điểm: khi bạn hiển thị lỗi, khi bạn di chuyển focus, và cách bạn phản ứng với server.

Lỗi phổ biến là nói quá sớm. Nếu bạn hiển thị lỗi ngay ký tự đầu, người dùng cảm thấy bị mắng khi đang gõ. Chờ đến khi trường bị touched (rời nó, hoặc cố submit) thường giải quyết được.

Các phản hồi async từ server cũng có thể phá vỡ luồng. Nếu yêu cầu signup trả về và bạn đột nhiên nhảy focus, cảm giác sẽ ngẫu nhiên. Giữ focus ở chỗ người dùng ở lần cuối, và chỉ di chuyển khi họ nhấn Next hoặc khi bạn xử lý submit.

Một bẫy khác là xóa sạch khi mọi chỉnh sửa. Xóa tất cả lỗi ngay khi có bất kỳ ký tự nào thay đổi có thể che giấu vấn đề thực, đặc biệt với thông báo máy chủ. Chỉ xóa lỗi của trường đang chỉnh sửa, và giữ lại phần còn lại cho đến khi chúng thực sự được sửa.

Tránh nút submit “thất bại im lặng”. Vô hiệu hóa Submit mãi mà không giải thích tại sao ép người dùng đoán. Nếu bạn vô hiệu hóa, đi kèm với gợi ý cụ thể, hoặc cho phép submit rồi hướng dẫn họ tới vấn đề đầu tiên.

Các yêu cầu chậm và nhấn đúp dễ bị bỏ sót. Nếu bạn không hiển thị tiến trình và ngăn chặn nhấn đúp, người dùng sẽ nhấn hai lần, nhận hai phản hồi, và cuối cùng có lỗi khó hiểu.

Kiểm tra nhanh:

  • Hoãn lỗi tới khi blur hoặc submit, không phải ký tự đầu.
  • Đừng di chuyển focus sau phản hồi server trừ khi người dùng yêu cầu.
  • Xóa lỗi theo trường, không toàn bộ cùng lúc.
  • Giải thích tại sao submit bị chặn (hoặc cho phép submit và hướng dẫn).
  • Hiển thị loading và bỏ qua nhấn thêm trong khi chờ.

Ví dụ: nếu server trả “email already in use” (có thể từ backend bạn xây bằng AppMaster), giữ thông báo dưới Email, giữ Password yên, và để người dùng sửa Email mà không phải khởi động lại toàn bộ form.

Bảng kiểm nhanh và bước tiếp theo

Một trải nghiệm xác thực có cảm giác gốc phần lớn là về thời điểm và kiềm chế. Bạn có thể có quy tắc nghiêm ngặt mà vẫn giữ màn hình yên.

Trước khi phát hành, kiểm tra:

  • Xác thực vào đúng lúc. Đừng hiển thị lỗi ngay ký tự đầu trừ khi rõ ràng hữu ích.
  • Di chuyển focus có mục đích. Khi submit, nhảy tới trường sai đầu tiên và làm rõ chỗ sai.
  • Giữ cách diễn đạt ngắn và cụ thể. Nói phải làm gì tiếp theo, không chỉ “bạn đã làm sai”.
  • Tôn trọng loading và retry. Vô hiệu hóa nút submit khi gửi và giữ dữ liệu đã gõ nếu request thất bại.
  • Xử lý lỗi máy chủ như phản hồi theo trường khi có thể. Map code server tới trường, và dùng thông báo trên đầu chỉ cho vấn đề toàn cục.

Rồi test như người thật. Cầm điện thoại bằng một tay và cố hoàn thành form bằng ngón cái. Sau đó bật VoiceOver và đảm bảo thứ tự focus, thông báo lỗi và nhãn nút vẫn hợp lý.

Để tiện debug và hỗ trợ, nên log các mã xác thực server (không phải thông điệp thô) cùng màn hình và tên trường. Khi người dùng nói “không thể đăng ký”, bạn có thể nhanh chóng biết đó là email_taken, weak_password, hay timeout mạng.

Để giữ nhất quán trên toàn app, tiêu chuẩn hóa mô hình trường (value, touched, local error, server error), vị trí lỗi và quy tắc focus. Nếu bạn muốn tạo biểu mẫu iOS gốc nhanh hơn mà không viết tay từng màn hình, AppMaster (appmaster.io) có thể sinh SwiftUI apps cùng backend services, giúp dễ dàng giữ quy tắc client và server đồng bộ.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu