SwiftUI

[SwiftUI] Custom Picker로 textField 입력값 받기

하이D:) 2023. 6. 12. 09:48

SwiftUI기반 앱을 만들면서 UIViewRepresentable를 써줘야하는 실질적 상황이 생겼다.

textField에서는 키보드 말고도 다양한 형태의 inputView에서 입력값을 받아줄 수 있어야한다. 나는 커스텀하여 만든 picker에서 선택한 값이 textField의 입력값으로 반영되는 기능을 구현하고싶었다. 하지만 SwiftUI에서는 UIKit에서 처럼 UITextField에서의 inputView를 따로 커스텀해줄 수 없었기에 UIViewRepresentable를 활용해서 UIKit 뷰를 만들어주어야했다.

 

UIViewRepresentable의 개념적 내용이 궁금하다면 아래 두 개의 포스팅이 도움이 될 것 이다.

 

https://heidi-dev.tistory.com/6

 

SwiftUI에서 UIKit 사용하기 UIViewRepresentable (1) | TextField에서 clear button 사용하고 싶다면

SwiftUI 기반인 앱을 구현하면서 UIKit에서는 지원하지만 SwiftUI에서는 지원하지 않는 뷰들이 아직 존재한다는 것을 알게 되었다. 그럴 때 UIKit에서 제공하는 뷰를 SwiftUI에서 사용할 수 있도록 래핑

heidi-dev.tistory.com

https://heidi-dev.tistory.com/7

 

SwiftUI에서 UIKit 사용하기 UIViewRepresentable(2) | @Binding 이란? Coordinator 란?

앞서 SwiftUI에서 UIKit를 사용하기 위해 UIViewRepresentable 프로토콜을 사용하는 방법을 간단히 알아보았다. 이제 여기서 궁금한 것은 SwiftUI와 UIKit 간에 데이터를 주고받고 변화를 감지할 수 있는가이

heidi-dev.tistory.com

# 🥨 기본적인 UITextField 생성

SwiftUI에서 입력값을 받아주어야하기 때문에 @State 로 변수를 설정해주었고 @Binding property 로 구조체에 값을 넘겨주었다. picker에 넣어주고 싶은 데이터들은 프로퍼티로 받아준다.

import SwiftUI

struct UIVIewRepresentablePracticeView: View {
    @State var picked : String = ""
    
    var body: some View {
        VStack{
            CustomPickerTextField(
                dataArrays :Array(0...100).map{String($0)},
                bindingString: self.$picked
            )
        }
        .padding()
        
    }
}
struct CustomPickerTextField : UIViewRepresentable {
    private let textField = UITextField()
    
    public var dataArrays : [String]
    public var placeholder: String = "입력하기"
    @Binding public var bindingString: String

    func makeUIView(context: UIViewRepresentableContext<CustomPickerTextField>) -> UITextField {  
        textField.placeholder = placeholder

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomPickerTextField>) {
    }

}

 

# 🥨 키보드 대신 custom picker를 inputView로서 띄우기

UITextField의 inputView로서 UIPickerView를 지정해주고, UITextField의 inputAccessoryView로서 UIToolbar를 지정해주면 텍스트필드를 눌렀을 때 아까 키보드가 나타난 것과는 다른 뷰를 확인할 수 있을것이다.

struct CustomPickerTextField : UIViewRepresentable {
    private let textField = UITextField()
    private let picker = UIPickerView()
    private let toolbar = UIToolbar()
    
    public var dataArrays : [String]
    public var placeholder: String = "입력하기"
    @Binding public var bindingString: String

    func makeUIView(context: UIViewRepresentableContext<CustomPickerTextField>) -> UITextField {
        textField.placeholder = placeholder
        textField.inputView = picker
        
        //툴바
        toolbar.sizeToFit()
        textField.inputAccessoryView = toolbar

        return textField
    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomPickerTextField>) {
    }

}

 

# 🥨 Picker에서 띄워줄 데이터 지정 및 뷰 생성

Coordinator 클래스에서 UIPickerViewDataSource, UIPickerViewDelegate 를 채택하고 내부메서드를 활용해서 Picker에서 띄워줄 데이터를 지정하고 뷰를 생성해준다. 또한, 툴바의 Done 버튼을 만들어주고 Done 버튼이 눌릴 때 실행할 로직을 설정해준다.

struct CustomPickerTextField : UIViewRepresentable {
    private let textField = UITextField()
    private let picker = UIPickerView()
    private let toolbar = UIToolbar()
    private let helper = Helper()
    
    public var dataArrays : [String]
    public var placeholder: String = "입력하기"
    @Binding public var bindingString: String

    
    class Helper {
        public var onDoneButtonTapped: (() -> Void)?
        @objc func doneButtonTapped() {
            onDoneButtonTapped?()
        }
    }

    //makeCoordinator()
    func makeCoordinator() -> CustomPickerTextField.Coordinator {
        CustomPickerTextField.Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<CustomPickerTextField>) -> UITextField {
        let defaultIndex : Int = 0
        
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator
        picker.selectRow(defaultIndex, inComponent: 0, animated: true)
        
        textField.placeholder = placeholder
        textField.inputView = picker
        
        //툴바
        toolbar.sizeToFit()
        let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: helper, action: #selector(helper.doneButtonTapped))
        toolbar.setItems([flexibleSpace, doneButton], animated: true)
        textField.inputAccessoryView = toolbar
        
        helper.onDoneButtonTapped = {
            textField.resignFirstResponder()
        }
        

        return textField

    }

    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomPickerTextField>) {
    }

    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var parent: CustomPickerTextField
        init(_ pickerView: CustomPickerTextField) {
            self.parent = pickerView
        }
        
        //Number Of Components
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
        
        //Number Of Rows In Component
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return parent.dataArrays.count
        }

        //Width for component
        func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
            return UIScreen.main.bounds.width
        }

        //Row height
        func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
            return 40
        }

        //View for Row
        func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
            let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width/4, height: 60))
            let pickerLabel = UILabel(frame: view.bounds)
            pickerLabel.text = parent.dataArrays[row]
            pickerLabel.adjustsFontSizeToFitWidth = true
            pickerLabel.textAlignment = .center
            pickerLabel.lineBreakMode = .byWordWrapping
            pickerLabel.numberOfLines = 0

            view.addSubview(pickerLabel)
            view.clipsToBounds = true

            return view
        }
        
    }
}

 

# 🥨 사용자 입력값 UITextField에 반영 및 Swiftui와 UIKit간에 데이터 주고받기

사용자가 선택한 값을 UITextField에 반영되게 해주고, Swiftui와 UIViewRepresentable로 래핑해준 UIKit간에 데이터를 전달하기위한 코드를 추가해야한다

 

func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomPickerTextField>) {
	//🌸 UITextField에 사용자가 입력한 값이 반영 될 수 있도록한다.
    uiView.text = bindingString
}
//🌸 사용자가 picker에서 입력값 선택했을 때마다 호출
// bindingString값을 사용자가 입력한 값으로 바인딩 시킨다. (UIKit -> SwiftUI 방향으로의 데이터 전달)
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
	parent.bindingString = parent.dataArrays[pickerView.selectedRow(inComponent: component)]
}

 

SwiftUI에서 변화된 입력값에 대한 데이터를 제대로 받고 있는지 알고싶다면 아래 코드를 활용해서 시뮬레이터를 실행시키고 xcode 터미널에서 확인해보면 데이터를 잘 받고 있는것을 확인할 수 있다 :)

import SwiftUI

struct UIVIewRepresentablePracticeView: View {
    
    @State var picked : String = ""
    
    var body: some View {
        VStack{
            CustomPickerTextField(
                dataArrays :Array(0...100).map{String($0)},
                bindingString: self.$picked
            )
            
            Text("@State 확인")
                .onTapGesture {
                    print("@State var picked : ", picked)
                }
        }
        .padding()
        
    }
}