From 116d2154f8b06a7a8ffec6842b51bdeaf9c4aff1 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 21 Sep 2022 17:50:13 +0300 Subject: [PATCH 1/5] feat: range filters view --- CHANGELOG.md | 1 + .../DefaultRangeFilterAppearance.swift | 62 ++++ .../Models/Appearance/RangeFilterLayout.swift | 4 + .../Formatter/BaseRangeValuesFormatter.swift | 25 ++ .../RangeValuesFormatterProtocol.swift | 4 + .../Protocols/RangeFilterDelegate.swift | 4 + .../ViewModels/BaseRangeFilterViewModel.swift | 69 ++++ .../RangeFilterViewModelProtocol.swift | 17 + .../FilterRangeView/BaseFilterRangeView.swift | 311 ++++++++++++++++++ .../FilterRangeViewRepresenter.swift | 4 + .../BaseIntervalInputView.swift | 187 +++++++++++ .../SliderView/BaseFilterRangeSlider.swift | 210 ++++++++++++ .../Views/SliderView/StepRangeSlider.swift | 289 ++++++++++++++++ .../Sources/Wrappers/EdgeConstraints.swift | 11 + 14 files changed, 1198 insertions(+) create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f99da122..c99e1605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **Add**: Tag like filter collection view - **ADD**: List like filter table view +- **ADD**: Range like filter view ### 1.26.0 diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift new file mode 100644 index 00000000..14ab70e6 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift @@ -0,0 +1,62 @@ +import UIKit + +public struct DefaultRangeFilterAppearance { + + public var textFieldsHeight: CGFloat + public var textFieldsWidth: CGFloat + public var textFieldLabelsSpacing: CGFloat + public var textFieldContentInsets: UIEdgeInsets + public var textFieldsBorderColor: UIColor + public var textFieldsBorderWidth: CGFloat + + public var sliderColor: UIColor + public var sliderOffColor: UIColor + public var thumbSize: CGFloat + + public var textFieldsSpacing: CGFloat + public var spacing: CGFloat + public var leadingSpacing: CGFloat + public var trailingSpacing: CGFloat + public var fontColor: UIColor + + public init(textFieldsHeight: CGFloat = 32, + textFieldsWidth: CGFloat = 100, + textFieldLabelsSpacing: CGFloat = 4, + textFieldContentInsets: UIEdgeInsets = .zero, + textFieldsBorderColor: UIColor = .black, + textFieldsBorderWidth: CGFloat = 1, + sliderColor: UIColor = .cyan, + sliderOffColor: UIColor = .darkGray, + thumbSize: CGFloat = 21, + textFieldsSpacing: CGFloat = 10, + spacing: CGFloat = 16, + leadingSpacing: CGFloat = 16, + trailingSpacing: CGFloat = 16, + fontColor: UIColor = .black) { + + self.textFieldsHeight = textFieldsHeight + self.textFieldsWidth = textFieldsWidth + self.textFieldLabelsSpacing = textFieldLabelsSpacing + self.textFieldContentInsets = textFieldContentInsets + self.textFieldsBorderColor = textFieldsBorderColor + self.textFieldsBorderWidth = textFieldsBorderWidth + self.sliderColor = sliderColor + self.sliderOffColor = sliderOffColor + self.thumbSize = thumbSize + self.textFieldsSpacing = textFieldsSpacing + self.spacing = spacing + self.leadingSpacing = leadingSpacing + self.trailingSpacing = trailingSpacing + self.fontColor = fontColor + } + + +} + +// MARK: - Default appearance + +public extension DefaultRangeFilterAppearance { + static var defaultAppearance: DefaultRangeFilterAppearance { + .init(textFieldContentInsets: .init(top: 4, left: 8, bottom: 4, right: 8)) + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift new file mode 100644 index 00000000..449cb178 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift @@ -0,0 +1,4 @@ +public enum RangeFilterLayout { + case textFieldsOnTop + case textFieldsFromBelow +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift new file mode 100644 index 00000000..afc1e68c --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift @@ -0,0 +1,25 @@ +import Foundation + +open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { + + open var formatter: NumberFormatter { + let formatter = NumberFormatter() + + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 1 + + return formatter + } + + public init() { } + + open func string(fromDouble value: Double) -> String { + let nsNumber = NSNumber(floatLiteral: value) + + return formatter.string(from: nsNumber) ?? "" + } + + open func double(fromString value: String) -> Double { + formatter.number(from: value)?.doubleValue ?? 0 + } +} \ No newline at end of file diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift new file mode 100644 index 00000000..4f915588 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift @@ -0,0 +1,4 @@ +public protocol RangeValuesFormatterProtocol { + func string(fromDouble value: Double) -> String + func double(fromString value: String) -> Double +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift new file mode 100644 index 00000000..b2d9c89d --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift @@ -0,0 +1,4 @@ +public protocol RangeFilterDelegate: AnyObject { + func valuesIsChanging(_ value: FilterRangeValue) + func valueDidEndChanging(_ value: FilterRangeValue) +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift new file mode 100644 index 00000000..3fb95107 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift @@ -0,0 +1,69 @@ +import CoreGraphics +import Foundation +import TISwiftUtils + +public typealias FilterRangeValue = (fromValue: Double, toValue: Double) + +open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { + + public let fromValue: Double + public let toValue: Double + public let stepValues: [Double] + + public var initialFromValue: Double? + public var initialToValue: Double? + + public weak var filterRangeView: FilterRangeViewRepresenter? + public weak var delegate: RangeFilterDelegate? + + open var initialValues: FilterRangeValue { + (initialFromValue ?? fromValue, initialToValue ?? toValue) + } + + open var isChanged: Bool { + initialFromValue != fromValue || initialToValue != toValue + } + + public init(fromValue: Double, + toValue: Double, + stepValues: [Double], + initialFromValue: Double? = nil, + initialToValue: Double? = nil) { + + self.fromValue = fromValue + self.toValue = toValue + self.stepValues = stepValues + self.initialFromValue = initialFromValue + self.initialToValue = initialToValue + } + + open func rangeSliderValueIsChanging(_ values: FilterRangeValue) { + filterRangeView?.configureTextFields(with: values) + delegate?.valuesIsChanging(values) + } + + open func rangeSliderValueDidEndChanging(_ values: FilterRangeValue) { + filterRangeView?.configureTextFields(with: values) + delegate?.valueDidEndChanging(values) + } + + open func fromValueIsChanging(_ values: FilterRangeValue) { + filterRangeView?.configureRangeView(with: values) + delegate?.valueDidEndChanging(values) + } + + open func toValueIsChanging(_ values: FilterRangeValue) { + filterRangeView?.configureRangeView(with: values) + delegate?.valueDidEndChanging(values) + } + + open func fromValueDidEndChanging(_ values: FilterRangeValue) { + filterRangeView?.configureRangeView(with: values) + delegate?.valueDidEndChanging(values) + } + + open func toValueDidEndChanging(_ values: FilterRangeValue) { + filterRangeView?.configureRangeView(with: values) + delegate?.valueDidEndChanging(values) + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift new file mode 100644 index 00000000..e614f9c3 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift @@ -0,0 +1,17 @@ +public protocol RangeFilterViewModelProtocol { + + var fromValue: Double { get } + var toValue: Double { get } + var stepValues: [Double] { get } + var initialFromValue: Double? { get } + var initialToValue: Double? { get } + + func rangeSliderValueIsChanging(_ values: FilterRangeValue) + func rangeSliderValueDidEndChanging(_ values: FilterRangeValue) + + func toValueIsChanging(_ values: FilterRangeValue) + func toValueDidEndChanging(_ values: FilterRangeValue) + + func fromValueDidEndChanging(_ values: FilterRangeValue) + func fromValueIsChanging(_ values: FilterRangeValue) +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift new file mode 100644 index 00000000..01b34987 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift @@ -0,0 +1,311 @@ +import UIKit +import TIUIElements +import TIUIKitCore + +open class BaseFilterRangeView: BaseInitializableControl, + FilterRangeViewRepresenter { + + // MARK: - Private properties + + private let layout: RangeFilterLayout + private var contentEdgesConstraints: EdgeConstraints? + private var toTextFieldWidthConstraint: NSLayoutConstraint? + private var fromTextFieldWidthConstraint: NSLayoutConstraint? + private var textFieldsHeightConstraint: NSLayoutConstraint? + + // MARK: - Public properties + + public let formatter: RangeValuesFormatterProtocol + public let rangeSlider: BaseFilterRangeSlider + + public let textFieldsContainer = UIView() + public let fromValueView = BaseIntervalInputView(state: .fromValue) + public let toValueView = BaseIntervalInputView(state: .toValue) + + public var viewModel: ViewModel? + + // MARK: - Open properties + + open var rangeSliderValue: FilterRangeValue { + (rangeSlider.leftValue, rangeSlider.rightValue) + } + + open var textFieldsValues: FilterRangeValue { + (min(fromValueView.currentValue, viewModel?.toValue ?? .zero), + max(toValueView.currentValue, viewModel?.fromValue ?? .zero)) + } + // MARK: - Text Fields Setup + + open var textFieldsBorderColor: UIColor = .black { + didSet { + toValueView.layer.borderColor = textFieldsBorderColor.cgColor + fromValueView.layer.borderColor = textFieldsBorderColor.cgColor + } + } + + open var textFieldsBorderWidth: CGFloat = 1 { + didSet { + toValueView.layer.borderWidth = textFieldsBorderWidth + fromValueView.layer.borderWidth = textFieldsBorderWidth + } + } + + open var textFieldsHeight: CGFloat = 32 { + didSet { + textFieldsHeightConstraint?.constant = textFieldsHeight + } + } + + open var textFieldsContentInsets: UIEdgeInsets = .zero { + didSet { + fromValueView.updateContentInsets(textFieldsContentInsets) + toValueView.updateContentInsets(textFieldsContentInsets) + } + } + + open var textFieldsLabelsSpacing: CGFloat = .zero { + didSet { + fromValueView.updateLabelsSpacing(textFieldsLabelsSpacing) + toValueView.updateLabelsSpacing(textFieldsLabelsSpacing) + } + } + + open var textFieldsMinWidth: CGFloat = 40 { + didSet { + toTextFieldWidthConstraint?.constant = textFieldsMinWidth + fromTextFieldWidthConstraint?.constant = textFieldsMinWidth + } + } + + open var fontColor: UIColor = .black { + didSet { + toValueView.fontColor = fontColor + fromValueView.fontColor = fontColor + rangeSlider.fontColor = fontColor + } + } + + open var contentSpacing: CGFloat = .zero { + didSet { + if layout == .textFieldsOnTop { + contentEdgesConstraints?.bottomConstraint.constant -= contentSpacing + } else { + contentEdgesConstraints?.topConstraint.constant += contentSpacing + } + } + } + + open var leadingInset: CGFloat = .zero { + didSet { + contentEdgesConstraints?.leadingConstraint.constant += leadingInset + } + } + + open var trailingInset: CGFloat = .zero { + didSet { + contentEdgesConstraints?.trailingConstraint.constant -= trailingInset + } + } + + open var appearance: DefaultRangeFilterAppearance = .defaultAppearance { + didSet { + updateAppearance() + } + } + + // MARK: - Init + + public init(layout: RangeFilterLayout = .textFieldsOnTop, + formatter: RangeValuesFormatterProtocol = BaseRangeValuesFormatter(), + viewModel: ViewModel) { + + self.viewModel = viewModel + self.layout = layout + self.formatter = formatter + self.rangeSlider = BaseFilterRangeSlider(layout: layout) + + super.init(frame: .zero) + } + + @available(*, unavailable) + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + open override func addViews() { + super.addViews() + + textFieldsContainer.addSubviews(fromValueView, toValueView) + addSubviews(textFieldsContainer, rangeSlider) + } + + open override func configureLayout() { + super.configureLayout() + + [textFieldsContainer, rangeSlider, toValueView, fromValueView] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + switch layout { + case .textFieldsOnTop: + configureTextFieldOnTop() + case .textFieldsFromBelow: + configureTextFieldFromBelow() + } + + guard let contentEdgesConstraints = contentEdgesConstraints else { + return + } + + let toTextFieldWidthConstraint = toValueView.widthAnchor.constraint(greaterThanOrEqualToConstant: textFieldsMinWidth) + let fromTextFieldWidthConstraint = fromValueView.widthAnchor.constraint(greaterThanOrEqualToConstant: textFieldsMinWidth) + + NSLayoutConstraint.activate( + contentEdgesConstraints.allConstraints + + + [ + toTextFieldWidthConstraint, + fromTextFieldWidthConstraint, + + toValueView.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), + toValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), + toValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), + fromValueView.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), + fromValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), + fromValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), + + rangeSlider.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), + rangeSlider.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), + layout == .textFieldsOnTop + ? rangeSlider.bottomAnchor.constraint(equalTo: bottomAnchor) + : rangeSlider.topAnchor.constraint(equalTo: topAnchor) + ] + ) + + self.toTextFieldWidthConstraint = toTextFieldWidthConstraint + self.fromTextFieldWidthConstraint = fromTextFieldWidthConstraint + } + + open override func bindViews() { + super.bindViews() + + fromValueView.configure(with: formatter) + toValueView.configure(with: formatter) + rangeSlider.configure(with: formatter) + + fromValueView.addTarget(self, valueIsChangedAction: #selector(fromValueIsChanging)) + toValueView.addTarget(self, valueIsChangedAction: #selector(toValueIsChanging)) + + fromValueView.addTarget(self, valueDidEndChangingAction: #selector(fromValueDidEndChanging)) + toValueView.addTarget(self, valueDidEndChangingAction: #selector(toValueDidEndChanging)) + + rangeSlider.addTarget(self, + action: #selector(rangeSliderValueIsChanging), + for: .valueChanged) + + rangeSlider.addTarget(self, + action: #selector(rangeSliderValueDidEndChanging), + for: .editingDidEnd) + } + + open override func configureAppearance() { + super.configureAppearance() + + defaultConfigure() + updateAppearance() + + layoutSubviews() + } + + // MARK: - Configuration + + open func defaultConfigure() { + guard let viewModel = viewModel else { + return + } + + fromValueView.configure(with: viewModel.initialFromValue ?? viewModel.fromValue) + toValueView.configure(with: viewModel.initialToValue ?? viewModel.toValue) + + rangeSlider.minimumValue = CGFloat(viewModel.fromValue) + rangeSlider.maximumValue = CGFloat(viewModel.toValue) + + rangeSlider.leftValue = CGFloat(viewModel.initialFromValue ?? viewModel.fromValue) + rangeSlider.rightValue = CGFloat(viewModel.initialToValue ?? viewModel.toValue) + + rangeSlider.addStep(stepValues: viewModel.stepValues.map { CGFloat($0) }) + } + + // MARK: - FilterRangeViewRepresenter + + open func configureTextFields(with values: FilterRangeValue) { + toValueView.configure(with: values.toValue) + fromValueView.configure(with: values.fromValue) + } + + open func configureRangeView(with values: FilterRangeValue) { + rangeSlider.configure(with: values) + } + + // MARK: - Private methods + + private func updateAppearance() { + contentSpacing = appearance.spacing + leadingInset = appearance.leadingSpacing + trailingInset = appearance.trailingSpacing + + textFieldsHeight = appearance.textFieldsHeight + textFieldsMinWidth = appearance.textFieldsWidth + textFieldsContentInsets = appearance.textFieldContentInsets + textFieldsLabelsSpacing = appearance.textFieldLabelsSpacing + textFieldsBorderColor = appearance.textFieldsBorderColor + textFieldsBorderWidth = appearance.textFieldsBorderWidth + fontColor = appearance.fontColor + + rangeSlider.sliderColor = appearance.sliderColor + rangeSlider.sliderOffColor = appearance.sliderOffColor + rangeSlider.thumbSize = appearance.thumbSize + + } + + private func configureTextFieldOnTop() { + contentEdgesConstraints = EdgeConstraints(leadingConstraint: textFieldsContainer.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: textFieldsContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: textFieldsContainer.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: textFieldsContainer.bottomAnchor.constraint(equalTo: rangeSlider.topAnchor)) + } + + private func configureTextFieldFromBelow() { + contentEdgesConstraints = EdgeConstraints(leadingConstraint: textFieldsContainer.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: textFieldsContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: textFieldsContainer.topAnchor.constraint(equalTo: rangeSlider.bottomAnchor), + bottomConstraint: textFieldsContainer.bottomAnchor.constraint(equalTo: bottomAnchor)) + } + + // MARK: - Actions + + @objc private func rangeSliderValueIsChanging() { + viewModel?.rangeSliderValueIsChanging(rangeSliderValue) + } + + @objc private func rangeSliderValueDidEndChanging() { + viewModel?.rangeSliderValueDidEndChanging(rangeSliderValue) + } + + @objc private func fromValueIsChanging() { + viewModel?.fromValueIsChanging(textFieldsValues) + } + + @objc private func toValueIsChanging() { + viewModel?.toValueIsChanging(textFieldsValues) + } + + @objc private func fromValueDidEndChanging() { + viewModel?.fromValueDidEndChanging(textFieldsValues) + } + + @objc private func toValueDidEndChanging() { + viewModel?.toValueDidEndChanging(textFieldsValues) + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift new file mode 100644 index 00000000..849e8b8a --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift @@ -0,0 +1,4 @@ +public protocol FilterRangeViewRepresenter: AnyObject { + func configureTextFields(with value: FilterRangeValue) + func configureRangeView(with value: FilterRangeValue) +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift new file mode 100644 index 00000000..1bcf8c04 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift @@ -0,0 +1,187 @@ +import TIUIElements +import TIUIKitCore +import UIKit + +open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { + + public enum TextFieldState { + case fromValue + case toValue + } + + private var contentEdgesConstraints: EdgeConstraints? + private var labelsSpacingConstraint: NSLayoutConstraint? + + public let state: TextFieldState + + public let intervalLabel = UILabel() + public let inputTextField = UITextField() + + public var formatter: RangeValuesFormatterProtocol? + + open var intervalTextLabel: String { + switch state { + case .fromValue: + return "от" + case .toValue: + return "до" + } + } + + open var validCharacterSet: CharacterSet { + CharacterSet.decimalDigits.union(CharacterSet.init(charactersIn: ".")) + } + + open var fontColor: UIColor = .black { + didSet { + inputTextField.textColor = fontColor + intervalLabel.textColor = fontColor + } + } + + open var intrinsicContentHeight: CGFloat? { + didSet { + invalidateIntrinsicContentSize() + } + } + + open var currentValue: Double { + formatter?.double(fromString: inputTextField.text ?? "") ?? 0 + } + + open override var intrinsicContentSize: CGSize { + if let height = intrinsicContentHeight { + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) + } + + // MARK: - Init + + public init(state: TextFieldState) { + self.state = state + + super.init(frame: .zero) + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life Cycle + + open override func addViews() { + super.addViews() + + addSubviews(intervalLabel, inputTextField) + } + + open override func configureLayout() { + super.configureLayout() + + [intervalLabel, inputTextField].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + let contentEdgesConstraints = EdgeConstraints(leadingConstraint: intervalLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: inputTextField.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), + topConstraint: intervalLabel.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: intervalLabel.bottomAnchor.constraint(equalTo: bottomAnchor)) + + let labelsSpacingConstraint = inputTextField.leadingAnchor.constraint(equalTo: intervalLabel.trailingAnchor) + + NSLayoutConstraint.activate( + contentEdgesConstraints.allConstraints + + + [ + labelsSpacingConstraint, + inputTextField.centerYAnchor.constraint(equalTo: intervalLabel.centerYAnchor) + ] + ) + + self.labelsSpacingConstraint = labelsSpacingConstraint + self.contentEdgesConstraints = contentEdgesConstraints + } + + open override func configureAppearance() { + super.configureAppearance() + + inputTextField.keyboardType = .numberPad + + layer.borderColor = UIColor.darkGray.cgColor + layer.borderWidth = 1 + + layer.round(corners: .allCorners, radius: 8) + } + + open override func bindViews() { + super.bindViews() + + inputTextField.delegate = self + inputTextField.addTarget(target, action: #selector(formatValue), for: .editingChanged) + } + + open override func localize() { + super.localize() + + intervalLabel.text = intervalTextLabel + } + + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + inputTextField.becomeFirstResponder() + } + + open func configure(with value: Double) { + inputTextField.text = formatter?.string(fromDouble: value) + } + + open func configure(with formatter: RangeValuesFormatterProtocol) { + self.formatter = formatter + } + + // MARK: - UITextFieldDelegate + + open func textField(_ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String) -> Bool { + + let characterSet = CharacterSet(charactersIn: string) + + textField.undoManager?.removeAllActions() + + return validCharacterSet.isSuperset(of: characterSet) + } + + // MARK: - Open methods + + open func updateContentInsets(_ insets: UIEdgeInsets) { + contentEdgesConstraints?.update(from: insets) + } + + open func updateLabelsSpacing(_ spacing: CGFloat) { + labelsSpacingConstraint?.constant += spacing + } + + // MARK: - Public methods + + public func addTarget(_ target: Any?, valueIsChangedAction: Selector) { + inputTextField.addTarget(target, action: valueIsChangedAction, for: .editingChanged) + } + + public func addTarget(_ target: AnyObject, valueDidEndChangingAction: Selector) { + inputTextField.addTarget(target, action: valueDidEndChangingAction, for: .editingDidEnd) + } + + // MARK: - Private methods + + @objc private func formatValue() { + let onlyDigits = inputTextField.text.orEmpty + .components(separatedBy: validCharacterSet.inverted) + .joined() + + let newValue = formatter?.double(fromString: onlyDigits) + inputTextField.text = formatter?.string(fromDouble: newValue ?? 0) + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift new file mode 100644 index 00000000..3d27e16d --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift @@ -0,0 +1,210 @@ +import TIUIKitCore +import UIKit + +open class BaseFilterRangeSlider: StepRangeSlider { + + private let layout: RangeFilterLayout + + private var stepCircleViews: [UIView] = [] + private var stepLabels: [UILabel] = [] + + private(set) var stepValues: [CGFloat] = [] + + public var formatter: RangeValuesFormatterProtocol? + + open var numberOfSteps: Int { + stepValues.count + } + + open var fontColor: UIColor = .black { + didSet { + stepLabels.forEach { $0.textColor = fontColor } + } + } + + open var sliderColor: UIColor = .systemOrange { + didSet { + stepCircleInColor = sliderColor + activeTrackColor = sliderColor + thumbColor = sliderColor + } + } + + open var sliderOffColor: UIColor = .darkGray { + didSet { + stepCircleOutColor = sliderOffColor + trackColor = sliderOffColor + } + } + + open var stepCircleInColor: UIColor = .systemOrange { + didSet { + setNeedsLayout() + } + } + + open var stepCircleOutColor: UIColor = .darkGray { + didSet { + setNeedsLayout() + } + } + + open var stepCircleSize: CGFloat = 5 { + didSet { + updateStepViewsRadius() + setNeedsLayout() + } + } + + open override var intrinsicContentSize: CGSize { + let superSize = super.intrinsicContentSize + + guard !stepLabels.isEmpty else { + return superSize + } + + return .init(width: superSize.width, height: stepLabels[.zero].frame.maxY) + } + + // MARK: - Init + + public init(layout: RangeFilterLayout) { + self.layout = layout + + super.init(frame: .zero) + } + + open override func layoutSubviews() { + super.layoutSubviews() + + updateStepCircleViewsPosition() + updateStepLabelsPosition() + updateStepViewsColors() + } + + // MARK: - Open methods + + open func configure(with values: FilterRangeValue) { + leftValue = CGFloat(values.fromValue) + rightValue = CGFloat(values.toValue) + } + + open func configure(with formatter: RangeValuesFormatterProtocol) { + self.formatter = formatter + + createStepLabels() + } + + open func addStep(stepValues: [CGFloat]) { + self.stepValues = stepValues + + createStepCircleViews() + createStepLabels() + updateStepViewsRadius() + updateStepViewsColors() + + setNeedsLayout() + layoutIfNeeded() + invalidateIntrinsicContentSize() + } + + // MARK: - Private methods + + private func createStepCircleViews() { + stepCircleViews.forEach { + $0.removeFromSuperview() + } + stepCircleViews.removeAll() + + for _ in .zero ..< numberOfSteps { + let stepCircle = UIView() + stepCircleViews.append(stepCircle) + } + + addSubviews(stepCircleViews) + + thumbs.forEach { + bringSubviewToFront($0) + } + } + + private func updateStepCircleViewsPosition() { + for index in 0 ..< stepCircleViews.count { + guard index >= .zero && index < stepCircleViews.count else { + return + } + + let circle = stepCircleViews[index] + + let positionX = getPositionX(from: stepValues[index]) + (thumbSize - stepCircleSize) / 2 + let positionY = trackView.frame.minY + (trackView.frame.height - stepCircleSize) / 2 + + circle.frame = .init(x: positionX, + y: positionY, + width: stepCircleSize, + height: stepCircleSize) + } + } + + private func updateStepViewsRadius() { + stepCircleViews.forEach { + $0.layer.round(corners: .allCorners, radius: stepCircleSize / 2) + } + } + + private func updateStepViewsColors() { + stepCircleViews.forEach { + let leftThumbMaxX = leftThumbView.frame.maxX + let rightThumbMinX = rightThumbView.frame.minX + let isEnterRange = $0.frame.maxX >= leftThumbMaxX && $0.frame.minX <= rightThumbMinX + $0.backgroundColor = isEnterRange ? stepCircleInColor : stepCircleOutColor + } + } + + private func createStepLabels() { + stepLabels.forEach { + $0.removeFromSuperview() + } + + stepValues.forEach { + let label = UILabel() + let formattedText = formatter?.string(fromDouble: $0) + label.text = formattedText ?? "" + + stepLabels.append(label) + } + + addSubviews(stepLabels) + } + + private func updateStepLabelsPosition() { + for (index, label) in stepLabels.enumerated() { + guard index >= .zero && index < stepCircleViews.count else { + return + } + + let circleView = stepCircleViews[index] + let yPosition = layout == .textFieldsOnTop + ? circleView.frame.maxY + .stepLabelsTopInset + : circleView.frame.minY - label.bounds.height - .stepLabelsTopInset + + label.center.x = circleView.center.x + label.sizeToFit() + label.frame = .init(x: label.frame.minX, + y: yPosition, + width: label.bounds.width, + height: label.bounds.height) + } + } + + private func getStepCirclePositionX(_ value: CGFloat) -> CGFloat { + let unit = trackUsingWidth / (maximumValue - minimumValue) + return (value - minimumValue) * unit + } +} + +// MARK: - Constant + +private extension CGFloat { + static let stepLabelsTopInset: CGFloat = 11 +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift new file mode 100644 index 00000000..38faa578 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift @@ -0,0 +1,289 @@ +import TIUIKitCore +import UIKit + +open class StepRangeSlider: UIControl, InitializableViewProtocol { + + // MARK: - Public properties + + public let leftThumbView = UIView() + public let rightThumbView = UIView() + + public let activeTrack = UIView() + public let trackView = UIView() + + // MARK: - Open properties + + open var disregardedWidth: CGFloat { + thumbSize + } + + open var trackUsingWidth: CGFloat { + frame.width - disregardedWidth + } + + open var thumbs: [UIView] { + [leftThumbView, rightThumbView] + } + + open var leftValue: CGFloat = 0 { + didSet { + if leftValue < minimumValue || leftValue > rightValue { + leftValue = oldValue + } + + setNeedsLayout() + } + } + + open var rightValue: CGFloat = 1000 { + didSet { + if rightValue < leftValue || rightValue > maximumValue { + rightValue = oldValue + } + + setNeedsLayout() + } + } + + open var minimumValue: CGFloat = .zero { + didSet { + if minimumValue > maximumValue { + minimumValue = oldValue + } + + resetCurrentValues() + setNeedsLayout() + } + } + + open var maximumValue: CGFloat = 1000 { + didSet { + if minimumValue > maximumValue && maximumValue <= 0 { + maximumValue = oldValue + } + + resetCurrentValues() + setNeedsLayout() + } + } + + open var thumbSize: CGFloat = .thumbSize { + didSet { + setNeedsLayout() + } + } + + open var trackColor: UIColor = .darkGray { + didSet { + trackView.backgroundColor = trackColor + } + } + + open var activeTrackColor: UIColor = .systemOrange { + didSet { + activeTrack.backgroundColor = activeTrackColor + } + } + + open var thumbColor: UIColor = .systemOrange { + didSet { + thumbs.forEach { + $0.backgroundColor = thumbColor + } + } + } + + open var linePath: CGPath { + let linePath = UIBezierPath() + + linePath.move(to: CGPoint(x: .zero, y: bounds.height / 2)) + linePath.addLine(to: CGPoint(x: frame.width, y: bounds.height / 2)) + + return linePath.cgPath + } + + open var leftThumbCenterPositionToValue: CGFloat { + let unit = CGFloat(maximumValue) / trackUsingWidth + return leftThumbView.center.y * unit + } + + open var rightThumbCenterPositionToValue: CGFloat { + let unit = CGFloat(maximumValue) / trackUsingWidth + return rightThumbView.center.y * unit + } + + open override var intrinsicContentSize: CGSize { + .init(width: UIView.noIntrinsicMetric, height: thumbSize) + } + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + + initializeView() + } + + @available(*, unavailable) + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + // MARK: - Life Cycle + + open override func layoutSubviews() { + updateTrackViewFrame() + updateThumbsFrame() + updateActiveTrack() + + super.layoutSubviews() + } + + open func addViews() { + addSubviews(trackView, leftThumbView, rightThumbView) + trackView.addSubviews(activeTrack) + } + + open func configureLayout() { + // override + } + + open func bindViews() { + thumbs.forEach { + let panGestureRecognizer = UIPanGestureRecognizer(target: self, + action: #selector(draggingThumb(_:))) + $0.addGestureRecognizer(panGestureRecognizer) + } + } + + open func configureAppearance() { + trackView.backgroundColor = trackColor + activeTrack.backgroundColor = activeTrackColor + + thumbs.forEach { + $0.backgroundColor = thumbColor + } + + trackView.layer.round(corners: .allCorners, radius: 2) + updateActiveTrack() + } + + open func localize() { + // override + } + + // MARK: - Internal Methods + + func getPositionX(from value: CGFloat) -> CGFloat { + let unit = trackUsingWidth / (maximumValue - minimumValue) + return (value - minimumValue) * unit + } + + // MARK: - Private Methods + + @objc private func draggingThumb(_ gesture: UIPanGestureRecognizer) { + guard let view = gesture.view else { + return + } + + let translation = gesture.translation(in: self) + + switch gesture.state { + case .began: + bringSubviewToFront(view) + + case .changed: + draggingChanged(view: view, translation: translation.x) + + case .ended: + sendActions(for: .editingDidEnd) + default: + break + } + + gesture.setTranslation(.zero, in: self) + } + + private func draggingChanged(view: UIView, translation: CGFloat) { + switch view { + case leftThumbView: + handleDragginLeftThumb(translation: translation) + + case rightThumbView: + handleDraggingRightThumb(translation: translation) + + default: + break + } + + sendActions(for: .valueChanged) + } + + private func handleDragginLeftThumb(translation: CGFloat) { + let valueTranslation = maximumValue / (trackUsingWidth / translation) + var newLeftValue = max(minimumValue, leftValue + valueTranslation) + newLeftValue = min(newLeftValue, maximumValue) + newLeftValue = min(newLeftValue, rightValue) + + leftValue = newLeftValue + } + + private func handleDraggingRightThumb(translation: CGFloat) { + let valueTranslation = maximumValue / (trackUsingWidth / translation) + var newRightValue = min(maximumValue, rightValue + valueTranslation) + newRightValue = max(newRightValue, minimumValue) + newRightValue = max(newRightValue, leftValue) + + rightValue = newRightValue + } + + private func updateActiveTrack() { + activeTrack.frame = .init(x: leftThumbView.frame.maxX, + y: .zero, + width: rightThumbView.frame.minX - leftThumbView.frame.maxX, + height: trackView.frame.height) + } + + private func updateThumbsFrame() { + thumbs.forEach { + $0.layer.round(corners: .allCorners, radius: thumbSize / 2) + } + + leftThumbView.frame = getLeftThumbRect(from: leftValue) + rightThumbView.frame = getRightThumbRect(from: rightValue) + } + + private func updateTrackViewFrame() { + let trackViewHeight: CGFloat = 3 + + trackView.frame = .init(x: .zero, + y: (thumbSize - trackViewHeight) / 2, + width: bounds.width, + height: trackViewHeight) + } + + private func getLeftThumbRect(from value: CGFloat) -> CGRect { + .init(x: getPositionX(from: value), + y: .zero, + width: thumbSize, + height: thumbSize) + } + + private func getRightThumbRect(from value: CGFloat) -> CGRect { + .init(x: getPositionX(from: value), + y: .zero, + width: thumbSize, + height: thumbSize) + } + + private func resetCurrentValues() { + leftValue = minimumValue + rightValue = maximumValue + } +} + +// MARK: - Constant + +private extension CGFloat { + static let thumbSize: CGFloat = 21 +} diff --git a/TIUIElements/Sources/Wrappers/EdgeConstraints.swift b/TIUIElements/Sources/Wrappers/EdgeConstraints.swift index 0274afd8..cccfdd8f 100644 --- a/TIUIElements/Sources/Wrappers/EdgeConstraints.swift +++ b/TIUIElements/Sources/Wrappers/EdgeConstraints.swift @@ -37,6 +37,17 @@ public struct EdgeConstraints { ] } + public init(leadingConstraint: NSLayoutConstraint, + trailingConstraint: NSLayoutConstraint, + topConstraint: NSLayoutConstraint, + bottomConstraint: NSLayoutConstraint) { + + self.leadingConstraint = leadingConstraint + self.trailingConstraint = trailingConstraint + self.topConstraint = topConstraint + self.bottomConstraint = bottomConstraint + } + public func activate() { NSLayoutConstraint.activate(allConstraints) } From ca7bf6326d18ff5c0884dc2eb74c5a9b56089ac4 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 21 Sep 2022 18:05:17 +0300 Subject: [PATCH 2/5] refactor: naming fixes --- ...gate.swift => RangeFiltersPickerDelegate.swift} | 2 +- .../ViewModels/BaseRangeFilterViewModel.swift | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/{RangeFilterDelegate.swift => RangeFiltersPickerDelegate.swift} (66%) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift similarity index 66% rename from TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift rename to TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift index b2d9c89d..0b416053 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFilterDelegate.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift @@ -1,4 +1,4 @@ -public protocol RangeFilterDelegate: AnyObject { +public protocol RangeFiltersPickerDelegate: AnyObject { func valuesIsChanging(_ value: FilterRangeValue) func valueDidEndChanging(_ value: FilterRangeValue) } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift index 3fb95107..442a8086 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift @@ -14,7 +14,7 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { public var initialToValue: Double? public weak var filterRangeView: FilterRangeViewRepresenter? - public weak var delegate: RangeFilterDelegate? + public weak var pickerDelegate: RangeFiltersPickerDelegate? open var initialValues: FilterRangeValue { (initialFromValue ?? fromValue, initialToValue ?? toValue) @@ -39,31 +39,31 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { open func rangeSliderValueIsChanging(_ values: FilterRangeValue) { filterRangeView?.configureTextFields(with: values) - delegate?.valuesIsChanging(values) + pickerDelegate?.valuesIsChanging(values) } open func rangeSliderValueDidEndChanging(_ values: FilterRangeValue) { filterRangeView?.configureTextFields(with: values) - delegate?.valueDidEndChanging(values) + pickerDelegate?.valueDidEndChanging(values) } open func fromValueIsChanging(_ values: FilterRangeValue) { filterRangeView?.configureRangeView(with: values) - delegate?.valueDidEndChanging(values) + pickerDelegate?.valueDidEndChanging(values) } open func toValueIsChanging(_ values: FilterRangeValue) { filterRangeView?.configureRangeView(with: values) - delegate?.valueDidEndChanging(values) + pickerDelegate?.valueDidEndChanging(values) } open func fromValueDidEndChanging(_ values: FilterRangeValue) { filterRangeView?.configureRangeView(with: values) - delegate?.valueDidEndChanging(values) + pickerDelegate?.valueDidEndChanging(values) } open func toValueDidEndChanging(_ values: FilterRangeValue) { filterRangeView?.configureRangeView(with: values) - delegate?.valueDidEndChanging(values) + pickerDelegate?.valueDidEndChanging(values) } } From a8785b35e8b396f545c2126741291d05e77b37f5 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Wed, 21 Sep 2022 19:58:24 +0300 Subject: [PATCH 3/5] fix: code review notes --- .../DefaultIntervalInputAppearance.swift | 48 +++++++++++++ .../DefaultRangeFilterAppearance.swift | 69 ++++++++----------- .../DefaultStepSliderAppearance.swift | 39 +++++++++++ .../Models/Appearance/RangeFilterLayout.swift | 22 ++++++ .../Formatter/BaseRangeValuesFormatter.swift | 45 ++++++++++-- .../RangeValuesFormatterProtocol.swift | 26 +++++++ .../RangeFiltersPickerDelegate.swift | 22 ++++++ .../ViewModels/BaseRangeFilterViewModel.swift | 22 ++++++ .../RangeFilterViewModelProtocol.swift | 22 ++++++ .../FilterRangeView/BaseFilterRangeView.swift | 46 ++++++++++--- .../FilterRangeViewRepresenter.swift | 22 ++++++ .../BaseIntervalInputView.swift | 47 +++++++++---- .../SliderView/BaseFilterRangeSlider.swift | 22 ++++++ .../Views/SliderView/StepRangeSlider.swift | 22 ++++++ 14 files changed, 401 insertions(+), 73 deletions(-) create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultIntervalInputAppearance.swift create mode 100644 TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultIntervalInputAppearance.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultIntervalInputAppearance.swift new file mode 100644 index 00000000..e96f390f --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultIntervalInputAppearance.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public struct DefaultIntervalInputAppearance { + + public var textFieldsHeight: CGFloat + public var textFieldsWidth: CGFloat + public var textFieldLabelsSpacing: CGFloat + public var textFieldContentInsets: UIEdgeInsets + public var textFieldsBorderColor: UIColor + public var textFieldsBorderWidth: CGFloat + + public init(textFieldsHeight: CGFloat = 32, + textFieldsWidth: CGFloat = 100, + textFieldLabelsSpacing: CGFloat = 4, + textFieldContentInsets: UIEdgeInsets = .init(top: 4, left: 8, bottom: 4, right: 8), + textFieldsBorderColor: UIColor = .black, + textFieldsBorderWidth: CGFloat = 1) { + + self.textFieldsHeight = textFieldsHeight + self.textFieldsWidth = textFieldsWidth + self.textFieldLabelsSpacing = textFieldLabelsSpacing + self.textFieldContentInsets = textFieldContentInsets + self.textFieldsBorderColor = textFieldsBorderColor + self.textFieldsBorderWidth = textFieldsBorderWidth + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift index 14ab70e6..84cde2d0 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultRangeFilterAppearance.swift @@ -1,62 +1,49 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import UIKit public struct DefaultRangeFilterAppearance { - public var textFieldsHeight: CGFloat - public var textFieldsWidth: CGFloat - public var textFieldLabelsSpacing: CGFloat - public var textFieldContentInsets: UIEdgeInsets - public var textFieldsBorderColor: UIColor - public var textFieldsBorderWidth: CGFloat + public var intervalInputAppearance: DefaultIntervalInputAppearance + public var stepSliderAppearance: DefaultStepSliderAppearance - public var sliderColor: UIColor - public var sliderOffColor: UIColor - public var thumbSize: CGFloat - - public var textFieldsSpacing: CGFloat public var spacing: CGFloat public var leadingSpacing: CGFloat public var trailingSpacing: CGFloat public var fontColor: UIColor - public init(textFieldsHeight: CGFloat = 32, - textFieldsWidth: CGFloat = 100, - textFieldLabelsSpacing: CGFloat = 4, - textFieldContentInsets: UIEdgeInsets = .zero, - textFieldsBorderColor: UIColor = .black, - textFieldsBorderWidth: CGFloat = 1, - sliderColor: UIColor = .cyan, - sliderOffColor: UIColor = .darkGray, - thumbSize: CGFloat = 21, - textFieldsSpacing: CGFloat = 10, + public init(intervalInputAppearance: DefaultIntervalInputAppearance = .init(), + stepSliderAppearance: DefaultStepSliderAppearance = .init(), spacing: CGFloat = 16, leadingSpacing: CGFloat = 16, trailingSpacing: CGFloat = 16, fontColor: UIColor = .black) { - self.textFieldsHeight = textFieldsHeight - self.textFieldsWidth = textFieldsWidth - self.textFieldLabelsSpacing = textFieldLabelsSpacing - self.textFieldContentInsets = textFieldContentInsets - self.textFieldsBorderColor = textFieldsBorderColor - self.textFieldsBorderWidth = textFieldsBorderWidth - self.sliderColor = sliderColor - self.sliderOffColor = sliderOffColor - self.thumbSize = thumbSize - self.textFieldsSpacing = textFieldsSpacing + self.intervalInputAppearance = intervalInputAppearance + self.stepSliderAppearance = stepSliderAppearance self.spacing = spacing self.leadingSpacing = leadingSpacing self.trailingSpacing = trailingSpacing self.fontColor = fontColor } - - -} - -// MARK: - Default appearance - -public extension DefaultRangeFilterAppearance { - static var defaultAppearance: DefaultRangeFilterAppearance { - .init(textFieldContentInsets: .init(top: 4, left: 8, bottom: 4, right: 8)) - } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift new file mode 100644 index 00000000..3bd0b683 --- /dev/null +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public struct DefaultStepSliderAppearance { + + public var sliderColor: UIColor + public var sliderOffColor: UIColor + public var thumbSize: CGFloat + + public init(sliderColor: UIColor = .cyan, + sliderOffColor: UIColor = .darkGray, + thumbSize: CGFloat = 21) { + + self.sliderColor = sliderColor + self.sliderOffColor = sliderOffColor + self.thumbSize = thumbSize + } +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift index 449cb178..f07075ae 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/RangeFilterLayout.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + public enum RangeFilterLayout { case textFieldsOnTop case textFieldsFromBelow diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift index afc1e68c..121293ad 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift @@ -1,17 +1,50 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import Foundation open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { - open var formatter: NumberFormatter { - let formatter = NumberFormatter() + public let formatter: NumberFormatter + + open var floatValueDelimiter: String { + "." + } + + public init() { + formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 1 - - return formatter } - public init() { } + open func getIntervalInputLabel(state: BaseIntervalInputView.TextFieldState) -> String { + switch state { + case .fromValue: + return "от" + case .toValue: + return "до" + } + } open func string(fromDouble value: Double) -> String { let nsNumber = NSNumber(floatLiteral: value) @@ -22,4 +55,4 @@ open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { open func double(fromString value: String) -> Double { formatter.number(from: value)?.doubleValue ?? 0 } -} \ No newline at end of file +} diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift index 4f915588..4ef312dc 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift @@ -1,4 +1,30 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + public protocol RangeValuesFormatterProtocol { + var floatValueDelimiter: String { get } + + func getIntervalInputLabel(state: BaseIntervalInputView.TextFieldState) -> String + func string(fromDouble value: Double) -> String func double(fromString value: String) -> Double } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift index 0b416053..44ac121a 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Protocols/RangeFiltersPickerDelegate.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + public protocol RangeFiltersPickerDelegate: AnyObject { func valuesIsChanging(_ value: FilterRangeValue) func valueDidEndChanging(_ value: FilterRangeValue) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift index 442a8086..deaf5d82 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import CoreGraphics import Foundation import TISwiftUtils diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift index e614f9c3..d9965c90 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + public protocol RangeFilterViewModelProtocol { var fromValue: Double { get } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift index 01b34987..9da1217d 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import UIKit import TIUIElements import TIUIKitCore @@ -107,7 +129,7 @@ open class BaseFilterRangeView: BaseIni } } - open var appearance: DefaultRangeFilterAppearance = .defaultAppearance { + open var appearance: DefaultRangeFilterAppearance = .init() { didSet { updateAppearance() } @@ -251,22 +273,24 @@ open class BaseFilterRangeView: BaseIni // MARK: - Private methods private func updateAppearance() { + let intervalAppearance = appearance.intervalInputAppearance + let sliderAppearance = appearance.stepSliderAppearance + contentSpacing = appearance.spacing leadingInset = appearance.leadingSpacing trailingInset = appearance.trailingSpacing - - textFieldsHeight = appearance.textFieldsHeight - textFieldsMinWidth = appearance.textFieldsWidth - textFieldsContentInsets = appearance.textFieldContentInsets - textFieldsLabelsSpacing = appearance.textFieldLabelsSpacing - textFieldsBorderColor = appearance.textFieldsBorderColor - textFieldsBorderWidth = appearance.textFieldsBorderWidth fontColor = appearance.fontColor - rangeSlider.sliderColor = appearance.sliderColor - rangeSlider.sliderOffColor = appearance.sliderOffColor - rangeSlider.thumbSize = appearance.thumbSize + textFieldsHeight = intervalAppearance.textFieldsHeight + textFieldsMinWidth = intervalAppearance.textFieldsWidth + textFieldsContentInsets = intervalAppearance.textFieldContentInsets + textFieldsLabelsSpacing = intervalAppearance.textFieldLabelsSpacing + textFieldsBorderColor = intervalAppearance.textFieldsBorderColor + textFieldsBorderWidth = intervalAppearance.textFieldsBorderWidth + rangeSlider.sliderColor = sliderAppearance.sliderColor + rangeSlider.sliderOffColor = sliderAppearance.sliderOffColor + rangeSlider.thumbSize = sliderAppearance.thumbSize } private func configureTextFieldOnTop() { diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift index 849e8b8a..cd9e41ed 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/FilterRangeViewRepresenter.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + public protocol FilterRangeViewRepresenter: AnyObject { func configureTextFields(with value: FilterRangeValue) func configureRangeView(with value: FilterRangeValue) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift index 1bcf8c04..3dff48a2 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import TIUIElements import TIUIKitCore import UIKit @@ -17,19 +39,16 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { public let intervalLabel = UILabel() public let inputTextField = UITextField() - public var formatter: RangeValuesFormatterProtocol? - - open var intervalTextLabel: String { - switch state { - case .fromValue: - return "от" - case .toValue: - return "до" + public var formatter: RangeValuesFormatterProtocol? { + didSet { + updateState() } } open var validCharacterSet: CharacterSet { - CharacterSet.decimalDigits.union(CharacterSet.init(charactersIn: ".")) + CharacterSet + .decimalDigits + .union(CharacterSet(charactersIn: formatter?.floatValueDelimiter ?? ".")) } open var fontColor: UIColor = .black { @@ -121,12 +140,6 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { inputTextField.addTarget(target, action: #selector(formatValue), for: .editingChanged) } - open override func localize() { - super.localize() - - intervalLabel.text = intervalTextLabel - } - open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) @@ -141,6 +154,10 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { self.formatter = formatter } + open func updateState() { + intervalLabel.text = formatter?.getIntervalInputLabel(state: state) + } + // MARK: - UITextFieldDelegate open func textField(_ textField: UITextField, diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift index 3d27e16d..8bb7e87e 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import TIUIKitCore import UIKit diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift index 38faa578..508c3fe7 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/StepRangeSlider.swift @@ -1,3 +1,25 @@ +// +// Copyright (c) 2022 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + import TIUIKitCore import UIKit From 070d7c199ae6bbfe35dc0d69cf67e79e7d0495a6 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Mon, 3 Oct 2022 12:01:32 +0300 Subject: [PATCH 4/5] fix: code review notes --- .../Formatter/BaseRangeValuesFormatter.swift | 10 +-- .../ViewModels/BaseRangeFilterViewModel.swift | 56 +++++++------- .../RangeFilterViewModelProtocol.swift | 7 +- .../FilterRangeView/BaseFilterRangeView.swift | 77 +++++++++++-------- .../BaseIntervalInputView.swift | 25 +++--- .../SliderView/BaseFilterRangeSlider.swift | 16 ++-- 6 files changed, 103 insertions(+), 88 deletions(-) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift index 121293ad..001a05d5 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift @@ -26,9 +26,9 @@ open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { public let formatter: NumberFormatter - open var floatValueDelimiter: String { - "." - } + public var floatValueDelimiter = "." + + public var formattingFailureValue = 0.0 public init() { formatter = NumberFormatter() @@ -49,10 +49,10 @@ open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { open func string(fromDouble value: Double) -> String { let nsNumber = NSNumber(floatLiteral: value) - return formatter.string(from: nsNumber) ?? "" + return formatter.string(from: nsNumber) ?? "\(formattingFailureValue)" } open func double(fromString value: String) -> Double { - formatter.number(from: value)?.doubleValue ?? 0 + formatter.number(from: value)?.doubleValue ?? formattingFailureValue } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift index deaf5d82..ed3c90a4 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift @@ -24,16 +24,16 @@ import CoreGraphics import Foundation import TISwiftUtils -public typealias FilterRangeValue = (fromValue: Double, toValue: Double) +public typealias FilterRangeValue = (fromValue: CGFloat, toValue: CGFloat) open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { - public let fromValue: Double - public let toValue: Double - public let stepValues: [Double] + public let fromValue: CGFloat + public let toValue: CGFloat + public let stepValues: [CGFloat] - public var initialFromValue: Double? - public var initialToValue: Double? + public var initialFromValue: CGFloat? + public var initialToValue: CGFloat? public weak var filterRangeView: FilterRangeViewRepresenter? public weak var pickerDelegate: RangeFiltersPickerDelegate? @@ -46,11 +46,11 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { initialFromValue != fromValue || initialToValue != toValue } - public init(fromValue: Double, - toValue: Double, - stepValues: [Double], - initialFromValue: Double? = nil, - initialToValue: Double? = nil) { + public init(fromValue: CGFloat, + toValue: CGFloat, + stepValues: [CGFloat], + initialFromValue: CGFloat? = nil, + initialToValue: CGFloat? = nil) { self.fromValue = fromValue self.toValue = toValue @@ -69,23 +69,27 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { pickerDelegate?.valueDidEndChanging(values) } - open func fromValueIsChanging(_ values: FilterRangeValue) { - filterRangeView?.configureRangeView(with: values) - pickerDelegate?.valueDidEndChanging(values) + open func intervalInputValueIsChanging(_ values: FilterRangeValue, side: RangeBoundSide) { + switch side { + case lower: + filterRangeView?.configureRangeView(with: values) + pickerDelegate?.valueDidEndChanging(values) + + case upper: + filterRangeView?.configureRangeView(with: values) + pickerDelegate?.valueDidEndChanging(values) + } } - open func toValueIsChanging(_ values: FilterRangeValue) { - filterRangeView?.configureRangeView(with: values) - pickerDelegate?.valueDidEndChanging(values) - } + open func intervalInputValueDidEndChanging(_ values: FilterRangeValue, side: RangeBoundSide) { + switch side { + case lower: + filterRangeView?.configureRangeView(with: values) + pickerDelegate?.valueDidEndChanging(values) - open func fromValueDidEndChanging(_ values: FilterRangeValue) { - filterRangeView?.configureRangeView(with: values) - pickerDelegate?.valueDidEndChanging(values) - } - - open func toValueDidEndChanging(_ values: FilterRangeValue) { - filterRangeView?.configureRangeView(with: values) - pickerDelegate?.valueDidEndChanging(values) + case upper: + filterRangeView?.configureRangeView(with: values) + pickerDelegate?.valueDidEndChanging(values) + } } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift index d9965c90..81c925a4 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift @@ -31,9 +31,6 @@ public protocol RangeFilterViewModelProtocol { func rangeSliderValueIsChanging(_ values: FilterRangeValue) func rangeSliderValueDidEndChanging(_ values: FilterRangeValue) - func toValueIsChanging(_ values: FilterRangeValue) - func toValueDidEndChanging(_ values: FilterRangeValue) - - func fromValueDidEndChanging(_ values: FilterRangeValue) - func fromValueIsChanging(_ values: FilterRangeValue) + func intervalInputValueIsChanging(_ values: FilterRangeValue, side: RangeBoundSide) + func intervalInputValueDidEndChanging(_ values: FilterRangeValue, side: RangeBoundSide) } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift index 9da1217d..0bf85b40 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift @@ -41,20 +41,22 @@ open class BaseFilterRangeView: BaseIni public let rangeSlider: BaseFilterRangeSlider public let textFieldsContainer = UIView() - public let fromValueView = BaseIntervalInputView(state: .fromValue) - public let toValueView = BaseIntervalInputView(state: .toValue) + public let fromValueView = BaseIntervalInputView(state: .lower) + public let toValueView = BaseIntervalInputView(state: .upper) public var viewModel: ViewModel? // MARK: - Open properties open var rangeSliderValue: FilterRangeValue { - (rangeSlider.leftValue, rangeSlider.rightValue) + (fromValue: rangeSlider.leftValue, toValue: rangeSlider.rightValue) } open var textFieldsValues: FilterRangeValue { - (min(fromValueView.currentValue, viewModel?.toValue ?? .zero), - max(toValueView.currentValue, viewModel?.fromValue ?? .zero)) + ( + fromValue: min(fromValueView.currentValue, viewModel?.toValue ?? .zero), + toValue: max(toValueView.currentValue, viewModel?.fromValue ?? .zero) + ) } // MARK: - Text Fields Setup @@ -183,26 +185,36 @@ open class BaseFilterRangeView: BaseIni let toTextFieldWidthConstraint = toValueView.widthAnchor.constraint(greaterThanOrEqualToConstant: textFieldsMinWidth) let fromTextFieldWidthConstraint = fromValueView.widthAnchor.constraint(greaterThanOrEqualToConstant: textFieldsMinWidth) + let fromTextFieldConstraints = [ + fromTextFieldWidthConstraint, + fromValueView.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), + fromValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), + fromValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), + ] + + let toTextFieldConstraints = [ + toTextFieldWidthConstraint, + toValueView.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), + toValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), + toValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), + ] + + let sliderConstraints = [ + rangeSlider.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), + rangeSlider.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), + layout == .textFieldsOnTop + ? rangeSlider.bottomAnchor.constraint(equalTo: bottomAnchor) + : rangeSlider.topAnchor.constraint(equalTo: topAnchor) + ] + NSLayoutConstraint.activate( contentEdgesConstraints.allConstraints + - [ - toTextFieldWidthConstraint, - fromTextFieldWidthConstraint, - - toValueView.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), - toValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), - toValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), - fromValueView.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), - fromValueView.topAnchor.constraint(equalTo: textFieldsContainer.topAnchor), - fromValueView.bottomAnchor.constraint(equalTo: textFieldsContainer.bottomAnchor), - - rangeSlider.leadingAnchor.constraint(equalTo: textFieldsContainer.leadingAnchor), - rangeSlider.trailingAnchor.constraint(equalTo: textFieldsContainer.trailingAnchor), - layout == .textFieldsOnTop - ? rangeSlider.bottomAnchor.constraint(equalTo: bottomAnchor) - : rangeSlider.topAnchor.constraint(equalTo: topAnchor) - ] + fromTextFieldConstraints + + + toTextFieldConstraints + + + sliderConstraints ) self.toTextFieldWidthConstraint = toTextFieldWidthConstraint @@ -212,6 +224,8 @@ open class BaseFilterRangeView: BaseIni open override func bindViews() { super.bindViews() + defaultConfigure() + fromValueView.configure(with: formatter) toValueView.configure(with: formatter) rangeSlider.configure(with: formatter) @@ -234,7 +248,6 @@ open class BaseFilterRangeView: BaseIni open override func configureAppearance() { super.configureAppearance() - defaultConfigure() updateAppearance() layoutSubviews() @@ -250,13 +263,13 @@ open class BaseFilterRangeView: BaseIni fromValueView.configure(with: viewModel.initialFromValue ?? viewModel.fromValue) toValueView.configure(with: viewModel.initialToValue ?? viewModel.toValue) - rangeSlider.minimumValue = CGFloat(viewModel.fromValue) - rangeSlider.maximumValue = CGFloat(viewModel.toValue) + rangeSlider.minimumValue = viewModel.fromValue + rangeSlider.maximumValue = viewModel.toValue - rangeSlider.leftValue = CGFloat(viewModel.initialFromValue ?? viewModel.fromValue) - rangeSlider.rightValue = CGFloat(viewModel.initialToValue ?? viewModel.toValue) + rangeSlider.leftValue = viewModel.initialFromValue ?? viewModel.fromValue + rangeSlider.rightValue = viewModel.initialToValue ?? viewModel.toValue - rangeSlider.addStep(stepValues: viewModel.stepValues.map { CGFloat($0) }) + rangeSlider.addStep(stepValues: viewModel.stepValues) } // MARK: - FilterRangeViewRepresenter @@ -318,18 +331,18 @@ open class BaseFilterRangeView: BaseIni } @objc private func fromValueIsChanging() { - viewModel?.fromValueIsChanging(textFieldsValues) + viewModel?.intervalInputValueIsChanging(textFieldsValues, side: .lower) } @objc private func toValueIsChanging() { - viewModel?.toValueIsChanging(textFieldsValues) + viewModel?.intervalInputValueIsChanging(textFieldsValues, side: .upper) } @objc private func fromValueDidEndChanging() { - viewModel?.fromValueDidEndChanging(textFieldsValues) + viewModel?.intervalInputValueDidEndChanging(textFieldsValues, side: .lower) } @objc private func toValueDidEndChanging() { - viewModel?.toValueDidEndChanging(textFieldsValues) + viewModel?.intervalInputValueDidEndChanging(textFieldsValues, side: .upper) } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift index 3dff48a2..2859b633 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift @@ -24,17 +24,17 @@ import TIUIElements import TIUIKitCore import UIKit -open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { +public enum RangeBoundSide { + case lower + case upper +} - public enum TextFieldState { - case fromValue - case toValue - } +open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { private var contentEdgesConstraints: EdgeConstraints? private var labelsSpacingConstraint: NSLayoutConstraint? - public let state: TextFieldState + public let state: RangeBoundSide public let intervalLabel = UILabel() public let inputTextField = UITextField() @@ -45,11 +45,11 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { } } - open var validCharacterSet: CharacterSet { + open lazy var validCharacterSet: CharacterSet = { CharacterSet .decimalDigits .union(CharacterSet(charactersIn: formatter?.floatValueDelimiter ?? ".")) - } + }() open var fontColor: UIColor = .black { didSet { @@ -65,7 +65,12 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { } open var currentValue: Double { - formatter?.double(fromString: inputTextField.text ?? "") ?? 0 + guard let formatter = formatter, + let inputText = inputTextField.text else { + return 0 + } + + return formatter.double(fromString: inputTextField.text) } open override var intrinsicContentSize: CGSize { @@ -78,7 +83,7 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { // MARK: - Init - public init(state: TextFieldState) { + public init(state: RangeBoundSide) { self.state = state super.init(frame: .zero) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift index 8bb7e87e..c3d2b27a 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift @@ -184,14 +184,16 @@ open class BaseFilterRangeSlider: StepRangeSlider { } private func createStepLabels() { + guard let formatter = formatter else { return } + stepLabels.forEach { $0.removeFromSuperview() } stepValues.forEach { let label = UILabel() - let formattedText = formatter?.string(fromDouble: $0) - label.text = formattedText ?? "" + let formattedText = formatter.string(fromDouble: $0) + label.text = formattedText stepLabels.append(label) } @@ -207,8 +209,8 @@ open class BaseFilterRangeSlider: StepRangeSlider { let circleView = stepCircleViews[index] let yPosition = layout == .textFieldsOnTop - ? circleView.frame.maxY + .stepLabelsTopInset - : circleView.frame.minY - label.bounds.height - .stepLabelsTopInset + ? circleView.frame.maxY + : circleView.frame.minY - label.bounds.height label.center.x = circleView.center.x label.sizeToFit() @@ -224,9 +226,3 @@ open class BaseFilterRangeSlider: StepRangeSlider { return (value - minimumValue) * unit } } - -// MARK: - Constant - -private extension CGFloat { - static let stepLabelsTopInset: CGFloat = 11 -} From 72aabd4412f87da2d076725511962e29677703b4 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Mon, 3 Oct 2022 12:42:26 +0300 Subject: [PATCH 5/5] fix: code review notes + small fixes --- .../Appearance/DefaultStepSliderAppearance.swift | 5 ++++- .../Models/Formatter/BaseRangeValuesFormatter.swift | 6 +++--- .../Formatter/RangeValuesFormatterProtocol.swift | 2 +- .../ViewModels/BaseRangeFilterViewModel.swift | 8 ++++---- .../ViewModels/RangeFilterViewModelProtocol.swift | 12 +++++++----- .../Views/FilterRangeView/BaseFilterRangeView.swift | 5 +++-- .../IntervalInputView/BaseIntervalInputView.swift | 2 +- .../Views/SliderView/BaseFilterRangeSlider.swift | 10 ++++++++-- 8 files changed, 31 insertions(+), 19 deletions(-) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift index 3bd0b683..5d3d25ea 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Appearance/DefaultStepSliderAppearance.swift @@ -27,13 +27,16 @@ public struct DefaultStepSliderAppearance { public var sliderColor: UIColor public var sliderOffColor: UIColor public var thumbSize: CGFloat + public var stepLabelsOffset: CGFloat public init(sliderColor: UIColor = .cyan, sliderOffColor: UIColor = .darkGray, - thumbSize: CGFloat = 21) { + thumbSize: CGFloat = 21, + stepLabelsOffset: CGFloat = 11) { self.sliderColor = sliderColor self.sliderOffColor = sliderOffColor self.thumbSize = thumbSize + self.stepLabelsOffset = stepLabelsOffset } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift index 001a05d5..647211b4 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/BaseRangeValuesFormatter.swift @@ -37,11 +37,11 @@ open class BaseRangeValuesFormatter: RangeValuesFormatterProtocol { formatter.maximumFractionDigits = 1 } - open func getIntervalInputLabel(state: BaseIntervalInputView.TextFieldState) -> String { + open func getIntervalInputLabel(state: RangeBoundSide) -> String { switch state { - case .fromValue: + case .lower: return "от" - case .toValue: + case .upper: return "до" } } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift index 4ef312dc..22915c1b 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Models/Formatter/RangeValuesFormatterProtocol.swift @@ -23,7 +23,7 @@ public protocol RangeValuesFormatterProtocol { var floatValueDelimiter: String { get } - func getIntervalInputLabel(state: BaseIntervalInputView.TextFieldState) -> String + func getIntervalInputLabel(state: RangeBoundSide) -> String func string(fromDouble value: Double) -> String func double(fromString value: String) -> Double diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift index ed3c90a4..ce1722c1 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/BaseRangeFilterViewModel.swift @@ -71,11 +71,11 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { open func intervalInputValueIsChanging(_ values: FilterRangeValue, side: RangeBoundSide) { switch side { - case lower: + case .lower: filterRangeView?.configureRangeView(with: values) pickerDelegate?.valueDidEndChanging(values) - case upper: + case .upper: filterRangeView?.configureRangeView(with: values) pickerDelegate?.valueDidEndChanging(values) } @@ -83,11 +83,11 @@ open class BaseRangeFilterViewModel: RangeFilterViewModelProtocol { open func intervalInputValueDidEndChanging(_ values: FilterRangeValue, side: RangeBoundSide) { switch side { - case lower: + case .lower: filterRangeView?.configureRangeView(with: values) pickerDelegate?.valueDidEndChanging(values) - case upper: + case .upper: filterRangeView?.configureRangeView(with: values) pickerDelegate?.valueDidEndChanging(values) } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift index 81c925a4..a8ae07e5 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/ViewModels/RangeFilterViewModelProtocol.swift @@ -20,13 +20,15 @@ // THE SOFTWARE. // +import CoreGraphics + public protocol RangeFilterViewModelProtocol { - var fromValue: Double { get } - var toValue: Double { get } - var stepValues: [Double] { get } - var initialFromValue: Double? { get } - var initialToValue: Double? { get } + var fromValue: CGFloat { get } + var toValue: CGFloat { get } + var stepValues: [CGFloat] { get } + var initialFromValue: CGFloat? { get } + var initialToValue: CGFloat? { get } func rangeSliderValueIsChanging(_ values: FilterRangeValue) func rangeSliderValueDidEndChanging(_ values: FilterRangeValue) diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift index 0bf85b40..cfb123f1 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/FilterRangeView/BaseFilterRangeView.swift @@ -224,8 +224,6 @@ open class BaseFilterRangeView: BaseIni open override func bindViews() { super.bindViews() - defaultConfigure() - fromValueView.configure(with: formatter) toValueView.configure(with: formatter) rangeSlider.configure(with: formatter) @@ -243,6 +241,8 @@ open class BaseFilterRangeView: BaseIni rangeSlider.addTarget(self, action: #selector(rangeSliderValueDidEndChanging), for: .editingDidEnd) + + defaultConfigure() } open override func configureAppearance() { @@ -304,6 +304,7 @@ open class BaseFilterRangeView: BaseIni rangeSlider.sliderColor = sliderAppearance.sliderColor rangeSlider.sliderOffColor = sliderAppearance.sliderOffColor rangeSlider.thumbSize = sliderAppearance.thumbSize + rangeSlider.stepLabelsOffset = sliderAppearance.stepLabelsOffset } private func configureTextFieldOnTop() { diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift index 2859b633..4002abd7 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/IntervalInputView/BaseIntervalInputView.swift @@ -70,7 +70,7 @@ open class BaseIntervalInputView: BaseInitializableView, UITextFieldDelegate { return 0 } - return formatter.double(fromString: inputTextField.text) + return formatter.double(fromString: inputText) } open override var intrinsicContentSize: CGSize { diff --git a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift index c3d2b27a..33806717 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/RangeFilters/Views/SliderView/BaseFilterRangeSlider.swift @@ -78,6 +78,12 @@ open class BaseFilterRangeSlider: StepRangeSlider { } } + open var stepLabelsOffset: CGFloat = 0 { + didSet { + setNeedsLayout() + } + } + open override var intrinsicContentSize: CGSize { let superSize = super.intrinsicContentSize @@ -209,8 +215,8 @@ open class BaseFilterRangeSlider: StepRangeSlider { let circleView = stepCircleViews[index] let yPosition = layout == .textFieldsOnTop - ? circleView.frame.maxY - : circleView.frame.minY - label.bounds.height + ? circleView.frame.maxY + stepLabelsOffset + : circleView.frame.minY - label.bounds.height - stepLabelsOffset label.center.x = circleView.center.x label.sizeToFit()