Add placeholder for UITextView (phần 2)

by nguyenmanhhung131298
472 views

Hello everybody 🙂

Như chia sẻ ở phần trước thì tại phần này mình sẽ hướng dẫn các bạn một cách để có thể add placeholder cho UITextView. Hì hì, nói hướng dẫn thì hơi quá, mình chỉ muốn chia sẻ cho các bạn cách làm của mình khi gặp phải vấn đề như này mà thôi! 🙂

OK không dài dòng nữa chúng ta cùng bắt đầu nào!

Ứng dụng mà chúng ta sẽ tạo ra rất đơn giản và nó có tên là PlaceholderTextView (chả có ý nghĩa gì nhỉ 🙂 ). Sơ qua thì app này chỉ có một cái màn hình, bên trong nó có một UITextView để người dùng nhập message, một UIButton để gửi đi message (send message đi đâu thì mình sẽ không implement 🙂 ). Và mục đích chính của chúng ta đó là thêm placeholder cho thanh nhập message, chính là UITextView đó. OK, bước đầu tiên đó là hãy tạo ra UI giống như thế này:

Tạo UI đơn giản như này thì chắc ai cũng làm được nhỉ? Nhưng nếu bạn là người mới thì có thể xem code mẫu của mình ở đây nhé! Mình làm bằng storyboard chứ không code chay tốn nhiều time lắm hix hix.

Đã done phần UI, bây giờ mình sẽ giải thích chi tiết về cách mình add placeholder cho UITextView nhé! Mình sử dụng cách tạo extension của UITextView để thêm thuộc tính placeholder vào. Bạn có thể xem file extension UITextView của mình ở đây luôn! Ý tưởng của mình cũng rất là đơn giản thôi, khi đã thêm một thuộc tính placeholder cho UITextView rồi thì lúc đó chỉ cần set thuộc tính placeholder != nil và != empty là ta sẽ có được cái placeholder mà mình mong muốn, và khi text của UITextView là != empty thì placeholder phải mất đi còn khi text của UITextView = empty thì placeholder lại phải xuất hiện.

Mình sẽ giải thích từng thành phần một ở dưới đây:

Đầu tiên đây là thuộc tính placeholder mình thêm vào trong extension của UITextView, vì trong extension không thể thêm một stored property nên mới phải làm phức tạp ra như này đấy 🙁

fileprivate var placeholderKey = "placeholderKey"

var placeholder: String? {
    get {
        if let value = objc_getAssociatedObject(self, &placeholderKey) as? String {
            return value
        }
        return nil
    }
    set {
        objc_setAssociatedObject(self, &placeholderKey, newValue, .OBJC_ASSOCIATION_COPY)
        if newValue?.isEmpty ?? true {
            removePlaceholder()
            removeObserverText()
        }
        else {
            addPlaceholder(newValue!)
            addObserverText()
        }
    }
}

Với đoạn code trên thì khi ta set thuộc tính placeholder là không nil và không empty thì sẽ gọi hàm addPlaceholder(), đây là hàm sẽ làm công việc tạo ra một UILabel với text là argument được truyền vào sau đó sẽ được addSubview vào UITextView, chính UILabel đó sẽ là placeholder của UITextView. Cùng với việc gọi hàm addPlaceholder() thì cũng gọi hàm addObserverText(), đây là một hàm sẽ thêm một đối tượng observer sự kiện thay đổi text của UITextView và khi người dùng edit text trên UITextView, mục đích của việc này chính là để mình control việc add placeholder và remove placehodler cho UITextView. Ngược lại khi ta set thuộc tính placeholder là nil hoặc là empty thì sẽ gọi hàm removePlaceholder() và hàm removeObserverText() đây là những hàm dùng để remove đi UILabel mà mình đã thêm vào UITextView tại hàm addPlaceholder() và remove đối tượng observer mà mình đã thêm tại hàm addObserverText()

Dưới đây là hàm addPlaceholder() và hàm removePlaceholder()

func addPlaceholder(_ text: String) {
    placeholderLabel?.removeFromSuperview()
    placeholderLabel = UILabel()
    placeholderLabel?.text = text
    if let selfFont = self.font {
        placeholderLabel?.font = UIFont.systemFont(ofSize: selfFont.pointSize, weight: .thin)
    }
    else {
        placeholderLabel?.font = UIFont.systemFont(ofSize: 12.0, weight: .thin)
    }
    placeholderLabel?.textColor = .lightGray
    placeholderLabel?.numberOfLines = 0
    let padding = self.textContainer.lineFragmentPadding
    let estimateSize = placeholderLabel!.sizeThatFits(CGSize(width: self.frame.size.width - 2*padding, height: self.frame.size.height))
    placeholderLabel?.frame = CGRect(x: padding, y: self.textContainerInset.top, width: estimateSize.width, height: estimateSize.height)
    self.addSubview(placeholderLabel!)
}

func removePlaceholder() {
    placeholderLabel?.removeFromSuperview()
}

Ở 2 func trên thì mình có sử dụng thuộc tính placeholderLabel, đây là thuộc tính mà mình cũng đã thêm vào phần extension của UITextView. Lý do mình thêm thuộc tính này là bởi vì để việc remove placeholder trở nên dễ dàng hơn thay vì việc phải duyệt hết các subView của UITextView, kiểm tra subView nào đang là placeholder rồi mới remove đi.

Đây là phần implement cho thuộc tính placeholderLabel của mình

fileprivate var placeholderLabelKey = "placeholderLabelKey"

fileprivate var placeholderLabel: UILabel? {
    get {
        if let label = objc_getAssociatedObject(self, &placeholderLabelKey) as? UILabel {
            return label
        }
        return nil
    }
    set {
        objc_setAssociatedObject(self, &placeholderLabelKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

Dưới đây là hàm addObserverText() và hàm removeObserverText()

 fileprivate let observerKeyPath = "text"

 func addObserverText() {
     observerText = ObserverText()
     self.addObserver(observerText!, forKeyPath: observerKeyPath, options: .new, context: nil)
 }
    
 func removeObserverText() {
     if let observerText = observerText {
        self.removeObserver(observerText, forKeyPath: observerKeyPath)
     }
     observerText = nil
 }

Ở 2 func trên thì có thể thấy khi mình add observer thì cũng đã add observer sự kiện thay đổi thuộc tính text của UITextView (self ở trên chính là UITextView, những hàm này đều đặt trong extension của UITextView). À còn với thuộc tính observerText mình sử dụng ở trên thì cũng là một thuộc tính mình thêm vào extension của UITextView, implement của nó đây 🙂

fileprivate var observerTextKey = "observerTextKey"

fileprivate var observerText: ObserverText? {
    get {
         if let observer = objc_getAssociatedObject(self, &observerTextKey) as? ObserverText {
             return observer
         }
         return nil
     }
     set {
          objc_setAssociatedObject(self, &observerTextKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
     }
}

Class ObserverText mình sử dụng ở trên là một class mà mình tự tạo ra, nhiệm vụ của nó là xử lý hành động khi thuộc tính text của UITextView thay đổi và xử lý hành động khi người dùng edit text trên UITextView. Implement của nó ở đây:

fileprivate class ObserverText: NSObject {
    
    override init() {
        super.init()
        NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange(notification:)), name: UITextView.textDidChangeNotification, object: nil)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func textViewDidChange(notification: Notification) {
        if let textView = notification.object as? UITextView {
            if let text = textView.placeholder, textView.text.isEmpty {
                textView.addPlaceholder(text)
            }
            else {
                textView.removePlaceholder()
            }
        }
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let keyPath = keyPath, keyPath == observerKeyPath, let textView = object as? UITextView {
            if let newText = change?[NSKeyValueChangeKey.newKey] as? String, !newText.isEmpty {
                textView.removePlaceholder()
            }
            else {
                if let text = textView.placeholder {
                    textView.addPlaceholder(text)
                }
            }
        }
    }
    
}

Class này cũng khá là dễ hiểu phải không nào, chỉ là implement để bắt các sự kiện thôi mà 🙂.

OK 🙂 vậy là ở trên mình đã trình bày toàn bộ cách mà mình coding để có thể thêm placeholder cho UITextView. Bây giờ, khi sử dụng UITextView, bạn chỉ cần set thuộc tính placeholder là khác nil và khác empty là bạn sẽ có placeholder của UITextView rồi, và mỗi khi người dùng edit text trên UITextView thì nó cũng sẽ tự động thêm placeholder khi mà người dùng xoá tất cả các kí tự trên UITextView và cũng tự động remove placeholder khi người dùng nhập một kí tự nào đó lên UITextView. Giống như trong file gif này nè 🙂

Cảm ơn bạn đã dành thời gian để đọc bài viết này. Happy reading

Source code tham khảo

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.