포미 프로젝트에서 사용되는 TextView UI는 아래 사진과 같이 placeholder와 글자 수 카운팅 기능이 포함되어 있다.
슬프게도 iOS에는 UITextView에 placeholder 기능이 없다.. 직접 구현해야 한다는 뜻이다^^
(이럴 때 그리워지는 안드로이드..)
이번 포스팅에서는 UITextView에 placeholder 기능과 글자 수 카운팅 기능에 대해서 작성해보도록 하겠다.
먼저 구글링을 통해서 placeholder와 글자수 카운팅 기능에 대한 조사 후, 구현에 최종적으로 사용한 메서드들은 아래와 같다.
여기서 2.a인 trimmingCharacters는 공식 문서 및 예제 코드를 통해 간단하게 어떤 기능인지 살펴보도록 하겠다.
👉🏻 Returns a new string made by removing from both ends of the receiver characters contained in a given character set.
trimmingCharacters 은 문자열 끝을 기준으로 in에 인자로 넘겨준 character set에 해당할 경우, 이를 제거하는 기능이다.
처음 trimmingCharacters 를 사용할 때 주어진 문자열 내에서 인자로 넘긴 character set을 모두 제거하는 것으로 생각했는데, 이는 공식 문서를 제대로 확인하지 않아 발생한 오해였다..ㅎ
아래는 in 인자로 공백&줄바꿈을 넘겼을 경우의 예시 코드이다.
@ 공식문서
@ placeholder 및 글자수 카운팅 기능 참고
[IOS][SWIFT] UITextView 글자 수 제한 및 글자 수 세는 Label 만들기!
[iOS - swift] TextView placeholder 적용 방법
먼저 공통 리소스인 TextView를 어떻게 구조화했는지 살펴보도록 하겠다.
RecordRegisterContentViewController
—RecordRegisterContentView
—CharactersCountTextView
—DefaultTextView
* 여기서 — 는 프로퍼티를 의미함. 내부 클래스가 아님
원래는 DefaultTextView에 카운팅 레이블도 추가해서 한꺼번에 구현하려고 했으나, UITextView에 view를 추가가 제대로 되지 않아서 따로 분리했다.
(카운팅 기능 없이 placeholder 기능만 존재하는 TextView가 나중에 사용될 수도 있으니 그냥 CharactersCountTextView라는 UIView를 만들어 따로 관리하기로 결정)
func updateCharactersCount(count: Int){
var countString = "\(count)/150"
if(count < 10){
countString = "0" + countString
}
charactersCountLabel.text = countString
}
func setTextViewTextEmptyMode(){
charactersCountLabel.textColor = UIColor(red: 173/255, green: 184/255, blue: 205/255, alpha: 1)
recordTextView.setEmptyMode()
updateCharactersCount(count: 0)
}
func setTextViewTextEditingMode(){
charactersCountLabel.textColor = Color.body
recordTextView.setEditingMode()
}
setEmptyMode와 setEditingMode는 각각 placeholder를 띄우는 경우와 띄우지 않는 경우에 대한 UI 변화를 주는 함수이다.
func setEmptyMode(){
self.text = CharactersCountTextView.placeholder
self.textColor = Color.grey5
}
func setEditingMode(){
self.text = nil
self.textColor = Color.body
}
func textViewDidBeginEditing(_ textView: UITextView) {
guard let contentView = textView.superview as? CharactersCountTextView else { return }
if textView.text == CharactersCountTextView.placeholder {
contentView.setTextViewTextEditingMode()
}
}
func textViewDidBeginEditing(_ textView: UITextView) {
guard let contentView = textView.superview as? CharactersCountTextView else { return }
if textView.text == CharactersCountTextView.placeholder {
contentView.setTextViewTextEditingMode()
}
}
func textViewDidChange(_ textView: UITextView){
guard let contentView = textView.superview as? CharactersCountTextView else { return }
contentView.updateCharactersCount(count: textView.text.count)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(textView.text.count < 150){
return true
}
guard let oldString = textView.text, let changedRange = Range(range, in: oldString) else { return false }
let newString = oldString.replacingCharacters(in: changedRange, with: text)
return newString.count <= 150
}
textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool 메서드를 통해 새로운 문자의 입력 여부를 결정하고, true를 반환하는 경우 textViewDidChange 를 통해 글자 수 카운팅이 업데이트된다.
현재 text의 글자 수가 150자 미만인 경우는 추가 코드 없이 바로 true를 반환해 입력한 글자 그대로 받아들인다.
하지만 150자인 경우부터는 delete인 경우와 추가 입력인 경우에 대해서 입력 여부를 결정해야 하므로
Range와 replacingCharacters 을 통해 새로 적용될 텍스트를 미리 점검하고, 유효성 여부에 따라 입력 여부를 결정하게 된다.
class DefaultTextView: UITextView{
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
style()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func style(){
self.setTypoStyleWithMultiLine(typoStyle: .body2)
self.backgroundColor = Color.transparent
}
func setEmptyMode(){
self.text = CharactersCountTextView.placeholder
self.textColor = Color.grey5
}
func setEditingMode(){
self.text = nil
self.textColor = Color.body
}
}
class CharactersCountTextView: BaseView{
static let placeholder = "소비에 대한 감상을 적어주세요 (150자)"
let charactersCountLabel = UILabel().then{
$0.setTypoStyleWithSingleLine(typoStyle: .body2)
}
let recordTextView = DefaultTextView()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func style() {
self.backgroundColor = Color.grey0
self.layer.cornerRadius = 6
setTextViewTextEmptyMode()
}
override func layout(){
...
}
func updateCharactersCount(count: Int){
var countString = "\\(count)/150"
if(count < 10){
countString = "0" + countString
}
charactersCountLabel.text = countString
}
func setTextViewTextEmptyMode(){
charactersCountLabel.textColor = UIColor(red: 173/255, green: 184/255, blue: 205/255, alpha: 1)
recordTextView.setEmptyMode()
updateCharactersCount(count: 0)
}
func setTextViewTextEditingMode(){
charactersCountLabel.textColor = Color.body
recordTextView.setEditingMode()
}
}
extension RecordRegisterContentViewController: UITextViewDelegate{
func textViewDidBeginEditing(_ textView: UITextView) {
guard let contentView = textView.superview as? CharactersCountTextView else { return }
if textView.text == CharactersCountTextView.placeholder {
contentView.setTextViewTextEditingMode()
}
}
func textViewDidEndEditing(_ textView: UITextView) {
guard let contentView = textView.superview as? CharactersCountTextView else { return }
if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
contentView.setTextViewTextEmptyMode()
}
}
func textViewDidChange(_ textView: UITextView){
guard let contentView = textView.superview as? CharactersCountTextView else { return }
contentView.updateCharactersCount(count: textView.text.count)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(textView.text.count < 150){
return true
}
guard let oldString = textView.text, let changedRange = Range(range, in: oldString) else { return false }
let newString = oldString.replacingCharacters(in: changedRange, with: text)
return newString.count <= 150
}
}
UITableView 상단 space 제거 (0) | 2023.01.03 |
---|---|
View의 tag 프로퍼티 활용하기 (0) | 2023.01.01 |
하위View touch event 부모View로 인식/전달 (0) | 2022.11.12 |
UITableView, UITableViewCell transparent (0) | 2022.11.08 |
UIStackView View 겹치기 (0) | 2022.11.07 |