有没有一种方法可以将我所有的视图控制器日期选择器逻辑放入一个单独的 class 中以保持我的代码井井有条?

Is there a way I can put all of my view controller date picker logic into a separate class to keep my code organized?

我正在设置一个非常简单的 iPhone 应用程序,它有多个文本字段,使用日期选择器控件输入日期和时间字符串。

问题是,虽然一切都运行良好,但我发现我正在为多个字段复制大量代码,这感觉很糟糕。

我想要的是一种将这段代码压缩成单独的 class 的方法,并在需要时从任何地方简单地调用它。


下面是我的视图控制器中的一个示例,它运行良好,但必须为多个字段重复多次:

@IBOutlet weak var dateField: UITextField!

@objc func dateFieldModified(sender:UIDatePicker) {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "MM/dd/yyyy"
    dateField.text = dateFormatter.string(from: sender.date)
}

@IBAction func dateFieldModify(_ sender: UITextField) {
    let datePicker = UIDatePicker()
    datePicker.datePickerMode = UIDatePicker.Mode.date
    datePicker.addTarget(self, action: #selector(self.dateFieldModified(sender:)), for: UIControl.Event.valueChanged)
    dateField.inputView = datePicker
}

总而言之,当用户输入文本字段 "dateField" 时,会弹出一个日期选择器并允许用户滚动和 select 一个日期。当用户选择日期时,文本字段会设置为反映 selected 日期。

我希望能够在我的视图控制器中执行如下操作:

@IBOutlet weak var dateField: UITextField!

@IBAction func dateFieldModify(_ sender: UITextField) {
    let datePicker = DatePicker(field:dateField, mode:"date", format: "MM/dd/yyyy")
    datePicker.loadDatePicker()
}

并且有一个单独定义的 class,可能看起来像这样:

class DatePicker {

    var field:UITextField
    var mode:String
    var format:String

    init(field:UITextField, mode:String, format:String) {
        self.field = field
        self.mode = mode
        self.format = format
    }

    @objc func dateFieldModified(sender:UIDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = format
        field.text = dateFormatter.string(from: sender.date)
    }

    func loadDatePicker() {
        let datePicker = UIDatePicker()
        datePicker.datePickerMode = UIDatePicker.Mode.date
        datePicker.addTarget(self, action: #selector(self.dateFieldModified(sender:)), for: UIControl.Event.valueChanged)
        field.inputView = datePicker
    }

}

实施我上面写的新 DatePicker class,几乎一切正常:当我单击 "dateField" 文本字段时,日期选择器打开,我可以滚动和 select一个日期。然而,在 select 输入一个日期之后... dateField 仍然是空的。

我是 Swift 和 OOP 的新手,所以我确定我缺少某些代码或重要概念。

管理两个单独的类实例有点烦人(UITextField和你的DatePicker),而且很容易造成引用循环。

如何定义您自己的 TextField 类型?

像这样:

class DatePickerTextField: UITextField {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }

    private func setUp() {
        self.addTarget(self, action: #selector(dateFieldModify(_:)), for: .editingDidBegin)
    }

    @objc func dateFieldModified(_ sender: UIDatePicker) {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "MM/dd/yyyy"
        self.text = dateFormatter.string(from: sender.date)
    }

    @IBAction func dateFieldModify(_ sender: UITextField) {
        let datePicker = UIDatePicker()
        datePicker.datePickerMode = .date
        datePicker.addTarget(self, action: #selector(self.dateFieldModified), for: .valueChanged)
        self.inputView = datePicker
    }

}

我认为你可以自己改进。

是的,您可以(而且可能应该)将此逻辑封装在一个单独的 class 中。但是正如 OOPer 所说,您可能想为此定义一个 UITextField subclass。

我会提出一些其他建议:

  1. 我建议你给这个文本字段一个 date 属性,这样你就可以设置和检索与该文本字段关联的 Date。就像 UIDatePicker 一样,视图控制器应该通过 date 属性 与这个新控件交互,而不必自己处理日期格式化程序。

  2. 当使用日期选择器作为输入源时,我认为添加一个带有“完成”按钮的工具栏是很好的,这样用户就可以关闭输入源(就像您可以使用弹出的键盘。

  3. 我不建议使用 dateFormatDateFormatter,而是 dateStyletimeStyle。您总是希望您的用户界面显示本地化的日期字符串。

  4. 您可能应该处理可能连接到设备的硬件键盘(如果目标为 iPad,则尤其重要)。因此,您可能希望日期文本字段直接了解文本字段中字符串的修改以及从日期选择器中选择日期。

  5. 您可能需要自己的文本字段协议来告知 UI 日期值的更改。

因此,我可能会建议如下:

/// Date text field delegate protocol

@objc protocol DateTextFieldDelegate {
    @objc optional
    func dateTextField(_ dateTextField: DateTextField, didChangeDate: Date?)

    @objc optional
    func didTapDone(dateTextField: DateTextField)
}

/// Date text field
///
/// Used for entry of dates in UITextField, replacing keyboard with date picker.

class DateTextField: UITextField {

    /// `DateTextField` delegate
    ///
    /// You don't need to supply a delegate, but if you do, this will tell you as
    /// the user changes the date.

    weak var dateTextFieldDelegate: DateTextFieldDelegate?

    /// Default date
    ///
    /// If `nil`, uses today's date

    var defaultDate: Date?

    /// Date formatter for date
    ///
    /// Feel free to change `dateStyle` and `timeStyle` to suit the needs of your app.

    let formatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .none
        return formatter
    }()

    /// Date
    ///
    /// The user's selected date.

    var date: Date? {
        didSet {
            dateTextFieldDelegate?.dateTextField?(self, didChangeDate: date)
            if !isManuallyEditing {
                text = date.map { formatter.string(from: [=10=]) }
            }
            datePicker.date = date ?? defaultDate ?? Date()
        }
    }

    var dateTextFieldButtonType: DateTextFieldButtonType = .done {
        didSet { doneButton?.title = dateTextFieldButtonType.buttonText }
    }

    /// The date picker.

    lazy var datePicker: UIDatePicker = {
        let picker = UIDatePicker()
        picker.datePickerMode = .date
        return picker
    }()

    // MARK: - Private properties

    /// Private reference for "Done" button

    private var doneButton: UIBarButtonItem!

    /// Private flag is the user is manually changing the date.

    private var isManuallyEditing = false

    // MARK: - Initialization

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }
}

// MARK: - Private utility methods

private extension DateTextField {
    func configure() {
        inputView = datePicker
        datePicker.addTarget(self, action: #selector(datePickerModified(_:)), for: .valueChanged)

        let toolBar = UIToolbar()
        toolBar.barStyle = .default
        toolBar.isTranslucent = true

        let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        doneButton = UIBarButtonItem(title: dateTextFieldButtonType.buttonText, style: .done, target: self, action: #selector(didTapDone(_:)))

        clearButtonMode = .whileEditing

        toolBar.setItems([space, doneButton], animated: false)
        toolBar.isUserInteractionEnabled = true
        toolBar.sizeToFit()

        inputAccessoryView = toolBar

        addTarget(self, action: #selector(textFieldModified(_:)), for: .editingChanged)
    }
}

// MARK: - Actions

extension DateTextField {
    @objc func didTapDone(_ sender: Any) {
        if dateTextFieldButtonType == .select {
            date = datePicker.date
        }

        resignFirstResponder()

        dateTextFieldDelegate?.didTapDone?(dateTextField: self)
    }

    @objc func textFieldModified(_ textField: UITextField) {
        isManuallyEditing = true
        date = text.flatMap { formatter.date(from: [=10=]) }
        isManuallyEditing = false
    }

    @objc func datePickerModified(_ datePicker: UIDatePicker) {
        date = datePicker.date
    }
}

// MARK: - Enumerations

extension DateTextField {
    enum DateTextFieldButtonType {
        case select
        case done
        case next
    }
}

extension DateTextField.DateTextFieldButtonType {
    var buttonText: String {
        switch self {
        case .select: return NSLocalizedString("Select", comment: "DateTextFieldButtonType")
        case .done:   return NSLocalizedString("Done",   comment: "DateTextFieldButtonType")
        case .next:   return NSLocalizedString("Next",   comment: "DateTextFieldButtonType")
        }
    }
}

然后您可以在 IB 中指定 DateTextField 的基数 class 而不是 UITextField,就完成了。

或者,如果你想实现委托协议,你可以这样做:

class ViewController: UIViewController {

    @IBOutlet weak var textField: DateTextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.dateTextFieldDelegate = self
    }

}

extension ViewController: DateTextFieldDelegate {
    func didTapDone(dateTextField: DateTextField) {
        print("keyboard dismissed")
    }

    func dateTextField(_ dateTextField: DateTextField, didChangeDate date: Date?) {
        print(date ?? "No date specified")
    }
}

或者如果您想要日期和时间:

class ViewController: UIViewController {

    @IBOutlet weak var textField: DateTextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        textField.datePicker.datePickerMode = .dateAndTime
        textField.formatter.dateStyle = .full
        textField.formatter.timeStyle = .medium
    }

}

显然,您可以根据需要修改此行为,但它说明了这个想法。

但是你是正确的,你应该从你的视图控制器中抽象出这个带有日期选择器的文本字段,并将它放在它自己的 class 中。并根据您认为最适合您的应用的任何行为来定制此子class。