Thường thấy trên form đăng ký: hiển thị số ký tự đã nhập / giới hạn (ví dụ: 140/280 ký tự tweet). Dùng UITextFieldDelegate method shouldChangeCharactersIn.
Triển khai cơ bản
Bước 1: Tạo dự án
Xcode → Single View App → Tên: "TextFieldCount".
Bước 2: Storyboard
Thêm UITextField + UILabel (hiển thị count) → @IBOutlet tên txtInputBox, lblCount.

Bước 3: ViewController.swift
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var txtInputBox: UITextField!
@IBOutlet weak var lblCount: UILabel!
let maxCharacters = 140 // Giới hạn ký tự
override func viewDidLoad() {
super.viewDidLoad()
txtInputBox.delegate = self
lblCount.text = "0/\(maxCharacters)"
}
// Delegate method: Gọi TRƯỚC KHI text thay đổi
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard textField == txtInputBox else { return true }
let currentText = textField.text ?? ""
let newText = (currentText as NSString).replacingCharacters(in: range, with: string)
let newLength = newText.count
// Cập nhật label
lblCount.text = "\(newLength)/\(maxCharacters)"
lblCount.textColor = newLength > maxCharacters ? .systemRed : .systemGray
// Cho phép hoặc chặn nhập
return newLength <= maxCharacters
}
}
Bước 4: Chạy app
Gõ text → Label cập nhật real-time. Vượt giới hạn → không gõ được, label đỏ.

Mở rộng: Cho phép paste nhưng cắt bớt nếu vượt limit
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard textField == txtInputBox else { return true }
let currentText = textField.text ?? ""
let newText = (currentText as NSString).replacingCharacters(in: range, with: string)
let newLength = newText.count
if newLength > maxCharacters {
// Cho phép paste nhưng tự cắt bớt
let allowedText = String(newText.prefix(maxCharacters))
textField.text = allowedText
lblCount.text = "\(maxCharacters)/\(maxCharacters)"
lblCount.textColor = .systemRed
// Gửi notification text did change (vì set text trực tiếp không trigger delegate)
NotificationCenter.default.post(name: UITextField.textDidChangeNotification, object: textField)
return false // Chặn paste gốc
}
lblCount.text = "\(newLength)/\(maxCharacters)"
lblCount.textColor = newLength > maxCharacters ? .systemRed : .systemGray
return true
}
Dùng cho UITextView (Text View)
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var countLabel: UILabel!
let maxChars = 500
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
}
func textView(_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText text: String) -> Bool {
let currentText = textView.text ?? ""
let newText = (currentText as NSString).replacingCharacters(in: range, with: text)
let newLength = newText.count
countLabel.text = "\(newLength)/\(maxChars)"
countLabel.textColor = newLength > maxChars ? .systemRed : .systemGray
return newLength <= maxChars
}
}
Modern Swift: Combine / SwiftUI
// Combine (UIKit)
textField.textPublisher
.map { $0?.count ?? 0 }
.map { "\($0)/\(maxCharacters)" }
.assign(to: \.text, on: lblCount)
.store(in: &cancellables)
// SwiftUI
TextField("Nhập nội dung", text: $text)
.onChange(of: text) { newValue in
if newValue.count > maxChars {
text = String(newValue.prefix(maxChars))
}
}
Text("\(text.count)/\(maxChars)")
.foregroundColor(text.count > maxChars ? .red : .gray)
Lưu ý
shouldChangeCharactersIngọi trước khi text thay đổi - tính toán dựa trênrange+replacementString- Emoji counts as 1 character trong Swift (
String.count= extended grapheme clusters) - Return
falseđể chặn thay đổi,trueđể cho phép - Cần set
textField.delegate = self(Storyboard hoặc code)