From 65858ebfe78aa4233a97c12ddc0b9a55ad206aed Mon Sep 17 00:00:00 2001 From: Yurii Lysytsia Date: Sat, 24 Oct 2020 13:18:59 +0300 Subject: [PATCH] 1.4.1 --- AirCollection.podspec | 2 +- .../contents.xcworkspacedata | 17 + .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../collection.md | 0 README_TABLE.md => Documents/table.md | 0 Documents/view.md | 315 +++++++++++ Example.xcodeproj/project.pbxproj | 528 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Example.xcscheme | 78 +++ Example/AppDelegate.swift | 29 + Example/Models/Story.swift | 20 + Example/Models/User.swift | 27 + Example/ModuleFabric.swift | 41 ++ .../Cell/DynamicStoryTableViewCell.swift | 36 ++ .../Cell/DynamicStoryTableViewCell.xib | 62 ++ .../DynamicStoryTablePresenter.swift | 115 ++++ .../DynamicStoryTableViewController.swift | 66 +++ .../DynamicStoryTableViewController.xib | 24 + ...DynamicTitleDescriptionTableViewCell.swift | 45 ++ .../DynamicTitleDescriptionTableViewCell.xib | 54 ++ .../DynamicTable/DynamicTablePresenter.swift | 100 ++++ .../DynamicTableViewController.swift | 75 +++ .../Cells/DynamicUserTableViewCell.swift | 37 ++ .../Cells/DynamicUserTableViewCell.xib | 53 ++ .../DynamicUserTablePresenter.swift | 191 +++++++ .../DynamicUserTableViewController.swift | 70 +++ .../Views/DynamicUserFooterView.swift | 35 ++ .../Views/DynamicUserFooterView.xib | 38 ++ Example/Modules/Home/HomePresenter.swift | 68 +++ .../Cell/StaticTableViewCell.swift | 45 ++ .../StaticTable/StaticTablePresenter.swift | 70 +++ .../StaticTableViewController.swift | 61 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Resources/Assets.xcassets/Contents.json | 6 + Example/Resources/Info.plist | 45 ++ Example/Resources/LaunchScreen.storyboard | 49 ++ README.md | 19 +- README_VIEW.md | 7 - .../project.pbxproj | 118 ++-- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Source.xcscheme | 17 +- Source/AirCollection.h | 17 - .../CollectionView+Extension.swift | 20 +- .../CollectionView+TextField.swift | 78 +++ .../CollectionView+TextFieldDatePicker.swift | 1 + .../CollectionView+TextView.swift | 81 +++ .../CollectionViewControllerProtocol.swift | 44 +- .../CollectionView/CollectionViewData.swift | 9 +- .../CollectionViewDelegate.swift | 1 + .../CollectionViewPresenterProtocol.swift | 1 + Source/Common/Array+Extension.swift | 2 +- Source/Common/ConfigurableView.swift | 40 +- .../Common/DatePickerControllerProtocol.swift | 2 +- .../Common/DatePickerPresenterProtocol.swift | 5 +- Source/Common/IdentificableView.swift | 9 +- Source/Common/ModelConfigurableView.swift | 43 ++ Source/Common/NibLoadableView.swift | 52 +- .../Common/PickerViewControllerProtocol.swift | 4 +- .../Common/PickerViewPresenterProtocol.swift | 4 +- Source/Common/PickerViewTitle.swift | 4 +- Source/Common/TextFieldConfiguration.swift | 181 +++++- .../Common/TextFieldControllerProtocol.swift | 14 + .../TextFieldDatePickerConfiguration.swift | 21 +- .../TextFieldPickerViewConfiguration.swift | 19 +- .../Common/TextFieldPresenterProtocol.swift | 73 +++ Source/Common/TextInputConfigurableView.swift | 83 ++- Source/Common/TextViewConfiguration.swift | 274 +++++++++ .../Common/TextViewControllerProtocol.swift | 15 + .../TextViewInputModelConfigurable.swift | 43 -- Source/Common/TextViewPresenterProtocol.swift | 77 +++ Source/TableView/TableView+Extension.swift | 8 +- Source/TableView/TableView+TextField.swift | 77 +++ .../TableView+TextFieldDatePicker.swift | 1 + Source/TableView/TableView+TextView.swift | 80 +++ .../TableViewControllerProtocol.swift | 44 +- Source/TableView/TableViewData.swift | 26 +- Source/TableView/TableViewDelegate.swift | 1 + .../TableViewPresenterProtocol.swift | 1 + 81 files changed, 3863 insertions(+), 287 deletions(-) create mode 100644 AirCollection.xcworkspace/contents.xcworkspacedata rename {AirCollection.xcodeproj/project.xcworkspace => AirCollection.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename README_COLLECTION.md => Documents/collection.md (100%) rename README_TABLE.md => Documents/table.md (100%) create mode 100644 Documents/view.md create mode 100644 Example.xcodeproj/project.pbxproj create mode 100644 Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme create mode 100644 Example/AppDelegate.swift create mode 100644 Example/Models/Story.swift create mode 100644 Example/Models/User.swift create mode 100644 Example/ModuleFabric.swift create mode 100644 Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.swift create mode 100644 Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.xib create mode 100644 Example/Modules/DynamicStoryTable/DynamicStoryTablePresenter.swift create mode 100644 Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.swift create mode 100644 Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.xib create mode 100644 Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.swift create mode 100644 Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.xib create mode 100644 Example/Modules/DynamicTable/DynamicTablePresenter.swift create mode 100644 Example/Modules/DynamicTable/DynamicTableViewController.swift create mode 100644 Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.swift create mode 100644 Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.xib create mode 100644 Example/Modules/DynamicUserTable/DynamicUserTablePresenter.swift create mode 100644 Example/Modules/DynamicUserTable/DynamicUserTableViewController.swift create mode 100644 Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.swift create mode 100644 Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.xib create mode 100644 Example/Modules/Home/HomePresenter.swift create mode 100644 Example/Modules/StaticTable/Cell/StaticTableViewCell.swift create mode 100644 Example/Modules/StaticTable/StaticTablePresenter.swift create mode 100644 Example/Modules/StaticTable/StaticTableViewController.swift create mode 100644 Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/Resources/Assets.xcassets/Contents.json create mode 100644 Example/Resources/Info.plist create mode 100644 Example/Resources/LaunchScreen.storyboard delete mode 100644 README_VIEW.md rename {AirCollection.xcodeproj => Source.xcodeproj}/project.pbxproj (80%) rename {AirCollection.xcodeproj => Source.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 Source.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename AirCollection.xcodeproj/xcshareddata/xcschemes/AirCollection.xcscheme => Source.xcodeproj/xcshareddata/xcschemes/Source.xcscheme (75%) delete mode 100644 Source/AirCollection.h create mode 100644 Source/CollectionView/CollectionView+TextField.swift create mode 100644 Source/CollectionView/CollectionView+TextView.swift create mode 100644 Source/Common/ModelConfigurableView.swift create mode 100644 Source/Common/TextFieldControllerProtocol.swift create mode 100644 Source/Common/TextFieldPresenterProtocol.swift create mode 100644 Source/Common/TextViewConfiguration.swift create mode 100644 Source/Common/TextViewControllerProtocol.swift delete mode 100644 Source/Common/TextViewInputModelConfigurable.swift create mode 100644 Source/Common/TextViewPresenterProtocol.swift create mode 100644 Source/TableView/TableView+TextField.swift create mode 100644 Source/TableView/TableView+TextView.swift diff --git a/AirCollection.podspec b/AirCollection.podspec index 9dec1ac..28ba7cc 100644 --- a/AirCollection.podspec +++ b/AirCollection.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |spec| # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # spec.name = "AirCollection" - spec.version = "1.4.0" + spec.version = "1.4.1" spec.summary = "AirCollection is a wrapper for UITableView / UICollectionView for VIPER / MVP architecture" spec.homepage = "https://github.com/YuriFox/AirCollection" diff --git a/AirCollection.xcworkspace/contents.xcworkspacedata b/AirCollection.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..228ef2d --- /dev/null +++ b/AirCollection.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/AirCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/AirCollection.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from AirCollection.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to AirCollection.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/README_COLLECTION.md b/Documents/collection.md similarity index 100% rename from README_COLLECTION.md rename to Documents/collection.md diff --git a/README_TABLE.md b/Documents/table.md similarity index 100% rename from README_TABLE.md rename to Documents/table.md diff --git a/Documents/view.md b/Documents/view.md new file mode 100644 index 0000000..8240db9 --- /dev/null +++ b/Documents/view.md @@ -0,0 +1,315 @@ +# AirCollection.View +All `AirCollection` views (include cells) based on protocols. View able to implement several protocols to describe needed functionality + +- [Base protocols](#base-protocols) + - [Identificable View](#identificable-view) + - [Nib Loadable View](#nib-loadable-view) + - [Configurable View](#configurable-view) + - [Model Configurable View](#model-configurable-view) + - [Text Input Configurable View](#text-input-configurable-view) +- [Table view cell](#table-view-cell) + - [Code layouts](#code-layouts) + - [Xib layouts](#xib-layouts) + - [Implement `TextInputConfigurableView`](#implement-textinputconfigurableview) +- [Collection view cell](#collection-view-cell) + +## Base protocols + +### Identificable View +Protocol needed for implement unique view identifier and defines only one property `viewIdentifier`. By default this property is equal to `String(describing: self)` +```swift +public protocol IdentificableView: class { + static var viewIdentifier: String { get } +} +``` + +### Nib Loadable View +Protocol needed for implement view nib instantiate and defines only one property `viewNib` +```swift +public protocol NibLoadableView: class { + static var viewNib: UINib { get } +} +``` + +This protocol has default implementation for view which implement `IdentificableView` protocol. So when you use both protocols you don't need implement any properties by default +```swift +static var viewNib: UINib { + let nibName = self.viewIdentifier + let bundle = Bundle(for: Self.self) + return UINib(nibName: nibName, bundle: bundle) +} +``` + +### Configurable View +Abstract protocol needed for implement configure view method and defines one method `configure(_:)`. You shouldn't use this protocol for your views (include cells) implementation, but you able to create additional protocol that implement default implementation. Read about [ModelConfigurableView](#model-configurable-view) for additional information +``` swift +public protocol ConfigurableView: class { + func configure(_ model: Any) +} +``` + +### Model Configurable View +Base `ConfigurableView` protocol implementation and defines new one safe method `configure(model:)` based on predefined associated `Model` type. You should use this protocol for your views (include cells) implementation instead `ConfigurableView` +```swift +public protocol ModelConfigurableView: ConfigurableView { + associatedtype Model + func configure(model: Model) +} +``` + +### Text Input Configurable View +Child `ModelConfigurableView` protocol implementation and defines new property `textInputView` and associated `TextInputView` type. Use this protocol when you need observe some text input subview (e.g. `UITextField` or `UITextView`) inside your reusable view. **Important!** This protocol is able to implement only for `Model` which implement [TextInputConfigurableModel](#text-input-configurable-model). +```swift +public protocol TextInputConfigurableView: ModelConfigurableView where Model: TextInputConfigurableModel { + associatedtype TextInputView: UIView + var textInputView: TextInputView { get } +} +``` + +#### Text Input Configurable Model +Aditional protocol for `ConfigurableView.Model` that defines new property `textInputConfiguration` and associated `Configuration` type. Read topics below for aditional information +```swift +public protocol TextInputConfigurableModel { + associatedtype Configuration: TextInputConfiguration + var textInputConfiguration: Configuration { get } +} +``` + +Use this protocol for `ConfigurableView.Model` and implement `textInputConfiguration` with type that you need. e.g. `TextFieldConfiguration`: +```Swift +struct Model: TextInputConfigurableModel { + let textInputConfiguration: TextFieldConfiguration +} +``` + +#### Text Input Configuration +Abstract protocol needed for implement `TextInputConfigurableView` that defines one method `configure(textInputView:)` and associated `TextInputView` type. You shouldn't use this protocol for your views (include cells) implementation, but you able to create additional protocol that implement default implementation. Read topics below for aditional information. +```swift +public protocol TextInputConfiguration { + associatedtype TextInputView: UIView + func configure(textInputView: TextInputView) +} +``` + +You are able to use already created implementation. But if you won't find anything suitable could create it or write to author. + +#### Text Field Configuration +Configuration for `UITextField` with keyboard and some basic configurations. Use it both with `TextFieldDelegate` +```swift +public protocol TextFieldDelegate: class { + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool + func textFieldDidBeginEditing(_ textField: UITextField) + func textFieldShouldEndEditing(_ textField: UITextField) -> Bool + func textFieldDidEndEditing(_ textField: UITextField) + func textField(_ textField: UITextField, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool + func textFieldShouldClear(_ textField: UITextField) -> Bool + func textFieldShouldReturn(_ textField: UITextField) -> Bool + func textFieldEditingChanged(_ textField: UITextField) +} + +open class TextFieldConfiguration: TextInputConfiguration { + /// The auto-capitalization style for the text object. Default is `UITextAutocapitalizationType.sentences` + public var autocapitalizationType: UITextAutocapitalizationType = .sentences + /// The autocorrection style for the text object. Default is `UITextAutocorrectionType.default` + public var autocorrectionType: UITextAutocorrectionType = .default + /// Controls when the standard clear button appears in the text field. Default is `UITextField.ViewMode.never` + public var clearButtonMode: UITextField.ViewMode = .never + /// The custom input view to display when the text field becomes the first responder. Default is nil + public var inputView: UIView? = nil + /// Identifies whether the text object should disable text copying and in some cases hide the text being entered. Default is `false` + public var isSecureTextEntry: Bool = false + /// The appearance style of the keyboard that is associated with the text object. Default is `UIKeyboardAppearance.default` + public var keyboardAppearance: UIKeyboardAppearance = .default + /// The keyboard style associated with the text object. Default is `UIKeyboardType.default` + public var keyboardType: UIKeyboardType = .default + /// The visible title of the Return key. Default is `UIReturnKeyType.default` + public var returnKeyType: UIReturnKeyType = .default + /// The configuration state for smart dashes. Default is `UITextSmartDashesType.default` + public var smartDashesType: UITextSmartDashesType = .default + /// The configuration state for smart quotes. Default is `UITextSmartQuotesType.default` + public var smartQuotesType: UITextSmartQuotesType = .default + /// The configuration state for the smart insertion and deletion of space characters. Default is `UITextSmartInsertDeleteType.default` + public var smartInsertDeleteType: UITextSmartInsertDeleteType = .default + /// The spell-checking style for the text object. Default is `UITextSpellCheckingType.default` + public var spellCheckingType: UITextSpellCheckingType = .default + /// The semantic meaning expected by a text input area. Default is nil + public var textContentType: UITextContentType? = nil +} +``` + +#### Text Field Picker View Configuration +Configuration for `UITextField` too. But there is `UIPickerView` instead keyboard and some basic configurations. Use it both with `TextFieldPickerViewDelegate` and `TextFieldPickerViewDataSource` +```swift +public protocol TextFieldPickerViewDataSource: class { + func textField(_ textField: UITextField, numberOfComponentsInPickerView pickerView: UIPickerView) -> Int + func textField(_ textField: UITextField, pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int + func textField(_ textField: UITextField, pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> PickerViewTitle? +} + +public protocol TextFieldPickerViewDelegate: TextFieldDelegate { + func textField(_ textField: UITextField, pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) + func textField(_ textField: UITextField, pickerView: UIPickerView, shouldUpdateTextFromRow row: Int, inComponent component: Int) -> Bool + func textField(_ textField: UITextField, pickerView: UIPickerView, selectedRowInComponent component: Int) -> Int +} + +public class TextFieldPickerViewConfiguration: TextFieldConfiguration { + /// Picker view which will use as text field `inputView` + public let pickerView: UIPickerView + /// Methods will call by picker view for needed actions + public unowned let pickerViewDataSource: TextFieldPickerViewDataSource + /// Methods will call by picker view for notify about actions actions + public unowned let pickerViewDelegate: TextFieldPickerViewDelegate + + public init(pickerView: UIPickerView = UIPickerView(), dataSource: TextFieldPickerViewDataSource, delegate: TextFieldPickerViewDelegate) + + public convenience init(pickerView: UIPickerView = UIPickerView(), controller: TextFieldPickerViewControllerProtocol) + +} +``` + +#### Text Field Date Picker Configuration +Configuration for `UITextField` too. But there is `UIDatePicker` instead keyboard and some basic configurations. Use it both with `TextFieldDatePickerDelegate` +```swift +public protocol TextFieldDatePickerDelegate: TextFieldDelegate { + func textField(_ textField: UITextField, datePicker: UIDatePicker, didSelectDate date: Date) + func textField(_ textField: UITextField, datePicker: UIDatePicker, shouldUpdateTextFromDate date: Date) -> String? +} + +public class TextFieldDatePickerConfiguration: TextFieldConfiguration { + public let datePicker: UIDatePicker + public unowned let datePickerDelegate: TextFieldDatePickerDelegate + + public init(datePicker: UIDatePicker, delegate: TextFieldDatePickerDelegate) + + public convenience init(mode: UIDatePicker.Mode, date: Date = Date(), minimumDate: Date? = nil, maximumDate: Date? = nil, delegate: TextFieldDatePickerDelegate) +``` + +#### Text View Configuration +Configuration for `UITextView` with keyboard and some basic configurations. Use it both with `TextViewDelegate` +```swift +public protocol TextViewDelegate: class { + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool + func textViewShouldEndEditing(_ textView: UITextView) -> Bool + func textViewDidBeginEditing(_ textView: UITextView) + func textViewDidEndEditing(_ textView: UITextView) + func textView(_ textView: UITextView, shouldChangeTextIn range: Range, replacementText text: String) -> Bool + func textViewDidChange(_ textView: UITextView) + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool +} + +open class TextViewConfiguration: TextInputConfiguration { + public var autocapitalizationType: UITextAutocapitalizationType = .sentences + public var autocorrectionType: UITextAutocorrectionType = .default + public var dataDetectorTypes: UIDataDetectorTypes = .all + public var inputView: UIView? = nil + public var isSecureTextEntry: Bool = false + public var isSelectable: Bool = true + public var keyboardAppearance: UIKeyboardAppearance = .default + public var keyboardType: UIKeyboardType = .default + public var returnKeyType: UIReturnKeyType = .default + public var smartDashesType: UITextSmartDashesType = .default + public var smartQuotesType: UITextSmartQuotesType = .default + public var smartInsertDeleteType: UITextSmartInsertDeleteType = .default + public var spellCheckingType: UITextSpellCheckingType = .default + public var textContentType: UITextContentType? = nil +} + +``` + + +## Table view cell +Table view cell should implement [Identificable View](#identificable-view), one child implementation of [Configurable View](#configurable-view) and optionally [Nib Loadable View](#nib-loadable-view) if cell layouts have `Xib` file. + +### Code layouts +First of all create your `UITableViewCell` class and implement `IdentificableView` and `ModelConfigurableView` protocols. +```swift +class TableViewCell: UITableViewCell, IdentificableView, ModelConfigurableView +``` + +Than add needed subviews and their layouts. In my example it just `titleLabel` +```swift +private let titleLabel: UILabel = UILabel() + +override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.addSubview(self.titleLabel) +} + +required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") +} + +override func layoutSubviews() { + super.layoutSubviews() + self.titleLabel.frame = self.bounds.inset(by: self.layoutMargins) +} +``` + +Finally implement `ModelConfigurableView`. Create predefined model for configuration (in my example is `struct Model`) with needed primitive properties (in my example is `let title: String`) and add required `configure(model:)` method where configure all needed subviews by predefined model properties. **Caution!** Make sure you are using a `configure(model: Model)` and `configure(_ model: Any)`. +```swift +func configure(model: Model) { + self.titleLabel.text = model.title +} + +struct Model { + let title: String +} +``` + +There is the simplest example when you need to create table view cell with code layouts. For more details see [StaticTableViewCell](Example/Modules/StaticTable/Cell/StaticTableViewCell.swift) in my project example + +### Xib layouts +First of all create your `UITableViewCell` class and implement `IdentificableView`, `NibLoadableView` and `ModelConfigurableView` protocols. +```swift +class NibTableViewCell: UITableViewCell, IdentificableView, NibLoadableView, ModelConfigurableView +``` + +Than add outlets for subviews and `.xib` file for layouts. In my example it `titleLabel` and `descriptionLabel` +```swift +@IBOutlet private weak var titleLabel: UILabel! +@IBOutlet private weak var descriptionLabel: UILabel! +``` + +Finally implement `ModelConfigurableView`. Create predefined model for configuration (in my example is `struct Model`) with needed primitive properties (in my example is `let title: String` and `let description: String?`) and add required `configure(model:)` method where configure all needed subviews by predefined model properties. **Caution!** Make sure you are using a `configure(model: Model)` and `configure(_ model: Any)`. +```swift +func configure(model: Model) { + self.titleLabel.text = model.title + self.descriptionLabel.text = model.description + self.descriptionLabel.isHidden = model.description == nil +} + +struct Model { + let title: String + let description: String? +} +``` + +There is the simplest example when you need to create table view cell with xib layouts. For more details see [DynamicTitleDescriptionTableViewCell](Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.swift) in my project example + +### Implement `TextInputConfigurableView` +Implementation is similar to previous steps but you have to implement `TextInputConfigurableView` instead `ModelConfigurableView` protocol (e.g we create table view cell with text field) +```swift +class TextInputTableViewCell: UITableViewCell, IdentificableView, NibLoadableView, TextInputConfigurableView { + @IBOutlet private weak var textField: UITextField! +} +``` + +Next add `textInputView` property implementation and return reference to your text input view (e.g. `textField`) +```swift +var textInputView: UITextField { + return self.textField +} +``` + +Finally modify your model implementation and add `textInputConfiguration`. It have to one of implementations of `TextInputConfiguration` protocol (e.g. `TextFieldConfiguration` or `TextViewConfiguration`) +```swift +struct Model: TextInputConfigurableModel { + ... + let textInputConfiguration: TextFieldConfiguration +} +``` +There is the simplest example when you need to create table view cell with text input. For more details see [DynamicUserTableViewCell](Example/Modules/DynamicUserTable/Cell/DynamicUserTableViewCell.swift) and [DynamicStoryTableViewCell](Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.swift) in my project example + +## Collection view cell +Collection view cell implementation is exactly the same as table view cell. Just use `UICollectionViewCell` instead `UITableViewCell` diff --git a/Example.xcodeproj/project.pbxproj b/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..764ea33 --- /dev/null +++ b/Example.xcodeproj/project.pbxproj @@ -0,0 +1,528 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5B2D23CC25441059007DB77D /* DynamicUserFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B2D23CA25441059007DB77D /* DynamicUserFooterView.xib */; }; + 5B2D23CD25441059007DB77D /* DynamicUserFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2D23CB25441059007DB77D /* DynamicUserFooterView.swift */; }; + 5B4C374525299ABB00A53EE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C374425299ABB00A53EE0 /* AppDelegate.swift */; }; + 5B4C374E25299ABC00A53EE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B4C374D25299ABC00A53EE0 /* Assets.xcassets */; }; + 5B4C377B25299E4000A53EE0 /* StaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C377625299E4000A53EE0 /* StaticTableViewController.swift */; }; + 5B4C377F25299E4000A53EE0 /* StaticTablePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C377A25299E4000A53EE0 /* StaticTablePresenter.swift */; }; + 5B4C378425299E5E00A53EE0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5B4C378325299E5E00A53EE0 /* LaunchScreen.storyboard */; }; + 5B4C378825299F8300A53EE0 /* Source.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4C378725299F8300A53EE0 /* Source.framework */; }; + 5B4C378925299F8300A53EE0 /* Source.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4C378725299F8300A53EE0 /* Source.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5B4C37912529A0E000A53EE0 /* StaticTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C378F2529A0E000A53EE0 /* StaticTableViewCell.swift */; }; + 5B4C37B02529C90700A53EE0 /* ModuleFabric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37AF2529C90700A53EE0 /* ModuleFabric.swift */; }; + 5B4C37B32529DC1D00A53EE0 /* DynamicTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37B22529DC1D00A53EE0 /* DynamicTableViewController.swift */; }; + 5B4C37B52529DC2900A53EE0 /* DynamicTablePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37B42529DC2900A53EE0 /* DynamicTablePresenter.swift */; }; + 5B4C37BA2529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4C37B82529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.xib */; }; + 5B4C37BB2529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37B92529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.swift */; }; + 5B4C37BE2529E53E00A53EE0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37BD2529E53E00A53EE0 /* User.swift */; }; + 5B4C37C02529E56800A53EE0 /* Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37BF2529E56800A53EE0 /* Story.swift */; }; + 5B4C37CB2529EBE200A53EE0 /* DynamicUserTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37C72529EBE200A53EE0 /* DynamicUserTableViewController.swift */; }; + 5B4C37CE2529EBE200A53EE0 /* DynamicUserTablePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37CA2529EBE200A53EE0 /* DynamicUserTablePresenter.swift */; }; + 5B4C37D22529ECB200A53EE0 /* DynamicUserTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4C37D02529ECB200A53EE0 /* DynamicUserTableViewCell.xib */; }; + 5B4C37D32529ECB200A53EE0 /* DynamicUserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37D12529ECB200A53EE0 /* DynamicUserTableViewCell.swift */; }; + 5B4C37EE252A5AB400A53EE0 /* DynamicStoryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37EA252A5AB400A53EE0 /* DynamicStoryTableViewController.swift */; }; + 5B4C37F0252A5AB400A53EE0 /* DynamicStoryTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4C37EC252A5AB400A53EE0 /* DynamicStoryTableViewController.xib */; }; + 5B4C37F1252A5AB400A53EE0 /* DynamicStoryTablePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37ED252A5AB400A53EE0 /* DynamicStoryTablePresenter.swift */; }; + 5B9B16B7252A5DEE00C19CD7 /* DynamicStoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B9B16B5252A5DEE00C19CD7 /* DynamicStoryTableViewCell.xib */; }; + 5B9B16B8252A5DEE00C19CD7 /* DynamicStoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9B16B6252A5DEE00C19CD7 /* DynamicStoryTableViewCell.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5B4C378A25299F8300A53EE0 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5B4C378925299F8300A53EE0 /* Source.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 5B2D23CA25441059007DB77D /* DynamicUserFooterView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DynamicUserFooterView.xib; sourceTree = ""; }; + 5B2D23CB25441059007DB77D /* DynamicUserFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicUserFooterView.swift; sourceTree = ""; }; + 5B4C374125299ABB00A53EE0 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B4C374425299ABB00A53EE0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5B4C374D25299ABC00A53EE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5B4C375225299ABC00A53EE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5B4C377625299E4000A53EE0 /* StaticTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewController.swift; sourceTree = ""; }; + 5B4C377A25299E4000A53EE0 /* StaticTablePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTablePresenter.swift; sourceTree = ""; }; + 5B4C378325299E5E00A53EE0 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 5B4C378725299F8300A53EE0 /* Source.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Source.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B4C378F2529A0E000A53EE0 /* StaticTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewCell.swift; sourceTree = ""; }; + 5B4C37AF2529C90700A53EE0 /* ModuleFabric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleFabric.swift; sourceTree = ""; }; + 5B4C37B22529DC1D00A53EE0 /* DynamicTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTableViewController.swift; sourceTree = ""; }; + 5B4C37B42529DC2900A53EE0 /* DynamicTablePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTablePresenter.swift; sourceTree = ""; }; + 5B4C37B82529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DynamicTitleDescriptionTableViewCell.xib; sourceTree = ""; }; + 5B4C37B92529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTitleDescriptionTableViewCell.swift; sourceTree = ""; }; + 5B4C37BD2529E53E00A53EE0 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 5B4C37BF2529E56800A53EE0 /* Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Story.swift; sourceTree = ""; }; + 5B4C37C72529EBE200A53EE0 /* DynamicUserTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicUserTableViewController.swift; sourceTree = ""; }; + 5B4C37CA2529EBE200A53EE0 /* DynamicUserTablePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicUserTablePresenter.swift; sourceTree = ""; }; + 5B4C37D02529ECB200A53EE0 /* DynamicUserTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DynamicUserTableViewCell.xib; sourceTree = ""; }; + 5B4C37D12529ECB200A53EE0 /* DynamicUserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicUserTableViewCell.swift; sourceTree = ""; }; + 5B4C37EA252A5AB400A53EE0 /* DynamicStoryTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicStoryTableViewController.swift; sourceTree = ""; }; + 5B4C37EC252A5AB400A53EE0 /* DynamicStoryTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DynamicStoryTableViewController.xib; sourceTree = ""; }; + 5B4C37ED252A5AB400A53EE0 /* DynamicStoryTablePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicStoryTablePresenter.swift; sourceTree = ""; }; + 5B9B16B5252A5DEE00C19CD7 /* DynamicStoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DynamicStoryTableViewCell.xib; sourceTree = ""; }; + 5B9B16B6252A5DEE00C19CD7 /* DynamicStoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicStoryTableViewCell.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5B4C373E25299ABB00A53EE0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B4C378825299F8300A53EE0 /* Source.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5B2D23C925441040007DB77D /* Views */ = { + isa = PBXGroup; + children = ( + 5B2D23CB25441059007DB77D /* DynamicUserFooterView.swift */, + 5B2D23CA25441059007DB77D /* DynamicUserFooterView.xib */, + ); + path = Views; + sourceTree = ""; + }; + 5B4C373825299ABB00A53EE0 = { + isa = PBXGroup; + children = ( + 5B4C374325299ABB00A53EE0 /* Example */, + 5B4C374225299ABB00A53EE0 /* Products */, + 5B4C378625299F8300A53EE0 /* Frameworks */, + ); + sourceTree = ""; + }; + 5B4C374225299ABB00A53EE0 /* Products */ = { + isa = PBXGroup; + children = ( + 5B4C374125299ABB00A53EE0 /* Example.app */, + ); + name = Products; + sourceTree = ""; + }; + 5B4C374325299ABB00A53EE0 /* Example */ = { + isa = PBXGroup; + children = ( + 5B4C374425299ABB00A53EE0 /* AppDelegate.swift */, + 5B4C37AF2529C90700A53EE0 /* ModuleFabric.swift */, + 5B4C37BC2529E53800A53EE0 /* Models */, + 5B4C377225299DEE00A53EE0 /* Modules */, + 5B4C376D25299D1800A53EE0 /* Resources */, + ); + path = Example; + sourceTree = ""; + }; + 5B4C376D25299D1800A53EE0 /* Resources */ = { + isa = PBXGroup; + children = ( + 5B4C375225299ABC00A53EE0 /* Info.plist */, + 5B4C378325299E5E00A53EE0 /* LaunchScreen.storyboard */, + 5B4C374D25299ABC00A53EE0 /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + 5B4C377225299DEE00A53EE0 /* Modules */ = { + isa = PBXGroup; + children = ( + 5B4C37AB2529C8A200A53EE0 /* StaticTable */, + 5B4C37B12529DBF300A53EE0 /* DynamicTable */, + 5B4C37C62529EBC600A53EE0 /* DynamicUserTable */, + 5B4C37E9252A59E300A53EE0 /* DynamicStoryTable */, + ); + path = Modules; + sourceTree = ""; + }; + 5B4C378625299F8300A53EE0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5B4C378725299F8300A53EE0 /* Source.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5B4C378B2529A08700A53EE0 /* Cell */ = { + isa = PBXGroup; + children = ( + 5B4C378F2529A0E000A53EE0 /* StaticTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + 5B4C37AB2529C8A200A53EE0 /* StaticTable */ = { + isa = PBXGroup; + children = ( + 5B4C377A25299E4000A53EE0 /* StaticTablePresenter.swift */, + 5B4C377625299E4000A53EE0 /* StaticTableViewController.swift */, + 5B4C378B2529A08700A53EE0 /* Cell */, + ); + path = StaticTable; + sourceTree = ""; + }; + 5B4C37B12529DBF300A53EE0 /* DynamicTable */ = { + isa = PBXGroup; + children = ( + 5B4C37B42529DC2900A53EE0 /* DynamicTablePresenter.swift */, + 5B4C37B22529DC1D00A53EE0 /* DynamicTableViewController.swift */, + 5B4C37B62529DCF500A53EE0 /* Cell */, + ); + path = DynamicTable; + sourceTree = ""; + }; + 5B4C37B62529DCF500A53EE0 /* Cell */ = { + isa = PBXGroup; + children = ( + 5B4C37B92529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.swift */, + 5B4C37B82529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; + 5B4C37BC2529E53800A53EE0 /* Models */ = { + isa = PBXGroup; + children = ( + 5B4C37BD2529E53E00A53EE0 /* User.swift */, + 5B4C37BF2529E56800A53EE0 /* Story.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5B4C37C62529EBC600A53EE0 /* DynamicUserTable */ = { + isa = PBXGroup; + children = ( + 5B4C37CA2529EBE200A53EE0 /* DynamicUserTablePresenter.swift */, + 5B4C37C72529EBE200A53EE0 /* DynamicUserTableViewController.swift */, + 5B2D23C925441040007DB77D /* Views */, + 5B4C37CF2529ECA400A53EE0 /* Cells */, + ); + path = DynamicUserTable; + sourceTree = ""; + }; + 5B4C37CF2529ECA400A53EE0 /* Cells */ = { + isa = PBXGroup; + children = ( + 5B4C37D12529ECB200A53EE0 /* DynamicUserTableViewCell.swift */, + 5B4C37D02529ECB200A53EE0 /* DynamicUserTableViewCell.xib */, + ); + path = Cells; + sourceTree = ""; + }; + 5B4C37E9252A59E300A53EE0 /* DynamicStoryTable */ = { + isa = PBXGroup; + children = ( + 5B4C37ED252A5AB400A53EE0 /* DynamicStoryTablePresenter.swift */, + 5B4C37EA252A5AB400A53EE0 /* DynamicStoryTableViewController.swift */, + 5B4C37EC252A5AB400A53EE0 /* DynamicStoryTableViewController.xib */, + 5B9B16B4252A5DBB00C19CD7 /* Cell */, + ); + path = DynamicStoryTable; + sourceTree = ""; + }; + 5B9B16B4252A5DBB00C19CD7 /* Cell */ = { + isa = PBXGroup; + children = ( + 5B9B16B6252A5DEE00C19CD7 /* DynamicStoryTableViewCell.swift */, + 5B9B16B5252A5DEE00C19CD7 /* DynamicStoryTableViewCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5B4C374025299ABB00A53EE0 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5B4C375525299ABC00A53EE0 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 5B4C373D25299ABB00A53EE0 /* Sources */, + 5B4C373E25299ABB00A53EE0 /* Frameworks */, + 5B4C373F25299ABB00A53EE0 /* Resources */, + 5B4C378A25299F8300A53EE0 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 5B4C374125299ABB00A53EE0 /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5B4C373925299ABB00A53EE0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "Lysytsia Yurii"; + TargetAttributes = { + 5B4C374025299ABB00A53EE0 = { + CreatedOnToolsVersion = 12.0; + }; + }; + }; + buildConfigurationList = 5B4C373C25299ABB00A53EE0 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5B4C373825299ABB00A53EE0; + productRefGroup = 5B4C374225299ABB00A53EE0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5B4C374025299ABB00A53EE0 /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5B4C373F25299ABB00A53EE0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B4C37D22529ECB200A53EE0 /* DynamicUserTableViewCell.xib in Resources */, + 5B9B16B7252A5DEE00C19CD7 /* DynamicStoryTableViewCell.xib in Resources */, + 5B4C378425299E5E00A53EE0 /* LaunchScreen.storyboard in Resources */, + 5B4C374E25299ABC00A53EE0 /* Assets.xcassets in Resources */, + 5B2D23CC25441059007DB77D /* DynamicUserFooterView.xib in Resources */, + 5B4C37F0252A5AB400A53EE0 /* DynamicStoryTableViewController.xib in Resources */, + 5B4C37BA2529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5B4C373D25299ABB00A53EE0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B4C37912529A0E000A53EE0 /* StaticTableViewCell.swift in Sources */, + 5B4C37B02529C90700A53EE0 /* ModuleFabric.swift in Sources */, + 5B4C37F1252A5AB400A53EE0 /* DynamicStoryTablePresenter.swift in Sources */, + 5B9B16B8252A5DEE00C19CD7 /* DynamicStoryTableViewCell.swift in Sources */, + 5B4C37CB2529EBE200A53EE0 /* DynamicUserTableViewController.swift in Sources */, + 5B4C37B52529DC2900A53EE0 /* DynamicTablePresenter.swift in Sources */, + 5B4C374525299ABB00A53EE0 /* AppDelegate.swift in Sources */, + 5B4C37EE252A5AB400A53EE0 /* DynamicStoryTableViewController.swift in Sources */, + 5B4C37BB2529DD0D00A53EE0 /* DynamicTitleDescriptionTableViewCell.swift in Sources */, + 5B4C37D32529ECB200A53EE0 /* DynamicUserTableViewCell.swift in Sources */, + 5B4C37BE2529E53E00A53EE0 /* User.swift in Sources */, + 5B4C377F25299E4000A53EE0 /* StaticTablePresenter.swift in Sources */, + 5B4C37B32529DC1D00A53EE0 /* DynamicTableViewController.swift in Sources */, + 5B4C37CE2529EBE200A53EE0 /* DynamicUserTablePresenter.swift in Sources */, + 5B4C377B25299E4000A53EE0 /* StaticTableViewController.swift in Sources */, + 5B2D23CD25441059007DB77D /* DynamicUserFooterView.swift in Sources */, + 5B4C37C02529E56800A53EE0 /* Story.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 5B4C375325299ABC00A53EE0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 5B4C375425299ABC00A53EE0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5B4C375625299ABC00A53EE0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XJ2USXCGT4; + INFOPLIST_FILE = Example/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.4.1; + PRODUCT_BUNDLE_IDENTIFIER = "lysytsia.yurii.air-collection.example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5B4C375725299ABC00A53EE0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XJ2USXCGT4; + INFOPLIST_FILE = Example/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.4.1; + PRODUCT_BUNDLE_IDENTIFIER = "lysytsia.yurii.air-collection.example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5B4C373C25299ABB00A53EE0 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B4C375325299ABC00A53EE0 /* Debug */, + 5B4C375425299ABC00A53EE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5B4C375525299ABC00A53EE0 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5B4C375625299ABC00A53EE0 /* Debug */, + 5B4C375725299ABC00A53EE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5B4C373925299ABB00A53EE0 /* Project object */; +} diff --git a/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 0000000..1bd5de3 --- /dev/null +++ b/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift new file mode 100644 index 0000000..3c71236 --- /dev/null +++ b/Example/AppDelegate.swift @@ -0,0 +1,29 @@ +// +// AppDelegate.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Create navigation controller + let rootViewController = ModuleFabric.createStaticTableModule() + let navigationController = UINavigationController(rootViewController: rootViewController) + // Create window and set root view controller + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = navigationController + window.makeKeyAndVisible() + self.window = window + return true + } + +} + diff --git a/Example/Models/Story.swift b/Example/Models/Story.swift new file mode 100644 index 0000000..17bf189 --- /dev/null +++ b/Example/Models/Story.swift @@ -0,0 +1,20 @@ +// +// Story.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation + +class Story { + var title: String + var text: String + + init(title: String, text: String) { + self.title = title + self.text = text + } + +} diff --git a/Example/Models/User.swift b/Example/Models/User.swift new file mode 100644 index 0000000..a077157 --- /dev/null +++ b/Example/Models/User.swift @@ -0,0 +1,27 @@ +// +// User.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation + +class User { + + var name: String + var birthdate: Date? + var gender: Gender? + + init(name: String) { + self.name = name + } + + enum Gender: String, CaseIterable { + case male + case female + case other + } + +} diff --git a/Example/ModuleFabric.swift b/Example/ModuleFabric.swift new file mode 100644 index 0000000..1b1a234 --- /dev/null +++ b/Example/ModuleFabric.swift @@ -0,0 +1,41 @@ +// +// ModuleFabric.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit + +enum ModuleFabric { + + static func createStaticTableModule() -> UIViewController { + let view = StaticTableViewController() + let presenter = StaticTablePresenter(view: view) + view.output = presenter + return view + } + + static func createDynamicTableModule() -> UIViewController { + let view = DynamicTableViewController() + let presenter = DynamicTablePresenter(view: view) + view.output = presenter + return view + } + + static func createDynamicUserTableModule(user: User) -> UIViewController { + let view = DynamicUserTableViewController() + let presenter = DynamicUserTablePresenter(user: user, view: view) + view.output = presenter + return view + } + + static func createDynamicStoryTableModule(story: Story) -> UIViewController { + let view = DynamicStoryTableViewController() + let presenter = DynamicStoryTablePresenter(story: story, view: view) + view.output = presenter + return view + } + +} diff --git a/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.swift b/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.swift new file mode 100644 index 0000000..c6d51e3 --- /dev/null +++ b/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.swift @@ -0,0 +1,36 @@ +// +// DynamicStoryTableViewCell.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +class DynamicStoryTableViewCell: UITableViewCell, IdentificableView, NibLoadableView, TextInputConfigurableView { + + // MARK: Outlet properties + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionTextView: UITextView! + + // MARK: Computed property + var textInputView: UITextView { + return self.descriptionTextView + } + + // MARK: Functions + func configure(model: Model) { + self.titleLabel.text = model.title + self.descriptionTextView.text = model.description + } + + // MARK: Helpers + struct Model: TextInputConfigurableModel { + let title: String + let description: String + let textInputConfiguration: TextViewConfiguration + } + +} diff --git a/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.xib b/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.xib new file mode 100644 index 0000000..63ade4d --- /dev/null +++ b/Example/Modules/DynamicStoryTable/Cell/DynamicStoryTableViewCell.xib @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Modules/DynamicStoryTable/DynamicStoryTablePresenter.swift b/Example/Modules/DynamicStoryTable/DynamicStoryTablePresenter.swift new file mode 100644 index 0000000..3c0a1a7 --- /dev/null +++ b/Example/Modules/DynamicStoryTable/DynamicStoryTablePresenter.swift @@ -0,0 +1,115 @@ +// +// DynamicStoryTablePresenter.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation +import Source + +protocol DynamicStoryTableViewOutput: TableViewPresenterProtocol, TextFieldPresenterProtocol, TextViewPresenterProtocol { + +} + +final class DynamicStoryTablePresenter { + + // MARK: Stored properties + private let rows: [[Row]] = [ + [.title], + [.text] + ] + + // MARK: Dependency properties + private unowned let story: Story + private unowned let view: DynamicStoryTableViewInput + + // MARK: Lifecycle + init(story: Story, view: DynamicStoryTableViewInput) { + self.story = story + self.view = view + } + + // MARK: Helpers + private enum Row: Int { + case title + case text + } + +} + +// MARK: - DynamicStoryTableViewOutput +extension DynamicStoryTablePresenter: DynamicStoryTableViewOutput { + + // MARK: TableViewPresenterProtocol + var tableSections: Int { + return self.rows.count + } + + func tableRows(for section: Int) -> Int { + return self.rows[section].count + } + + func tableRowIdentifier(for indexPath: IndexPath) -> String { + switch self.rows[indexPath.section][indexPath.row] { + case .title: + return DynamicUserTableViewCell.viewIdentifier + case .text: + return DynamicStoryTableViewCell.viewIdentifier + } + } + + func tableRowHeight(for indexPath: IndexPath) -> TableViewRowHeight { + switch self.rows[indexPath.section][indexPath.row] { + case .title: + return .flexible + case .text: + return .fixed(240) + } + } + + func tableRowModel(for indexPath: IndexPath) -> Any? { + switch self.rows[indexPath.section][indexPath.row] { + case .title: + let text = self.story.title + let configuration = TextFieldConfiguration(delegate: self.view) + return DynamicUserTableViewCell.Model(title: "Title", text: text, textInputConfiguration: configuration) + case .text: + let text = self.story.text + let configuration = TextViewConfiguration(delegate: self.view) + return DynamicStoryTableViewCell.Model(title: "Text", description: text, textInputConfiguration: configuration) + } + } + + func tableRowDidSelect(at indexPath: IndexPath) { + self.view.deselectTableViewRow(at: indexPath, animated: true) + self.view.becomeTableViewRowFirstResponder(at: indexPath) + } + + // MARK: TextFieldPresenterProtocol + func textFieldTextDidChanged(_ text: String?, at indexPath: IndexPath) { + switch self.rows[indexPath.section][indexPath.row] { + case .title: + self.story.title = text ?? "" + default: + return + } + } + + func textFieldShouldReturn(at indexPath: IndexPath) -> Bool { + self.view.resignTableViewRowFirstResponder(at: indexPath) + return true + } + + // MARK: TextViewPresenterProtocol + func textViewDidChange(text: String, at indexPath: IndexPath) { + switch self.rows[indexPath.section][indexPath.row] { + case .text: + self.story.text = text + default: + return + } + } + +} diff --git a/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.swift b/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.swift new file mode 100644 index 0000000..80c81b5 --- /dev/null +++ b/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.swift @@ -0,0 +1,66 @@ +// +// DynamicStoryTableViewController.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +protocol DynamicStoryTableViewInput: TableViewControllerProtocol, TextFieldControllerProtocol, TextViewControllerProtocol { + +} + +final class DynamicStoryTableViewController: UIViewController { + + // MARK: Stored properties + var output: DynamicStoryTableViewOutput! + + // MARK: Outlet properties + private let tableView = UITableView(frame: .zero, style: .grouped) + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(self.tableView) + + self.configureTableView { (tableView) in + tableView.register(DynamicUserTableViewCell.self) + tableView.register(DynamicStoryTableViewCell.self) + } + + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.tableView.frame = self.view.bounds + } + +} + +// MARK: - DynamicStoryTableViewInput +extension DynamicStoryTableViewController: DynamicStoryTableViewInput { + + // MARK: TableViewControllerProtocol + var tableViewSource: UITableView { + return self.tableView + } + + var tableViewPresenter: TableViewPresenterProtocol { + return self.output + } + + // MARK: TextFieldControllerProtocol + var textFieldPresenter: TextFieldPresenterProtocol { + return self.output + } + + // MARK: TextViewControllerProtocol + var textViewPresenter: TextViewPresenterProtocol { + return self.output + } + +} diff --git a/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.xib b/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.xib new file mode 100644 index 0000000..6d11300 --- /dev/null +++ b/Example/Modules/DynamicStoryTable/DynamicStoryTableViewController.xib @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.swift b/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.swift new file mode 100644 index 0000000..6723de9 --- /dev/null +++ b/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.swift @@ -0,0 +1,45 @@ +// +// DynamicTitleDescriptionTableViewCell.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +class DynamicTitleDescriptionTableViewCell: UITableViewCell, IdentificableView, NibLoadableView, ModelConfigurableView { + + // MARK: Outlet properties + @IBOutlet private weak var stackView: UIStackView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! + + // MARK: Lifecycle + override func awakeFromNib() { + super.awakeFromNib() + // Configure title label + self.titleLabel.numberOfLines = 2 + self.titleLabel.textColor = .black + self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) + // Configure description label + self.descriptionLabel.numberOfLines = 0 + self.descriptionLabel.textColor = .darkGray + self.descriptionLabel.font = UIFont.preferredFont(forTextStyle: .body) + } + + // MARK: Functions + func configure(model: Model) { + self.titleLabel.text = model.title + self.descriptionLabel.text = model.description + self.descriptionLabel.isHidden = model.description == nil + } + + // MARK: Helpers + struct Model { + let title: String + let description: String? + } + +} diff --git a/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.xib b/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.xib new file mode 100644 index 0000000..cfd7575 --- /dev/null +++ b/Example/Modules/DynamicTable/Cell/DynamicTitleDescriptionTableViewCell.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Modules/DynamicTable/DynamicTablePresenter.swift b/Example/Modules/DynamicTable/DynamicTablePresenter.swift new file mode 100644 index 0000000..c26fbba --- /dev/null +++ b/Example/Modules/DynamicTable/DynamicTablePresenter.swift @@ -0,0 +1,100 @@ +// +// DynamicTablePresenter.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation +import Source + +protocol DynamicTableViewOutput: TableViewPresenterProtocol { + +} + +final class DynamicTablePresenter { + + // MARK: Stored properties + private let sections: [Section] = Section.allCases + + private let users: [User] = [ + User(name: "Jason"), + User(name: "Andrew"), + User(name: "David") + ] + + private let stories: [Story] = [ + Story(title: "The Bogey Beast", text: "A woman finds a pot of treasure on the road while she is returning from work. Delighted with her luck, she decides to keep it. As she is taking it home, it keeps changing. However, her enthusiasm refuses to fade away."), + Story(title: "The Tortoise and the Hare", text: "This classic fable tells the story of a very slow tortoise (another word for turtle) and a speedy hare (another word for rabbit). The tortoise challenges the hare to a race. The hare laughs at the idea that a tortoise could run faster than him, but when the two actually race, the results are surprising."), + Story(title: "The Night Train at Deoli", text: "Ruskin Bond used to spend his summer at his grandmother’s house in Dehradun. While taking the train, he always had to pass through a small station called Deoli. No one used to get down at the station and nothing happened there. Until one day he sees a girl selling fruit and he is unable to forget her") + ] + + // MARK: Dependency properties + private unowned let view: DynamicTableViewInput + + // MARK: Lifecycle + init(view: DynamicTableViewInput) { + self.view = view + } + + // MARK: Helpers + private enum Section: Int, CaseIterable { + case users + case stories + } + +} + +// MARK: - HomeViewOutput +extension DynamicTablePresenter: DynamicTableViewOutput { + + var tableSections: Int { + return self.sections.count + } + + func tableRows(for section: Int) -> Int { + switch self.sections[section] { + case .users: + return self.users.count + case .stories: + return self.stories.count + } + } + + func tableRowIdentifier(for indexPath: IndexPath) -> String { + switch self.sections[indexPath.section] { + case .users, .stories: + return DynamicTitleDescriptionTableViewCell.viewIdentifier + } + } + + func tableRowHeight(for indexPath: IndexPath) -> TableViewRowHeight { + return .flexible + } + + func tableRowModel(for indexPath: IndexPath) -> Any? { + switch self.sections[indexPath.section] { + case .users: + let user = self.users[indexPath.row] + return DynamicTitleDescriptionTableViewCell.Model(title: user.name, description: nil) + case .stories: + let story = self.stories[indexPath.row] + return DynamicTitleDescriptionTableViewCell.Model(title: story.title, description: story.text) + } + } + + func tableRowDidSelect(at indexPath: IndexPath) { + self.view.deselectTableViewRow(at: indexPath, animated: true) + switch self.sections[indexPath.section] { + case .users: + let user = self.users[indexPath.row] + self.view.showUser(user) + case .stories: + let story = self.stories[indexPath.row] + self.view.showStory(story) + } + } + +} + diff --git a/Example/Modules/DynamicTable/DynamicTableViewController.swift b/Example/Modules/DynamicTable/DynamicTableViewController.swift new file mode 100644 index 0000000..866a7f7 --- /dev/null +++ b/Example/Modules/DynamicTable/DynamicTableViewController.swift @@ -0,0 +1,75 @@ +// +// DynamicTableViewController.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +protocol DynamicTableViewInput: TableViewControllerProtocol { + func showUser(_ user: User) + func showStory(_ story: Story) +} + +final class DynamicTableViewController: UIViewController { + + // MARK: Stored properties + var output: DynamicTableViewOutput! + + // MARK: Outlet properties + private let tableView: UITableView = UITableView(frame: .zero, style: .plain) + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(self.tableView) + + self.configureTableView { (tableView) in + tableView.register(DynamicTitleDescriptionTableViewCell.self) + } + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.reloadTableView() + + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + self.tableView.frame = self.view.bounds + + } + +} + +// MARK: - StaticTableViewInput +extension DynamicTableViewController: DynamicTableViewInput { + + func showUser(_ user: User) { + let view = ModuleFabric.createDynamicUserTableModule(user: user) + self.navigationController?.pushViewController(view, animated: true) + } + + func showStory(_ story: Story) { + let view = ModuleFabric.createDynamicStoryTableModule(story: story) + self.navigationController?.pushViewController(view, animated: true) + } + + var tableViewSource: UITableView { + return self.tableView + } + + var tableViewPresenter: TableViewPresenterProtocol { + return self.output + } + +} + diff --git a/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.swift b/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.swift new file mode 100644 index 0000000..b422e31 --- /dev/null +++ b/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.swift @@ -0,0 +1,37 @@ +// +// DynamicUserTableViewCell.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +class DynamicUserTableViewCell: UITableViewCell, IdentificableView, NibLoadableView, TextInputConfigurableView { + + // MARK: Outlet properties + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var textField: UITextField! + + // MARK: Computed properties + var textInputView: UITextField { + return self.textField + } + + // MARK: Functions + func configure(model: Model) { + self.titleLabel.text = model.title + self.textField.text = model.text + self.textField.placeholder = model.title + } + + // MARK: Helpers + struct Model: TextInputConfigurableModel { + let title: String + let text: String + let textInputConfiguration: TextFieldConfiguration + } + +} diff --git a/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.xib b/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.xib new file mode 100644 index 0000000..c699382 --- /dev/null +++ b/Example/Modules/DynamicUserTable/Cells/DynamicUserTableViewCell.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Modules/DynamicUserTable/DynamicUserTablePresenter.swift b/Example/Modules/DynamicUserTable/DynamicUserTablePresenter.swift new file mode 100644 index 0000000..3ec494e --- /dev/null +++ b/Example/Modules/DynamicUserTable/DynamicUserTablePresenter.swift @@ -0,0 +1,191 @@ +// +// DynamicUserTablePresenter.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation +import Source + +protocol DynamicUserTableViewOutput: TableViewPresenterProtocol, TextFieldPresenterProtocol, DatePickerPresenterProtocol, PickerViewPresenterProtocol { + // Add presenter properties which will use by view +} + +final class DynamicUserTablePresenter: NSObject { + + // MARK: Stored properties + private let sections: [Section] = Section.allCases + + private let birthdateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + private let availableGenders: [User.Gender?] = { + var genders: [User.Gender?] = User.Gender.allCases + genders.insert(nil, at: 0) + return genders + }() + + // MARK: Dependency properties + private unowned let user: User + private unowned let view: DynamicUserTableViewInput + + // MARK: Lifecycle + init(user: User, view: DynamicUserTableViewInput) { + self.user = user + self.view = view + } + + // MARK: Helpers + private enum Section: Int, CaseIterable { + case name + case birthdate + case gender + } + +} + +// MARK: - DynamicUserTableViewOutput +extension DynamicUserTablePresenter: DynamicUserTableViewOutput { + + // MARK: TableViewPresenterProtocol + var tableSections: Int { + return self.sections.count + } + + func tableRows(for section: Int) -> Int { + return 1 + } + + func tableRowIdentifier(for indexPath: IndexPath) -> String { + return DynamicUserTableViewCell.viewIdentifier + } + + func tableRowHeight(for indexPath: IndexPath) -> TableViewRowHeight { + return .flexible + } + + func tableRowModel(for indexPath: IndexPath) -> Any? { + switch self.sections[indexPath.section] { + case .name: + let configuration = TextFieldConfiguration(delegate: self.view) + return DynamicUserTableViewCell.Model(title: "Username", text: self.user.name, textInputConfiguration: configuration) + case .birthdate: + let text = self.user.birthdate.flatMap({ self.birthdateFormatter.string(from: $0) }) ?? "" + let date = self.user.birthdate ?? Date() + let configuration = TextFieldDatePickerConfiguration(mode: .date, date: date, maximumDate: Date(), delegate: self.view) + return DynamicUserTableViewCell.Model(title: "Birthdate", text: text, textInputConfiguration: configuration) + case .gender: + let text = self.user.gender?.rawValue ?? "" + let configuration = TextFieldPickerViewConfiguration(controller: self.view) + return DynamicUserTableViewCell.Model(title: "Gender", text: text, textInputConfiguration: configuration) + } + } + + func tableRowDidSelect(at indexPath: IndexPath) { + self.view.deselectTableViewRow(at: indexPath, animated: true) + self.view.becomeTableViewRowFirstResponder(at: indexPath) + } + + func tableFooterIdentifier(for section: Int) -> String? { + return DynamicUserFooterView.viewIdentifier + } + + func tableFooterHeight(for section: Int) -> TableViewHeaderFooterViewHeight { + return .flexible + } + + func tableFooterModel(for section: Int) -> Any? { + switch self.sections[section] { + case .name: + return DynamicUserFooterView.Model(title: "You can enter any name you want") + case .birthdate: + return DynamicUserFooterView.Model(title: "Please select correct date of birth") + case .gender: + return DynamicUserFooterView.Model(title: "Select gender if you want") + } + } + + // MARK: TextFieldPresenterProtocol + func textFieldTextDidChanged(_ text: String?, at indexPath: IndexPath) { + switch self.sections[indexPath.section] { + case .name: + self.user.name = text ?? "" + default: + return + } + } + + func textFieldShouldReturn(at indexPath: IndexPath) -> Bool { + self.view.resignTableViewRowFirstResponder(at: indexPath) + return true + } + + // MARK: DatePickerPresenterProtocol + func datePickerDidSelectDate(_ date: Date, at indexPath: IndexPath) { + switch self.sections[indexPath.section] { + case .birthdate: + self.user.birthdate = date + default: + return + } + } + + func datePickerShouldUpdateTextFromDate(_ date: Date, at indexPath: IndexPath) -> String? { + switch self.sections[indexPath.section] { + case .birthdate: + return self.birthdateFormatter.string(from: date) + default: + return nil + } + } + + // MARK: PickerViewPresenterProtocol + func pickerViewNumberOfComponents(at indexPath: IndexPath) -> Int { + return 1 + } + + func pickerViewNumberOfRows(inComponent component: Int, at indexPath: IndexPath) -> Int { + switch self.sections[indexPath.section] { + case .gender: + return self.availableGenders.count + default: + return 0 + } + } + + func pickerViewTitle(for row: Int, inComponent component: Int, at indexPath: IndexPath) -> PickerViewTitle { + switch self.sections[indexPath.section] { + case .gender: + let gender = self.availableGenders[row]?.rawValue ?? "" + return .title(gender) + default: + return .title("") + } + } + + func pickerViewDidSelectRow(_ row: Int, inComponent component: Int, at indexPath: IndexPath) { + switch self.sections[indexPath.section] { + case .gender: + self.user.gender = self.availableGenders[row] + default: + return + } + } + + func pickerViewSelectedRow(inComponent component: Int, at indexPath: IndexPath) -> Int { + switch self.sections[indexPath.section] { + case .gender: + let gender = self.user.gender + return self.availableGenders.firstIndex(of: gender) ?? 0 + default: + return 0 + } + } + +} diff --git a/Example/Modules/DynamicUserTable/DynamicUserTableViewController.swift b/Example/Modules/DynamicUserTable/DynamicUserTableViewController.swift new file mode 100644 index 0000000..a92ae68 --- /dev/null +++ b/Example/Modules/DynamicUserTable/DynamicUserTableViewController.swift @@ -0,0 +1,70 @@ +// +// DynamicUserTableViewController.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +protocol DynamicUserTableViewInput: TableViewControllerProtocol, TextFieldControllerProtocol, TextFieldDatePickerControllerProtocol, TextFieldPickerViewControllerProtocol { + +} + +final class DynamicUserTableViewController: UIViewController { + + // MARK: Stored properties + var output: DynamicUserTableViewOutput! + + // MARK: Outlet properties + private let tableView = UITableView(frame: .zero, style: .grouped) + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(self.tableView) + + self.configureTableView { (tableView) in + tableView.register(DynamicUserTableViewCell.self) + tableView.register(DynamicUserFooterView.self) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.tableView.frame = self.view.bounds + } + +} + +// MARK: - DynamicUserTableViewInput +extension DynamicUserTableViewController: DynamicUserTableViewInput { + + // MARK: TableViewControllerProtocol + var tableViewSource: UITableView { + return self.tableView + } + + var tableViewPresenter: TableViewPresenterProtocol { + return self.output + } + + // MARK: TextFieldControllerProtocol + var textFieldPresenter: TextFieldPresenterProtocol { + return self.output + } + + // MARK: TextFieldDatePickerControllerProtocol + var datePickerPresenter: DatePickerPresenterProtocol { + return self.output + } + + // MARK: TextFieldPickerViewControllerProtocol + var pickerViewPresenter: PickerViewPresenterProtocol { + return self.output + } + +} diff --git a/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.swift b/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.swift new file mode 100644 index 0000000..97a7649 --- /dev/null +++ b/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.swift @@ -0,0 +1,35 @@ +// +// DynamicUserFooterView.swift +// Example +// +// Created by Lysytsia Yurii on 24.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +class DynamicUserFooterView: UITableViewHeaderFooterView, IdentificableView, NibLoadableView, ModelConfigurableView { + + // MARK: Outlet properties + @IBOutlet private weak var titleLabel: UILabel! + + // MARK: Lifecycle + override func awakeFromNib() { + super.awakeFromNib() + self.contentView.backgroundColor = UIColor.groupTableViewBackground + self.titleLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + self.titleLabel.textColor = UIColor.gray + } + + // MARK: Functions + func configure(model: Model) { + self.titleLabel.text = model.title + } + + // MARK: Helpers + struct Model { + let title: String + } + +} diff --git a/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.xib b/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.xib new file mode 100644 index 0000000..5e1c875 --- /dev/null +++ b/Example/Modules/DynamicUserTable/Views/DynamicUserFooterView.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Modules/Home/HomePresenter.swift b/Example/Modules/Home/HomePresenter.swift new file mode 100644 index 0000000..faacbb3 --- /dev/null +++ b/Example/Modules/Home/HomePresenter.swift @@ -0,0 +1,68 @@ +// +// StaticTablePresenter.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation +import Source + +protocol HomeViewOutput: TableViewPresenterProtocol { + +} + +final class StaticTablePresenter { + + // MARK: Stored properties + private let rows: [[Row]] = [ + [.nib] + ] + + // MARK: Dependency properties + private unowned let view: HomeViewInput + + // MARK: Lifecycle + init(view: HomeViewInput) { + self.view = view + } + + // MARK: Functions + + // MARK: Helpers + private enum Row: String, CaseIterable { + case nib = "Load UIView from Nib" + } + +} + +// MARK: - HomeViewOutput +extension StaticTablePresenter: HomeViewOutput { + + var tableSections: Int { + return self.rows.count + } + + func tableRows(for section: Int) -> Int { + return self.rows[section].count + } + + func tableRowIdentifier(for indexPath: IndexPath) -> String { + return HomeTableViewCell.viewIdentifier + } + + func tableRowHeight(for indexPath: IndexPath) -> TableViewRowHeight { + return .fixed(44) + } + + func tableRowModel(for indexPath: IndexPath) -> Any? { + let row = self.rows[indexPath.section][indexPath.row] + return HomeTableViewCell.Model(title: row.rawValue) + } + + func tableRowDidSelect(at indexPath: IndexPath) { + self.view.deselectTableViewRow(at: indexPath, animated: true) + } + +} diff --git a/Example/Modules/StaticTable/Cell/StaticTableViewCell.swift b/Example/Modules/StaticTable/Cell/StaticTableViewCell.swift new file mode 100644 index 0000000..5237c83 --- /dev/null +++ b/Example/Modules/StaticTable/Cell/StaticTableViewCell.swift @@ -0,0 +1,45 @@ +// +// StaticTableViewCell.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +class StaticTableViewCell: UITableViewCell, IdentificableView, ModelConfigurableView { + + // MARK: Outlet properties + private let titleLabel: UILabel = UILabel() + + // MARK: Lifecycle + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.addSubview(self.titleLabel) + self.accessoryType = .disclosureIndicator + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.titleLabel.frame = self.bounds.inset(by: self.layoutMargins) + + } + + // MARK: Functions + func configure(model: Model) { + self.titleLabel.text = model.title + } + + // MARK: Helpers + struct Model { + let title: String + } + +} diff --git a/Example/Modules/StaticTable/StaticTablePresenter.swift b/Example/Modules/StaticTable/StaticTablePresenter.swift new file mode 100644 index 0000000..8559206 --- /dev/null +++ b/Example/Modules/StaticTable/StaticTablePresenter.swift @@ -0,0 +1,70 @@ +// +// StaticTablePresenter.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import Foundation +import Source + +protocol StaticTableViewOutput: TableViewPresenterProtocol { + +} + +final class StaticTablePresenter { + + // MARK: Stored properties + private let rows: [[Row]] = [ + [.dynamicTableView] + ] + + // MARK: Dependency properties + private unowned let view: StaticTableViewInput + + // MARK: Lifecycle + init(view: StaticTableViewInput) { + self.view = view + } + + // MARK: Helpers + private enum Row: String, CaseIterable { + case dynamicTableView = "Dynamic table view" + } + +} + +// MARK: - HomeViewOutput +extension StaticTablePresenter: StaticTableViewOutput { + + var tableSections: Int { + return self.rows.count + } + + func tableRows(for section: Int) -> Int { + return self.rows[section].count + } + + func tableRowIdentifier(for indexPath: IndexPath) -> String { + return StaticTableViewCell.viewIdentifier + } + + func tableRowHeight(for indexPath: IndexPath) -> TableViewRowHeight { + return .fixed(44) + } + + func tableRowModel(for indexPath: IndexPath) -> Any? { + let row = self.rows[indexPath.section][indexPath.row] + return StaticTableViewCell.Model(title: row.rawValue) + } + + func tableRowDidSelect(at indexPath: IndexPath) { + self.view.deselectTableViewRow(at: indexPath, animated: true) + switch self.rows[indexPath.section][indexPath.row] { + case .dynamicTableView: + self.view.showDynamicViewController() + } + } + +} diff --git a/Example/Modules/StaticTable/StaticTableViewController.swift b/Example/Modules/StaticTable/StaticTableViewController.swift new file mode 100644 index 0000000..cd199f4 --- /dev/null +++ b/Example/Modules/StaticTable/StaticTableViewController.swift @@ -0,0 +1,61 @@ +// +// StaticTableViewController.swift +// Example +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import UIKit +import Source + +protocol StaticTableViewInput: TableViewControllerProtocol { + func showDynamicViewController() +} + +final class StaticTableViewController: UIViewController { + + // MARK: Stored properties + var output: StaticTableViewOutput! + + // MARK: Outlet properties + private let tableView: UITableView = UITableView(frame: .zero, style: .plain) + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + self.view.addSubview(self.tableView) + + self.configureTableView { (tableView) in + tableView.register(StaticTableViewCell.self) + } + + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + self.tableView.frame = self.view.bounds + + } + +} + +// MARK: - StaticTableViewInput +extension StaticTableViewController: StaticTableViewInput { + + var tableViewSource: UITableView { + return self.tableView + } + + var tableViewPresenter: TableViewPresenterProtocol { + return self.output + } + + func showDynamicViewController() { + let viewController = ModuleFabric.createDynamicTableModule() + self.navigationController?.pushViewController(viewController, animated: true) + } + +} diff --git a/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Resources/Assets.xcassets/Contents.json b/Example/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Resources/Info.plist b/Example/Resources/Info.plist new file mode 100644 index 0000000..be04fa0 --- /dev/null +++ b/Example/Resources/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/Resources/LaunchScreen.storyboard b/Example/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..d490a56 --- /dev/null +++ b/Example/Resources/LaunchScreen.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index aeb7336..51050ce 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ [![platform](https://img.shields.io/badge/Platform-iOS%2011+-blue.svg)]() [![language](https://img.shields.io/badge/Language-Swift-red.svg)]() -[![language](https://img.shields.io/badge/pod-1.4.0-blue.svg)]() +[![language](https://img.shields.io/badge/pod-1.4.1-blue.svg)]() [![license](https://img.shields.io/badge/license-MIT-lightgray.svg)]() Save your coding time with AirCollection written on Swift. It's a great wrapper for UITableView / UICollectionView and VIPER / MVP architecture. -- [Features](#features) - [Requirements](#requirements) - [Installation](#installation) - [CocoaPods](#CocoaPods) @@ -15,14 +14,7 @@ Save your coding time with AirCollection written on Swift. It's a great wrapper - [Reusable View](#reusable-view) - [Table view](#table-view) - [Collection view](#collection-view) -- [License](#license) - - -## Features -- [ ] Collection view documentation -- [ ] Static table view with from programmatically model -- [ ] Implement interactor array model to display table view -- [ ] Example +- [License](#license) ## Requirements @@ -42,14 +34,13 @@ pod 'AirCollection' ## Usage ### Reusable View -All about reusable view usage you can read [there](README_VIEW.md) +All about reusable view, table view cells and collection view cells usage you can read [here](Documents/view.md) ### Table view -All about table view usage you can read [there](README_TABLE.md) +All about table view usage you can read [here](Documents/table.md) ### Collection view -All about collection view usage you can read [there](README_COLLECTION.md) - +All about collection view usage you can read [here](Documents/collection.md) ## License Released under the MIT license. See [LICENSE](LICENSE) for details. diff --git a/README_VIEW.md b/README_VIEW.md deleted file mode 100644 index 317a9a0..0000000 --- a/README_VIEW.md +++ /dev/null @@ -1,7 +0,0 @@ -# AirCollection.ConfigurableVIew - -`NibLoadableView` and `ConfigurableView` will be described here - -Not available now but will be fixed soon - - diff --git a/AirCollection.xcodeproj/project.pbxproj b/Source.xcodeproj/project.pbxproj similarity index 80% rename from AirCollection.xcodeproj/project.pbxproj rename to Source.xcodeproj/project.pbxproj index a2a42c2..c9573ad 100644 --- a/AirCollection.xcodeproj/project.pbxproj +++ b/Source.xcodeproj/project.pbxproj @@ -13,6 +13,17 @@ 5B0D9FA7251567CE000C6673 /* PickerViewControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D9FA6251567CE000C6673 /* PickerViewControllerProtocol.swift */; }; 5B0D9FA9251567E2000C6673 /* PickerViewPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D9FA8251567E2000C6673 /* PickerViewPresenterProtocol.swift */; }; 5B0D9FAE25156F66000C6673 /* PickerViewTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D9FAD25156F66000C6673 /* PickerViewTitle.swift */; }; + 5B2D23C3254404ED007DB77D /* collection.md in Resources */ = {isa = PBXBuildFile; fileRef = 5B2D23C0254404ED007DB77D /* collection.md */; }; + 5B2D23C4254404ED007DB77D /* view.md in Resources */ = {isa = PBXBuildFile; fileRef = 5B2D23C1254404ED007DB77D /* view.md */; }; + 5B2D23C5254404ED007DB77D /* table.md in Resources */ = {isa = PBXBuildFile; fileRef = 5B2D23C2254404ED007DB77D /* table.md */; }; + 5B491788252A7220006D5837 /* CollectionView+TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B491787252A7220006D5837 /* CollectionView+TextView.swift */; }; + 5B4C37952529BC4800A53EE0 /* ModelConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37942529BC4800A53EE0 /* ModelConfigurableView.swift */; }; + 5B4C37D52529FD4C00A53EE0 /* TextFieldControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37D42529FD4C00A53EE0 /* TextFieldControllerProtocol.swift */; }; + 5B4C37D82529FE3A00A53EE0 /* TextFieldPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37D72529FE3A00A53EE0 /* TextFieldPresenterProtocol.swift */; }; + 5B4C37DB252A07A900A53EE0 /* TableView+TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37DA252A07A900A53EE0 /* TableView+TextField.swift */; }; + 5B4C37DF252A286A00A53EE0 /* TextViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37DE252A286A00A53EE0 /* TextViewConfiguration.swift */; }; + 5B4C37E4252A2DFB00A53EE0 /* TextViewControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37E3252A2DFB00A53EE0 /* TextViewControllerProtocol.swift */; }; + 5B4C37E7252A2E2300A53EE0 /* TextViewPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4C37E6252A2E2300A53EE0 /* TextViewPresenterProtocol.swift */; }; 5B4ED7A625167DC00027E92A /* DatePickerControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4ED7A525167DC00027E92A /* DatePickerControllerProtocol.swift */; }; 5B4ED7A825167E030027E92A /* DatePickerPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4ED7A725167E030027E92A /* DatePickerPresenterProtocol.swift */; }; 5B4ED7AD251682D10027E92A /* TableView+TextFieldDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4ED7AC251682D10027E92A /* TableView+TextFieldDatePicker.swift */; }; @@ -23,18 +34,14 @@ 5B5A8DEC24CF49510001CB13 /* TableViewPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5A8DEB24CF49510001CB13 /* TableViewPresenterProtocol.swift */; }; 5B5A8DEF24CF49670001CB13 /* TableViewControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5A8DEE24CF49670001CB13 /* TableViewControllerProtocol.swift */; }; 5B8BA20524D35BFB0070CC77 /* TableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BA20424D35BFB0070CC77 /* TableViewDelegate.swift */; }; - 5B8BA20924D365750070CC77 /* AirCollection.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 5B8BA20824D365750070CC77 /* AirCollection.podspec */; }; 5B94A91E24BF7D0A00B2D01B /* CollectionViewElementKindSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B94A91D24BF7D0A00B2D01B /* CollectionViewElementKindSection.swift */; }; + 5B9B16BA252A63BB00C19CD7 /* CollectionView+TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9B16B9252A63BB00C19CD7 /* CollectionView+TextField.swift */; }; + 5B9B16BE252A642500C19CD7 /* TableView+TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9B16BD252A642500C19CD7 /* TableView+TextView.swift */; }; 5BAB775C2517DE10002961AF /* CollectionView+TextFieldPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB775B2517DE10002961AF /* CollectionView+TextFieldPickerView.swift */; }; 5BAB775E2517DE5F002961AF /* CollectionView+TextFieldDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAB775D2517DE5F002961AF /* CollectionView+TextFieldDatePicker.swift */; }; - 5BAB77602517DF63002961AF /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 5BAB775F2517DF62002961AF /* README.md */; }; - 5BAB77642517DF6A002961AF /* README_COLLECTION.md in Resources */ = {isa = PBXBuildFile; fileRef = 5BAB77612517DF6A002961AF /* README_COLLECTION.md */; }; - 5BAB77652517DF6A002961AF /* README_VIEW.md in Resources */ = {isa = PBXBuildFile; fileRef = 5BAB77622517DF6A002961AF /* README_VIEW.md */; }; - 5BAB77662517DF6A002961AF /* README_TABLE.md in Resources */ = {isa = PBXBuildFile; fileRef = 5BAB77632517DF6A002961AF /* README_TABLE.md */; }; 5BB3298C24BF1E4A00323F11 /* IdentificableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB3298B24BF1E4A00323F11 /* IdentificableView.swift */; }; 5BB3298E24BF1E8A00323F11 /* NibLoadableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB3298D24BF1E8A00323F11 /* NibLoadableView.swift */; }; 5BB3299024BF1E9800323F11 /* ConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB3298F24BF1E9800323F11 /* ConfigurableView.swift */; }; - 5BB3299424BF1EDB00323F11 /* TextViewInputModelConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB3299324BF1EDB00323F11 /* TextViewInputModelConfigurable.swift */; }; 5BB3299A24BF1F2900323F11 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB3299924BF1F2900323F11 /* Array+Extension.swift */; }; 5BB329A524BF239C00323F11 /* CollectionViewItemSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB329A424BF239C00323F11 /* CollectionViewItemSize.swift */; }; 5BB329A924BF29B400323F11 /* CollectionViewSupplementaryViewHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB329A824BF29B400323F11 /* CollectionViewSupplementaryViewHeight.swift */; }; @@ -45,7 +52,6 @@ 5BB7AC85250FECF0004F8394 /* TextInputConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB7AC84250FECF0004F8394 /* TextInputConfigurableView.swift */; }; 5BB7AC89250FF267004F8394 /* TextFieldConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB7AC88250FF267004F8394 /* TextFieldConfiguration.swift */; }; 5BD5B1C324D803D80065E5DB /* CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD5B1C224D803D80065E5DB /* CollectionViewDelegate.swift */; }; - DA182C912211757900163B42 /* AirCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = DA182C8F2211757900163B42 /* AirCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -55,6 +61,17 @@ 5B0D9FA6251567CE000C6673 /* PickerViewControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerViewControllerProtocol.swift; sourceTree = ""; }; 5B0D9FA8251567E2000C6673 /* PickerViewPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerViewPresenterProtocol.swift; sourceTree = ""; }; 5B0D9FAD25156F66000C6673 /* PickerViewTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerViewTitle.swift; sourceTree = ""; }; + 5B2D23C0254404ED007DB77D /* collection.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = collection.md; sourceTree = ""; }; + 5B2D23C1254404ED007DB77D /* view.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = view.md; sourceTree = ""; }; + 5B2D23C2254404ED007DB77D /* table.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = table.md; sourceTree = ""; }; + 5B491787252A7220006D5837 /* CollectionView+TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+TextView.swift"; sourceTree = ""; }; + 5B4C37942529BC4800A53EE0 /* ModelConfigurableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfigurableView.swift; sourceTree = ""; }; + 5B4C37D42529FD4C00A53EE0 /* TextFieldControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldControllerProtocol.swift; sourceTree = ""; }; + 5B4C37D72529FE3A00A53EE0 /* TextFieldPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPresenterProtocol.swift; sourceTree = ""; }; + 5B4C37DA252A07A900A53EE0 /* TableView+TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableView+TextField.swift"; sourceTree = ""; }; + 5B4C37DE252A286A00A53EE0 /* TextViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewConfiguration.swift; sourceTree = ""; }; + 5B4C37E3252A2DFB00A53EE0 /* TextViewControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewControllerProtocol.swift; sourceTree = ""; }; + 5B4C37E6252A2E2300A53EE0 /* TextViewPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewPresenterProtocol.swift; sourceTree = ""; }; 5B4ED7A525167DC00027E92A /* DatePickerControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerControllerProtocol.swift; sourceTree = ""; }; 5B4ED7A725167E030027E92A /* DatePickerPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerPresenterProtocol.swift; sourceTree = ""; }; 5B4ED7AC251682D10027E92A /* TableView+TextFieldDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableView+TextFieldDatePicker.swift"; sourceTree = ""; }; @@ -65,18 +82,14 @@ 5B5A8DEB24CF49510001CB13 /* TableViewPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewPresenterProtocol.swift; sourceTree = ""; }; 5B5A8DEE24CF49670001CB13 /* TableViewControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewControllerProtocol.swift; sourceTree = ""; }; 5B8BA20424D35BFB0070CC77 /* TableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDelegate.swift; sourceTree = ""; }; - 5B8BA20824D365750070CC77 /* AirCollection.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = AirCollection.podspec; sourceTree = ""; }; 5B94A91D24BF7D0A00B2D01B /* CollectionViewElementKindSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewElementKindSection.swift; sourceTree = ""; }; + 5B9B16B9252A63BB00C19CD7 /* CollectionView+TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+TextField.swift"; sourceTree = ""; }; + 5B9B16BD252A642500C19CD7 /* TableView+TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableView+TextView.swift"; sourceTree = ""; }; 5BAB775B2517DE10002961AF /* CollectionView+TextFieldPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+TextFieldPickerView.swift"; sourceTree = ""; }; 5BAB775D2517DE5F002961AF /* CollectionView+TextFieldDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+TextFieldDatePicker.swift"; sourceTree = ""; }; - 5BAB775F2517DF62002961AF /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 5BAB77612517DF6A002961AF /* README_COLLECTION.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README_COLLECTION.md; sourceTree = ""; }; - 5BAB77622517DF6A002961AF /* README_VIEW.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README_VIEW.md; sourceTree = ""; }; - 5BAB77632517DF6A002961AF /* README_TABLE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README_TABLE.md; sourceTree = ""; }; 5BB3298B24BF1E4A00323F11 /* IdentificableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentificableView.swift; sourceTree = ""; }; 5BB3298D24BF1E8A00323F11 /* NibLoadableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibLoadableView.swift; sourceTree = ""; }; 5BB3298F24BF1E9800323F11 /* ConfigurableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableView.swift; sourceTree = ""; }; - 5BB3299324BF1EDB00323F11 /* TextViewInputModelConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewInputModelConfigurable.swift; sourceTree = ""; }; 5BB3299924BF1F2900323F11 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 5BB329A424BF239C00323F11 /* CollectionViewItemSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewItemSize.swift; sourceTree = ""; }; 5BB329A824BF29B400323F11 /* CollectionViewSupplementaryViewHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSupplementaryViewHeight.swift; sourceTree = ""; }; @@ -87,8 +100,7 @@ 5BB7AC84250FECF0004F8394 /* TextInputConfigurableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputConfigurableView.swift; sourceTree = ""; }; 5BB7AC88250FF267004F8394 /* TextFieldConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldConfiguration.swift; sourceTree = ""; }; 5BD5B1C224D803D80065E5DB /* CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDelegate.swift; sourceTree = ""; }; - DA182C8C2211757900163B42 /* AirCollection.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AirCollection.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DA182C8F2211757900163B42 /* AirCollection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AirCollection.h; sourceTree = ""; }; + DA182C8C2211757900163B42 /* Source.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Source.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA182C902211757900163B42 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -113,6 +125,16 @@ name = PickerView; sourceTree = ""; }; + 5B2D23BF254404ED007DB77D /* Documents */ = { + isa = PBXGroup; + children = ( + 5B2D23C1254404ED007DB77D /* view.md */, + 5B2D23C2254404ED007DB77D /* table.md */, + 5B2D23C0254404ED007DB77D /* collection.md */, + ); + path = Documents; + sourceTree = ""; + }; 5B4ED7A425167DA90027E92A /* DatePicker */ = { isa = PBXGroup; children = ( @@ -157,6 +179,7 @@ 5BB3298B24BF1E4A00323F11 /* IdentificableView.swift */, 5BB3298D24BF1E8A00323F11 /* NibLoadableView.swift */, 5BB3298F24BF1E9800323F11 /* ConfigurableView.swift */, + 5B4C37942529BC4800A53EE0 /* ModelConfigurableView.swift */, 5BB7AC84250FECF0004F8394 /* TextInputConfigurableView.swift */, 5BB7AC86250FED97004F8394 /* TextField */, 5BB7AC87250FEE5B004F8394 /* TextView */, @@ -199,8 +222,10 @@ isa = PBXGroup; children = ( 5BB329B124BF2D4F00323F11 /* CollectionView+Extension.swift */, + 5B9B16B9252A63BB00C19CD7 /* CollectionView+TextField.swift */, 5BAB775B2517DE10002961AF /* CollectionView+TextFieldPickerView.swift */, 5BAB775D2517DE5F002961AF /* CollectionView+TextFieldDatePicker.swift */, + 5B491787252A7220006D5837 /* CollectionView+TextView.swift */, ); name = Extension; sourceTree = ""; @@ -209,8 +234,10 @@ isa = PBXGroup; children = ( 5B5A8DE124CF48FE0001CB13 /* TableView+Extension.swift */, + 5B4C37DA252A07A900A53EE0 /* TableView+TextField.swift */, 5B0D9FA325156155000C6673 /* TableView+TextFieldPickerView.swift */, 5B4ED7AC251682D10027E92A /* TableView+TextFieldDatePicker.swift */, + 5B9B16BD252A642500C19CD7 /* TableView+TextView.swift */, ); name = Extension; sourceTree = ""; @@ -241,6 +268,8 @@ 5BB7AC88250FF267004F8394 /* TextFieldConfiguration.swift */, 5B0D9F9C2515476D000C6673 /* TextFieldPickerViewConfiguration.swift */, 5B0D9F9E251555FB000C6673 /* TextFieldDatePickerConfiguration.swift */, + 5B4C37D42529FD4C00A53EE0 /* TextFieldControllerProtocol.swift */, + 5B4C37D72529FE3A00A53EE0 /* TextFieldPresenterProtocol.swift */, ); name = TextField; sourceTree = ""; @@ -248,7 +277,9 @@ 5BB7AC87250FEE5B004F8394 /* TextView */ = { isa = PBXGroup; children = ( - 5BB3299324BF1EDB00323F11 /* TextViewInputModelConfigurable.swift */, + 5B4C37DE252A286A00A53EE0 /* TextViewConfiguration.swift */, + 5B4C37E3252A2DFB00A53EE0 /* TextViewControllerProtocol.swift */, + 5B4C37E6252A2E2300A53EE0 /* TextViewPresenterProtocol.swift */, ); name = TextView; sourceTree = ""; @@ -256,12 +287,8 @@ DA182C822211757900163B42 = { isa = PBXGroup; children = ( - 5B8BA20824D365750070CC77 /* AirCollection.podspec */, - 5BAB775F2517DF62002961AF /* README.md */, - 5BAB77622517DF6A002961AF /* README_VIEW.md */, - 5BAB77632517DF6A002961AF /* README_TABLE.md */, - 5BAB77612517DF6A002961AF /* README_COLLECTION.md */, DA182C8E2211757900163B42 /* Source */, + 5B2D23BF254404ED007DB77D /* Documents */, DA182C8D2211757900163B42 /* Products */, ); sourceTree = ""; @@ -269,7 +296,7 @@ DA182C8D2211757900163B42 /* Products */ = { isa = PBXGroup; children = ( - DA182C8C2211757900163B42 /* AirCollection.framework */, + DA182C8C2211757900163B42 /* Source.framework */, ); name = Products; sourceTree = ""; @@ -277,7 +304,6 @@ DA182C8E2211757900163B42 /* Source */ = { isa = PBXGroup; children = ( - DA182C8F2211757900163B42 /* AirCollection.h */, 5B5A8DD724CF47AC0001CB13 /* Common */, 5B5A8DD824CF47B30001CB13 /* TableView */, 5B5A8DDA24CF47C10001CB13 /* CollectionView */, @@ -293,16 +319,15 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - DA182C912211757900163B42 /* AirCollection.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - DA182C8B2211757900163B42 /* AirCollection */ = { + DA182C8B2211757900163B42 /* Source */ = { isa = PBXNativeTarget; - buildConfigurationList = DA182C942211757900163B42 /* Build configuration list for PBXNativeTarget "AirCollection" */; + buildConfigurationList = DA182C942211757900163B42 /* Build configuration list for PBXNativeTarget "Source" */; buildPhases = ( DA182C872211757900163B42 /* Headers */, DA182C882211757900163B42 /* Sources */, @@ -313,9 +338,9 @@ ); dependencies = ( ); - name = AirCollection; + name = Source; productName = AirCollection; - productReference = DA182C8C2211757900163B42 /* AirCollection.framework */; + productReference = DA182C8C2211757900163B42 /* Source.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ @@ -324,8 +349,8 @@ DA182C832211757900163B42 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1010; - ORGANIZATIONNAME = "Developer Lysytsia"; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "Lysytsia Yurii"; TargetAttributes = { DA182C8B2211757900163B42 = { CreatedOnToolsVersion = 10.1; @@ -333,7 +358,7 @@ }; }; }; - buildConfigurationList = DA182C862211757900163B42 /* Build configuration list for PBXProject "AirCollection" */; + buildConfigurationList = DA182C862211757900163B42 /* Build configuration list for PBXProject "Source" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -346,7 +371,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - DA182C8B2211757900163B42 /* AirCollection */, + DA182C8B2211757900163B42 /* Source */, ); }; /* End PBXProject section */ @@ -356,11 +381,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5BAB77602517DF63002961AF /* README.md in Resources */, - 5BAB77652517DF6A002961AF /* README_VIEW.md in Resources */, - 5BAB77642517DF6A002961AF /* README_COLLECTION.md in Resources */, - 5BAB77662517DF6A002961AF /* README_TABLE.md in Resources */, - 5B8BA20924D365750070CC77 /* AirCollection.podspec in Resources */, + 5B2D23C3254404ED007DB77D /* collection.md in Resources */, + 5B2D23C4254404ED007DB77D /* view.md in Resources */, + 5B2D23C5254404ED007DB77D /* table.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -371,6 +394,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5B4C37DF252A286A00A53EE0 /* TextViewConfiguration.swift in Sources */, 5B0D9F9D2515476D000C6673 /* TextFieldPickerViewConfiguration.swift in Sources */, 5B8BA20524D35BFB0070CC77 /* TableViewDelegate.swift in Sources */, 5B0D9FA7251567CE000C6673 /* PickerViewControllerProtocol.swift in Sources */, @@ -382,25 +406,33 @@ 5B5A8DEA24CF49440001CB13 /* TableViewData.swift in Sources */, 5BB3298C24BF1E4A00323F11 /* IdentificableView.swift in Sources */, 5BB329B024BF2AC000323F11 /* CollectionViewPresenterProtocol.swift in Sources */, + 5B491788252A7220006D5837 /* CollectionView+TextView.swift in Sources */, 5B5A8DEC24CF49510001CB13 /* TableViewPresenterProtocol.swift in Sources */, - 5BB3299424BF1EDB00323F11 /* TextViewInputModelConfigurable.swift in Sources */, 5B94A91E24BF7D0A00B2D01B /* CollectionViewElementKindSection.swift in Sources */, 5BB3298E24BF1E8A00323F11 /* NibLoadableView.swift in Sources */, 5BD5B1C324D803D80065E5DB /* CollectionViewDelegate.swift in Sources */, 5BB3299A24BF1F2900323F11 /* Array+Extension.swift in Sources */, 5BAB775C2517DE10002961AF /* CollectionView+TextFieldPickerView.swift in Sources */, + 5B4C37E4252A2DFB00A53EE0 /* TextViewControllerProtocol.swift in Sources */, + 5B4C37DB252A07A900A53EE0 /* TableView+TextField.swift in Sources */, + 5B9B16BA252A63BB00C19CD7 /* CollectionView+TextField.swift in Sources */, 5B0D9F9F251555FB000C6673 /* TextFieldDatePickerConfiguration.swift in Sources */, + 5B9B16BE252A642500C19CD7 /* TableView+TextView.swift in Sources */, 5BB3299024BF1E9800323F11 /* ConfigurableView.swift in Sources */, + 5B4C37D52529FD4C00A53EE0 /* TextFieldControllerProtocol.swift in Sources */, 5B5A8DE224CF48FE0001CB13 /* TableView+Extension.swift in Sources */, 5BAB775E2517DE5F002961AF /* CollectionView+TextFieldDatePicker.swift in Sources */, 5B5A8DEF24CF49670001CB13 /* TableViewControllerProtocol.swift in Sources */, 5BB7AC89250FF267004F8394 /* TextFieldConfiguration.swift in Sources */, 5BB7AC85250FECF0004F8394 /* TextInputConfigurableView.swift in Sources */, + 5B4C37952529BC4800A53EE0 /* ModelConfigurableView.swift in Sources */, 5BB329AE24BF2AB900323F11 /* CollectionViewControllerProtocol.swift in Sources */, 5B0D9FAE25156F66000C6673 /* PickerViewTitle.swift in Sources */, 5BB329A924BF29B400323F11 /* CollectionViewSupplementaryViewHeight.swift in Sources */, 5B4ED7AD251682D10027E92A /* TableView+TextFieldDatePicker.swift in Sources */, 5B4ED7A825167E030027E92A /* DatePickerPresenterProtocol.swift in Sources */, + 5B4C37D82529FE3A00A53EE0 /* TextFieldPresenterProtocol.swift in Sources */, + 5B4C37E7252A2E2300A53EE0 /* TextViewPresenterProtocol.swift in Sources */, 5BB329AC24BF2A6C00323F11 /* CollectionViewData.swift in Sources */, 5B5A8DE824CF49330001CB13 /* TableViewHeaderFooterViewHeight.swift in Sources */, 5B0D9FA425156155000C6673 /* TableView+TextFieldPickerView.swift in Sources */, @@ -436,6 +468,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -500,6 +533,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -551,7 +585,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "lysytsia.yurii.air-collection"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -581,7 +615,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "lysytsia.yurii.air-collection"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -594,7 +628,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - DA182C862211757900163B42 /* Build configuration list for PBXProject "AirCollection" */ = { + DA182C862211757900163B42 /* Build configuration list for PBXProject "Source" */ = { isa = XCConfigurationList; buildConfigurations = ( DA182C922211757900163B42 /* Debug */, @@ -603,7 +637,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DA182C942211757900163B42 /* Build configuration list for PBXNativeTarget "AirCollection" */ = { + DA182C942211757900163B42 /* Build configuration list for PBXNativeTarget "Source" */ = { isa = XCConfigurationList; buildConfigurations = ( DA182C952211757900163B42 /* Debug */, diff --git a/AirCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Source.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from AirCollection.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Source.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Source.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Source.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Source.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/AirCollection.xcodeproj/xcshareddata/xcschemes/AirCollection.xcscheme b/Source.xcodeproj/xcshareddata/xcschemes/Source.xcscheme similarity index 75% rename from AirCollection.xcodeproj/xcshareddata/xcschemes/AirCollection.xcscheme rename to Source.xcodeproj/xcshareddata/xcschemes/Source.xcscheme index cf32962..04a2eab 100644 --- a/AirCollection.xcodeproj/xcshareddata/xcschemes/AirCollection.xcscheme +++ b/Source.xcodeproj/xcshareddata/xcschemes/Source.xcscheme @@ -1,6 +1,6 @@ + BuildableName = "Source.framework" + BlueprintName = "Source" + ReferencedContainer = "container:Source.xcodeproj"> @@ -47,15 +47,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - - - - diff --git a/Source/AirCollection.h b/Source/AirCollection.h deleted file mode 100644 index 456a522..0000000 --- a/Source/AirCollection.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// AirCollection.h -// AirCollection -// -// Created by Yuri Fox on 2/11/19. -// Copyright © 2019 Developer Lysytsia. All rights reserved. -// - -#import - -//! Project version number for AirCollection. -FOUNDATION_EXPORT double AirCollectionVersionNumber; - -//! Project version string for AirCollection. -FOUNDATION_EXPORT const unsigned char AirCollectionVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Source/CollectionView/CollectionView+Extension.swift b/Source/CollectionView/CollectionView+Extension.swift index fe02de7..78f3afa 100644 --- a/Source/CollectionView/CollectionView+Extension.swift +++ b/Source/CollectionView/CollectionView+Extension.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import class UIKit.UICollectionView import class UIKit.UICollectionViewCell import class UIKit.UICollectionReusableView @@ -18,8 +19,8 @@ public extension UICollectionView { } /// Register a nib file for use in creating new collection view cells - func register(_ cellClass: T.Type) where T: NibLoadableView { - self.register(cellClass.nib(), forCellWithReuseIdentifier: cellClass.viewIdentifier) + func register(_ cellClass: T.Type) where T: NibLoadableView & IdentificableView { + self.register(cellClass.viewNib, forCellWithReuseIdentifier: cellClass.viewIdentifier) } /// Registers a class for use in creating supplementary views for the collection view @@ -28,8 +29,19 @@ public extension UICollectionView { } /// Registers a nib file for use in creating supplementary views for the collection view - func register(_ viewClass: T.Type, for kind: CollectionViewElementKindSection) where T: NibLoadableView { - self.register(viewClass.nib(), forSupplementaryViewOfKind: kind.rawValue, withReuseIdentifier: viewClass.viewIdentifier) + func register(_ viewClass: T.Type, for kind: CollectionViewElementKindSection) where T: NibLoadableView & IdentificableView { + self.register(viewClass.viewNib, forSupplementaryViewOfKind: kind.rawValue, withReuseIdentifier: viewClass.viewIdentifier) + } + + /// Gets the index path of supplementary view of the specified type. + /// - Parameters: + /// - supplementaryView: The supplementary view object whose index path you want. + /// - kind: The kind of supplementary view to locate. This value is defined by the layout object. This parameter must not be nil + func indexPath(forSupplementaryView supplementaryView: UICollectionReusableView, of elementKind: CollectionViewElementKindSection) -> IndexPath? { + let visibleIndexPaths = self.indexPathsForVisibleSupplementaryElements(ofKind: elementKind.rawValue) + return visibleIndexPaths.first(where: { + self.supplementaryView(forElementKind: elementKind.rawValue, at: $0) == supplementaryView + }) } } diff --git a/Source/CollectionView/CollectionView+TextField.swift b/Source/CollectionView/CollectionView+TextField.swift new file mode 100644 index 0000000..be8a199 --- /dev/null +++ b/Source/CollectionView/CollectionView+TextField.swift @@ -0,0 +1,78 @@ +// +// CollectionView+TextField.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import class UIKit.UITextField + +extension CollectionViewControllerProtocol where Self: TextFieldControllerProtocol { + + public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldBeginEditing(at: indexPath) + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldDidBeginEditing(at: indexPath) + } + + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldEndEditing(at: indexPath) + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldDidEndEditing(at: indexPath) + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textField(at: indexPath, shouldChangeCharactersIn: range, replacementString: string) + } + + public func textFieldShouldClear(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldClear(at: indexPath) + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldReturn(at: indexPath) + } + + public func textFieldEditingChanged(_ textField: UITextField) { + guard let indexPath = self.indexPathForItem(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `collectionView` items. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldTextDidChanged(textField.text, at: indexPath) + } + +} + diff --git a/Source/CollectionView/CollectionView+TextFieldDatePicker.swift b/Source/CollectionView/CollectionView+TextFieldDatePicker.swift index b9bc00d..5dc43bf 100644 --- a/Source/CollectionView/CollectionView+TextFieldDatePicker.swift +++ b/Source/CollectionView/CollectionView+TextFieldDatePicker.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.Date import class UIKit.UITextField import class UIKit.UIDatePicker diff --git a/Source/CollectionView/CollectionView+TextView.swift b/Source/CollectionView/CollectionView+TextView.swift new file mode 100644 index 0000000..521e9f5 --- /dev/null +++ b/Source/CollectionView/CollectionView+TextView.swift @@ -0,0 +1,81 @@ +// +// CollectionView+TextView.swift +// Source +// +// Created by Lysytsia Yurii on 05.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import enum UIKit.UITextItemInteraction +import struct Foundation.URL +import class UIKit.UITextView +import class UIKit.NSTextAttachment + +extension CollectionViewControllerProtocol where Self: TextViewControllerProtocol { + + public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textViewShouldBeginEditing(at: indexPath) + } + + public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textViewShouldEndEditing(at: indexPath) + } + + public func textViewDidBeginEditing(_ textView: UITextView) { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidBeginEditing(at: indexPath) + } + + public func textViewDidEndEditing(_ textView: UITextView) { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidEndEditing(at: indexPath) + } + + public func textView(_ textView: UITextView, shouldChangeTextIn range: Range, replacementText text: String) -> Bool { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldChangeTextIn: range, replacementText: text) + } + + public func textViewDidChange(_ textView: UITextView) { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidChange(text: textView.text, at: indexPath) + } + + public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldInteractWith: URL, in: characterRange, interaction: interaction) + } + + public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + guard let indexPath = self.indexPathForItem(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `collectionView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) + } + +} + diff --git a/Source/CollectionView/CollectionViewControllerProtocol.swift b/Source/CollectionView/CollectionViewControllerProtocol.swift index 13bf74f..9954f58 100644 --- a/Source/CollectionView/CollectionViewControllerProtocol.swift +++ b/Source/CollectionView/CollectionViewControllerProtocol.swift @@ -6,10 +6,14 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath +import struct Foundation.IndexSet import struct CoreGraphics.CGPoint import class UIKit.UIView import class UIKit.UICollectionView import protocol UIKit.UIScrollViewDelegate +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject public protocol CollectionViewControllerProtocol: class { @@ -19,7 +23,7 @@ public protocol CollectionViewControllerProtocol: class { /// Return an instanse of the collection view present var collectionViewPresenter: CollectionViewPresenterProtocol { get } - /// Configure `UICollectionViewDataSource` and `UICollectionViewDelegate` for specific collection view data model + /// Configure `UICollectionViewDataSource` and `UICollectionViewDelegate` for specific collection view and presenter. Also automatically add `CollectionViewDelegate` to current view controller if implemented. /// - Parameter configurator: Use this block to set up the collection view. You should register collection view cell, headers and footer is this block func configureCollectionView(configurator: (UICollectionView) -> Void) @@ -58,6 +62,12 @@ public protocol CollectionViewControllerProtocol: class { /// Deselects the collection view item at the specified index. func deselectCollectionViewItem(at indexPath: IndexPath, animated: Bool) + /// Make an item input view in the collection view identified by index path the first responder in its window at. + func becomeCollectionViewRowFirstResponder(at indexPath: IndexPath) + + /// Notifies an item input view in the collection view identified by index path that it has been asked to relinquish its status as first responder in its window. + func resignCollectionViewRowFirstResponder(at indexPath: IndexPath) + /// Reloads the data in the specified sections of the collection view. func reloadCollectionViewSections(_ sections: [Int]) @@ -87,6 +97,10 @@ public extension CollectionViewControllerProtocol { func configureCollectionView(configurator: (UICollectionView) -> Void) { self.collectionViewSource.dataSource = self.collectionViewData self.collectionViewSource.delegate = self.collectionViewData + if let delegate = self as? CollectionViewDelegate { + // Forward available collection view delegate to current view controller. + self.collectionViewData.collectionViewDelegate = delegate + } configurator(self.collectionViewSource) } @@ -159,6 +173,20 @@ public extension CollectionViewControllerProtocol { self.collectionViewSource.deselectItem(at: indexPath, animated: animated) } + func becomeTableViewRowFirstResponder(at indexPath: IndexPath) { + guard let cell = self.collectionViewSource.cellForItem(at: indexPath) as? InputConfigurableView else { + return + } + cell.becomeInputViewFirstResponder() + } + + func resignTableViewRowFirstResponder(at indexPath: IndexPath) { + guard let cell = self.collectionViewSource.cellForItem(at: indexPath) as? InputConfigurableView else { + return + } + cell.resignInputViewFirstResponder() + } + // MARK: Sections func reloadCollectionViewSections(_ sections: [Int]) { self.collectionViewData.reloadSections(sections) @@ -198,19 +226,9 @@ public extension CollectionViewControllerProtocol { } func indexPathForItem(with view: UIView) -> IndexPath? { - let point = view.convert(CGPoint.zero, to: self.collectionViewSource) - return self.collectionViewSource.indexPathForItem(at: point) - } -} - -// MARK: - CollectionViewDelegateForward -public extension CollectionViewControllerProtocol where Self: CollectionViewDelegate { - - /// Forward all collection view `UIScrollViewDelegate` to current view controller. - func forwardCollectionViewDelegate() { - self.collectionViewData.collectionViewDelegate = self + let rect = view.convert(view.bounds, to: self.collectionViewSource) + return self.collectionViewSource.indexPathForItem(at: rect.origin) } - } // MARK: - CollectionViewData diff --git a/Source/CollectionView/CollectionViewData.swift b/Source/CollectionView/CollectionViewData.swift index adc6e3c..c09aa8c 100644 --- a/Source/CollectionView/CollectionViewData.swift +++ b/Source/CollectionView/CollectionViewData.swift @@ -6,11 +6,14 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath +import struct Foundation.IndexSet import struct CoreGraphics.CGFloat import struct CoreGraphics.CGPoint import struct CoreGraphics.CGSize import struct UIKit.UIEdgeInsets import struct UIKit.UILayoutPriority +import class Foundation.NSObject import class UIKit.UIView import class UIKit.UIScrollView import class UIKit.UICollectionView @@ -175,7 +178,7 @@ class CollectionViewData: NSObject { assertionFailure("For use `CollectionViewData.configureCell(_:for:)`and configure cell you must implement `ConfigurableView` protocol for item type `\(type(of: cell))`") return } - configurableCell.configure(model: model) + configurableCell.configure(model) } func configureHeaderView(_ view: UICollectionReusableView, for section: Int) { @@ -186,7 +189,7 @@ class CollectionViewData: NSObject { assertionFailure("For use `CollectionViewData.configureHeaderView(_:for:)`and configure header view you must implement `ConfigurableView` protocol for header view type `\(type(of: view))`") return } - configurableView.configure(model: model) + configurableView.configure(model) } func configureFooterView(_ view: UICollectionReusableView, for section: Int) { @@ -197,7 +200,7 @@ class CollectionViewData: NSObject { assertionFailure("For use `CollectionViewData.configureFooterView(_:for:)`and configure footer view you must implement `ConfigurableView` protocol for footer view type `\(type(of: view))`") return } - configurableView.configure(model: model) + configurableView.configure(model) } } diff --git a/Source/CollectionView/CollectionViewDelegate.swift b/Source/CollectionView/CollectionViewDelegate.swift index 142c940..56b13d8 100644 --- a/Source/CollectionView/CollectionViewDelegate.swift +++ b/Source/CollectionView/CollectionViewDelegate.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import class UIKit.UICollectionView import class UIKit.UICollectionViewCell import class UIKit.UICollectionReusableView diff --git a/Source/CollectionView/CollectionViewPresenterProtocol.swift b/Source/CollectionView/CollectionViewPresenterProtocol.swift index 26bbc0d..1847ed8 100644 --- a/Source/CollectionView/CollectionViewPresenterProtocol.swift +++ b/Source/CollectionView/CollectionViewPresenterProtocol.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import struct CoreGraphics.CGFloat import struct UIKit.UIEdgeInsets diff --git a/Source/Common/Array+Extension.swift b/Source/Common/Array+Extension.swift index a15d49c..02b8d5d 100644 --- a/Source/Common/Array+Extension.swift +++ b/Source/Common/Array+Extension.swift @@ -3,7 +3,7 @@ // AirCollection // // Created by Lysytsia Yurii on 15.07.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // extension Collection { diff --git a/Source/Common/ConfigurableView.swift b/Source/Common/ConfigurableView.swift index 0da9777..2c76ac6 100644 --- a/Source/Common/ConfigurableView.swift +++ b/Source/Common/ConfigurableView.swift @@ -3,35 +3,21 @@ // AirCollection // // Created by Lysytsia Yurii on 15.07.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // -// MARK: - ConfigurableView -public protocol ConfigurableView: IdentificableView { +/// Abstract protocol needed for implement configure view method and defines one method *configure(_:)*. +/// +/// You shouldn't use this protocol for your views (include cells) implementation, but you able to create additional protocol that implement default implementation. See [ModelConfigurableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#model-configurable-view) for your custom implementation sample. +/// +/// Read more about [ConfigurableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#configurable-view). +public protocol ConfigurableView: class { - /// Configure view with some non specific model - func configure(model: Any?) - -} - -// MARK: - ModelConfigurableView -public protocol ModelConfigurableView: ConfigurableView { - - /// Configure view with some specific model type - func configure(model: Model) - - associatedtype Model - -} - -extension ModelConfigurableView { - - public func configure(model: Any?) { - guard let configurableModel = model as? Model else { - assertionFailure("Invalid model for configure view of type `\(type(of: self))`. Model is not confirm to model type \(Model.self)") - return - } - self.configure(model: configurableModel) - } + /// Abstract method called when view have to update. + /// + /// You shouldn't use this method implementation, but you able to create additional protocol that implement default implementation based on this method. See [ModelConfigurableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#model-configurable-view) for your custom implementation sample. + /// + /// - Parameter model: Any model that will use for update view + func configure(_ model: Any) } diff --git a/Source/Common/DatePickerControllerProtocol.swift b/Source/Common/DatePickerControllerProtocol.swift index 2668cf4..2386afb 100644 --- a/Source/Common/DatePickerControllerProtocol.swift +++ b/Source/Common/DatePickerControllerProtocol.swift @@ -3,7 +3,7 @@ // AirCollection // // Created by Lysytsia Yurii on 19.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // public protocol DatePickerControllerProtocol: class { diff --git a/Source/Common/DatePickerPresenterProtocol.swift b/Source/Common/DatePickerPresenterProtocol.swift index dbe47fa..dc148f0 100644 --- a/Source/Common/DatePickerPresenterProtocol.swift +++ b/Source/Common/DatePickerPresenterProtocol.swift @@ -3,9 +3,12 @@ // AirCollection // // Created by Lysytsia Yurii on 19.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // +import struct Foundation.Date +import struct Foundation.IndexPath + public protocol DatePickerPresenterProtocol: class { /// Called by controller (`DatePickerControllerProtocol`) when the user selects a row with date for index path func datePickerDidSelectDate(_ date: Date, at indexPath: IndexPath) diff --git a/Source/Common/IdentificableView.swift b/Source/Common/IdentificableView.swift index ed67689..2449d30 100644 --- a/Source/Common/IdentificableView.swift +++ b/Source/Common/IdentificableView.swift @@ -3,12 +3,17 @@ // AirCollection // // Created by Lysytsia Yurii on 15.07.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // +/// Protocol needed for implement unique view identifier and defines only one property *viewIdentifier*. +/// +/// Read more about [IdentificableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#identificable-view). public protocol IdentificableView: class { - /// Unique view identefier. Default is `String(describing: self)` + /// Unique view identefier. + /// + /// Default is *String.init(describing: self)*. static var viewIdentifier: String { get } } diff --git a/Source/Common/ModelConfigurableView.swift b/Source/Common/ModelConfigurableView.swift new file mode 100644 index 0000000..c530d62 --- /dev/null +++ b/Source/Common/ModelConfigurableView.swift @@ -0,0 +1,43 @@ +// +// ModelConfigurableView.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +/// Base *ConfigurableView* protocol implementation and defines new one safe method *configure(model:)* based on predefined associated *Model* type. +/// +/// You should use this protocol for your views (include cells) implementation instead *ConfigurableView*. +/// +/// Read more about [ModelConfigurableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#model-configurable-view). +public protocol ModelConfigurableView: ConfigurableView { + + /// Predefined model type for configure this view. + associatedtype Model + + /// Configure view with predefined model type. + /// + /// By default this method use as delegate and called when abstract *configure(_:)* method called with similar *Model* type parameter. + /// + /// - Parameter model: Predefined model that will use for update view. + func configure(model: Model) + +} + +extension ModelConfigurableView { + + /// Overridden abstract method that called when view have to update. + /// + /// **Caution!** Don't override this method implementation because it able to break other functionality + /// + /// - Parameter model: Any model that will use for update view + public func configure(_ model: Any) { + guard let predefinedModel = model as? Model else { + assertionFailure("\(#function). `\(String(reflecting: Self.self))` not able to `configure(model:)` because received model `\(String(reflecting: type(of: model).self))` not confirm to `\(String(reflecting: Model.self))`") + return + } + self.configure(model: predefinedModel) + } + +} diff --git a/Source/Common/NibLoadableView.swift b/Source/Common/NibLoadableView.swift index 2738d82..b4867b4 100644 --- a/Source/Common/NibLoadableView.swift +++ b/Source/Common/NibLoadableView.swift @@ -3,56 +3,30 @@ // AirCollection // // Created by Lysytsia Yurii on 15.07.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // import class UIKit.UINib import class UIKit.UIView +import class Foundation.Bundle -public protocol NibLoadableView: IdentificableView { +/// Protocol needed for implement view nib instantiate and defines only one property *viewNib*. +/// +/// Read more about [NibLoadableView](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#nib-loadable-view). +public protocol NibLoadableView: class { - /// Default nib name for this view. Default is `IdentificableView.viewIdentifier` - static func nibName() -> String - - /// Default created nib for this view using bunle for `Self` - static func nib() -> UINib - - /// Unarchives and instantiates the in-memory contents of the receiver’s nib file, creating a distinct object tree and set of top level objects. Caution This method may be unsafe! - static func loadFromNib() -> Self - - /// Unarchives and instantiates the in-memory contents of the receiver’s nib file, creating a distinct object tree and set of top level objects. Caution This method may be unsafe! - static func loadFromNib(owner: Any) -> Self - - /// Unarchives and instantiates the in-memory contents of the receiver’s nib file, creating a distinct object tree and set of top level objects. Caution This method may be unsafe! - static func loadFromNib(owner: Any, options: [UINib.OptionsKey : Any]?) -> Self + /// Created nib instance for this view. + static var viewNib: UINib { get } } -public extension NibLoadableView { +public extension NibLoadableView where Self: IdentificableView { - static func nibName() -> String { - return self.viewIdentifier - } - -} - -public extension NibLoadableView where Self: UIView { - - static func nib() -> UINib { + /// Created nib instance for this view. Will use *viewIdentifier* for nibName and *Bundle(for: Self.self)* for bunlde by default. + static var viewNib: UINib { + let nibName = self.viewIdentifier let bundle = Bundle(for: Self.self) - return UINib(nibName: self.nibName(), bundle: bundle) - } - - static func loadFromNib() -> Self { - return self.loadFromNib(owner: self) - } - - static func loadFromNib(owner: Any) -> Self { - return self.loadFromNib(owner: owner, options: nil) - } - - static func loadFromNib(owner: Any, options: [UINib.OptionsKey : Any]?) -> Self { - return nib().instantiate(withOwner: owner, options: options).first as! Self + return UINib(nibName: nibName, bundle: bundle) } } diff --git a/Source/Common/PickerViewControllerProtocol.swift b/Source/Common/PickerViewControllerProtocol.swift index cfbdb97..5fae0b3 100644 --- a/Source/Common/PickerViewControllerProtocol.swift +++ b/Source/Common/PickerViewControllerProtocol.swift @@ -3,11 +3,11 @@ // AirCollection // // Created by Lysytsia Yurii on 19.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // public protocol PickerViewControllerProtocol: class { - /// Return an instanse of the picker view presented + /// Return an instanse of the picker view presenter var pickerViewPresenter: PickerViewPresenterProtocol { get } } diff --git a/Source/Common/PickerViewPresenterProtocol.swift b/Source/Common/PickerViewPresenterProtocol.swift index e20af5d..5990ee9 100644 --- a/Source/Common/PickerViewPresenterProtocol.swift +++ b/Source/Common/PickerViewPresenterProtocol.swift @@ -3,9 +3,11 @@ // AirCollection // // Created by Lysytsia Yurii on 19.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // +import struct Foundation.IndexPath + public protocol PickerViewPresenterProtocol: class { /// Called by controller (`PickerViewControllerProtocol`) when picker view needs the number of components for index path func pickerViewNumberOfComponents(at indexPath: IndexPath) -> Int diff --git a/Source/Common/PickerViewTitle.swift b/Source/Common/PickerViewTitle.swift index 2ebe60d..b1df6e7 100644 --- a/Source/Common/PickerViewTitle.swift +++ b/Source/Common/PickerViewTitle.swift @@ -3,9 +3,11 @@ // AirCollection // // Created by Lysytsia Yurii on 19.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // +import Foundation.NSAttributedString + // MARK: - PickerViewTitle public enum PickerViewTitle { case title(String) diff --git a/Source/Common/TextFieldConfiguration.swift b/Source/Common/TextFieldConfiguration.swift index fff49b8..5881d5a 100644 --- a/Source/Common/TextFieldConfiguration.swift +++ b/Source/Common/TextFieldConfiguration.swift @@ -3,13 +3,15 @@ // AirCollection // // Created by Lysytsia Yurii on 14.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // import protocol UIKit.UITextFieldDelegate +import class Foundation.NSObject import class UIKit.UITextField import class UIKit.UIView import class UIKit.UIColor +import struct Foundation.NSRange import struct UIKit.UITextContentType import enum UIKit.UITextAutocapitalizationType import enum UIKit.UITextAutocorrectionType @@ -21,6 +23,70 @@ import enum UIKit.UITextSmartQuotesType import enum UIKit.UITextSmartInsertDeleteType import enum UIKit.UITextSpellCheckingType import enum UIKit.NSTextAlignment +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject + +public protocol TextFieldDelegate: class { + /// Asks the delegate if editing should begin in the specified text field + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool + + /// Tells the delegate that editing began in the specified text field + func textFieldDidBeginEditing(_ textField: UITextField) + + /// Asks the delegate if editing should stop in the specified text field + func textFieldShouldEndEditing(_ textField: UITextField) -> Bool + + /// Tells the delegate that editing stopped for the specified text field + func textFieldDidEndEditing(_ textField: UITextField) + + /// Asks the delegate if the specified text should be changed + func textField(_ textField: UITextField, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool + + /// Asks the delegate if the text field’s current contents should be removed + func textFieldShouldClear(_ textField: UITextField) -> Bool + + /// Asks the delegate if the text field should process the pressing of the return button + func textFieldShouldReturn(_ textField: UITextField) -> Bool + + /// Tells the delegate that editing text changed for the specified text field. + func textFieldEditingChanged(_ textField: UITextField) +} + +public extension TextFieldDelegate { + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + return true + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + + } + + func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + return true + } + + func textFieldDidEndEditing(_ textField: UITextField) { + + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool { + return true + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return true + } + + func textFieldEditingChanged(_ textField: UITextField) { + + } + +} open class TextFieldConfiguration: TextInputConfiguration { @@ -30,8 +96,6 @@ open class TextFieldConfiguration: TextInputConfiguration { public var autocorrectionType: UITextAutocorrectionType = .default /// Controls when the standard clear button appears in the text field. Default is `UITextField.ViewMode.never` public var clearButtonMode: UITextField.ViewMode = .never - /// The receiver’s delegate. Default is nil - public var delegate: UITextFieldDelegate? = nil /// The custom input view to display when the text field becomes the first responder. Default is nil public var inputView: UIView? = nil /// Identifies whether the text object should disable text copying and in some cases hide the text being entered. Default is `false` @@ -53,17 +117,22 @@ open class TextFieldConfiguration: TextInputConfiguration { /// The semantic meaning expected by a text input area. Default is nil public var textContentType: UITextContentType? = nil - public init(keyboardType: UIKeyboardType = .default, textContentType: UITextContentType? = nil, delegate: UITextFieldDelegate? = nil) { + /// The receiver’s delegate + public unowned let textFieldDelegate: TextFieldDelegate + + public init(keyboardType: UIKeyboardType = .default, textContentType: UITextContentType? = nil, delegate: TextFieldDelegate) { self.keyboardType = keyboardType self.textContentType = textContentType - self.delegate = delegate + self.textFieldDelegate = delegate } public func configure(textInputView: UITextField) { + let textFieldData = TextFieldData(textField: textInputView, delegate: self.textFieldDelegate) + textInputView.delegate = textFieldData + textInputView.textFieldData = textFieldData textInputView.autocapitalizationType = self.autocapitalizationType textInputView.autocorrectionType = self.autocorrectionType textInputView.clearButtonMode = self.clearButtonMode - textInputView.delegate = self.delegate textInputView.inputView = self.inputView textInputView.isSecureTextEntry = self.isSecureTextEntry textInputView.keyboardAppearance = self.keyboardAppearance @@ -77,3 +146,103 @@ open class TextFieldConfiguration: TextInputConfiguration { } } + +// MARK: - TextFieldData +fileprivate class TextFieldData: NSObject, UITextFieldDelegate { + + private unowned let textField: UITextField + private unowned let delegate: TextFieldDelegate + + init(textField: UITextField, delegate: TextFieldDelegate) { + self.textField = textField + self.delegate = delegate + super.init() + self.textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged) + } + + @objc private func textFieldEditingChanged(_ textField: UITextField) { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textFieldEditingChanged(textField) + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textFieldShouldBeginEditing(textField) + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textFieldDidBeginEditing(textField) + } + + func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textFieldShouldEndEditing(textField) + } + + func textFieldDidEndEditing(_ textField: UITextField) { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textFieldDidEndEditing(textField) + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + let text = textField.text ?? "" + guard let stringRange = Range.init(range, in: text) else { + assertionFailure("\(#function) something went wrong and `Swift.Range` didn't evaluate with `NSRange`. Please don't use this method for other destination") + return true + } + return self.delegate.textField(textField, shouldChangeCharactersIn: stringRange, replacementString: string) + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textFieldShouldClear(textField) + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard textField == self.textField else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textFieldShouldReturn(textField) + } + +} + +// MARK: - Wrapper Associated Object +fileprivate var textFieldDataKey: String = "UITextField.textFieldData" +fileprivate extension UITextField { + + /// Get associated `TableViewData` object with this text field + var textFieldData: TextFieldData? { + set { + objc_setAssociatedObject(self, &textFieldDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + get { + objc_getAssociatedObject(self, &textFieldDataKey) as? TextFieldData + } + } + +} diff --git a/Source/Common/TextFieldControllerProtocol.swift b/Source/Common/TextFieldControllerProtocol.swift new file mode 100644 index 0000000..aee974c --- /dev/null +++ b/Source/Common/TextFieldControllerProtocol.swift @@ -0,0 +1,14 @@ +// +// TextFieldControllerProtocol.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +public protocol TextFieldControllerProtocol: TextFieldDelegate { + + /// Return an instanse of the text field presenter + var textFieldPresenter: TextFieldPresenterProtocol { get } + +} diff --git a/Source/Common/TextFieldDatePickerConfiguration.swift b/Source/Common/TextFieldDatePickerConfiguration.swift index 1e0647d..be45a76 100644 --- a/Source/Common/TextFieldDatePickerConfiguration.swift +++ b/Source/Common/TextFieldDatePickerConfiguration.swift @@ -3,14 +3,19 @@ // AirCollection // // Created by Lysytsia Yurii on 18.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // +import struct Foundation.Date +import class Foundation.NSObject import class UIKit.UITextField import class UIKit.UIDatePicker +import enum UIKit.UIDatePickerStyle +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject // MARK: - TextFieldDatePickerDelegate -public protocol TextFieldDatePickerDelegate: class { +public protocol TextFieldDatePickerDelegate: TextFieldDelegate { /// Called by the text field date picker when the user selects a row with date func textField(_ textField: UITextField, datePicker: UIDatePicker, didSelectDate date: Date) @@ -32,6 +37,7 @@ public class TextFieldDatePickerConfiguration: TextFieldConfiguration { public init(datePicker: UIDatePicker, delegate: TextFieldDatePickerDelegate) { self.datePicker = datePicker self.datePickerDelegate = delegate + super.init(delegate: delegate) } public convenience init(mode: UIDatePicker.Mode, date: Date = Date(), minimumDate: Date? = nil, maximumDate: Date? = nil, delegate: TextFieldDatePickerDelegate) { @@ -40,13 +46,16 @@ public class TextFieldDatePickerConfiguration: TextFieldConfiguration { datePicker.date = date datePicker.minimumDate = minimumDate datePicker.maximumDate = maximumDate + if #available(iOS 13.4, *) { + datePicker.preferredDatePickerStyle = .wheels + } self.init(datePicker: datePicker, delegate: delegate) } public override func configure(textInputView: UITextField) { let data = TextFieldDatePickerData(textField: textInputView, datePicker: self.datePicker, delegate: self.datePickerDelegate) self.inputView = self.datePicker - textInputView.datePickerData = data + textInputView.textFieldDatePickerData = data super.configure(textInputView: textInputView) } @@ -56,7 +65,7 @@ public class TextFieldDatePickerConfiguration: TextFieldConfiguration { fileprivate class TextFieldDatePickerData: NSObject { private unowned let textField: UITextField - private unowned let datePicker: UIDatePicker + private let datePicker: UIDatePicker private unowned let delegate: TextFieldDatePickerDelegate init(textField: UITextField, datePicker: UIDatePicker, delegate: TextFieldDatePickerDelegate) { @@ -82,11 +91,11 @@ fileprivate class TextFieldDatePickerData: NSObject { } // MARK: - Wrapper Associated Object -fileprivate var textFieldDatePickerDataKey: String = "TextFieldDatePickerData.textField" +fileprivate var textFieldDatePickerDataKey: String = "UITextField.textFieldDatePickerData" fileprivate extension UITextField { /// Get associated `TextFieldDatePickerData` object with this text field - var datePickerData: TextFieldDatePickerData? { + var textFieldDatePickerData: TextFieldDatePickerData? { set { objc_setAssociatedObject(self, &textFieldDatePickerDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } diff --git a/Source/Common/TextFieldPickerViewConfiguration.swift b/Source/Common/TextFieldPickerViewConfiguration.swift index 0195a76..c1c652b 100644 --- a/Source/Common/TextFieldPickerViewConfiguration.swift +++ b/Source/Common/TextFieldPickerViewConfiguration.swift @@ -3,13 +3,17 @@ // AirCollection // // Created by Lysytsia Yurii on 18.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // import protocol UIKit.UIPickerViewDataSource import protocol UIKit.UIPickerViewDelegate +import class Foundation.NSObject +import class Foundation.NSAttributedString import class UIKit.UITextField import class UIKit.UIPickerView +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject // MARK: - TextFieldPickerViewDataSource public protocol TextFieldPickerViewDataSource: class { @@ -24,7 +28,7 @@ public protocol TextFieldPickerViewDataSource: class { } // MARK: - TextFieldPickerViewDelegate -public protocol TextFieldPickerViewDelegate: class { +public protocol TextFieldPickerViewDelegate: TextFieldDelegate { /// Called by the text field picker view when the user selects a row in a component func textField(_ textField: UITextField, pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) @@ -60,6 +64,7 @@ public class TextFieldPickerViewConfiguration: TextFieldConfiguration { self.pickerView = pickerView self.pickerViewDataSource = dataSource self.pickerViewDelegate = delegate + super.init(delegate: delegate) } /// Create configuration for text field that will be with picker view instead keyboard. Please set `controller` as a reference for strong object because are will be unowned property. Default `pickerView` is `UIPickerView()` @@ -72,7 +77,7 @@ public class TextFieldPickerViewConfiguration: TextFieldConfiguration { self.pickerView.dataSource = data self.pickerView.delegate = data self.inputView = self.pickerView - textInputView.pickerViewWrapper = data + textInputView.textFieldPickerViewData = data super.configure(textInputView: textInputView) } @@ -82,7 +87,7 @@ public class TextFieldPickerViewConfiguration: TextFieldConfiguration { fileprivate class TextFieldPickerViewData: NSObject, UIPickerViewDataSource, UIPickerViewDelegate { private unowned let textField: UITextField - private unowned let pickerView: UIPickerView + private let pickerView: UIPickerView private unowned let dataSource: TextFieldPickerViewDataSource private unowned let delegate: TextFieldPickerViewDelegate @@ -166,11 +171,11 @@ fileprivate class TextFieldPickerViewData: NSObject, UIPickerViewDataSource, UIP } // MARK: - Wrapper Associated Object -fileprivate var textFieldPickerViewDataKey: String = "TextFieldPickerViewData.textField" +fileprivate var textFieldPickerViewDataKey: String = "UITextField.textFieldPickerViewData" fileprivate extension UITextField { - /// Get associated `TableViewData` object with this text field - var pickerViewWrapper: TextFieldPickerViewData? { + /// Get associated `TextFieldPickerViewData` object with this text field + var textFieldPickerViewData: TextFieldPickerViewData? { set { objc_setAssociatedObject(self, &textFieldPickerViewDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } diff --git a/Source/Common/TextFieldPresenterProtocol.swift b/Source/Common/TextFieldPresenterProtocol.swift new file mode 100644 index 0000000..e0cbc6e --- /dev/null +++ b/Source/Common/TextFieldPresenterProtocol.swift @@ -0,0 +1,73 @@ +// +// TextFieldPresenterProtocol.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import struct Foundation.IndexPath + +public protocol TextFieldPresenterProtocol: class { + + /// Called by controller (`TextFieldControllerProtocol`) if editing should begin in the specified index path + func textFieldShouldBeginEditing(at indexPath: IndexPath) -> Bool + + /// Called by controller (`TextFieldControllerProtocol`) that editing began in the specified index path + func textFieldDidBeginEditing(at indexPath: IndexPath) + + /// Called by controller (`TextFieldControllerProtocol`) if editing should stop in the specified index path + func textFieldShouldEndEditing(at indexPath: IndexPath) -> Bool + + /// Called by controller (`TextFieldControllerProtocol`) that editing stopped for the specified index path + func textFieldDidEndEditing(at indexPath: IndexPath) + + /// Called by controller (`TextFieldControllerProtocol`) if the specified text should be changed at index path + func textField(at indexPath: IndexPath, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool + + /// Called by controller (`TextFieldControllerProtocol`) if the current contents at index path should be removed + func textFieldShouldClear(at indexPath: IndexPath) -> Bool + + /// Called by controller (`TextFieldControllerProtocol`) if the text input should process the pressing of the return button + func textFieldShouldReturn(at indexPath: IndexPath) -> Bool + + /// Called by controller (`TextFieldControllerProtocol`) that editing text changed for the specified index path + func textFieldTextDidChanged(_ text: String?, at indexPath: IndexPath) + +} + +public extension TextFieldPresenterProtocol { + + func textFieldShouldBeginEditing(at indexPath: IndexPath) -> Bool { + return true + } + + func textFieldDidBeginEditing(at indexPath: IndexPath) { + + } + + func textFieldShouldEndEditing(at indexPath: IndexPath) -> Bool { + return true + } + + func textFieldDidEndEditing(at indexPath: IndexPath) { + + } + + func textField(at indexPath: IndexPath, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool { + return true + } + + func textFieldShouldClear(at indexPath: IndexPath) -> Bool { + return true + } + + func textFieldShouldReturn(at indexPath: IndexPath) -> Bool { + return true + } + + func textFieldTextDidChanged(_ text: String?, at indexPath: IndexPath) { + + } + +} diff --git a/Source/Common/TextInputConfigurableView.swift b/Source/Common/TextInputConfigurableView.swift index 6c5950c..d4ccfee 100644 --- a/Source/Common/TextInputConfigurableView.swift +++ b/Source/Common/TextInputConfigurableView.swift @@ -3,39 +3,98 @@ // AirCollection // // Created by Lysytsia Yurii on 14.09.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. // import class UIKit.UIView +/// Abstract protocol needed for implement *TextInputConfigurableView* that defines one method *configure(textInputView:)* and associated *TextInputView* type. +/// +/// You shouldn't use this protocol for your views (include cells) implementation, but you able to create additional protocol that implement default implementation. +/// +/// See [TextFieldConfiguration](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#text-field-configuration) for your custom implementation sample. public protocol TextInputConfiguration { - func configure(textInputView: TextInputView) + + /// Predefined text input view type. associatedtype TextInputView: UIView + + /// Method called when text input view have to update. + /// + /// You should use this method for create custom text imput configuration implementation. See [TextFieldConfiguration](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#text-field-configuration) for your custom implementation sample. + /// + /// - Parameter textInputView: Predefined text input view which needs to configure + func configure(textInputView: TextInputView) + } +/// Aditional protocol for *ConfigurableView.Model* that defines new property *textInputConfiguration* and associated *Configuration* type. +/// +/// Use this protocol for *ConfigurableView.Model* and implement *textInputConfiguration* with type that you need. e.g. *TextFieldConfiguration* public protocol TextInputConfigurableModel { - var textInputConfiguration: Configuration { get } + + /// Predefined *ConfigurableView.Model* text input configuration. associatedtype Configuration: TextInputConfiguration + + /// Reference for text input configuration. + var textInputConfiguration: Configuration { get } + } -public protocol TextInputConfigurableView: ModelConfigurableView where Model: TextInputConfigurableModel { - var textInputView: TextInputView { get } +/// Abstract protocol implementation and defines new method to become or resign input view. +public protocol InputConfigurableView: class { + + /// Asks UIKit to make this input view the first responder in its window. + @discardableResult func becomeInputViewFirstResponder() -> Bool + + /// Notifies this input view that it has been asked to relinquish its status as first responder in its window + @discardableResult func resignInputViewFirstResponder() -> Bool + +} + +/// Child `ModelConfigurableView` protocol implementation and defines new property *textInputView* and associated *TextInputView* type. +/// +/// Use this protocol when you need observe some text input subview (e.g. *UITextField* or *UITextView*) inside your reusable view. +/// +/// **Important!** This protocol is able to implement only for `Model` which implement [TextInputConfigurableModel](https://github.com/YuriFox/AirCollection/blob/master/README_VIEW.md#text-input-configurable-model). +public protocol TextInputConfigurableView: ModelConfigurableView, InputConfigurableView where Model: TextInputConfigurableModel { + + /// Predefined text input view type for configure this view. associatedtype TextInputView: UIView + + /// Reference for text input view. + var textInputView: TextInputView { get } + +} + +public extension TextInputConfigurableView { + + @discardableResult func becomeInputViewFirstResponder() -> Bool { + return self.textInputView.becomeFirstResponder() + } + + @discardableResult func resignInputViewFirstResponder() -> Bool { + return self.textInputView.resignFirstResponder() + } + } public extension TextInputConfigurableView where Model: TextInputConfigurableModel, TextInputView == Model.Configuration.TextInputView { - func configure(model: Any?) { - guard let configurableModel = model as? Model else { - assertionFailure("Invalid model for configure view of type `\(type(of: self))`. Model is not confirm to model type \(Model.self)") + /// Overridden abstract method that called when view have to update. + /// + /// **Caution!** Don't override this method implementation because it able to break other functionality + /// + /// - Parameter model: Any model that will use for update view + func configure(_ model: Any) { + guard let predefinedModel = model as? Model else { + assertionFailure("\(#function). `\(String(reflecting: Self.self))` not able to `configure(model:)` because received model `\(String(reflecting: type(of: model).self))` not confirm to `\(String(reflecting: Model.self))`") return } - self.configure(model: configurableModel) - + // Base view configuration + self.configure(model: predefinedModel) // Configure text input view with configuration - let configuration = configurableModel.textInputConfiguration + let configuration = predefinedModel.textInputConfiguration configuration.configure(textInputView: self.textInputView) } - } diff --git a/Source/Common/TextViewConfiguration.swift b/Source/Common/TextViewConfiguration.swift new file mode 100644 index 0000000..6b91b81 --- /dev/null +++ b/Source/Common/TextViewConfiguration.swift @@ -0,0 +1,274 @@ +// +// TextViewConfiguration.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import protocol UIKit.UITextViewDelegate +import enum UIKit.UITextItemInteraction +import struct Foundation.URL +import struct Foundation.NSRange +import class UIKit.NSTextAttachment +import class Foundation.NSObject +import class UIKit.UITextView +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject + +public protocol TextViewDelegate: class { + + /// Asks the delegate if editing should begin in the specified text view + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool + + /// Asks the delegate if editing should stop in the specified text view + func textViewShouldEndEditing(_ textView: UITextView) -> Bool + + /// Tells the delegate that editing of the specified text view has begun + func textViewDidBeginEditing(_ textView: UITextView) + + /// Tells the delegate that editing of the specified text view has ended + func textViewDidEndEditing(_ textView: UITextView) + + /// Asks the delegate whether the specified text should be replaced in the text view + func textView(_ textView: UITextView, shouldChangeTextIn range: Range, replacementText text: String) -> Bool + + /// Tells the delegate that the text or attributes in the specified text view were changed by the user + func textViewDidChange(_ textView: UITextView) + + /// Asks the delegate if the specified text view should allow the specified type of user interaction with the given URL in the given range of text + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool + + /// Asks the delegate if the specified text view should allow the specified type of user interaction with the provided text attachment in the given range of text + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool + +} + +public extension TextViewDelegate { + + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + return true + } + + func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + return true + } + + func textViewDidBeginEditing(_ textView: UITextView) { + + } + + func textViewDidEndEditing(_ textView: UITextView) { + + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: Range, replacementText text: String) -> Bool { + return true + } + + func textViewDidChange(_ textView: UITextView) { + + } + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + return true + } + + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + return true + } + +} + + +import class UIKit.UIView +import struct UIKit.UITextContentType +import struct UIKit.UIDataDetectorTypes +import enum UIKit.UITextAutocapitalizationType +import enum UIKit.UITextAutocorrectionType +import enum UIKit.UIKeyboardAppearance +import enum UIKit.UIKeyboardType +import enum UIKit.UIReturnKeyType +import enum UIKit.UITextSmartDashesType +import enum UIKit.UITextSmartQuotesType +import enum UIKit.UITextSmartInsertDeleteType +import enum UIKit.UITextSpellCheckingType + +open class TextViewConfiguration: TextInputConfiguration { + + /// The auto-capitalization style for the text object. Default is `UITextAutocapitalizationType.sentences` + public var autocapitalizationType: UITextAutocapitalizationType = .sentences + + /// The autocorrection style for the text object. Default is `UITextAutocorrectionType.default` + public var autocorrectionType: UITextAutocorrectionType = .default + + /// The types of data converted to tappable URLs in the text view. Default is `UIDataDetectorTypes.all` + public var dataDetectorTypes: UIDataDetectorTypes = .all + + /// The custom input view to display when the text field becomes the first responder. Default is nil + public var inputView: UIView? = nil + + /// Identifies whether the text object should disable text copying and in some cases hide the text being entered. Default is `false` + public var isSecureTextEntry: Bool = false + + /// A Boolean value indicating whether the receiver is selectable. Default is `true` + public var isSelectable: Bool = true + + /// The appearance style of the keyboard that is associated with the text object. Default is `UIKeyboardAppearance.default` + public var keyboardAppearance: UIKeyboardAppearance = .default + + /// The keyboard style associated with the text object. Default is `UIKeyboardType.default` + public var keyboardType: UIKeyboardType = .default + + /// The visible title of the Return key. Default is `UIReturnKeyType.default` + public var returnKeyType: UIReturnKeyType = .default + + /// The configuration state for smart dashes. Default is `UITextSmartDashesType.default` + public var smartDashesType: UITextSmartDashesType = .default + + /// The configuration state for smart quotes. Default is `UITextSmartQuotesType.default` + public var smartQuotesType: UITextSmartQuotesType = .default + + /// The configuration state for the smart insertion and deletion of space characters. Default is `UITextSmartInsertDeleteType.default` + public var smartInsertDeleteType: UITextSmartInsertDeleteType = .default + + /// The spell-checking style for the text object. Default is `UITextSpellCheckingType.default` + public var spellCheckingType: UITextSpellCheckingType = .default + + /// The semantic meaning expected by a text input area. Default is nil + public var textContentType: UITextContentType? = nil + + /// The receiver’s delegate + public unowned let textViewDelegate: TextViewDelegate + + public init(delegate: TextViewDelegate) { + self.textViewDelegate = delegate + } + + public func configure(textInputView: UITextView) { + let textViewData = TextViewData(textView: textInputView, delegate: self.textViewDelegate) + textInputView.delegate = textViewData + textInputView.textViewData = textViewData + textInputView.autocapitalizationType = self.autocapitalizationType + textInputView.autocorrectionType = self.autocorrectionType + textInputView.dataDetectorTypes = self.dataDetectorTypes + textInputView.inputView = self.inputView + textInputView.isSecureTextEntry = self.isSecureTextEntry + textInputView.isSelectable = self.isSelectable + textInputView.keyboardAppearance = self.keyboardAppearance + textInputView.keyboardType = self.keyboardType + textInputView.returnKeyType = self.returnKeyType + textInputView.smartDashesType = self.smartDashesType + textInputView.smartQuotesType = self.smartQuotesType + textInputView.smartInsertDeleteType = self.smartInsertDeleteType + textInputView.spellCheckingType = self.spellCheckingType + textInputView.textContentType = self.textContentType + } + +} + +// MARK: - TextViewData +fileprivate class TextViewData: NSObject, UITextViewDelegate { + + private unowned let textView: UITextView + private unowned let delegate: TextViewDelegate + + init(textView: UITextView, delegate: TextViewDelegate) { + self.textView = textView + self.delegate = delegate + } + + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textViewShouldBeginEditing(textView) + } + + func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + return self.delegate.textViewShouldEndEditing(textView) + } + + func textViewDidBeginEditing(_ textView: UITextView) { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textViewDidBeginEditing(textView) + } + + func textViewDidEndEditing(_ textView: UITextView) { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textViewDidEndEditing(textView) + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + guard let textRange = Range(range, in: textView.text) else { + assertionFailure("\(#function) something went wrong and `Swift.Range` didn't evaluate with `NSRange`. Please don't use this method for other destination") + return true + } + return self.delegate.textView(textView, shouldChangeTextIn: textRange, replacementText: text) + } + + func textViewDidChange(_ textView: UITextView) { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return + } + self.delegate.textViewDidChange(textView) + } + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + guard let characterRange = Range(characterRange, in: textView.text) else { + assertionFailure("\(#function) something went wrong and `Swift.Range` didn't evaluate with `NSRange`. Please don't use this method for other destination") + return true + } + return self.delegate.textView(textView, shouldInteractWith: URL, in: characterRange, interaction: interaction) + } + + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + guard textView == self.textView else { + assertionFailure("\(#function) called for other text field. Please don't use this wrapper for other destination") + return true + } + guard let characterRange = Range(characterRange, in: textView.text) else { + assertionFailure("\(#function) something went wrong and `Swift.Range` didn't evaluate with `NSRange`. Please don't use this method for other destination") + return true + } + return self.delegate.textView(textView, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) + } + + +} + +// MARK: - Wrapper Associated Object +fileprivate var textViewDataKey: String = "UITextView.textViewData" +fileprivate extension UITextView { + + /// Get associated `TableViewData` object with this text field + var textViewData: TextViewData? { + set { + objc_setAssociatedObject(self, &textViewDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + get { + objc_getAssociatedObject(self, &textViewDataKey) as? TextViewData + } + } + +} diff --git a/Source/Common/TextViewControllerProtocol.swift b/Source/Common/TextViewControllerProtocol.swift new file mode 100644 index 0000000..766c93e --- /dev/null +++ b/Source/Common/TextViewControllerProtocol.swift @@ -0,0 +1,15 @@ +// +// TextViewControllerProtocol.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +public protocol TextViewControllerProtocol: TextViewDelegate { + + /// Return an instanse of the text view presenter + var textViewPresenter: TextViewPresenterProtocol { get } + +} + diff --git a/Source/Common/TextViewInputModelConfigurable.swift b/Source/Common/TextViewInputModelConfigurable.swift deleted file mode 100644 index a309542..0000000 --- a/Source/Common/TextViewInputModelConfigurable.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// TextViewInputModelConfigurable.swift -// AirCollection -// -// Created by Lysytsia Yurii on 15.07.2020. -// Copyright © 2020 Developer Lysytsia. All rights reserved. -// - -import enum UIKit.UIKeyboardType -import struct UIKit.UITextContentType -import class UIKit.UITextView -import protocol UIKit.UITextViewDelegate - -// MARK: - TextViewInputModelConfigurable -public protocol TextViewInputModelConfigurable { - var textViewConfiguration: TextViewInputModelConfiguration { get } -} - -// MARK: - TextViewInputModelConfiguration -public enum TextViewInputModelConfiguration { - case `default` - case keyboard(type: UIKeyboardType, textContentType: UITextContentType?, delegate: UITextViewDelegate?) -} - -// MARK: - InputModelConfigurableView -public extension TextInputConfigurableView where Model: TextViewInputModelConfigurable, TextInputView: UITextView { - - func configureTextInputView(model: Model) { - let textView = self.textInputView - let configuration = model.textViewConfiguration - - switch configuration { - case .default: - break - case .keyboard(let type, let textContentType, let delegate): - textView.keyboardType = type - textView.textContentType = textContentType - textView.delegate = delegate - } - } - -} - diff --git a/Source/Common/TextViewPresenterProtocol.swift b/Source/Common/TextViewPresenterProtocol.swift new file mode 100644 index 0000000..fda772a --- /dev/null +++ b/Source/Common/TextViewPresenterProtocol.swift @@ -0,0 +1,77 @@ +// +// TextViewPresenterProtocol.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import enum UIKit.UITextItemInteraction +import struct Foundation.IndexPath +import struct Foundation.URL +import class UIKit.NSTextAttachment + +public protocol TextViewPresenterProtocol: class { + + /// Asks by controller (`TextViewControllerProtocol`) if editing should begin in the specified index path + func textViewShouldBeginEditing(at indexPath: IndexPath) -> Bool + + /// Asks by controller (`TextViewControllerProtocol`) if editing should stop in the specified index path + func textViewShouldEndEditing(at indexPath: IndexPath) -> Bool + + /// Tells by controller (`TextViewControllerProtocol`) that editing of the specified text view has begun in the specified index path + func textViewDidBeginEditing(at indexPath: IndexPath) + + /// Tells by controller (`TextViewControllerProtocol`) that editing of the specified text view has ended in the specified index path + func textViewDidEndEditing(at indexPath: IndexPath) + + /// Asks by controller (`TextViewControllerProtocol`) whether the specified text should be replaced in the specified index path + func textView(at indexPath: IndexPath, shouldChangeTextIn range: Range, replacementText text: String) -> Bool + + /// Tells by controller (`TextViewControllerProtocol`) that the text or attributes in the specified text view were changed by the user in the specified index path + func textViewDidChange(text: String, at indexPath: IndexPath) + + /// Asks by controller (`TextViewControllerProtocol`) if the specified text view should allow the specified type of user interaction with the given URL in the given range of text in the specified index path + func textView(at indexPath: IndexPath, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool + + /// Asks by controller (`TextViewControllerProtocol`) if the specified text view should allow the specified type of user interaction with the provided text attachment in the given range of text in the specified index path + func textView(at indexPath: IndexPath, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool + +} + +public extension TextViewPresenterProtocol { + + func textViewShouldBeginEditing(at indexPath: IndexPath) -> Bool { + return true + } + + func textViewShouldEndEditing(at indexPath: IndexPath) -> Bool { + return true + } + + func textViewDidBeginEditing(at indexPath: IndexPath) { + + } + + func textViewDidEndEditing(at indexPath: IndexPath) { + + } + + func textView(at indexPath: IndexPath, shouldChangeTextIn range: Range, replacementText text: String) -> Bool { + return true + } + + func textViewDidChange(text: String, at indexPath: IndexPath) { + + } + + func textView(at indexPath: IndexPath, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + return true + } + + func textView(at indexPath: IndexPath, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + return true + } + +} + diff --git a/Source/TableView/TableView+Extension.swift b/Source/TableView/TableView+Extension.swift index da73121..e8bf56d 100644 --- a/Source/TableView/TableView+Extension.swift +++ b/Source/TableView/TableView+Extension.swift @@ -18,8 +18,8 @@ public extension UITableView { } /// Registers a nib object containing a cell with the table view under a specified identifier. - func register(_ cellClass: T.Type) where T: NibLoadableView { - self.register(cellClass.nib(), forCellReuseIdentifier: cellClass.viewIdentifier) + func register(_ cellClass: T.Type) where T: NibLoadableView & IdentificableView { + self.register(cellClass.viewNib, forCellReuseIdentifier: cellClass.viewIdentifier) } /// Registers a class for use in creating new table header or footer views. @@ -28,8 +28,8 @@ public extension UITableView { } /// Registers a nib object containing a header or footer with the table view under a specified identifier. - func register(_ viewClass: T.Type) where T: NibLoadableView { - self.register(T.nib(), forHeaderFooterViewReuseIdentifier: T.viewIdentifier) + func register(_ viewClass: T.Type) where T: NibLoadableView & IdentificableView { + self.register(viewClass.viewNib, forHeaderFooterViewReuseIdentifier: viewClass.viewIdentifier) } } diff --git a/Source/TableView/TableView+TextField.swift b/Source/TableView/TableView+TextField.swift new file mode 100644 index 0000000..6d97232 --- /dev/null +++ b/Source/TableView/TableView+TextField.swift @@ -0,0 +1,77 @@ +// +// TableView+TextField.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import class UIKit.UITextField + +extension TableViewControllerProtocol where Self: TextFieldControllerProtocol { + + public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldBeginEditing(at: indexPath) + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldDidBeginEditing(at: indexPath) + } + + public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldEndEditing(at: indexPath) + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldDidEndEditing(at: indexPath) + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: Range, replacementString string: String) -> Bool { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textField(at: indexPath, shouldChangeCharactersIn: range, replacementString: string) + } + + public func textFieldShouldClear(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldClear(at: indexPath) + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textFieldPresenter.textFieldShouldReturn(at: indexPath) + } + + public func textFieldEditingChanged(_ textField: UITextField) { + guard let indexPath = self.indexPathForRow(with: textField) else { + assertionFailure("\(#function) called but `textField` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textFieldPresenter.textFieldTextDidChanged(textField.text, at: indexPath) + } + +} diff --git a/Source/TableView/TableView+TextFieldDatePicker.swift b/Source/TableView/TableView+TextFieldDatePicker.swift index fff67a3..bc00c6d 100644 --- a/Source/TableView/TableView+TextFieldDatePicker.swift +++ b/Source/TableView/TableView+TextFieldDatePicker.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.Date import class UIKit.UITextField import class UIKit.UIDatePicker diff --git a/Source/TableView/TableView+TextView.swift b/Source/TableView/TableView+TextView.swift new file mode 100644 index 0000000..0310988 --- /dev/null +++ b/Source/TableView/TableView+TextView.swift @@ -0,0 +1,80 @@ +// +// TableView+TextView.swift +// Source +// +// Created by Lysytsia Yurii on 04.10.2020. +// Copyright © 2020 Lysytsia Yurii. All rights reserved. +// + +import enum UIKit.UITextItemInteraction +import struct Foundation.URL +import class UIKit.UITextView +import class UIKit.NSTextAttachment + +extension TableViewControllerProtocol where Self: TextViewControllerProtocol { + + public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textViewShouldBeginEditing(at: indexPath) + } + + public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textViewShouldEndEditing(at: indexPath) + } + + public func textViewDidBeginEditing(_ textView: UITextView) { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidBeginEditing(at: indexPath) + } + + public func textViewDidEndEditing(_ textView: UITextView) { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidEndEditing(at: indexPath) + } + + public func textView(_ textView: UITextView, shouldChangeTextIn range: Range, replacementText text: String) -> Bool { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldChangeTextIn: range, replacementText: text) + } + + public func textViewDidChange(_ textView: UITextView) { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return + } + self.textViewPresenter.textViewDidChange(text: textView.text, at: indexPath) + } + + public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldInteractWith: URL, in: characterRange, interaction: interaction) + } + + public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: Range, interaction: UITextItemInteraction) -> Bool { + guard let indexPath = self.indexPathForRow(with: textView) else { + assertionFailure("\(#function) called but `textView` didn't find on `tableView` rows. Please don't use this method for other destination") + return true + } + return self.textViewPresenter.textView(at: indexPath, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) + } + +} diff --git a/Source/TableView/TableViewControllerProtocol.swift b/Source/TableView/TableViewControllerProtocol.swift index 7427bff..9331fea 100644 --- a/Source/TableView/TableViewControllerProtocol.swift +++ b/Source/TableView/TableViewControllerProtocol.swift @@ -6,11 +6,15 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath +import struct Foundation.IndexSet import struct CoreGraphics.CGPoint import class UIKit.UIView import class UIKit.UITableView import class UIKit.UITableViewCell import protocol UIKit.UIScrollViewDelegate +import func Foundation.objc_getAssociatedObject +import func Foundation.objc_setAssociatedObject public protocol TableViewControllerProtocol: class { @@ -20,7 +24,7 @@ public protocol TableViewControllerProtocol: class { /// Return an instanse of the table view presenter var tableViewPresenter: TableViewPresenterProtocol { get } - /// Configure `UITableViewDataSource` and `UITableViewDelegate` for specific table view data model + /// Configure `UITableViewDataSource` and `UITableViewDelegate` for specific table view and presenter. Also automatically add `TableViewDelegate` to current view controller if implemented. /// - Parameter configurator: Use this block to set up the table view. You should register table view cell, headers and footer is this case func configureTableView(configurator: (UITableView) -> Void) @@ -59,6 +63,12 @@ public protocol TableViewControllerProtocol: class { /// Deselects a given row identified by index path, with an option to animate the deselection. func deselectTableViewRow(at indexPath: IndexPath, animated: Bool) + /// Make a row input view in the table view identified by index path the first responder in its window at. + func becomeTableViewRowFirstResponder(at indexPath: IndexPath) + + /// Notifies a row input view in the table view identified by index path that it has been asked to relinquish its status as first responder in its window. + func resignTableViewRowFirstResponder(at indexPath: IndexPath) + /// Reloads the specified sections using a given animation effect func reloadTableViewSections(_ sections: [Int], with animation: UITableView.RowAnimation) @@ -91,6 +101,10 @@ public extension TableViewControllerProtocol { func configureTableView(configurator: (UITableView) -> Void) { self.tableViewSource.dataSource = self.tableViewData self.tableViewSource.delegate = self.tableViewData + if let delegate = self as? TableViewDelegate { + // Forward available table view delegates to current view controller. + self.tableViewData.tableViewDelegate = delegate + } configurator(self.tableViewSource) } @@ -179,6 +193,20 @@ public extension TableViewControllerProtocol { self.tableViewSource.deselectRow(at: indexPath, animated: animated) } + func becomeTableViewRowFirstResponder(at indexPath: IndexPath) { + guard let cell = self.tableViewSource.cellForRow(at: indexPath) as? InputConfigurableView else { + return + } + cell.becomeInputViewFirstResponder() + } + + func resignTableViewRowFirstResponder(at indexPath: IndexPath) { + guard let cell = self.tableViewSource.cellForRow(at: indexPath) as? InputConfigurableView else { + return + } + cell.resignInputViewFirstResponder() + } + // MARK: Sections func reloadTableViewSections(_ sections: [Int], with animation: UITableView.RowAnimation) { self.tableViewData.reloadSections(sections) @@ -233,18 +261,8 @@ public extension TableViewControllerProtocol { } func indexPathForRow(with view: UIView) -> IndexPath? { - let point = view.convert(CGPoint.zero, to: self.tableViewSource) - return self.tableViewSource.indexPathForRow(at: point) - } - -} - -// MARK: - UIScrollViewDelegateForward -public extension TableViewControllerProtocol where Self: TableViewDelegate { - - /// Forward available table view delegates to current view controller. - func forwardTableViewDelegate() { - self.tableViewData.tableViewDelegate = self + let rect = view.convert(view.bounds, to: self.tableViewSource) + return self.tableViewSource.indexPathsForRows(in: rect)?.first } } diff --git a/Source/TableView/TableViewData.swift b/Source/TableView/TableViewData.swift index 9a7438e..dcee34b 100644 --- a/Source/TableView/TableViewData.swift +++ b/Source/TableView/TableViewData.swift @@ -6,9 +6,11 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import struct CoreGraphics.CGFloat import struct CoreGraphics.CGPoint import struct CoreGraphics.CGSize +import class Foundation.NSObject import class UIKit.UIScrollView import class UIKit.UITableView import class UIKit.UITableViewCell @@ -172,7 +174,7 @@ class TableViewData: NSObject { assertionFailure("For use `TableViewData.configureCell(_:for:)`and configure cell you must implement `ConfigurableView` protocol for cell type `\(type(of: cell))`") return } - configurableCell.configure(model: model) + configurableCell.configure(model) } /// Configure table header view with some model. Header view must implement `ConfigurableView` protocol. @@ -184,7 +186,7 @@ class TableViewData: NSObject { assertionFailure("For use `TableViewData.configureHeaderView(_:for:)`and configure header view you must implement `ConfigurableView` protocol for header view type `\(type(of: view))`") return } - configurableView.configure(model: model) + configurableView.configure(model) } /// Configure table footer view with some model. Footer view must implement `ConfigurableView` protocol. @@ -196,7 +198,7 @@ class TableViewData: NSObject { assertionFailure("For use `TableViewData.configureFooterView(_:for:)`and configure footer view you must implement `ConfigurableView` protocol for footer view type `\(type(of: view))`") return } - configurableView.configure(model: model) + configurableView.configure(model) } } @@ -318,7 +320,13 @@ extension TableViewData: UITableViewDelegate { return height case .flexible: - return UITableView.automaticDimension + guard let view = self.tableView(tableView, viewForHeaderInSection: section) else { + return UITableView.automaticDimension + } + view.layoutIfNeeded() + let targetSize = CGSize(width: tableView.frame.width, height: CGFloat.greatestFiniteMagnitude) + let prefferedSize = view.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) + return prefferedSize.height } } @@ -363,12 +371,16 @@ extension TableViewData: UITableViewDelegate { switch self.output.tableFooterHeight(for: section) { case .none: return 0 - case .fixed(let height): return height - case .flexible: - return UITableView.automaticDimension + guard let view = self.tableView(tableView, viewForFooterInSection: section) else { + return UITableView.automaticDimension + } + view.layoutIfNeeded() + let targetSize = CGSize(width: tableView.frame.width, height: CGFloat.greatestFiniteMagnitude) + let prefferedSize = view.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) + return prefferedSize.height } } diff --git a/Source/TableView/TableViewDelegate.swift b/Source/TableView/TableViewDelegate.swift index a8d8cd9..b7aba98 100644 --- a/Source/TableView/TableViewDelegate.swift +++ b/Source/TableView/TableViewDelegate.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import class UIKit.UIView import class UIKit.UITableView import class UIKit.UITableViewCell diff --git a/Source/TableView/TableViewPresenterProtocol.swift b/Source/TableView/TableViewPresenterProtocol.swift index 443bfe7..084f5f0 100644 --- a/Source/TableView/TableViewPresenterProtocol.swift +++ b/Source/TableView/TableViewPresenterProtocol.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Developer Lysytsia. All rights reserved. // +import struct Foundation.IndexPath import class UIKit.UITableView import class UIKit.UISwipeActionsConfiguration