Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Advanced)

10. UIViewRepresentable / Custom TextField

by Toughie 2023. 6. 25.

🦁UIViewRepresentable / Custom TextField🦁

SwiftUI가 등장하기 이전에는 UIKit을 활용했기 때문에(물론 지금도 활발히 사용되고 있지만) 

커스텀이나 다양한 기능면에서는 UIKit이 훨씬 자유로운 편이다. 

SwiftUI의 TextField만 봐도 placeholder에 대한 컬러변경조차 하지 못하니..

이와 같이 세부적인 커스텀이 필요한 것들은 UIKit 컴포넌트를 가져와서 활용할 수 있다.

이를 가능하게 하는 것이 바로 UIViewRepresentalble 프로토콜이다.

https://developer.apple.com/documentation/swiftui/uiviewrepresentable

 

UIViewRepresentable | Apple Developer Documentation

A wrapper for a UIKit view that you use to integrate that view into your SwiftUI view hierarchy.

developer.apple.com

SwiftUI 뷰 계층에 UIKit 뷰를 통합하기 위한 래퍼.

이 프로토콜을 준수하는 구조체,클래스를 정의하면 SwiftUI에서 UIView를 사용할 수 있다.

필수 요구조건은 아래와 같다.

1. makeUIView(context:) 메서드

UIView 인스턴스를 생성하고 초기화함.

SwiftUI가 UIView를 생성할 때 이 메서드를 호출, 생성된 UIView를 반환함.

2. updateUIView(_:context:) 메서드

SwiftUI가 UIView를 업데이트해야 할 때 호출됨.

새로운 데이터를 기반으로 UIView를 업데이트하고, 필요한 변경사항을 적용

 

*Context타입이란?

UIViewRepresentable에 대한 환경 정보를 제공.

UIViewRepresentable의 수명 주기와 관련된 작업에 사용됨.

 

(optional)

 

3. makeCoordinator() 메서드

updateUIView가 SwiftUI -> UIView라면, 이 메서드는 UIKit -> SwiftUI 역할로 이해하면 된다.

UIViewRepresentable과 SwiftUI 간의 상호작용을 관리하는 Coordinator를 생성한다.

Coordinator는 사용자 이벤트를 처리하고, UIKit과 SwiftUI 간의 데이터 전달을 돕는다.


TextField를 가지고 예시를 살펴보자.

import SwiftUI

struct UIViewRepresentablePrac: View {
    
    @State private var text: String = ""
    
    var body: some View {
        VStack {
            Text(text)
            HStack {
                Text("SwiftUI:")
                TextField("Type here", text: $text)
                    .frame(height: 55)
                    .background(Color.blue)
            }
        }
    }
}

SwiftUI 기본 텍스트필드는 위와 같이 사용할 수 있다. 타이틀과 바인딩 되어 있는 text.

하지만 여기서는 placeholder에 대한 커스텀을 제대로 할 수가 없다.

UIKit에도 TextField가 있지 않은가? UIKit의 TextField를 가져와서 써보자.

 

먼저 UIViewRepresentable 프로토콜을 채택한 구조체를 만들어 주자.

struct UITextFieldViewRepresentable: UIViewRepresentable {

...

makeUIView

메서드를 통해 텍스트필드 객체를 만들어준다.

    func makeUIView(context: Context) -> UITextField {
        let textField = getTextField()
        return textField
    }
    private func getTextField() -> UITextField {
        let textField = UITextField(frame: .zero)
        
        let placeholder = NSAttributedString(
            string: placeholder,
            attributes: [
                .foregroundColor: placeholderColor
            ])
        textField.attributedPlaceholder = placeholder
        return textField
    }

UIKit TextField에서 placeholder는 NSAttriutedString타입이다.

placeholder, placeholderColor는 각각 String, UIColor타입으로 self 상단에 따로 만들어 준다.

그리고 SwiftUI TextField에서 텍스트필드 텍스트를 바인딩 하는것처럼 바인딩 프로퍼티도 추가해준다.

struct UITextFieldViewRepresentable: UIViewRepresentable {
    
    var placeholder: String
    var placeholderColor: UIColor
    
    @Binding var text: String

updateUIView

SwiftUI의 변경 사항이 UIKit TextField와 연동되도록 updateUIView 메서드를 구현해준다.

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }

여기서 text는 @Binding var text


이렇게만 하면 

UIKit TextField의 text를 SwiftUI로 전달 할 수가 없다.

따라서 이 역할을 하는 Coordinator를 만들어준다.

UITextFieldViewRepresentable 내부에 class를 만들어준다.

보통 구조체 내부에 클래스를 만드는 경우는 잘 없지만.. 이 클래스는 여기서만 쓰일 것이기 때문에

중첩타입으로 구현한 것이다.

    class Coordinator: NSObject, UITextFieldDelegate {
    //NSObject, UITextFieldDelegate 프로토콜 둘 다 채택
        
        //데이터 전달을 위해 바인딩 처리
        @Binding var text: String
        
        init(text: Binding<String>) {
            self._text = text
        }
        
        //UIKit 텍스트 필드의 텍스트를 바인딩으로 전달
        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }
    }

그 다음 makeCoordinator 메서드도 구현해준다.

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

이제 텍스트필드의 delegate를 coordinator로 설정해준다.

    func makeUIView(context: Context) -> UITextField {
        let textField = getTextField()
        ⭐️textField.delegate = context.coordinator
        return textField
    }

초기화를 위해 생성자를 만들어 주자.

편의를 위해 placeholder와 컬러는 default parameter를 적용해 줬다.

struct UITextFieldViewRepresentable: UIViewRepresentable {
    
    @Binding var text: String
    var placeholder: String
    var placeholderColor: UIColor
    
    init(text: Binding<String>, placeholder: String = "Default placeholder", placeholderColor: UIColor = .white) {
        self._text = text
        self.placeholder = placeholder
        self.placeholderColor = placeholderColor
    }
    
 ...

손 쉽게 placeholder와 배경색을 변경할 수 있도록 아래와 같은 메서드도 추가할 수 있다.

    func updatePlaceholder(_ text: String) -> UITextFieldViewRepresentable {
        //여기서 self는 UITextFieldViewRepresentable 객체를 가리킴
        var viewRepresentable = self
        //내부 프로퍼티에 파라미터로 받아온 값을 할당
        viewRepresentable.placeholder = text
        return viewRepresentable
    }
    
    func updatePlaceholderColor(_ color: UIColor) -> UITextFieldViewRepresentable {
        var viewRepresentable = self
        viewRepresentable.placeholderColor = color
        return viewRepresentable
    }

사용 예시

        VStack {
            Text(text)
            UITextFieldViewRepresentable(text: $text)
                .updatePlaceholderColor(.red)
                .updatePlaceholder("UPDATE")
                .padding(.leading)
                .frame(height: 55)
                .background(Color.gray)
                .padding()
        }

Full Code

//  Created by Toughie on 2023/06/25.

import SwiftUI

struct UIViewRepresentablePrac: View {
    
    @State private var text: String = ""
    
    var body: some View {
        VStack {
            Text(text)
            UITextFieldViewRepresentable(text: $text)
                .updatePlaceholderColor(.red)
                .updatePlaceholder("UPDATE")
                .padding(.leading)
                .frame(height: 55)
                .background(Color.gray)
                .padding()
        }
    }
}

struct UITextFieldViewRepresentable: UIViewRepresentable {
    
    var placeholder: String
    var placeholderColor: UIColor
    
    @Binding var text: String
    
    init(text: Binding<String>, placeholder: String = "Default placeholder", placeholderColor: UIColor = .white) {
        self._text = text
        self.placeholder = placeholder
        self.placeholderColor = placeholderColor
    }
    
    func makeUIView(context: Context) -> UITextField {
        let textField = getTextField()
        textField.delegate = context.coordinator
        return textField
    }
    //SwiftUI -> UIKit
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
    }
    
    private func getTextField() -> UITextField {
        let textField = UITextField(frame: .zero)
        
        let placeholder = NSAttributedString(
            string: placeholder,
            attributes: [
                .foregroundColor: placeholderColor
            ])
        textField.attributedPlaceholder = placeholder
        return textField
    }
    
    func updatePlaceholder(_ text: String) -> UITextFieldViewRepresentable {
        var viewRepresentable = self
        viewRepresentable.placeholder = text
        return viewRepresentable
    }
    
    func updatePlaceholderColor(_ color: UIColor) -> UITextFieldViewRepresentable {
        var viewRepresentable = self
        viewRepresentable.placeholderColor = color
        return viewRepresentable
    }
    
    //UIKit -> SwiftUI
    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }
    
    class Coordinator: NSObject, UITextFieldDelegate {
        
        @Binding var text: String
        
        init(text: Binding<String>) {
            self._text = text
        }
        
        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }
    }
}