-
UITextView를 SwiftUI에서 커스텀하게 사용하기SwiftUI 2023. 2. 17. 14:00
안녕하세요. 그린입니다🍏
이번 포스팅에서는 SwiftUI에서 텍스트뷰 구현을 위해 기본 제공하는 TextEditor를 사용하지 않고 UIKit 세상의 UITextView를 가져와 입맛대로 텍스트뷰를 커스텀하게 구현해보도록 하겠습니다🙌
왜 TextEditor 안쓰죠!?
우선 현재 기준 iOS 16에서 TextEditor가 분명 쓸만해진것은 사실입니다.
다만 최신 버전을 사용할 수 없는 환경에 처한 여러분들이 더 많을것 같고 저 역시 그렇습니다😭
14,15 버전 기준으로 간단히 짚어보면 우선 SwiftUI의 기본 제공되는 TextEditor에서는 우리가 흔히 사용하는 placeholder를 기본적으로 제공하지 않아 필요하면 overlay로 컨트롤 해줘야합니다.
또 텍스트가 입력되는 칸의 Inset을 조정할때도 모두 커스텀하게 구현해야합니다.
또..... 가변적인 높이를 위해서도 별도 작업을 더 거쳐야합니다.
즉 정리해보면 SwiftUI의 기본 TextEditor로 구현을 할 순 있다!
하지만 공수가 많이 들고 뷰 코드가 오히려 더 복잡해질 수 있다🥲그래서 이걸 해결해보고자 우리가 익숙한 UIKit의 UITextView를 끌어들여와 작업 해보겠습니다.
UITextView in SwiftUI
자 제가 필요한 구현은 아래와 같습니다.
1. 텍스트 라인 수에 따라 가변적인 텍스트 인풋창 높이 조절
2. 플레이스홀더 구현
3. 화면 진입 시 자동으로 키보드 노출 및 텍스트 필드 인풋 영역이 해당 키보드 위에 위치하기
4. 글자 수 제한
5. 그 외 디자인적 요소 커스텀하게 구현
이거 해보려합니다!
그럼 해당 커스텀 TextView 코드부터 보시죠!
import SwiftUI public struct CustomTextView: UIViewRepresentable { @Binding var text: String @Binding var height: CGFloat var maxHeight: CGFloat var textFont: UIFont var textColor: UIColor = .black var textLimit: Int = 10 var cornerRadius: CGFloat? = nil var borderWidth: CGFloat? = nil var borderColor: CGColor? = nil var isScrollEnabled: Bool = true var isEditable: Bool = true var isUserInteractionEnabled: Bool = true var lineFragmentPadding: CGFloat = 0 var textContainerInset: UIEdgeInsets = .init(top: 10, left: 10, bottom: 10, right: 10) var placeholder: String? = nil var placeholderColor: UIColor = .gray public func makeUIView(context: Context) -> UITextView { let textView = UITextView() if let cornerRadius = cornerRadius { textView.layer.cornerRadius = cornerRadius textView.layer.masksToBounds = true } if let borderWidth = borderWidth { textView.layer.borderWidth = borderWidth } if let borderColor = borderColor { textView.layer.borderColor = borderColor } if let placeholder = placeholder { textView.text = placeholder textView.textColor = placeholderColor } else { textView.textColor = textColor } textView.font = textFont textView.isScrollEnabled = isScrollEnabled textView.isEditable = isEditable textView.isUserInteractionEnabled = isUserInteractionEnabled textView.textContainer.lineFragmentPadding = lineFragmentPadding textView.textContainerInset = textContainerInset textView.delegate = context.coordinator textView.becomeFirstResponder() return textView } public func updateUIView(_ uiView: UITextView, context: Context) { updateHeight(uiView) } public func makeCoordinator() -> Coordinator { return Coordinator(parent: self) } private func updateHeight(_ uiView: UITextView) { let size = uiView.sizeThatFits(CGSize(width: uiView.frame.width, height: .infinity)) DispatchQueue.main.async { if size.height <= maxHeight { height = size.height } } } public class Coordinator: NSObject, UITextViewDelegate { var parent: CustomTextView init(parent: CustomTextView) { self.parent = parent } public func textViewDidChange(_ textView: UITextView) { parent.text = textView.text if textView.text.isEmpty { textView.textColor = parent.placeholderColor } else { textView.textColor = parent.textColor } if textView.text.count > parent.textLimit { textView.text.removeLast() } parent.updateHeight(textView) } public func textViewDidBeginEditing(_ textView: UITextView) { if textView.text == parent.placeholder { textView.text = "" } } public func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { textView.text = parent.placeholder } } } }
으악 좀 길긴한데... 커스텀한거니까⭐️
일단 UIViewRepresentable로 UIKit 세상에 있는 UITextView를 가져와줍니다.
각 파라미터로 들어갈 인자들은 생략하고~
updateHeight 메서드를 보시죠!
해당 메서드를 보시면 현재 UIView의 사이즈를 판단하며 maxHeight까지만 리밋을 걸며 가변 변환을 해줍니다.
그리고 updateUIView에서 해당 메서드를 호출해 계속 판단하죠.
그 다음 UITextViewDelegate의 기능을 가져와 필요한 구현을 해줍니다.
여기서 필요한 구현은 플레이스홀더 구분 및 글자 수 입력 제한 등과 같은 다양한 기능들을 넣어줄 수 있어요.
자 나머지는 거의 디자인적인 커스텀이니 패스하고 실제 해당 뷰를 얹어볼까요?
import SwiftUI struct ContentView: View { var body: some View { VStack(alignment: .leading, spacing: 10) { Spacer() .frame(height: 100) Button( action: { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }, label: { Text("Hide Keyboard") } ) Spacer() TextWritingView() .background(.gray) } } } private struct TextWritingView: View { @State private var height: CGFloat = 30 @State var text: String = "" private var registerButtonDisabled: Bool { text.isEmpty } private var registerButtonTextColor: Color { text.isEmpty ? .red : .green } var body: some View { HStack(alignment: .bottom, spacing: 10) { // 댓글 입력창 CustomTextView( text: $text, height: $height, maxHeight: 200, textFont: .boldSystemFont(ofSize: 14), cornerRadius: 5, borderWidth: 1, borderColor: CGColor.init(red: 255, green: 255, blue: 255, alpha: 1), placeholder: "댓글을 입력해 주세요" ) .frame(minHeight: height, maxHeight: .infinity) // 등록 버튼 Button( action: { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }, label: { Text("등록") .foregroundColor(registerButtonTextColor) } ) .disabled(registerButtonDisabled) .padding(.bottom, 5) } .padding(.all, 10) .frame(height: 50) .frame(minHeight: height + 20) } }
먼저 봐야할건 TextWritingView입니다.
커스텀한 TextView와 height를 공유하도록 @State 변수를 초기 높이를 주어 지정하고 text도 마찬가지로 지정합니다.
그 후 frame 작업만 잘 컨트롤 해주면 됩니다.
TextField의 최소 높이는 height이고 최대 높이에 대해서는 커스텀하게 구현한 텍스트뷰 내부에서 컨트롤 됩니다.
등록 버튼이나 특정한 onTapGesture를 했을때 키보드를 숨겨줄 수 있도록 구현해줍니다.
이러면 사실 끝이라 크게 설명보다 코드 보고 이해하는게 빠를것 같긴 하네요🥲
시연 영상
마무리
그냥 생활 속 발견 같은 느낌으로 가볍게 구현해보면서 공유해봐요🙌
[참고 자료]
제 아래 깃헙 레포에 위 예제 소스코드를 올려놨습니다.
https://github.com/GREENOVER/playground/tree/main/customTextView
'SwiftUI' 카테고리의 다른 글
AVPlayer in SwiftUI (feat. PIP) (6) 2023.03.23 UIKit과 SwiftUI에서 텍스트의 자간&행간 조절하기 (8) 2023.02.20 SwiftUI - View를 구조체로 생성하는 이유 (7) 2022.12.26 SwiftUI - View (3) 2022.12.24 SwiftUI - refreshable (0) 2022.12.08