Есть ли способ, которым я могу поместить всю свою логику выбора даты контроллера представления в отдельный класс, чтобы сохранить мой код организованным? - PullRequest
3 голосов
/ 07 апреля 2019

Я настраиваю очень простое приложение для iPhone, которое имеет несколько текстовых полей, которые используют элементы управления выбора даты для ввода строк даты и времени.

Проблема в том, что, когда все работает отлично, яобнаружение, что я дублирую много кода для нескольких полей, что кажется плохой практикой.

Что мне хотелось бы, так это способ сжать этот код в отдельный класс и просто вызывать его из любого места, когда янужно это.


Ниже приведен пример того, что у меня есть в моем контроллере представления, который работает очень хорошо, но его пришлось бы многократно дублировать для нескольких полей:

@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», всплывающее окно выбора даты и позволяет пользователю прокручивать и выбирать дату.Когда пользователь выбирает дату, текстовое поле устанавливается так, чтобы оно отражало выбранную дату.

То, что я хотел бы сделать в моем контроллере представления, - это сделать что-то вроде этого:

@IBOutlet weak var dateField: UITextField!

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

И есть класс, который определен отдельно, который может выглядеть примерно так:

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, который я написал выше, почти все работает: когда я нажимаю на текстовое поле "dateField", открывается окно выбора даты, и я могу прокрутить и выбрать дату.Однако после выбора даты ... поле dateField остается пустым.

Я новичок в Swift и ООП в целом, поэтому я уверен, что есть код или важная концепция, которую я упускаю.

Ответы [ 2 ]

2 голосов
/ 07 апреля 2019

Немного раздражает управление двумя экземплярами отдельных классов (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
    }

}

Я думаю, вы можете улучшить это сами.

1 голос
/ 07 апреля 2019

Да, вы можете (и, вероятно, должны) инкапсулировать эту логику в отдельный класс.Но, как сказал OOPer, вы, вероятно, захотите определить для этого подкласс UITextField.

Я бы сделал несколько других предложений:

  1. Я бы предложил вамдайте этому текстовому полю свойство date, чтобы вы могли установить и получить Date, связанный с этим текстовым полем.Как и UIDatePicker, контроллер представления должен взаимодействовать с этим новым элементом управления через свойство date и не должен иметь дело с самими форматерами даты.

  2. При использовании средства выбора даты в качествеисточник ввода, я думаю, что было бы хорошо добавить панель инструментов с кнопкой «готово», чтобы у пользователя был способ отклонить источник ввода (так же, как вы можете с помощью всплывающей клавиатуры.

  3. Я бы не советовал использовать dateFormat с DateFormatter, а скорее dateStyle и timeStyle. Вы всегда хотите, чтобы ваш пользовательский интерфейс отображал локализованные строки даты.

  4. Вы, вероятно, должны работать с аппаратными клавиатурами, которые могут быть подключены к устройству (особенно важно, если вы нацелены на iPad). Поэтому вы можете захотеть, чтобы ваше текстовое поле даты понимало изменение строки непосредственно в текстовом поле, а также выбирало даты изсредство выбора даты.

  5. Возможно, вы хотите, чтобы ваш собственный протокол для текстового поля сообщал пользовательскому интерфейсу об изменениях значения даты.

Таким образом, я мог бы предложить что-то вроде:

/// 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: $0) }
            }
            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: $0) }
        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")
        }
    }
}

Затем вы можете указать базовый класс DateTextField вместо UITextField в IB, и все готово.

Или, если вы хотите реализовать протокол делегата, вы можете сделать что-то вроде:

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
    }

}

Очевидно, вы можете изменитьЭто поведение, какое бы вы ни хотели, но оно иллюстрирует идею.

Но вы правы, что вы должны абстрагировать это текстовое поле с помощью выбора даты из вашего контроллера представления и поместить его в свой собственный класс.И приспособьте этот подкласс к тому поведению, которое, по вашему мнению, лучше всего подходит для вашего приложения.

...