관리 메뉴

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

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

iOS 응용 (swift)

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

jake-kim 2023. 9. 19. 00:09

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

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

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

붙여넣기 글자 제한 로직

  • 123과 456 사이에 최대 글자 300을 넘는 문자열을 붙여넣는 경우
    • 요구사항1) 123과 456사이에 붙여넣기 적용, 300자가 넘는 문자열은 뒤에가 잘리도록 처리
    • 요구사항2) 붙여넣기 이후 커서 위치는 붙여넣은 문자열 바로 뒤에 위치

  • 요구사항3) 이미 300자가 초과하는 부분 중간에 붙여넣기 > 커서 위치가 제자리
    • 커스텀 처리를 안해주면, 디폴트는 붙여넣은 만큼 이동되므로 어색한 현상 발생

글자 제한 로직 구현에 앞서, 예제 UI 준비

  • 필요한 예제 UI 코드 준비
import UIKit

class ViewController: UIViewController {
    private let textView = {
        let view = UITextView()
        view.textColor = .black
        view.backgroundColor = .lightGray
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    private let label: UILabel = {
        let label = UILabel()
        label.text = "0/0"
        label.textColor = .blue
        label.font = .systemFont(ofSize: 24, weight: .regular)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let maxCount = 300
    private var textCount = 0 {
        didSet { label.text = "\(textCount)/\(maxCount)" }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textView.delegate = self
        
        view.addSubview(textView)
        view.addSubview(label)
        NSLayoutConstraint.activate([
            textView.heightAnchor.constraint(equalToConstant: 300),
            textView.widthAnchor.constraint(equalToConstant: 300),
            textView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            textView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
}
  • 문자열 처리에 사용될 substring, inserting 메소드 정의
extension String {
    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 inserting(_ string: String, at index: Int) -> String {
        var originalString = self
        originalString.insert(contentsOf: string, at: self.index(self.startIndex, offsetBy: index))
        return originalString
    }
}

글자 제한 로직 구현

  • 문자열 처리 - shouldChangeTextIn 델리게이트 메소드에서 구현
extension ViewController: UITextViewDelegate {
    func textView(
        _ textView: UITextView,
        shouldChangeTextIn range: NSRange,
        replacementText text: String
    ) -> Bool {
		// TODO ...
    }
}
  • 글자 제한 로직 아이디어
    • 제한 글자수를 넘게 입력된 경우 (붙여넣기로 1000자 입력한 경우): textView.text에 직접 값 입력해주고 false를 리턴
    • 제한 글자수를 넘지 않게 입력된 경우: 단순히 true 리턴 
  • 현재 입력한 텍스트(lastText), 현재 입력한 텍스트를 포함한 전체 텍스트(allText) 준비
    •  defer 에서 textCount에 업데이트하여 UILabel에 현재까지 입력된 정보 표출
let lastText = textView.text as NSString
let allText = lastText.replacingCharacters(in: range, with: text)

let canUseInput = allText.count <= maxCount

defer {
    if canUseInput {
        textCount = allText.count
    } else {
        textCount = textView.text.count
    }
}

// TODO...
  • canUseInput이 true이면 바로 입력되게하면 되고, false이면 textView.text에 직접 입력해줘야 하므로 guard문으로 처리
guard !canUseInput else { return canUseInput }

// TODO...
  • 분기문 추가
    • 현재까지 입력된 문자열이 maxCount를 넘지 않은 경우에는 문자열 붙여넣기 처리를 따로 해주는 부분
    • 현재까지 입력된 문자열이 maxCount를 이미 넘는 경우, 붙여넣기해도 커서가 제자리로 이동시키는 부분
if textView.text.count < maxCount {
    /// "abc{최대글자가 넘는 문자열 붙여넣기}def"
    /// 기대결과: "abc{문자열}def"
    
} else {
    /// 카운트 값을 넘었을때 중간 커서에서 붙여넣기 > 커서가 문자열만큼 뒤로 가는 버그 > 다시 커서 제자리로 위치시키는 코드
    
}

return canUseInput
  • 첫번째 if문을 처리
    • 1) 새로 추가할 문자열들을 최대 개수만큼 자름
    • 2) 현재 커서로부터 문자열을 추가
    • 3) 커서 이동: 현재 커서로부터 추가된 문자열의 길이만큼 더해줌
if textView.text.count < maxCount {
    /// "abc{최대글자가 넘는 문자열 붙여넣기}def"
    /// 기대결과: "abc{문자열}def"
    
    // 1) 새로 추가할 문자열들을 최대 개수만큼 자름
    let appendingText = text.substring(from: 0, to: maxCount - textView.text.count - 1)
    
    // 2) 현재 커서로부터 문자열을 추가
    textView.text = textView.text.inserting(appendingText, at: range.lowerBound)
    
    // 3) 커서 이동: 현재 커서로부터 추가된 문자열의 길이만큼 더해줌
    let isLastCursor = range.lowerBound >= textView.text.count
    let movingCursorPosition = isLastCursor ? maxCount : (range.lowerBound + appendingText.count)
    DispatchQueue.main.async {
        textView.selectedRange = NSMakeRange(movingCursorPosition, 0)
    }
} else {
    // TODO...
}
  • else부분 구현
    • 붙여넣기해도 커서가 제자리로 이동되어야하므로, 단순히 range.lowerBound로 위치
DispatchQueue.main.async {
    textView.selectedRange = NSMakeRange(range.lowerBound, 0)
}

(완료)

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

https://lettercounter.net/#google_vignette

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

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

* 참고

https://lettercounter.net/#google_vignette

Comments