관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. 붙여넣기 글자 제한 UITextView 처리, 커서 이동 처리 방법 - UTF16 (이모지를 고려한 처리) 본문

iOS 응용 (swift)

[iOS - swift] 2. 붙여넣기 글자 제한 UITextView 처리, 커서 이동 처리 방법 - UTF16 (이모지를 고려한 처리)

jake-kim 2023. 9. 23. 01:17

1. 붙여넣기 글자 제한 UITextView 처리, 커서 이동 처리 방법 - 단순 텍스트

2. 붙여넣기 글자 제한 UITextView 처리, 커서 이동 처리 방법 - UTF16 (이모지를 고려한 처리)

3. 붙여넣기 글자 제한 UITextView 처리, 커서 이동 처리 방법 - isScrollEnabled=false인 상태에서 커서 위치로 스크롤링 방법(#caretRect(for:), #scrollToCursor)

문자열 처리 시 주의사항

  • UTF16를 사용하고 있으므로 🇰🇷 이모지의 크기는 4이며, 1이 아닌 것에 주의

  • "some string".count의 값은 단순히 글자 수이며, shouldChangeTextIn에서 현재 포커스를 구할 때 NSRange를 사용하는데 이 값 기준은 UTF16으로 글자의 크기 기준이므로 처리할 때 .count로 처리하면 안되므로 주의

(아래와 같이 UITextView에 텍스트를 입력하면 텍스트를 반영할지 아래 델리게이트에서 판단하는데, 현재 커서 위치를 나타내는 range 값은 String의 Count값이 아닌 UTF16의 크기 기준이므로 주의)

extension ViewController: UITextViewDelegate {
    func textView(
        _ textView: UITextView,
        shouldChangeTextIn range: NSRange,
        replacementText text: String
    ) -> Bool {
		// TODO ...
    }
}

UTF16 기준 예시

  • 이모지 같은 경우 UTF16기준으로 값 카운트가 다름
  • 이모지 입력 시, range.lowerBound 값을 출력하여 확인
extension ViewController: UITextViewDelegate {
    func textView(
        _ textView: UITextView,
        shouldChangeTextIn range: NSRange,
        replacementText text: String
    ) -> Bool {
        rangeLabel.text = "range.lowerBound: " + String(range.lowerBound)
        return true
    }
}

(이모지 입력 시 4씩 증가)

(1씩 이모지 입력 시 늘어나지 않고, 국기 이모지는 4씩 증가)

UTF16을 고려한 처리

  • UTF16을 고려하지 않으면 이모지 입력 시 range값이 일반 count와 다르므로 커서 위치가 이상하게 이동되는 현상 발생
  • 기대 결과
    • "abc{붙여넣기}def" -> "abc{초과한 만큼 잘린 문자열}def"
    • 커서가 정상 위치

(아래에서 구현할 모습)

utf16 고려한 구현 방법

  • 필요한 String extension 정의
extension String {
    var utf16Size: Int {
        utf16.count
    }
    
    func substring(from: Int, to: Int) -> String {
        guard from < count, to >= 0, to - from >= 0 else { return "" }
        let startIndex = index(startIndex, offsetBy: from)
        let endIndex = index(startIndex, offsetBy: to + 1)
        return String(self[startIndex ..< endIndex])
    }
    
    func inserted(string: String, utf16Index: Int) -> String {
        let startIndex = index(utf16Index: utf16Index)
        guard 0 <= count - startIndex else { return string }
        return String(prefix(startIndex)) + string + String(suffix(count - startIndex))
    }
    
    func index(utf16Index: Int) -> Int {
        var ret = 0
        var count = 0
        
        for (i, v) in enumerated() {
            guard count <= utf16Index else { break }
            count += v.utf16.count
            ret = i
        }
        return ret
    }
}
  • shouldChangesTextIn 델리게이트에서 관련 로직 구현
extension ViewController: UITextViewDelegate {
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    ...
    }
}
  • 사용자가 붙여넣기한지 체크하는 방법은 입력된 text의 크기가 1보다 크인지 확인하면 완료
let lastNSString = textView.text as NSString
let allText = lastNSString.replacingCharacters(in: range, with: text)

let overSize = allText.utf16Size <= maxCount
let isPasted = 1 < text.utf16Size
guard !overSize else {
    textCount = allText.utf16Size
    return true
}
guard isPasted else {
    textCount = textView.text?.utf16Size ?? 0
    return false
}
  • maxCount보다 utf16size가 작으면 붙여넣기 처리 작업 수행
  •  핵심은 substirng(from:to:)메소드에서는 utf16 카운트 값이 아닌 일반적인 String 카운트 값을 사용하므로 utf16기준 index를 일반 index로 변경후 처리
if textView.text.utf16Size < maxCount {
    let isLastCursor = range.lowerBound >= textView.text.utf16Size

    /// "abc{붙여넣기}def" -> "abc{초과한 만큼 잘린 문자열}def"
    let utf16Index = (maxCount - textView.text.utf16Size)
    let index = text.index(utf16Index: utf16Index)
    let appendingText = text.substring(from: 0, to: index - 1)
    textView.text = textView.text.inserted(string: appendingText, utf16Index: range.lowerBound)

    /// 커서
    let movingCursorPosition = isLastCursor ? maxCount : (range.lowerBound + appendingText.utf16Size)
    let selectedRange = NSMakeRange(movingCursorPosition, 0)
    DispatchQueue.main.async {
        textView.selectedRange = selectedRange
    }
} else {
    /// 이전에 입력된 문자열이 maxCount 넘을 때, 붙여넣기 시도한 경우 > 새로운 문자열이 입력되 않지만 커서가 뒤로 이동 > 다시 이전 위치로 커서 이동
    let selectedRange = NSMakeRange(range.lowerBound, 0)
    DispatchQueue.main.async {
        textView.selectedRange = selectedRange
    }
}

textCount = textView.text.utf16Size
return false

결과

* 전체 코드: https://github.com/JK0369/ExLimitedStringUtf

* 참고

https://www.lipsum.com/

https://lettercounter.net/#google_vignette

Comments