Skip to content

Instantly share code, notes, and snippets.

@woozoobro
Last active October 19, 2023 02:53
Show Gist options
  • Select an option

  • Save woozoobro/efb001518be4c3534bf5c185d67fc8d5 to your computer and use it in GitHub Desktop.

Select an option

Save woozoobro/efb001518be4c3534bf5c185d67fc8d5 to your computer and use it in GitHub Desktop.
SwiftUI에서 UIViewRepresentable을 이용해 TextField에 Picker로 값을 변경할 수 있게 해주는 컴포넌트 입니다.
import SwiftUI

class CustomUITextField: UITextField {
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        if action == #selector(UIResponderStandardEditActions.paste(_:)) || action == #selector(UIResponderStandardEditActions.copy(_:)) || action == #selector(UIResponderStandardEditActions.cut(_:)) {
            return false
        }
        return super.canPerformAction(action, withSender: sender)
    }
}

struct DateTextField: UIViewRepresentable {
    @Binding var date: Date?
    
    var didChange: () -> Void = {}
    
    private var minimumYear: Int = 1990
    private var maximumYear: Int = Calendar.current.component(.year, from: Date())
    private var selectedYear: Int {
        get {
            return Calendar.current.component(.year, from: date ?? Date())
        }
        set {
            let newDate = Calendar.current.date(bySetting: .year, value: newValue, of: date ?? Date())
            if let newDate = newDate {
                date = newDate
            }
        }
    }
    private var selectedMonth: Int {
        get {
            return Calendar.current.component(.month, from: date ?? Date())
        }
        set {
            let newDate = Calendar.current.date(bySetting: .month, value: newValue, of: date ?? Date())
            if let newDate = newDate {
                date = newDate
            }
        }
    }
    
    private var placeholder: String? = ""
//    private var dateFormatter: DateFormatter = {
//        let formatter = DateFormatter()
//        formatter.dateFormat = "yyyy년 MM월"
//        return formatter
//    }()
        
//    private var foregroundColor: UIColor?
    private var accentColor: UIColor?
    private var textAlignment: NSTextAlignment?
    private var contentType: UITextContentType?
    
    private var autocorrection: UITextAutocorrectionType = .default
    private var autocapitalization: UITextAutocapitalizationType = .sentences
    private var keyboardType: UIKeyboardType = .default
    private var returnKeyType: UIReturnKeyType = .default
    
    private var isSecure: Bool = false
    private var isUserInteractionEnabled: Bool = true
    private var clearsOnBeginEditing: Bool = false
    
    @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
    
    init(date: Binding<Date?>, didChange: @escaping () -> Void = { }) {
        self._date = date
        self.didChange = didChange
    }
    
    func makeUIView(context: Context) -> UITextField {
        let textField = CustomUITextField()
        textField.delegate = context.coordinator
        textField.placeholder = placeholder
        textField.font = UIFont(name: "Pretendard-Medium", size: 18)
        textField.textColor = UIColor(Color.theme.title)
        if let textAlignment = textAlignment {
            textField.textAlignment = textAlignment
        }
        if let contentType = contentType {
            textField.textContentType = contentType
        }
        if let accentColor = accentColor {
            textField.tintColor = accentColor
        }
        textField.autocorrectionType = autocorrection
        textField.autocapitalizationType = autocapitalization
        textField.keyboardType = keyboardType
        textField.returnKeyType = returnKeyType
        textField.clearsOnBeginEditing = clearsOnBeginEditing
        textField.isSecureTextEntry = isSecure
        textField.isUserInteractionEnabled = isUserInteractionEnabled
        
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        textField.textAlignment = .center
        textField.contentVerticalAlignment = .center
        
        let pickerView = UIPickerView()
        pickerView.dataSource = context.coordinator
        pickerView.delegate = context.coordinator
        pickerView.selectRow(selectedYear - minimumYear, inComponent: 0, animated: false)
        pickerView.selectRow(selectedMonth - 1, inComponent: 1, animated: false)
        
        textField.inputView = pickerView
        addDoneButtonToKeyboard(textField)
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        if let date = date {
            uiView.text = date.yearAndMonthString()
        } else {
            uiView.text = ""
        }        
    }
    
    private func addDoneButtonToKeyboard(_ view: UITextField) {
        let doneToolbar: UIToolbar = UIToolbar()
        doneToolbar.barStyle = .default
        let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let done: UIBarButtonItem = UIBarButtonItem(title: "완료", style: .done, target: view, action: #selector(UITextField.resignFirstResponder))
        done.setTitleTextAttributes([NSAttributedString.Key.font:UIFont.boldSystemFont(ofSize: 18)], for: .normal)
        
        var items = [UIBarButtonItem]()
        items.append(flexSpace)
        items.append(done)
        
        doneToolbar.items = items
        doneToolbar.sizeToFit()
        view.inputAccessoryView = doneToolbar
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(date: $date, minYear: minimumYear, maxYear: maximumYear, didChange: didChange)
    }
    
    final class Coordinator: NSObject, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
        @Binding var date: Date?
        let minimumYear: Int
        let maximumYear: Int
        var didChange: () -> Void
        
        init(date: Binding<Date?>, minYear: Int, maxYear: Int,didChange: @escaping () -> Void) {
            self._date = date
            self.minimumYear = minYear
            self.maximumYear = maxYear
            self.didChange = didChange
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 2
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            if date == nil {
                date = Date()
            }
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            if component == 0 { // Year component
                return maximumYear - minimumYear + 1
            } else if component == 1 { // Month component
                let currentYear = Calendar.current.component(.year, from: Date())
                if pickerView.selectedRow(inComponent: 0) + minimumYear == currentYear {
                    // If the selected year is the current year, limit the months to the current month
                    let currentMonth = Calendar.current.component(.month, from: Date())
                    return currentMonth
                } else {
                    return 12
                }
            }
            return 0
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            let currentYear = Calendar.current.component(.year, from: Date())
            let currentMonth = Calendar.current.component(.month, from: Date())

            if component == 0 { // Year component
                let year = minimumYear + row
                return (year <= currentYear) ? "\(year)" : nil
            } else if component == 1 { // Month component
                if pickerView.selectedRow(inComponent: 0) + minimumYear == currentYear {
                    // If the selected year is the current year, filter out future months
                    return (row + 1 <= currentMonth) ? DateFormatter().monthSymbols[row] : nil
                } else {
                    return DateFormatter().monthSymbols[row]
                }
            }
            
            return nil
        }
        
        func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
            return 120
        }
        
        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            let currentYear = Calendar.current.component(.year, from: Date())
            let currentMonth = Calendar.current.component(.month, from: Date())
            
            if component == 0 { // Year component
                let selectedYear = minimumYear + pickerView.selectedRow(inComponent: 0)
                if selectedYear == currentYear {
                    // If the selected year is the current year, filter out future months
                    let currentMonth = Calendar.current.component(.month, from: Date())
                    pickerView.reloadComponent(1) // Reload the month component
                    pickerView.selectRow(min(currentMonth - 1, 11), inComponent: 1, animated: true)
                } else {
                    pickerView.reloadComponent(1)
                }
            } else if component == 1 { // Month component
                let selectedYear = minimumYear + pickerView.selectedRow(inComponent: 0)
                let selectedMonth = pickerView.selectedRow(inComponent: 1) + 1
                
                if selectedYear == currentYear && selectedMonth > currentMonth {
                    pickerView.selectRow(currentMonth - 1, inComponent: 1, animated: true)
                }
            }
            
            updateDate(pickerView)
        }
        
        func updateDate(_ pickerView: UIPickerView) {
            let selectedYear = minimumYear + pickerView.selectedRow(inComponent: 0)
            let selectedMonth = pickerView.selectedRow(inComponent: 1) + 1

            let calendar = Calendar.current
            var components = DateComponents()
            components.year = selectedYear
            components.month = selectedMonth
            if let newDate = calendar.date(from: components) {
                date = newDate
                didChange()
            }
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            return false
        }
    }
}


extension DateTextField {
    func placeholder(_ text: String?) -> some View {
        var view = self
        view.placeholder = text
        
        return view
    }
    
//    func dateFormatter(_ formatter: DateFormatter) -> some View {
//        var view = self
//        view.dateFormatter = formatter
//        return view
//    }
    
    func accentColor(_ accentColor: UIColor?) -> some View {
        var view = self
        view.accentColor = accentColor
        return view
    }
    
    func multilineTextAlignment(_ alignment: TextAlignment) -> some View {
        var view = self
        switch alignment {
            case .leading:
                view.textAlignment = layoutDirection ~= .leftToRight ? .left : .right
            case .trailing:
                view.textAlignment = layoutDirection ~= .leftToRight ? .right: .left
            case .center:
                view.textAlignment = .center
        }
        return view
    }
    
    func textContentType(_ textContentType: UITextContentType?) -> some View {
        var view = self
        view.contentType = textContentType
        return view
    }
    
    func disableAutocorrection(_ disable: Bool?) -> some View {
        var view = self
        if let disable = disable {
            view.autocorrection = disable ? .no : .yes
        } else {
            view.autocorrection = .default
        }
        return view
    }
    
    func autocapitalization(_ style: UITextAutocapitalizationType) -> some View {
        var view = self
        view.autocapitalization = style
        return view
    }
    
    func keyboardType(_ type: UIKeyboardType) -> some View {
        var view = self
        view.keyboardType = type
        return view
    }
    
    func returnKeyType(_ type: UIReturnKeyType) -> some View {
        var view = self
        view.returnKeyType = type
        return view
    }
    
    func isSecure(_ isSecure: Bool) -> some View {
        var view = self
        view.isSecure = isSecure
        return view
    }
    
    func clearsOnBeginEditing(_ shouldClear: Bool) -> some View {
        var view = self
        view.clearsOnBeginEditing = shouldClear
        return view
    }
    func disabled(_ disabled: Bool) -> some View {
        var view = self
        view.isUserInteractionEnabled = disabled
        return view
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment