Repository: xmartlabs/Eureka Branch: master Commit: 028ef8e3191a Files: 162 Total size: 943.5 KB Directory structure: gitextract_b_rh3mc8/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Documentation/ │ ├── Eureka 2.0 Migration Guide.md │ └── README_CN.md ├── Eureka.playground/ │ ├── Contents.swift │ ├── contents.xcplayground │ ├── playground.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── timeline.xctimeline ├── Eureka.podspec ├── Eureka.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── Eureka.xcscheme ├── Eureka.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── Example/ │ ├── EmojiCell.xib │ ├── Example/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Eureka.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── checkedDay.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── map_pin.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── plus_image.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── selected.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── selectedRectangle.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── uncheckedDay.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── unselected.imageset/ │ │ │ │ └── Contents.json │ │ │ └── unselectedRectangle.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── EurekaSectionHeader.xib │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── Main.storyboard │ │ │ └── WeekDaysCell.xib │ │ ├── Controllers/ │ │ │ ├── CustomCellsViewController.swift │ │ │ ├── CustomDesignController.swift │ │ │ ├── DisabledRowsExample.swift │ │ │ ├── FieldRowCustomizationController.swift │ │ │ ├── FormatterExample.swift │ │ │ ├── HiddenRowsExample.swift │ │ │ ├── InlineRowsExample.swift │ │ │ ├── ListSectionsController.swift │ │ │ ├── MultivaluedExamples.swift │ │ │ ├── NativeEventExample.swift │ │ │ ├── NavigationAccessoryController.swift │ │ │ ├── PlainTableViewExample.swift │ │ │ ├── RowsExample.swift │ │ │ ├── SwipeActionsController.swift │ │ │ └── ValidationsController.swift │ │ ├── CustomCells.swift │ │ ├── CustomDesign/ │ │ │ ├── DatePickerCell.xib │ │ │ ├── SwitchCell.xib │ │ │ └── TextCell.xib │ │ ├── CustomRows/ │ │ │ └── ImageRow/ │ │ │ ├── ImagePickerController.swift │ │ │ └── ImageRow.swift │ │ ├── Helpers/ │ │ │ └── FloatLabelTextField.swift │ │ ├── Info.plist │ │ └── ViewController.swift │ └── UITests/ │ ├── FieldRowUITests.swift │ └── Info.plist ├── Example.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── Example.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Source/ │ ├── Assets.xcassets/ │ │ └── Contents.json │ ├── Core/ │ │ ├── BaseRow.swift │ │ ├── Cell.swift │ │ ├── CellType.swift │ │ ├── Core.swift │ │ ├── Form.swift │ │ ├── HeaderFooterView.swift │ │ ├── Helpers.swift │ │ ├── InlineRowType.swift │ │ ├── NavigationAccessoryView.swift │ │ ├── Operators.swift │ │ ├── PresenterRowType.swift │ │ ├── ResultBuilders.swift │ │ ├── Row.swift │ │ ├── RowControllerType.swift │ │ ├── RowProtocols.swift │ │ ├── RowType.swift │ │ ├── Section.swift │ │ ├── SelectableRowType.swift │ │ ├── SelectableSection.swift │ │ ├── SwipeActions.swift │ │ └── Validation.swift │ ├── Eureka.h │ ├── Info.plist │ ├── PrivacyInfo.xcprivacy │ ├── Rows/ │ │ ├── ActionSheetRow.swift │ │ ├── AlertRow.swift │ │ ├── ButtonRow.swift │ │ ├── ButtonRowWithPresent.swift │ │ ├── CheckRow.swift │ │ ├── Common/ │ │ │ ├── AlertOptionsRow.swift │ │ │ ├── DateFieldRow.swift │ │ │ ├── DateInlineFieldRow.swift │ │ │ ├── DecimalFormatter.swift │ │ │ ├── FieldRow.swift │ │ │ ├── GenericMultipleSelectorRow.swift │ │ │ ├── OptionsRow.swift │ │ │ ├── Protocols.swift │ │ │ └── SelectorRow.swift │ │ ├── Controllers/ │ │ │ ├── MultipleSelectorViewController.swift │ │ │ ├── SelectorAlertController.swift │ │ │ └── SelectorViewController.swift │ │ ├── DateInlineRow.swift │ │ ├── DatePickerRow.swift │ │ ├── DateRow.swift │ │ ├── DoublePickerInputRow.swift │ │ ├── DoublePickerRow.swift │ │ ├── FieldsRow.swift │ │ ├── LabelRow.swift │ │ ├── MultipleSelectorRow.swift │ │ ├── PickerInlineRow.swift │ │ ├── PickerInputRow.swift │ │ ├── PickerRow.swift │ │ ├── PopoverSelectorRow.swift │ │ ├── PushRow.swift │ │ ├── SegmentedRow.swift │ │ ├── SelectableRows/ │ │ │ └── ListCheckRow.swift │ │ ├── SliderRow.swift │ │ ├── StepperRow.swift │ │ ├── SwitchRow.swift │ │ ├── TextAreaRow.swift │ │ ├── TriplePickerInputRow.swift │ │ └── TriplePickerRow.swift │ └── Validations/ │ ├── RuleClosure.swift │ ├── RuleEmail.swift │ ├── RuleEqualsToRow.swift │ ├── RuleLength.swift │ ├── RuleRange.swift │ ├── RuleRegExp.swift │ ├── RuleRequired.swift │ └── RuleURL.swift └── Tests/ ├── BaseEurekaTests.swift ├── CallbacksTests.swift ├── CollectionTests.swift ├── DateTests.swift ├── FormValuesTests.swift ├── HelperMethodTests.swift ├── HiddenRowsTests.swift ├── Info.plist ├── MultivaluedSectionTests.swift ├── OperatorsTest.swift ├── ResultBuildersTests.swift ├── RowByTagTests.swift ├── RowCallbackTests.swift ├── RowsInsertionTests.swift ├── SectionsInsertionTests.swift ├── SelectableSectionTests.swift ├── SetValuesTests.swift └── ValidationsTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: xmartlabs # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve. Please make sure to search for existing issues or in the Readme before posting. title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Versions (please complete the following information):** - Device: [e.g. iPhone 13] - OS: [e.g. iOS 15.0] - Eureka Version [e.g. 5.3.5] - Xcode version [e.g. 13.1] **Additional context** Add any other context about the problem here. Console output, stack traces, etc. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: 'type: feature request' assignees: '' --- **Before submitting...** Please check whether such a feature has already been requested. **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ Before submitting issues ... - Make sure you are using Eureka [latest release](https://github.com/xmartlabs/Eureka/releases) or master branch version. - Make sure your Xcode version is the latest stable one. - Check if the issue was [already reported or fixed](https://github.com/xmartlabs/Eureka/issues?utf8=%E2%9C%93&q=is%3Aissue). We add labels to each issue in order to easily find related issues. If you found a match add a brief comment "I have the same problem" or "+1". When submitting issues, please provide the following information to help maintainers to fix the problem faster: - Environment: Eureka, Xcode and iOS version you are using. - In case of reporting errors, provide Xcode console output of stack trace or code compilation error. - Any other additional detail such as your eureka form/section/row code that you think it would be useful to understand and solve the problem. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: Eureka CI on: pull_request: push: branches: - master jobs: iOS: runs-on: macos-12 strategy: matrix: destination: ['platform=iOS Simulator,OS=16.0,name=iPhone 14'] steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '14.0.1' - name: Build and test run: set -o pipefail && xcodebuild -project Eureka.xcodeproj -scheme 'Eureka' -sdk 'iphonesimulator' -destination "${{ matrix.destination }}" -configuration Debug test | xcpretty ================================================ FILE: .gitignore ================================================ # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate *.xcscmblueprint .LSOverride # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. ### [5.5.0](https://github.com/xmartlabs/Eureka/releases/tag/5.5.0) * Created PrivacyInfo file (#2255) ### [5.4.0](https://github.com/xmartlabs/Eureka/releases/tag/5.4.0) * Renamed several methods that clashed with private Obj-C methods and triggered warnings when uploading to App Store (#2231) * Other minor fixes (#2220, #2222) ### [5.3.6](https://github.com/xmartlabs/Eureka/releases/tag/5.3.6) * Fixed done button issue in keyboard (#2213) ### [5.3.5](https://github.com/xmartlabs/Eureka/releases/tag/5.3.5) * Fix a compiler error in Xcode 13 (#2162) * Change CI to GitHub Actions and remove Travis (#2185) * Fix crash when user taps tab key on external keyboard (#2205) * Other minor fixes (#2191, #2206, #2183) ### [5.3.4](https://github.com/xmartlabs/Eureka/releases/tag/5.3.4) * Resolve class protocol inheritance warnings (#2151) * Fix Chinese input issue (#2150) … * Specific special characters are allowed in the username of an email address (#2160) * Fix SliderRow not rendering properly (#2166) ### [5.3.3](https://github.com/xmartlabs/Eureka/releases/tag/5.3.3) * Add option for different ScrollPosition behavior on keyboard appearance (#2112) * Prefix Eureka to certain class names to avoid clashes with other pods (#2113) * Remove the default header title of section (#2128) * Modify hugging priority for PasswordRow (#2129) * Add replace method for all sections (#2105) * Fix: Non-public API usage (valueChanged, datePickerValueChanged) (#2130) * Fix: remove EXCLUDED_ARCHS build setting to support M1 mac iphone simulator (#2137) * Fix: removeAll method in sections (#2141) ### [5.3.2](https://github.com/xmartlabs/Eureka/releases/tag/5.3.2) * Make tableViewStyle public (#2092) * Fix CountDown rows crash (#2095) * Make ValidationRuleHelper's rule property public. (#2101) * Fix for building with Carthage and Xcode 12 (#2104) ### [5.3.1](https://github.com/xmartlabs/Eureka/releases/tag/5.3.1) * Fix building for Mac Catalyst on Xcode 12 (#2078) * Fix datePickerStyle for DateFieldRow and DatePickerCell (#2077) * Adjust font size of PasswordRow to avoid dots (#2080) ### [5.3.0](https://github.com/xmartlabs/Eureka/releases/tag/5.3.0) * Reload rows on viewWillAppear for iOS 12 and below. (#2029) * Change validation functions access level to open (#2049) * Start editing field rows at the end when tapping blank space in row (#2046) * Update SelectorViewController.swift (#2062) * Fix subtitle field row constraints (#2069) * Create a workaround for Swift crash (#2057, #2061) * Fix date row style/rendering issues for iOS 14 (#2067) ### [5.2.1](https://github.com/xmartlabs/Eureka/releases/tag/5.2.1) * Mark `tableView:editActionsForRowAt` as deprecated (#2000) * Removes unwanted assertionFailure Core.swift Navigation (#1998) * Fix for crash in iOS 13.4 with Xcode 11.4 (#2008) ### [5.2.0](https://github.com/xmartlabs/Eureka/releases/tag/5.2.0) * *Support for Swift Package Manager* (#1976) * Add GenericMultivaluedSection to allow changing add button (#1843) * Remove InputTypeInitiable constraint for PickerInputRow (#1975) * Fix: avoid reloading rows in viewWillAppear when tableView is not added to window (#1971) * Fix optional header and footer in SelectorViewController. (#1962) * Fix table view contentInset adjustment with safe area when keyboard shows (#1960) * Fix for iOS 13 that was not removing the row automatically anymore even after calling the completion handler in a destructive swipe action (#1944) * Fixed reference cycle (#1930) * Allows a section's header and footer to be `nil` (#1927) ### [5.1.0](https://github.com/xmartlabs/Eureka/releases/tag/5.1.0) * Support for Xcode 11 and iOS 13 * Added dark mode support ### [5.0.1](https://github.com/xmartlabs/Eureka/releases/tag/5.0.1) * Added `quietly` parameter for use with non-ui validation (#1817) * RuleMinLength (and others) now pass for nil and empty strings. * iOS deployment target back on iOS 9.0 * Several other fixes ### [5.0.0](https://github.com/xmartlabs/Eureka/releases/tag/5.0.0) * Bug fixes and stability improvements * iOS deployment target increased to iOS9.3 ### [4.3.1](https://github.com/xmartlabs/Eureka/releases/tag/4.3.1) * Bug fixes and stability improvements ### [4.3.0](https://github.com/xmartlabs/Eureka/releases/tag/4.3.0) * Changes for Swift 4.2, Xcode 10 and iOS 12 * Add ability to customise the text color of UIPickerView used by PickerRow * Make `onPresent` result discardable * Add `insert(row: after:)` method on Section which allows inserting rows after a hidden row * Other minor fixes ### [4.2.0](https://github.com/xmartlabs/Eureka/releases/tag/4.2.0) * Adding support for RowType.subtitle on FieldRow (#1468) * CompletionHandler for SwipeAction under iOS < 11 * Allow setting position of the cell after scrolling (#1452) * Add Chinese readme (#1487) * Implement readOnly textAreaRow feature (#1489) * Update cell when tintColor changes (#1492) * PickerRow for 2 and 3 components (#1540) * And several bug fixes ### [4.1.1](https://github.com/xmartlabs/Eureka/releases/tag/4.1.1) * Bug fixes and stability improvements. ### [4.1.0](https://github.com/xmartlabs/Eureka/releases/tag/4.1.0) * Add compatibility for Xcode 9.3 beta 2 and Swift 4.1. * New functionality 🎉. https://github.com/xmartlabs/eureka#swipe-actions. Thanks [@marbetschar](https://github.com/marbetschar). * Add sectionIndexTitles and sectionForSectionIndexTitles to FormViewController to allow for subclasses to override. * Fix SliderRow layout. * Fix regular expression for URLs to allow query and location parameter. * Corrected issue in section sorting function of MultipleSelectorViewController, where all options were placed in one section, with a section title based on the first option. * Added missing call to super.updateConstraints in SegmentedCell. * Add ability to setup alert cancel title from AlertRow. * remove blank section headers/footers from plain tables on iOS 11. This prevents blank section headers and footers from appearing on iOS 11 when setting the table view style to plain and there are no headers or footers. * Fix UIDatePicker bug when mode == .countDownTimer. * Allow non-selectable rows to exist besides selectable rows in a selectable section. * SliderRow - added option 'shouldHideValue' to hide value label (default to false). * Update cell when tintColor changes. * Support dynamic font size changes. ### [4.0.0](https://github.com/xmartlabs/Eureka/releases/tag/4.0.0) * Xcode 9, Swift 4 support. * Add the values of MultivaluedSection into form.values(). ### [3.1.0](https://github.com/xmartlabs/Eureka/releases/tag/3.1.0) Bug fixes & stability improvements: * Fixed how sections and rows are inserted using subscripts * Fixed issue with disbled rows in selectable sections. Disabled rows were still selectable. * Multivalued section does not display image in imagerow #977 * Improve currency formatter #1103 * Added option to use accessory view in ImageCheckCell * Exact length validation (new feature) * Change validation on demand behaviour when row was not valid (#1148) * Move FormDelegate methods to `FormViewController` class to make it extensible and customizable. * The slider was not being disabled when the row was disabled. ### [3.0.0](https://github.com/xmartlabs/Eureka/releases/tag/3.0.0) Bug fixes & stability improvements: * CountDownRow prints Date as Optional #683 * defaultOnCellHighlightChanged event not being called #704 * Error when compiling CustomRow with Swift 3.0 #693 * Fix strange animation when using `insertAtIndex` to add a new section to the form. #566 * Ability to customize validation message. * New RuleEqualsToRow validation rule. * fix RuleMaxLength & RuleMinLength issues. #729 * avoid `animateAlongsideTransition(in:` that could cause transition animation interruption. * Fix initialisation of SliderRow when it has no value at start. * Added isExpanded & isCollapsed helpers to InlineRowType. * fix #800, added form.validate() argument to indicate if hiddenRows should be considered. * Fix issue when showing StepperRow value. Now we use displayValueFor as we do with the rest of rows. * Added PickerInputRow #818 * Fix an issue when FieldCell has a Formatter and the formatter is not used during text editing #796, #768 * Tapping inside an enabled text area row crashes #795 * Added ability to change scroll position when a row is selected. * Added ability to specify that regex validation allows empty values. * Changed the Access Control of Class RuleRegExp to be open. * Implement init?(coder: NSCoder) for some cell types #860 * Removed fatalError from init(coder:) for selector view controllers #882 * Always call onSelectSelectableRow, added properties to control selector vc dismissal. * Reload the tableView even if it is not added to a window. * Adds 'cell.row.wasChanged' to check for cell validation. * Added ability to group options of PushRow MultipleSelectorRow by adding sections to the selector table view. * fix #887, TextAreaRow adds an enter when pressing next on keyboard coming from another row. * Minor fix on how SliderRow update method is called. * Adds RuleClosure example in code comments. * Removing the forced navigationAccessoryView.tintColor set #921. Thanks to all contributors!! 🍻🍻🍻🍻🍻🍻 ### [2.0.1](https://github.com/xmartlabs/Eureka/releases/tag/2.0.1) * Bug fixes and stability improvements. ### [2.0.0-beta.1](https://github.com/xmartlabs/Eureka/releases/tag/2.0.0) Pull requests associated with this milestone can be found in this [filter](https://github.com/xmartlabs/Eureka/issues?utf8=%E2%9C%93&q=milestone%3A2.0.0%20). We have made tons of changes to the Eureka API to follow the new Swift API design guidelines. It's hard to enumerate all changes and most of them will be automatically suggested by Xcode. We have also added to Eureka a extensible build-in validations support. These are the most important changes... #### Deleted * `PostalAddressRow` was removed. * `ImageRow` was removed. You can find these both rows under [EurekaCommunity] github organization. * Row's `func highlightCell()` * Row's `func unhighlightCell()` * Cell's `func highlight()` * Cell's `func unhighlight()` * Cell's `func didSelect()` #### Added * Rows's `var isHighlighted: Bool`. * Rows's `var isValid: Bool`. * Row's `func onCellHighlightChanged(_ callback: @escaping (_ cell: Cell, _ row: Self)->()) -> Self `. * Row's `func onRowValidationChanged(_ callback: @escaping (_ cell: Cell, _ row: Self)->()) -> Self`. * Row's `func validate() -> [ValidationError]` * Form's `func validate() -> [ValidationError]` * Row's `func add(rule: Rule)` * Row's `func remove(ruleWithIdentifier: String)` * `RuleSet` type. * `ValidationOptions` Enum type. * `RuleType` protocol. * `ValidationError` type. ##### Fixes * Fixed textlabel alignment for cells with custom constraints (FieldRow, SegmentedRow, TextAreaRow). * Set 'Require Only App-Extension-Safe API' to YES to enable code sharing in App Extensions. * Other bug fixes and minor improvements Take a look at [2.0.0 Migration guide]() for more information on how to solve breaking changes. ### [1.7.0](https://github.com/xmartlabs/Eureka/releases/tag/1.7.0) * **Breaking change**: Fixed typo in `hightlightCell`. You must now call / override `highlightCell`. * Added `allSections` method to Form. (by @programmarchy) * Updated navigation images for row navigation. (thanks @VladislavJevremovic) * Removed +++= operator. * Other bug fixes and minor improvements ### [1.6.0](https://github.com/xmartlabs/Eureka/releases/tag/1.6.0) * **Breaking change**: Remove controller: FormViewController parameter from HeaderFooterViewRepresentable `viewForSection` method. * Support for Xcode 7.3.1. * Fixed ImageRow issue when trying to access imageURL after selecting image. Now imageURL is properly set up. #346 * Made FieldRowConformance protocol public. * Added ability to override TextAreaRow constraints. * Fix. Now section headerView/footerView cache is deleted whenever section header/footer is assigned. * Made public `navigateToDirection(direction: Direction)` method. * Fixed autolayout in cells. #396 * Removed cell.setNeedsLayout() and cell.setNeedsUpdateConstraints() from updateCell process. * Added `ButtonRow` `onCellSelecttion` example. * Improve row deselection behavior during interactive transitions. #406 * Autosize TextAreaRow functionality added. * Moved `inputAccessoryViewForRow` method from extension to FormViewController allowing it to be overridden. * Added ability to show a text when there is no value selected to some rows. * Fixed: The top divider of a PickerInlineRow disappears upon selection. * Fixed crash when selecting a date. DatePickerRow. * Ensure inline row is visible when it’s expanded. * Fixed PostalAddressRow - When a long form is scrolled up/down, values in Address box disappears. ### [1.5.0](https://github.com/xmartlabs/Eureka/releases/tag/1.5.0) * Xcode 7.3 support. * Expose a public `KeyboardReturnTypeConfiguration` initializer. * Allow to override constraints of FieldRow. * Fixed SelectableSection wrong behaviour when the selectable rows was added to the section before adding the selectable section to the form. * Implemented StepperRow and added an example to the example project. * Allow AlertRow cancel title to be changed. * Enabled CI UI testing and added some tests. * Fixed "0 after decimal separator (DecimalRow)" * Added ability to customize selector and multiple selector view controller option rows. Added `selectableRowCellUpdate` property to `SelectorViewController` and `MultipleSelectorViewController`. * Performance improvement. Store values for each tag in a dictionary and do not calculate it every time we evaluateHidden. ### [1.4.1](https://github.com/xmartlabs/Eureka/releases/tag/1.4.1) Released on 2016-02-25. ##### Breaking Changes * `SelectorRow` now requires the cell among its generic values. This means it is easier to change the cell for a selector row. * `_AlertRow` and `_ActionSheetRow` require generic cell parameter If you are using custom rows that inherit from SelectorRow then you might want to change them as follows (or use your custom cell): ``` // before // public final class LocationRow : SelectorRow, RowType // now public final class LocationRow : SelectorRow>, RowType ``` ### [1.4.0](https://github.com/xmartlabs/Eureka/releases/tag/1.4.0) Released on 2016-02-25. * PopoverSelectorRow added. * BaseRow reload, select, deselect helpers added. * ImageRow update: allows clear button, image sources are public * Added PostalAddressRow * Lots of smaller bug fixes and new documentation ##### Breaking Changes * `BaseCellType` protocol method `func cellBecomeFirstResponder() -> Bool` was renamed to `func cellBecomeFirstResponder(direction: Direction) -> Bool` If you are using custom rows you may have to fix the compiler error by adding the new parameter. * DecimalRow value type changed from Float to Double. ### [1.3.1](https://github.com/xmartlabs/Eureka/releases/tag/1.3.1) Released on 2016-01-11. * Bug fixes and stability improvements. ### [1.3.0](https://github.com/xmartlabs/Eureka/releases/tag/1.3.0) Released on 2015-12-08. * Memory leak fix. * Removed HeaderFooterView inits from Section. * Removed HeaderFooterView inits from Section * Added documentation for Section Header and Footer * Added documentation for Implementing custom Presenter row * Inject table view style on init (by [mikaoj]) * Modified observeValueForKeyPath functions on FieldCell and SegmentedCell to correctly update constraints (by [dernster] and [estebansotoara]) * Added Keyboard next button * Fixed bug with minimum dates [#111](https://github.com/xmartlabs/Eureka/issues/111) * Fixed bug where autocorrected value would not be saved in row.value * Fixed currency formatter bugs * Added list sections ### [1.2.0](https://github.com/xmartlabs/Eureka/releases/tag/1.2.0) Released on 2015-11-12. * Added PickerRow. * Added PickerInlineRow. * Added ZipCodeRow. * Fix bug when inserting hidden row in midst of a section. * Example project: Added ability to set up a formatter to FloatLabelRow. * `rowValueHasBeenChanged` signature has been changed to `override func rowValueHasBeenChanged(row: BaseRow, oldValue: Any?, newValue: Any?)`. * Added `@noescape` attribute to initializer closures. * Fixed issue with tableView's bottom inset when keyboard was dismissed. * Changed NameRow keyboardType to make autocapitalization works. ### [1.1.0](https://github.com/xmartlabs/Eureka/releases/tag/1.1.0) Released on 2015-10-20. ### [1.0.0](https://github.com/xmartlabs/Eureka/releases/tag/1.0.0) Released on 2015-09-29. This is the initial version. [mikaoj]: https://github.com/mikaoj [estebansotoara]: https://github.com/estebansotoara [dernster]: https://github.com/dernster [EurekaCommunity]: https://github.com/EurekaCommunity ================================================ FILE: CONTRIBUTING.md ================================================ Contributing Guidelines -------------------------------------------------- This document provides general guidelines about how to contribute to the project. Keep in mind these important things before you start contributing. ### Asking Questions We do not use github issues for general library support. We think this questions should be posted on stack overflow using [eureka-forms](http://stackoverflow.com/questions/tagged/eureka-forms) tag. ### Reporting issues * Use [github issues](https://github.com/xmartlabs/Eureka/issues) to report a bug. * Before creating a new issue: * Make sure you are using the [latest release](https://github.com/xmartlabs/Eureka/releases). * Check if the issue was [already reported or fixed](https://github.com/xmartlabs/Eureka/issues?utf8=%E2%9C%93&q=is%3Aissue). Notice that it may not be released yet. * If you found a match add a brief comment "I have the same problem" or "+1". This helps prioritize the issues addressing the most common and critical first. If possible add additional information to help us reproduce and fix the issue. Please use your best judgement. * Reporting issues: * Please include the following information to help maintainers to fix the problem faster: * Xcode version you are using. * iOS version you are targeting. * Full Xcode console output of stack trace or code compilation error. * Any other additional detail you think it would be useful to understand and solve the problem. ### Pull requests The easiest way to start contributing is searching open issues by `help wanted` tag. We also add a `difficulty` tag (difficulty: easy, difficulty: moderate, difficulty: hard) in order to give an idea of how complex it can be to implement the feature according maintainers project experience. * Add test coverage to the feature or fix. We only accept new feature pull requests that have related test coverage. This allows us to keep the library stable as we move forward. * Remember to document the new feature. We do not accept new feature pull requests without its associated documentation. * In case of a new feature please update the example project showing the feature. * Please only one fix or feature per pull request. This will increase the chances your feature will be merged. ###### Suggested git workflow to contribute 1. Fork the Eureka repository. 2. Clone your forked project into your developer machine: `git clone git@github.com:/Eureka.git` 3. Add the original project repo as upstream repository in your forked project: `git remote add upstream git@github.com:xmartlabs/Eureka.git` 4. Before starting a new feature make sure your forked master branch is synchronized upstream master branch. Considering you do not merge your pull request into master you can run: `git checkout master` and then `git pull upstream master`. Optionally `git push origin master`. 5. Create a new branch. Note that the starting point is the upstream master branch HEAD. `git checkout -b my-feature-name` 6. Stage all your changes `git add .` and commit them `git commit -m "Your commit message"` 7. Make sure your branch is up to date with upstream master, `git pull --rebase upstream master`, resolve conflicts if necessary. This will move your commit to the top of git stack. 8. Squash your commits into one commit. `git rebase -i HEAD~6` considering you did 6 commits. 9. Push your branch into your forked remote repository. 10. Create a new pull request adding any useful comment. ###### Code style and conventions We try to follow our [swift style guide](https://github.com/xmartlabs/Swift-Style-Guide). Following it is not strictly necessary to contribute and to have a pull request accepted but project maintainers try to follow it. We would love to hear your ideas to improve our code style and conventions. Feel free to contribute. ### Feature proposal We would love to hear your ideas and make a discussions about it. * Use github issues to make feature proposals. * We use `type: feature request` label to mark all [feature request issues](https://github.com/xmartlabs/Eureka/labels/type%3A%20feature%20request). * Before submitting your proposal make sure there is no similar feature request. If you found a match feel free to join the discussion or just add a brief "+1" if you think the feature is worth implementing. * Be as specific as possible providing a precise explanation of feature request so anyone can understand the problem and the benefits of solving it. ================================================ FILE: Documentation/Eureka 2.0 Migration Guide.md ================================================ # Eureka 2.0 Migration Guide #### Requirements: Eureka 2.0.0 needs Xcode 8, Swift3+ to work. Minimum supported iOS version is 8.0. #### Changes Eureka 2.0.0 includes complete Swift 3 Compatibility and adopts the new [API Design Guidelines](https://swift.org/documentation/api-design-guidelines/). It also includes the whole new validations build-in feature. Bring support to swift 3 involves some API updates to follow Apple's Swift API best practices, we have also changed and deprecated some API's. Most important changes will be listed below... Many properties, methods, types were renamed and Xcode should suggest the fix automatically. These are some examples: |Old API| New API| |----|----| |`func rowByTag(_: String) -> RowOf?`|`func rowBy(tag: String) -> RowOf?`| |`func rowByTag(_: String) -> Row?`|`func rowBy(tag: String) -> Row?`| |`func rowByTag(_: String) -> BaseRow?`|`func rowBy(tag: String) -> BaseRow?`| |`func sectionByTag(_: String) -> Section?`|`func sectionBy(tag: String) -> Section?`| |`func rowValueHasBeenChanged(_: BaseRow, oldValue: Any?, newValue: Any?)`|`func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?)`| |`public final func indexPath() -> IndexPath?`|`public final var indexPath: IndexPath?`| |`func prepareForSegue(_ segue: UIStoryboardSegue)`|`func prepare(for segue: UIStoryboardSegue)`| |`func presentViewController(_ viewController: VCType!, row: BaseRow, presentingViewController:FormViewController)`|`present(_ viewController: VCType!, row: BaseRow, presentingViewController:FormViewController)`| |`func createController() -> VCType?`|`func makeController() -> VCType?`| There are also some breaking changes related with deprecated API: Removed APIs: * `PostalAddressRow` and `ImageRow` was deleted. You can find them and many other custom rows at EurekaCommunity [organization account](https://github.com/eurekaCommunity). * `highlightCell` and `unhighlightCell` callbacks were deleted, now we should use `row.isHighlighted` from cell update to check from highlighted status and make UI modification according its value. In case you want to do something when the row's highlighted state switches its value you can set up `onCellHighlightChanged` callback. Custom Rows changes: Row generic type no longer specify the value type. Its Value type is inferred from cell specification. Before: `public final class WeekDayRow: Row, WeekDayCell>, RowType` After: `public final class WeekDayRow: Row, RowType` ================================================ FILE: Documentation/README_CN.md ================================================ ![Eureka: 一个优雅的用Swift编写的表单生成框架](../Eureka.jpg)

Build status Platform iOS Swift 4 compatible Carthage compatible CocoaPods compatible License: MIT codebeat badge

由[XMARTLABS](http://xmartlabs.com)精心编写,是[XLForm]的Swift版本。 ## 概览
## 目录 * [要求] * [使用] + [如何创建表格] + [获取行的值] + [操作符] + [callbacks的使用] + [Section Header和Footer] + [动态地隐藏和显示row(或者Sections)] + [列表类型的sections] + [有多个值的sections] + [验证] + [滑动操作] * [自定义row] + [简单的自定义rows] + [自定义内联rows] + [自定义Presenter rows] * [row目录] * [安装] * [常见问题解答] **想了解更多,可以查看我们的关于*Eureka*的[博客](https://blog.xmartlabs.com/2015/09/29/Introducing-Eureka-iOS-form-library-written-in-pure-Swift/)。** ## 要求 * Xcode 9.2+ * Swift 4+ ### 示例程序 你可以clone这个项目,然后运行Example来欣赏Eureka的大部分特性。
## 使用 ### 如何创建表格 通过继承 `FormViewController`,你可以很容易地把sections和rows添加到`form`变量。 ```swift import Eureka class MyFormViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section("Section1") <<< TextRow(){ row in row.title = "Text Row" row.placeholder = "Enter text here" } <<< PhoneRow(){ $0.title = "Phone Row" $0.placeholder = "And numbers here" } +++ Section("Section2") <<< DateRow(){ $0.title = "Date Row" $0.value = Date(timeIntervalSinceReferenceDate: 0) } } } ``` 在这个例子中,我们创建了两个包含基本rows的sections,效果如下图:
Screenshot of Custom Cells
你也可以不继承`FormViewController`,然后自己设置`form`变量。但是通过继承的话,会方便很多。 #### 配置键盘导航辅助 如果需要改变导航辅助,你需要设置控制器的`navigationOptions`变量,是一个`OptionSet`类型,所以我们可以给它设置一个或者多个值: - **disabled**: 不显示 - **enabled**: 显示在底部 - **stopDisabledRow**: 如果下一行被禁用了,就隐藏 - **skipCanNotBecomeFirstResponderRow**: 如果当前行的`canBecomeFirstResponder()`返回`false`,导航辅助就跳过这行 默认值是 `enabled & skipCanNotBecomeFirstResponderRow` 如果需要流畅的滚动屏幕,要把`animateScroll`设置为`true`。默认情况下,在导航辅助上点击上一个和下一个按钮的时候,`FormViewController`是直接跳到上一行或者下一行的,包括上一行或者下一行不在屏幕内的时候。 如果要设置键盘和正在编辑的row的间距,设置`rowKeyboardSpacing`即可。默认情况下,当表格滚动到一个没有显示出来的view,键盘的顶部和编辑行的底部是没有间距的。 ```swift class MyFormViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form = ... // 开启导航辅助,并且遇到被禁用的行就隐藏导航 navigationOptions = RowNavigationOptions.Enabled.union(.StopDisabledRow) // 开启流畅地滚动到之前没有显示出来的行 animateScroll = true // 设置键盘顶部和正在编辑行底部的间距为20 rowKeyboardSpacing = 20 } } ``` 如果你想要改变导航辅助,需要在`FormViewController`的子类重写`navigationAccessoryView`。 ### 获取行的值 `Row`对象有一个具体类型的 ***value*** 。 例如,`SwitchRow`有一个`Bool`,而`TextRow`有一个`String`。 ```swift // 获取单个row的值 let row: TextRow? = form.rowBy(tag: "MyRowTag") let value = row.value // 获取表格中所有rows的值(必须给每个row的tag赋值) // 字典中包含的键值对为:['rowTag': value]。 let valuesDictionary = form.values() ``` ### 操作符 Eureka包含了自定义的操作符,使得我们更容易地创建表格: #### +++       添加一个section ```swift form +++ Section() // 连起来添加多个sections form +++ Section("First Section") +++ Section("Another Section") // 或者直接利用行来创建一个空白的section form +++ TextRow() +++ TextRow() // 每个row显示在一个独立的section ``` #### <<<       插入一行 ```swift form +++ Section() <<< TextRow() <<< DateRow() // 或者隐式地创建一个section form +++ TextRow() <<< DateRow() ``` #### +=        添加一个数组 ```swift // 添加多个Sections到表格中 form += [Section("A"), Section("B"), Section("C")] // 添加多个rows到一个section中 section += [TextRow(), DateRow()] ``` ### callbacks的使用 Eureka包含了很多callbacks来更改行的外观和行为。 #### 理解`Row`和`Cell` `Row`是抽象的,是Eureka用来存储 **value** 的并包含了一个`Cell`。这个`Cell`用来管理view,并继承自`UITableViewCell`。 例子: ```swift let row = SwitchRow("SwitchRow") { row in // 初始化函数 row.title = "The title" }.onChange { row in row.title = (row.value ?? false) ? "The title expands when on" : "The title" row.updateCell() }.cellSetup { cell, row in cell.backgroundColor = .lightGray }.cellUpdate { cell, row in cell.textLabel?.font = .italicSystemFont(ofSize: 18.0) } ``` Screenshot of Disabled Row #### Callbacks清单 * **onChange()** 当row的`value`改变时调用。你可以在这里调整一些参数,甚至显示或隐藏其他row。 * **onCellSelection()** 当用户点击row并且被选中的时候调用。 * **cellSetup()** 当cell第一次配置的时候调用,并且仅调用一次。可以在这做一些永久性的设置。 * **cellUpdate()** 当cell每次在屏幕上显示的时候调用。可以在里个更新外观。 * **onCellHighlightChanged()** 当cell或者里面的subview 成为或者辞去第一响应者 时调用。 * **onRowValidationChanged()** 当与row关联的验证错误改变时调用。 * **onExpandInlineRow()** 内联row展开前调用。适用于遵循`InlineRowType`协议的rows。 * **onCollapseInlineRow()** 内联row折叠前调用。适用于遵循`InlineRowType`协议的rows。 * **onPresent()** 在显示另外一个view controller之前调用。适用于遵循`PresenterRowType`的rows。可以在这里设置被显示的view controller。 ### Section Header和Footer 你可以将`String`的title或者自定义的`View`作为`Section`的header或者footer。 #### String title ```swift Section("Title") Section(header: "Title", footer: "Footer Title") Section(footer: "Footer Title") ``` #### 自定义View 你可以使用一个`.xib`作为自定义View: ```swift Section() { section in var header = HeaderFooterView(.nibFile(name: "MyHeaderNibFile", bundle: nil)) // header每次出现在屏幕的时候调用 header.onSetupView = { view, _ in // 通常是在这修改view里面的文字 // 不要在这修改view的大小或者层级关系 } section.header = header } ``` 或者是一个使用纯代码创建的`UIView` ```swift Section(){ section in var header = HeaderFooterView(.class) header.height = {100} header.onSetupView = { view, _ in view.backgroundColor = .red } section.header = header } ``` 或者直接用callback来创建view ```swift Section(){ section in section.header = { var header = HeaderFooterView(.callback({ let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) view.backgroundColor = .red return view })) header.height = { 100 } return header }() } ``` ### 动态地隐藏和显示row(或者Sections) Screenshot of Hidden Rows 在这个例子里,我们隐藏或显示整个sections。 为了达到这个效果,每个row有一个`Condition`的可选类型的变量`hidden`,`Condition`可以通过`function`或者`NSPredicate`来设置。 #### 使用function condition来隐藏 使用`Condition`的`function` case: ```swift Condition.function([String], (Form)->Bool) ``` `function`需要一个`Form`参数,并返回`Bool`,决定当前row是否需要隐藏。这是一个非常强大的设置`hidden`的方式,因为它没有明显的限制。 ```swift form +++ Section() <<< SwitchRow("switchRowTag"){ $0.title = "Show message" } <<< LabelRow(){ $0.hidden = Condition.function(["switchRowTag"], { form in return !((form.rowBy(tag: "switchRowTag") as? SwitchRow)?.value ?? false) }) $0.title = "Switch is on!" } ``` Screenshot of Hidden Rows ```swift public enum Condition { case function([String], (Form)->Bool) case predicate(NSPredicate) } ``` #### 使用NSPredicate隐藏 `hidden`也可以用NSPredicate来设置。在predicate string里面,你可以引用其他row的tags,然后决定一个row是否需要隐藏。 但是使用NSPredicate来设置`hidden`的方式只适用于其他rows的value继承自NSObject(String 和 Int 也适用,因为它们被桥接到OjbC对应的类型,但是enums不适用) 使用NSPredicate限制这么多,我们为什么要使用它呢? 因为它比function更简单、更短而且更易读。例如下面这个例子: ```swift $0.hidden = Condition.predicate(NSPredicate(format: "$switchTag == false")) ``` 我们还可以写的更简单,因为`Condition`遵循`ExpressibleByStringLiteral`: ```swift $0.hidden = "$switchTag == false" ``` *注意:我们会在执行的时候把'$switchTag'替换成tag为switchTag的row的value。* 所以的rows都必须有一个tag,我们会用这个tag去找到对应的row,这样才能达到我们想要的效果。 我们也可以直接隐藏一个row: ```swift $0.hidden = true ``` 因为`Condition`遵循`ExpressibleByBooleanLiteral`. 如果不设置`hidden`变量,那么对应的rows就会一直显示。 ##### Sections 对于section来说,也是一样的。我们也可以通过设置`hidden`属性来控制显示或隐藏。 ##### 禁用rows 为了禁用rows,每个row有一个`disable`变量,也是`Condition`的可选类型。使用的方式跟`hidden`一样,所以要求每个row有一个tag。 Note that if you want to disable a row permanently you can also set `disabled` variable to `true`. 注意:如果你想永久的禁用一个row,可以把`disabled`设置为`true`. ### 列表类型的sections 为了显示一个列表选项,Eukera有一个特殊的section,叫做`SelectableSection`。 当创建`SelectableSection`的时候,你需要传入在选项中使用的row的类型和`selectionTyle`。`selectionTyle`是一个枚举,`multipleSelection` 或者 `singleSelection(enableDeselection: Bool)`(`enableDeselection`决定选中的rows是否可以取消选中)。 ```swift form +++ SelectableSection>("Where do you live", selectionType: .singleSelection(enableDeselection: true)) let continents = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] for option in continents { form.last! <<< ListCheckRow(option){ listRow in listRow.title = option listRow.selectableValue = option listRow.value = nil } } ``` ##### 可以使用什么类型的row? 为了创建这样的section,你必须先创建一个遵循`SelectableRowType`协议的row。 ```swift public protocol SelectableRowType : RowType { var selectableValue : Value? { get set } } ``` `selectableValue`是row的value,将会被永久存储。而`value`变量将会用来决定row是否选中,如果被选中,它的值就等于`selectableValue`,否则为nil。Eureka包含上面例子用到的`ListCheckRow`,在自定义rows的实例程序中,你还可以找到`ImageCheckRow`。 ##### 获取选中的rows 为了获得`SelectableSection`中被选中的row,Eukera提供了两个方法:`selectedRow()`(获取`SingleSelection`选中的row) and `selectedRows()`获取`MultipleSelection`所有选中的rows。 ##### 在sections中把选项分组 另外,你可以使用`SelectorViewController`的属性把选项列表分组。 - `sectionKeyForValue` - 是一个闭包,返回特定row的value对应的key,这个key被用来把选项分组。 - `sectionHeaderTitleForKey` - 是一个闭包,返回特定key对应的section的header title,默认是key本身的值。 - `sectionFooterTitleForKey` - 是一个闭包,返回特定key对应的section的footer title。 ### 有多个值的sections Eureka可以通过有多个值的section来支持一个字段对应多个值的情况。它允许我们很容易地创建能插入的、能删除的和能排序的sections。 Screenshot of Multivalued Section #### 如何创建多值section 为了创建一个多值section,我们要使用`MultivaluedSection` ,而不是常规的`Section`。`MultivaluedSection`继承自`Section`,拥有一些额外的属性来设置多值section的行为。 让我们来看一个例子: ```swift form +++ MultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], header: "Multivalued TextField", footer: ".Insert adds a 'Add Item' (Add New Tag) button row as last cell.") { $0.addButtonProvider = { section in return ButtonRow(){ $0.title = "Add New Tag" } } $0.multivaluedRowToInsertAt = { index in return NameRow() { $0.placeholder = "Tag Name" } } $0 <<< NameRow() { $0.placeholder = "Tag Name" } } ``` 上面的代码演示了如何创建一个多值的section。在上面我们把insert, delete 和 reorder传给了`multivaluedOptions`。 `addButtonProvider`允许我们自定义button row,当点击这个button row并且`multivaluedOptions`包含`.Insert`的时候,就会添加一行。 `multivaluedRowToInsertAt`闭包在Eureka每次需要新的row插入的时候调用。为了提供一个row来插入到多值的section中,我们就要设置这个属性。Eureka传index作为闭包的参数。需要注意的是,我们可以返回任何类型的row,甚至自定义的row,即使在多数情况下多值section的rows都是同一类型的。 当我们创建多值section的时候,Eureka会自动添加button row。我们刚刚说到我们可以自定义button row的外观。默认情况下button row的左边有一个加号按钮,我们可以通过设置`showInsertIconInAddButton`属性来决定是否要显示加号按钮。 当创建一个可以插入的sections时,我们还需要更多的考虑。所以被添加到可插入的多值section的rows都应该放在Eureka通过点击按钮来插入的rows上面。我们可以在初始化section的时候,在最后一个闭包参数里添加额外的row,这样在点击button row的时候就会自动把要插入的rows放在最后面。 #### 编辑模式 默认情况下,Eureka只会在当表格中有含有MultivaluedSection的时候把tableView的`isEditing`设置为`true`,而且会在表格第一次显示时的`viewWillAppear`完成。 要了解更多如何使用多值section的相关信息,可以看下Eureka的示例程序,里面包含了多种用法。 ### 验证 Eureka 2.0.0 内置了很多验证特性。 一个row有很多规则和一个用于决定rules是否需要验证的特定配置。 有很多规则是默认提供的,但是你也可以自己创建自己的规则。 默认提供的规则: * RuleRequired * RuleEmail * RuleURL * RuleGreaterThan, RuleGreaterOrEqualThan, RuleSmallerThan, RuleSmallerOrEqualThan * RuleMinLength, RuleMaxLength * RuleClosure 让我们看看如何设置验证规则。 ```swift override func viewDidLoad() { super.viewDidLoad() form +++ Section(header: "Required Rule", footer: "Options: Validates on change") <<< TextRow() { $0.title = "Required Rule" $0.add(rule: RuleRequired()) // 这也可以通过一个闭包来实现:如果验证通过,返回nil,否则返回一个ValidationError。 /* let ruleRequiredViaClosure = RuleClosure { rowValue in return (rowValue == nil || rowValue!.isEmpty) ? ValidationError(msg: "Field required!") : nil } $0.add(rule: ruleRequiredViaClosure) */ $0.validationOptions = .validatesOnChange } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } +++ Section(header: "Email Rule, Required Rule", footer: "Options: Validates on change after blurred") <<< TextRow() { $0.title = "Email Rule" $0.add(rule: RuleRequired()) $0.add(rule: RuleEmail()) $0.validationOptions = .validatesOnChangeAfterBlurred } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } ``` 从上面的代码可以看到,我们可以通过 `add(rule:)`设置多个我们想要的规则。 Row还提供了`func remove(ruleWithIdentifier identifier: String)`来移除规则。为了使用这个方法,我们必须在创建规则的时候,给规则设置一个id。 有时候我们想要在这个row上使用的规则,跟其他rows是一样的。这种情况下,我们可以使用`RuleSet`来设置所以规则: ```swift var rules = RuleSet() rules.add(rule: RuleRequired()) rules.add(rule: RuleEmail()) let row = TextRow() { $0.title = "Email Rule" $0.add(ruleSet: rules) $0.validationOptions = .validatesOnChangeAfterBlurred } ``` Eureka允许我们指定何时执行验证规则。我们可以通过row的`validationOptions`属性来设置,它有以下这些值: * `.validatesOnChange` - 当row的value改变时执行。 * `.validatesOnBlur` - (默认值)当cell辞去第一响应者时执行,不适用于所有rows。 * `.validatesOnChangeAfterBlurred` - 在第一次辞去第一响应者之后,row的value改变时执行 * `.validatesOnDemand` - 我们需要手动调用`validate()`来验证row或者form 如果你想验证整个form(或者所有rows),你可以手动调用Form的`validate()`方法。 #### 如何获取验证错误 每个row都有一个`validationErrors`属性,可以用来获取所有验证错误。这个属性仅仅存储了最后一次验证的错误清单,并不会执行验证规则。 #### 注意类型 正如我们所想的那样,规则的类型必须与row的类型相同。要格外小心检查所使用的row类型。你可能会看到这个编译错误:`"(Incorrect arugment label in call (have 'rule:' expected 'ruleSet:')"`,这并不是把类型混淆了的问题。 ### 滑动操作 Eureka 4.1.0 引入了滑动特性. 现在你可以为每一行定义多个`leadingSwipe` 和 `trailingSwipe`操作。因为滑动操作决定于iOS系统的特性,所以`leadingSwipe`只能用于iOS 11 以上的系统。 让我们看看如何定义滑动操作: ```swift let row = TextRow() { let deleteAction = SwipeAction( style: .destructive, title: "Delete", handler: { (action, row, completionHandler) in // 在这添加你的代码 // 操作完成后一定要调用completionHandler completionHandler?(true) }) deleteAction.image = UIImage(named: "icon-trash") $0.trailingSwipe.actions = [deleteAction] $0.trailingSwipe.performsFirstActionWithFullSwipe = true // 请注意:`leadingSwipe`只能用于iOS 11以上的系统 let infoAction = SwipeAction( style: .normal, title: "Info", handler: { (action, row, completionHandler) in // 在这添加你的代码 // 操作完成后一定要调用completionHandler completionHandler?(true) }) infoAction.backgroundColor = .blue infoAction.image = UIImage(named: "icon-info") $0.leadingSwipe.actions = [infoAction] $0.leadingSwipe.performsFirstActionWithFullSwipe = true } ``` 滑动操作需要把`tableView.isEditing`设置为`false`。Eureka会在当表格中有含有MultivaluedSection的时候把tableView的`isEditing`设置为`true`(在`viewWillAppear`设置)。如果你在同一个表格中同时拥有MultivaluedSections和滑动操作,你要根据情况来设置`isEditing`。 ## 自定义row 通常你需要自定义不同于Eureka内置的row。这其实不是很难,你可以阅读[如何自定义rows的教程](https://blog.xmartlabs.com/2016/09/06/Eureka-custom-row-tutorial/)来开始。你也可以看看[EurekaCommunity],这里包含了其他rows,并准备加入到Eukera中。 ### 简单的自定义rows 为了创建一个拥有自定义行为和外观的row,你可能需要继承于`Row`和`Cell`。 请记住`Row`是Eureka使用的抽象类;而`Cell`实际上是`UITableViewCell`,用于管理view。 因为`Row`包含了`Cell`,所以`Row`和`Cell`必须同时定义。 ```swift // 自定义value类型是Bool的Cell // Cell是使用 .xib 定义的,所以我们可以直接设置outlets public class CustomCell: Cell, CellType { @IBOutlet weak var switchControl: UISwitch! @IBOutlet weak var label: UILabel! public override func setup() { super.setup() switchControl.addTarget(self, action: #selector(CustomCell.switchValueChanged), for: .valueChanged) } func switchValueChanged(){ row.value = switchControl.on row.updateCell() // Re-draws the cell which calls 'update' bellow } public override func update() { super.update() backgroundColor = (row.value ?? false) ? .white : .black } } // 自定义的Row,拥有CustomCell和对应的value public final class CustomRow: Row, RowType { required public init(tag: String?) { super.init(tag: tag) // 我们把对应CustomCell的 .xib 加载到cellProvidor cellProvider = CellProvider(nibName: "CustomCell") } } ``` 结果:
Screenshot of Disabled Row
自定义rows需要继承自 `Row`,并遵循`RowType`协议。 自定义的cells必须继承自`Cell`,并遵循`CellType`协议。 就像cellSetup和CellUpdate回调一样,`Cell`有setup和update方法,你可以在里面做自定义。 ### 自定义内联rows 内联row是一个特定的row类型,可以在它下面动态的显示一个row。正常来说内联row在被点击时在展开和折叠两种状态切换。 所以,为了创建一个内联row,我们需要两个rows,一个总是显示的row,另外一个被展开和折叠的row。 另外一个要求是,这两个rows的value类型必须是一样的。 一旦我们拥有了两个rows,我们要让第一个row遵循`InlineRowType`,这将会给第一个row添加一些方法: ```swift func expandInlineRow() func hideInlineRow() func toggleInlineRow() ``` 最后,当row被点击时我们要调用`toggleInlineRow()`,例如重写`customDidSelect()`方法: ```swift public override func customDidSelect() { toggleInlineRow() } ``` ### 自定义Presenter rows **注意:** *一个Presenter row 是可以弹出UIViewController的row。* 为了创建一个Presenter rows,必须创建一个遵循`PresenterRowType`的类。高度推荐继承自`SelectorRow`,因为它遵循了那个协议并且添加了其他很有用的方法。 `PresenterRowType`协议的定义如下: ```swift public protocol PresenterRowType: TypedRowType { typealias ProviderType : UIViewController, TypedRowControllerType var presentationMode: PresentationMode? { get set } var onPresentCallback: ((FormViewController, ProviderType)->())? { get set } } ``` `onPresentCallback`将会在row即将显示另外一个view controller的时候调用。在`SelectorRow`里面已经调用了,如果你没有继承自它的话,你需要自己手动调用。 `presentationMode`定义了应该显示哪个controller和怎么显示controller。我们可以通过Segue identifier、segue class、present或者push来展示controller。例如一个CustomPushRow可以像这样定义: ```swift public final class CustomPushRow: SelectorRow, SelectorViewController>, RowType { public required init(tag: String?) { super.init(tag: tag) presentationMode = .show(controllerProvider: ControllerProvider.callback { return SelectorViewController(){ _ in } }, onDismiss: { vc in _ = vc.navigationController?.popViewController(animated: true) }) } } ``` 你可以用自己的UIViewController替换`SelectorViewController`,用自己的cell替换`PushSelectorCell`。 ### 使用相同的row来子类化cells 有时候我们想要改变我们其中一个row的外观,但是不需要改变row的类型和已有的逻辑。如果我们使用的cell是通过`xib`文件来初始化的,那么现在有一种方法能达到我们的要求。目前,所有Eureka内置的rows都不是通过`xib`来初始化的,但是[EurekaCommunity]的一些自定义rows是通过`xib`初始化,例如[PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow)。 你所需要做的是: * 创建一个包含你想要创建的cell的nib文件。 * 然后把cell的class设置为你想要修改的cell(如果你想要更改除了UI以外的东西,你需要子类化那个cell。),并且确保那个cell的module是正确的。 * 把outlets连接到类中 * 告诉你的row使用这个新的nib文件。这是通过设置`cellProvider`来完成的,这个设置需要在具体的初始化过程或者`defaultRowInitializer`里面完成。例如: ```swift <<< PostalAddressRow() { $0.cellProvider = CellProvider(nibName: "CustomNib", bundle: Bundle.main) } ``` 另外,你也可以创建一个新的row来达到目的。这种情况下你要继承同一个父类,让这个row继承它的逻辑。 当我们在实现这个的时候,有些东西可以考虑下: * 如果你想要看例子,你可以看看[PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow)或者[CreditCardRow](https://github.com/EurekaCommunity/CreditCardRow),它们都是用了自定义的nib文件。 * 如果你遇到了这样的错误:`Unknown class in Interface Builder file`,可能是你需要在代码中的某个位置、在运行的时候实例化那个新的类型。我是通过调用`let t = YourClass.self`来解决的。 ## row目录 ### Controls Rows
Label Row


Button Row


Check Row


Switch Row


Slider Row


Stepper Row


Text Area Row


### Field Rows 这些rows在cell的右边有一个textField,它们的不同之处在于包含不同的大小写、自动更正和键盘类型配置。
TextRow

NameRow

URLRow

IntRow

PhoneRow

PasswordRow

EmailRow

DecimalRow

TwitterRow

AccountRow

ZipCodeRow
上面所有的`FieldRow`类型都有一个`NSFormatter`类型的`formatter`属性,可以用来控制row的value如何显示。Eureka内置了一个可以保留两位小数的formatter,`DecimalFormatter`。实例程序中包含了`CurrencyFormatter`,可以根据用户所在地显示相应的货币格式。 默认情况下,设置row的`formatter`只会影响到在没有被编辑的时候的显示格式。如果要在编辑的时候也要格式化,要在初始化row的时候把`useFormatterDuringInput`设置为`true`。编辑的时候格式化value的时候可能需要更新光标的位置,Eukera提供了下面的协议,你的formatter需要遵循这个协议来处理光标的位置: ```swift public protocol FormatterProtocol { func getNewPosition(forPosition forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition } ``` 另外,`FieldRow`有一个`useFormatterOnDidBeginEditing`属性。当使用`DecimalRow`的时候,并且有一个允许decimal value和遵循用户所在地的formatter,例如`DecimalFormatter`,如果`useFormatterDuringInput`是`false`,`useFormatterOnDidBeginEditing`必须设置为`true`,这样value中的小数点才能匹配键盘中的小数点。 ### Date Rows Date Rows存储了一个Date,并且允许我们通过UIDatePicker来设置一个新的值。UIDatePicker的模式和date picker view的显示方法如下图:
Date Row
Picker在键盘上显示
Date Row (Inline)
row展开
Date Row (Picker)
picker一直显示
有三种风格(Normal、Inline和Picker),Eureka还包括: + **DateRow** + **TimeRow** + **DateTimeRow** + **CountDownRow** ### Option Rows Option Rows关联着用户必须选择的一系列选项。 ```swift <<< ActionSheetRow() { $0.title = "ActionSheetRow" $0.selectorTitle = "Pick a number" $0.options = ["One","Two","Three"] $0.value = "Two" // initially selected } ```
Alert Row

通过Alert的方式显示。
ActionSheet Row

通过action sheet的方式显示。
Push Row

push到一个新的controller。
Multiple Selector Row

像PushRow一样,但是允许多选。
Segmented Row
Segmented Row (w/Title)
Picker Row

通过picker view来显示通用类型选项
(也有Picker内联Row)
### 建立自己的自定义row? 让我们知道它,我们会跟高兴的在这提到它。:) * **LocationRow** (在示例程序中作为自定义row) Screenshot of Location Row ## 安装 #### CocoaPods [CocoaPods](https://cocoapods.org/) 是一个管理Cocoa项目的依赖。 在项目中的`Podfile`文件指定Eureka: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! pod 'Eureka' ``` 然后运行下面的命令: ```bash $ pod install ``` #### Carthage [Carthage](https://github.com/Carthage/Carthage) 是一个简单的、分散的Cocoa依赖管理器。 在项目中的`Cartfile`文件文件指定Eureka: ```ogdl github "xmartlabs/Eureka" ~> 4.0 ``` #### 手动集成框架 * 在项目的根目录运行下列代码克隆Eureka,作为一个git [submodule](http://git-scm.com/docs/git-submodule)。 ```bash $ git submodule add https://github.com/xmartlabs/Eureka.git ``` * 克隆完成后,打开Eureka文件夹,然后把`Eureka.xcodeproj`拖动到Xcode项目的Project Navigator中 * 在Project Navigator选中`Eureka.xcodeproj`,检查deployment target是否跟应用的匹配 * 在Project Navigator选中你自己的项目,选择自己应用的target,然后选择在"General"选项卡,在`Embedded Binaries`点击加号 * 选择`Eureka.framework`,大功告成。 ## 参与其中 * 如果你想贡献,请随时提交pull request。 * 如果你有新功能要求,请开一个issue。 * 如果你找到了一个bug,在提交新的issue之前,请先查看旧的issues。 * 如果你需要帮助,或者询问常见的问题,使用[StackOverflow]。(Tag `eureka-forms`) **在贡献之前,请先查看[CONTRIBUTING](../CONTRIBUTING.md)文件了解更多信息。** 如果你在你的项目中使用了**Eureka**,我们很想听到这个消息。可以在[twitter]给我发消息。 ## 作者 * [Martin Barreto](https://github.com/mtnBarreto) ([@mtnBarreto](https://twitter.com/mtnBarreto)) * [Mathias Claassen](https://github.com/mats-claassen) ([@mClaassen26](https://twitter.com/mClaassen26)) ## 常见问题解答 #### 如果通过tag获取Row 我们可以通过调用`Form`暴露的下列方法来获取特定的row: ```swift public func rowBy(tag: String) -> RowOf? public func rowBy(tag: String) -> Row? public func rowBy(tag: String) -> BaseRow? ``` 例如: ```swift let dateRow : DateRow? = form.rowBy(tag: "dateRowTag") let labelRow: LabelRow? = form.rowBy(tag: "labelRowTag") let dateRow2: Row? = form.rowBy(tag: "dateRowTag") let labelRow2: BaseRow? = form.rowBy(tag: "labelRowTag") ``` #### 如果使用tag获取Section ```swift let section: Section? = form.sectionBy(tag: "sectionTag") ``` #### 如果使用dictionary来设置form的值 调用`Form`暴露的`setValues(values: [String: Any?])`方法. 例如: ```swift form.setValues(["IntRowTag": 8, "TextRowTag": "Hello world!", "PushRowTag": Company(name:"Xmartlabs")]) ``` `"IntRowTag"`、 `"TextRowTag"`、 `"PushRowTag"` 是row tag (每个都唯一地标识一个row), `8`、 `"Hello world!"`、 `Company(name:"Xmartlabs")` 是对应的要赋给row的值。 row的value类型必须匹配dictionary中对应值的类型,否则会将nil赋给row的value。 如果form已经显示了,我们必须刷新已经显示的rows,可以通过调用`tableView.reloadData()`来刷新table view,或者调用已经显示的row的`updateCell()`方法。 #### 更改hidden或者disable condition后,Row没有更新 在设置了一个condition之后,这个condition不会自动执行,如果你想要马上执行,你可以调用`.evaluateHidden()` 或者 `.evaluateDisabled()`。 这些方法仅仅在row被添加到form时和row改变时调用。如果这个condition被更改了,但是这个row处于显示状态,必须手动执行。 #### 除非onCellHighlight也被定义了,否则onCellUnHighlight不会被调用 查看这个[issue](https://github.com/xmartlabs/Eureka/issues/96). #### 如更新 Section header/footer * 设置新的 header/footer .... ```swift section.header = HeaderFooterView(title: "Header title \(variable)") // 使用 String interpolation //或者 var header = HeaderFooterView(.class) // 使用任何view类型更灵活地设置header header.height = { 60 } // 可以指高度 header.onSetupView = { view, section in // 每次view即将显示时,onSetupView被调用 view.backgroundColor = .orange } section.header = header ``` * 刷新Section ```swift section.reload() ``` #### 如何自定义Selector 和 MultipleSelector 选项的cells `selectableRowSetup`、`selectableRowCellUpdate` 和 `selectableRowCellSetup` 可以自定义 SelectorViewController 和 MultipleSelectorViewController的可选cells. ```swift let row = PushRow() { $0.title = "PushRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 $0.selectorTitle = "Choose an Emoji!" }.onPresent { from, to in to.dismissOnSelection = false to.dismissOnChange = false to.selectableRowSetup = { row in row.cellProvider = CellProvider>(nibName: "EmojiCell", bundle: Bundle.main) } to.selectableRowCellUpdate = { cell, row in cell.textLabel?.text = "Text " + row.selectableValue! // 自定义 cell.detailTextLabel?.text = "Detail " + row.selectableValue! } } ``` #### 不想使用Eureka提供的自定义操作符? 正如我们所说的,`Form`和`Section`遵循`MutableCollection`和`RangeReplaceableCollection`。一个Form是一个Sections的集合,一个Sections是一个Rows的集合。 `RangeReplaceableCollection` 协议扩展提供了很多方法来改变集合。 ```swift extension RangeReplaceableCollection { public mutating func append(_ newElement: Self.Element) public mutating func append(contentsOf newElements: S) where S : Sequence, Self.Element == S.Element public mutating func insert(_ newElement: Self.Element, at i: Self.Index) public mutating func insert(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element public mutating func remove(at i: Self.Index) -> Self.Element public mutating func removeSubrange(_ bounds: Range) public mutating func removeFirst(_ n: Int) public mutating func removeFirst() -> Self.Element public mutating func removeAll(keepingCapacity keepCapacity: Bool) public mutating func reserveCapacity(_ n: Self.IndexDistance) } ``` 上面的方法在内部用来实现自定义的操作: ```swift public func +++(left: Form, right: Section) -> Form { left.append(right) return left } public func +=(inout lhs: Form, rhs: C) where C.Element == Section { lhs.append(contentsOf: rhs) } public func <<<(left: Section, right: BaseRow) -> Section { left.append(right) return left } public func +=(inout lhs: Section, rhs: C) where C.Element == BaseRow { lhs.append(contentsOf: rhs) } ``` 你可以在[这里](https://github.com/xmartlabs/Eureka/blob/master/Source/Core/Operators.swift)看到剩下的自定义操作符是如何实现的。 是否要用自定义的操作符,完全由你自己决定。 [要求]: #要求 [使用]: #使用 [如何创建表格]: #如何创建表格 [获取行的值]: #获取行的值 [操作符]: #操作符 [callbacks的使用]: #callbacks的使用 [Section Header和Footer]: #section-header-footer [动态地隐藏和显示row(或者Sections)]: #hide-show-rows [列表类型的sections]: #列表类型的sections [有多个值的sections]: #有多个值的sections [验证]: #验证 [滑动操作]: #滑动操作 [自定义row]: #自定义row [简单的自定义rows]: #简单的自定义rows [自定义内联rows]: #自定义内联rows [自定义Presenter rows]: #presenter-rows [row目录]: #row目录 [安装]: #安装 [常见问题解答]: #常见问题解答 [CustomCellsController]: ../Example/Example/ViewController.swift [FormViewController]: ../Example/Source/Controllers.swift [XLForm]: https://github.com/xmartlabs/XLForm [DSL]: https://en.wikipedia.org/wiki/Domain-specific_language [StackOverflow]: http://stackoverflow.com/questions/tagged/eureka-forms [our blog post]: http://blog.xmartlabs.com/2015/09/29/Introducing-Eureka-iOS-form-library-written-in-pure-Swift/ [twitter]: https://twitter.com/xmartlabs [EurekaCommunity]: https://github.com/EurekaCommunity # 捐赠Eureka 所以我们可以让Eureka变得更好!

[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HRMAH7WZ4QQ8E) # 更改日志 可以在[CHANGELOG.md](../CHANGELOG.md)文件中查看。 ================================================ FILE: Eureka.playground/Contents.swift ================================================ //: **Eureka Playground** - let us walk you through Eureka! cool features, we will show how to //: easily create powerful forms using Eureka! //: It allows us to create complex dynamic table view forms and obviously simple static table view. It's ideal for data entry task or settings pages. import UIKit import XCPlayground import PlaygroundSupport //: Start by importing Eureka module import Eureka //: Any **Eureka** form must extend from `FormViewController` let formController = FormViewController() PlaygroundPage.current.liveView = formController.view let b = [Int]() b.last let f = Form() f.last //: ## Operators //: ### +++ //: Adds a Section to a Form when the left operator is a Form let form = Form() +++ Section() //: When both operators are a Section it creates a new Form containing both Section and return the created form. let form2 = Section("First") +++ Section("Second") +++ Section("Third") //: Notice that you don't need to add parenthesis in the above expresion, +++ operator is left associative so it will create a form containing the 2 first sections and then append last Section (Third) to the created form. //: form and form2 don't have any row and are not useful at all. Let's add some rows. //: ### +++ //: Can be used to append a row to a form without having to create a Section to contain the row. The form section is implicitly created as a result of the +++ operator. formController.form +++ TextRow("Text") //: it can also be used to append a Section like this: formController.form +++ Section() //: ### <<< //: Can be used to append rows to a section. formController.form.last! <<< SwitchRow("Switch") { $0.title = "Switch"; $0.value = true } //: it can also be used to create a section and append rows to it. Let's use that to add a new section to the form formController.form +++ PhoneRow("Phone") { $0.title = "Phone"} <<< IntRow("IntRow") { $0.title = "Int"; $0.value = 5 } formController.view //: # Callbacks //: Rows might have several closures associated to be called at different events. Let's see them one by one. Sadly, we can not interact with it in our playground. //: ### onChange callback //: Will be called when the value of a row changes. Lots of things can be done with this feature. Let's create a new section for the new rows: formController.form +++ Section("Callbacks") <<< SwitchRow("scr1") { $0.title = "Switch to turn red"; $0.value = false } .onChange({ row in if row.value == true { row.cell.backgroundColor = .red } else { row.cell.backgroundColor = .black } }) //: Now when we change the value of this row its background color will change to red. Try that by (un)commenting the following line: formController.view formController.form.last?.last?.baseValue = true formController.view //: Notice that we set the `baseValue` attribute because we did not downcast the result of `formController.form.last?.last`. It is essentially the same as the `value` attribute //: ### cellSetup and cellUpdate callbacks //: The cellSetup will be called when the cell of this row is configured (just once at the beginning) //: and the cellUpdate will be called when it is updated (each time it reappears on screen). Here you should define the appearance of the cell formController.form.last! <<< SegmentedRow("Segments") { $0.title = "Choose an animal"; $0.value = "🐼"; $0.options = ["🐼", "🐶", "🐻"]}.cellSetup({ cell, _ in cell.backgroundColor = .red }).cellUpdate({ (cell, _) -> Void in cell.textLabel?.textColor = .yellow }) //: ### onSelection and onPresent callbacks //: OnSelection will be called when this row is selected (tapped by the user). It might be useful for certain rows instead of the onChange callback. //: OnPresent will be called when a row that presents another view controller is tapped. It might be useful to set up the presented view controller. //: We can not try them out in the playground but they can be set just like the others. formController.form.last! <<< SegmentedRow("Segments2") { $0.title = "Choose an animal"; $0.value = "🐼"; $0.options = ["🐼", "🐶", "🐻"] }.onCellSelection { cell, row in print("\(cell) for \(row) got selected") } formController.view //: ### Hiding rows //: We can hide rows by defining conditions that will tell if they should appear on screen or not. Let's create a row that will hide when the previous one is "🐼". formController.form.last! <<< LabelRow("Confirm") { $0.title = "Are you sure you do not want the 🐼?" $0.hidden = "$Segments2 == '🐼'" } //: Now let's see how this works: formController.view formController.form.rowBy(tag: "Segments2")?.baseValue = "🐶" formController.view //: We can do the same using functions. Functions are specially useful for more complicated conditions. //: This applies when the value of the row we depend on is not compatible with NSPredicates (which is not the current case, but anyway). formController.form.last! <<< LabelRow("Confirm2") { $0.title = "Well chosen!!" $0.hidden = Condition.function(["Segments2"]) { form in if let r: SegmentedRow = form.rowBy(tag: "Segments2") { return r.value != "🐼" } return true } } //: Now let's see how this works: formController.view formController.form.rowBy(tag: "Segments2")?.baseValue = "🐼" formController.view ================================================ FILE: Eureka.playground/contents.xcplayground ================================================ ================================================ FILE: Eureka.playground/playground.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Eureka.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Eureka.playground/timeline.xctimeline ================================================ ================================================ FILE: Eureka.podspec ================================================ Pod::Spec.new do |s| s.name = 'Eureka' s.version = '5.5.0' s.license = 'MIT' s.summary = 'Elegant iOS Forms in pure Swift' s.homepage = 'https://github.com/xmartlabs/Eureka' s.social_media_url = 'http://twitter.com/xmartlabs' s.authors = { 'Martin Barreto' => 'martin@xmartlabs.com', 'Mathias Claassen' => 'mathias@xmartlabs.com' } s.source = { :git => 'https://github.com/xmartlabs/Eureka.git', :tag => s.version } s.ios.deployment_target = '9.0' s.ios.frameworks = 'UIKit', 'Foundation' s.source_files = 'Source/**/*.swift' s.requires_arc = true s.swift_version = '5.0' end ================================================ FILE: Eureka.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 280FBE5D1D87360900900064 /* RuleRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280FBE5C1D87360900900064 /* RuleRange.swift */; }; 280FBE5F1D873CCE00900064 /* RuleClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280FBE5E1D873CCE00900064 /* RuleClosure.swift */; }; 2859CD951C7CF1FD0002982F /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CD921C7CF1FD0002982F /* Core.swift */; }; 2859CD961C7CF1FD0002982F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CD931C7CF1FD0002982F /* Helpers.swift */; }; 2859CDBE1C7D137D0002982F /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDBD1C7D137D0002982F /* Form.swift */; }; 2859CDC01C7D138D0002982F /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDBF1C7D138D0002982F /* Section.swift */; }; 2859CDC41C7D19C50002982F /* HeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDC31C7D19C50002982F /* HeaderFooterView.swift */; }; 2859CDC61C7D1A2E0002982F /* NavigationAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDC51C7D1A2E0002982F /* NavigationAccessoryView.swift */; }; 2859CDE91C7DD78E0002982F /* BaseRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDE81C7DD78E0002982F /* BaseRow.swift */; }; 285BA44A1E8187480034EE92 /* MultivaluedSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285BA4481E81864E0034EE92 /* MultivaluedSectionTests.swift */; }; 287A142B1D89DF1E00FFE6EB /* ValidationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287A142A1D89DF1E00FFE6EB /* ValidationsTests.swift */; }; 287A14331D8A07ED00FFE6EB /* SelectableSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287A142D1D8A07B700FFE6EB /* SelectableSectionTests.swift */; }; 287E7D7E1D625A260065F4DE /* RuleRequired.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287E7D7D1D625A260065F4DE /* RuleRequired.swift */; }; 287E7D801D74E42D0065F4DE /* RuleRegExp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287E7D7F1D74E42D0065F4DE /* RuleRegExp.swift */; }; 287E7D821D74E64C0065F4DE /* RuleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287E7D811D74E64C0065F4DE /* RuleURL.swift */; }; 287E7D841D75F9C80065F4DE /* RuleEmail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287E7D831D75F9C80065F4DE /* RuleEmail.swift */; }; 287E7D901D7601850065F4DE /* RuleLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287E7D8F1D7601850065F4DE /* RuleLength.swift */; }; 28B12FD51BA0E83C00F27A23 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28B12FD41BA0E83C00F27A23 /* Assets.xcassets */; }; 28B1D7871C7F911900605EB3 /* BaseEurekaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7711C7F90C900605EB3 /* BaseEurekaTests.swift */; }; 28B1D7881C7F911900605EB3 /* CallbacksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7721C7F90C900605EB3 /* CallbacksTests.swift */; }; 28B1D7891C7F911900605EB3 /* CollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7731C7F90C900605EB3 /* CollectionTests.swift */; }; 28B1D78A1C7F911900605EB3 /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7741C7F90C900605EB3 /* DateTests.swift */; }; 28B1D78B1C7F911900605EB3 /* FormValuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7751C7F90C900605EB3 /* FormValuesTests.swift */; }; 28B1D78C1C7F911900605EB3 /* HelperMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7761C7F90C900605EB3 /* HelperMethodTests.swift */; }; 28B1D78D1C7F911900605EB3 /* HiddenRowsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7771C7F90C900605EB3 /* HiddenRowsTests.swift */; }; 28B1D78E1C7F911900605EB3 /* OperatorsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7781C7F90C900605EB3 /* OperatorsTest.swift */; }; 28B1D78F1C7F911900605EB3 /* RowByTagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D7791C7F90C900605EB3 /* RowByTagTests.swift */; }; 28B1D7901C7F911900605EB3 /* RowCallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D77A1C7F90C900605EB3 /* RowCallbackTests.swift */; }; 28B1D7911C7F911900605EB3 /* SetValuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B1D77B1C7F90C900605EB3 /* SetValuesTests.swift */; }; 28DB37301CE4F864009E46BA /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28DB372F1CE4F864009E46BA /* Protocols.swift */; }; 28EE0FDE1D5E889F00B91340 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EE0FDD1D5E889F00B91340 /* Validation.swift */; }; 28EE46A81C7F5D5200EFF4A2 /* SelectableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE231C7E141B0002982F /* SelectableSection.swift */; }; 28EE46A91C7F685900EFF4A2 /* PushRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDEC1C7DF0680002982F /* PushRow.swift */; }; 28EE46AC1C7F712300EFF4A2 /* DateInlineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDF41C7DF2740002982F /* DateInlineRow.swift */; }; 28EEBFE81C7E1D7E00300699 /* CellType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFE41C7E1B3B00300699 /* CellType.swift */; }; 28EEBFEC1C7E224200300699 /* PresenterRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFEB1C7E224200300699 /* PresenterRowType.swift */; }; 28EEBFEE1C7E231C00300699 /* RowControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFED1C7E231C00300699 /* RowControllerType.swift */; }; 28EEBFF01C7E23F400300699 /* Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFEF1C7E23F400300699 /* Row.swift */; }; 28EEBFF21C7E23FA00300699 /* RowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFF11C7E23FA00300699 /* RowType.swift */; }; 28EEBFF41C7E240000300699 /* RowProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFF31C7E240000300699 /* RowProtocols.swift */; }; 28EEBFF61C7E240B00300699 /* InlineRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFF51C7E240B00300699 /* InlineRowType.swift */; }; 28EEBFF81C7E241200300699 /* SelectableRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFF71C7E241200300699 /* SelectableRowType.swift */; }; 28EEBFFB1C7E25C500300699 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFFA1C7E25C500300699 /* Operators.swift */; }; 28EEBFFD1C7E25F100300699 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EEBFFC1C7E25F100300699 /* Cell.swift */; }; 28EEBFFE1C7E281F00300699 /* CheckRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDA41C7CF51A0002982F /* CheckRow.swift */; }; 28EEC0001C7E36EB00300699 /* SwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDA61C7CF5530002982F /* SwitchRow.swift */; }; 28EEC0021C7E37BD00300699 /* ButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDA21C7CF4050002982F /* ButtonRow.swift */; }; 28EEC0031C7E390300300699 /* DecimalFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE0A1C7E003B0002982F /* DecimalFormatter.swift */; }; 28EEC00B1C7E395700300699 /* OptionsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDB31C7D0E4B0002982F /* OptionsRow.swift */; }; 28EEC00C1C7E396000300699 /* SelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDBB1C7D12930002982F /* SelectorRow.swift */; }; 28EEC00E1C7E398A00300699 /* DateFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDEE1C7DF0F70002982F /* DateFieldRow.swift */; }; 28EEC00F1C7E398D00300699 /* FieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDF01C7DF1620002982F /* FieldRow.swift */; }; 28EEC0101C7E399500300699 /* DateInlineFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDF21C7DF22D0002982F /* DateInlineFieldRow.swift */; }; 28EEC0121C7E399500300699 /* GenericMultipleSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE211C7E13470002982F /* GenericMultipleSelectorRow.swift */; }; 28EEC0151C7E39C100300699 /* DateRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDFE1C7DF9A70002982F /* DateRow.swift */; }; 28EEC0161C7E3A3200300699 /* ListCheckRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE1B1C7E04630002982F /* ListCheckRow.swift */; }; 28EEC0181C7E3A3D00300699 /* MultipleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE121C7E02310002982F /* MultipleSelectorViewController.swift */; }; 28EEC0191C7E3A3D00300699 /* SelectorAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE141C7E02710002982F /* SelectorAlertController.swift */; }; 28EEC01C1C7E3A7400300699 /* ButtonRowWithPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CD991C7CF2020002982F /* ButtonRowWithPresent.swift */; }; 28EEC01D1C7E3A8600300699 /* SliderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CD9A1C7CF2020002982F /* SliderRow.swift */; }; 28EEC01F1C7E3AC800300699 /* LabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDA01C7CF3A10002982F /* LabelRow.swift */; }; 28EEC0201C7E3AD200300699 /* SegmentedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDA81C7D0B3C0002982F /* SegmentedRow.swift */; }; 28EEC0211C7E3AD700300699 /* TextAreaRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDAC1C7D0BCA0002982F /* TextAreaRow.swift */; }; 28EEC0221C7E3B1300300699 /* ActionSheetRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDAE1C7D0D990002982F /* ActionSheetRow.swift */; }; 28EEC0231C7E3B1900300699 /* AlertRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDB01C7D0E010002982F /* AlertRow.swift */; }; 28EEC0241C7E3B1D00300699 /* PickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDB51C7D0EC60002982F /* PickerRow.swift */; }; 28EEC0251C7E3B2200300699 /* PickerInlineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDB71C7D0FE70002982F /* PickerInlineRow.swift */; }; 28EEC0281C7E3B4B00300699 /* FieldsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE061C7DFBFA0002982F /* FieldsRow.swift */; }; 28EEC02A1C7E3B5A00300699 /* MultipleSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE1F1C7E092B0002982F /* MultipleSelectorRow.swift */; }; 28EEC02F1C7E49BF00300699 /* SelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE101C7E01EE0002982F /* SelectorViewController.swift */; }; 28EEC0321C7E64A400300699 /* DatePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CDFC1C7DF9430002982F /* DatePickerRow.swift */; }; 28EEC0331C7E64AB00300699 /* PopoverSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2859CE1D1C7E08300002982F /* PopoverSelectorRow.swift */; }; 3973000A1DF0E2A000080E4D /* PickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397300091DF0E2A000080E4D /* PickerInputRow.swift */; }; 49ADC7F91C8A83240073952B /* StepperRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ADC7F81C8A83240073952B /* StepperRow.swift */; }; 51729DF61B9A4F5E004A00EB /* Eureka.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51729DEB1B9A4F5E004A00EB /* Eureka.framework */; }; 51729E671B9A5FA5004A00EB /* Eureka.h in Headers */ = {isa = PBXBuildFile; fileRef = 51729E661B9A5FA5004A00EB /* Eureka.h */; settings = {ATTRIBUTES = (Public, ); }; }; 798C5ADA1EF1E35600A21F52 /* SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798C5AD91EF1E35600A21F52 /* SwipeActions.swift */; }; 8FCCF8EF20A24085004793A0 /* DoublePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCCF8EE20A24085004793A0 /* DoublePickerRow.swift */; }; 8FCCF8F120A32613004793A0 /* TriplePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCCF8F020A32613004793A0 /* TriplePickerRow.swift */; }; 8FCCF92320A473E7004793A0 /* DoublePickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCCF92220A473E7004793A0 /* DoublePickerInputRow.swift */; }; 8FCCF92520A4794B004793A0 /* TriplePickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCCF92420A4794B004793A0 /* TriplePickerInputRow.swift */; }; 9EFC727C291D0369004840A9 /* ResultBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFC727B291D0369004840A9 /* ResultBuildersTests.swift */; }; 9EFC727F291D05FB004840A9 /* ResultBuilders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFC727D291D05EA004840A9 /* ResultBuilders.swift */; }; B244E6541FE1C94F0026B944 /* AlertOptionsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B244E6531FE1C94F0026B944 /* AlertOptionsRow.swift */; }; B257FE2E1EC0F66900043911 /* RowsInsertionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B257FE2D1EC0F66900043911 /* RowsInsertionTests.swift */; }; B2A401161EC0BA140042EDF0 /* SectionsInsertionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2A401151EC0BA140042EDF0 /* SectionsInsertionTests.swift */; }; E110A1402BC9D08200FFD81D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E110A13F2BC9D08200FFD81D /* PrivacyInfo.xcprivacy */; }; FA56B6AF1DBAD38900407C94 /* RuleEqualsToRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA56B6AE1DBAD38900407C94 /* RuleEqualsToRow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 51729DF71B9A4F5E004A00EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729DE21B9A4F5E004A00EB /* Project object */; proxyType = 1; remoteGlobalIDString = 51729DEA1B9A4F5E004A00EB; remoteInfo = Eureka; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 280FBE5C1D87360900900064 /* RuleRange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleRange.swift; path = Source/Validations/RuleRange.swift; sourceTree = SOURCE_ROOT; }; 280FBE5E1D873CCE00900064 /* RuleClosure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleClosure.swift; path = Source/Validations/RuleClosure.swift; sourceTree = SOURCE_ROOT; }; 2859CD921C7CF1FD0002982F /* Core.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Core.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CD931C7CF1FD0002982F /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 2859CD991C7CF2020002982F /* ButtonRowWithPresent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ButtonRowWithPresent.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CD9A1C7CF2020002982F /* SliderRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderRow.swift; sourceTree = ""; }; 2859CDA01C7CF3A10002982F /* LabelRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelRow.swift; sourceTree = ""; }; 2859CDA21C7CF4050002982F /* ButtonRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ButtonRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDA41C7CF51A0002982F /* CheckRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CheckRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDA61C7CF5530002982F /* SwitchRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchRow.swift; sourceTree = ""; }; 2859CDA81C7D0B3C0002982F /* SegmentedRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedRow.swift; sourceTree = ""; }; 2859CDAC1C7D0BCA0002982F /* TextAreaRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAreaRow.swift; sourceTree = ""; }; 2859CDAE1C7D0D990002982F /* ActionSheetRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSheetRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDB01C7D0E010002982F /* AlertRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AlertRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDB31C7D0E4B0002982F /* OptionsRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OptionsRow.swift; path = Common/OptionsRow.swift; sourceTree = ""; }; 2859CDB51C7D0EC60002982F /* PickerRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerRow.swift; sourceTree = ""; }; 2859CDB71C7D0FE70002982F /* PickerInlineRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerInlineRow.swift; sourceTree = ""; }; 2859CDBB1C7D12930002982F /* SelectorRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SelectorRow.swift; path = Common/SelectorRow.swift; sourceTree = ""; }; 2859CDBD1C7D137D0002982F /* Form.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Form.swift; sourceTree = ""; }; 2859CDBF1C7D138D0002982F /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 2859CDC31C7D19C50002982F /* HeaderFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderFooterView.swift; sourceTree = ""; }; 2859CDC51C7D1A2E0002982F /* NavigationAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationAccessoryView.swift; sourceTree = ""; }; 2859CDE81C7DD78E0002982F /* BaseRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BaseRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDEC1C7DF0680002982F /* PushRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRow.swift; sourceTree = ""; }; 2859CDEE1C7DF0F70002982F /* DateFieldRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = DateFieldRow.swift; path = Common/DateFieldRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDF01C7DF1620002982F /* FieldRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FieldRow.swift; path = Common/FieldRow.swift; sourceTree = ""; }; 2859CDF21C7DF22D0002982F /* DateInlineFieldRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = DateInlineFieldRow.swift; path = Common/DateInlineFieldRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDF41C7DF2740002982F /* DateInlineRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DateInlineRow.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2859CDFC1C7DF9430002982F /* DatePickerRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerRow.swift; sourceTree = ""; }; 2859CDFE1C7DF9A70002982F /* DateRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateRow.swift; sourceTree = ""; }; 2859CE061C7DFBFA0002982F /* FieldsRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FieldsRow.swift; sourceTree = ""; }; 2859CE0A1C7E003B0002982F /* DecimalFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DecimalFormatter.swift; path = Common/DecimalFormatter.swift; sourceTree = ""; }; 2859CE101C7E01EE0002982F /* SelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SelectorViewController.swift; path = Controllers/SelectorViewController.swift; sourceTree = ""; }; 2859CE121C7E02310002982F /* MultipleSelectorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultipleSelectorViewController.swift; path = Controllers/MultipleSelectorViewController.swift; sourceTree = ""; }; 2859CE141C7E02710002982F /* SelectorAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SelectorAlertController.swift; path = Controllers/SelectorAlertController.swift; sourceTree = ""; }; 2859CE1B1C7E04630002982F /* ListCheckRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ListCheckRow.swift; path = SelectableRows/ListCheckRow.swift; sourceTree = ""; }; 2859CE1D1C7E08300002982F /* PopoverSelectorRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopoverSelectorRow.swift; sourceTree = ""; }; 2859CE1F1C7E092B0002982F /* MultipleSelectorRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleSelectorRow.swift; sourceTree = ""; }; 2859CE211C7E13470002982F /* GenericMultipleSelectorRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GenericMultipleSelectorRow.swift; path = Common/GenericMultipleSelectorRow.swift; sourceTree = ""; }; 2859CE231C7E141B0002982F /* SelectableSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableSection.swift; sourceTree = ""; }; 285BA4481E81864E0034EE92 /* MultivaluedSectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MultivaluedSectionTests.swift; path = Tests/MultivaluedSectionTests.swift; sourceTree = SOURCE_ROOT; }; 287A142A1D89DF1E00FFE6EB /* ValidationsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValidationsTests.swift; path = Tests/ValidationsTests.swift; sourceTree = SOURCE_ROOT; }; 287A142D1D8A07B700FFE6EB /* SelectableSectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SelectableSectionTests.swift; path = Tests/SelectableSectionTests.swift; sourceTree = SOURCE_ROOT; }; 287E7D7D1D625A260065F4DE /* RuleRequired.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleRequired.swift; path = Source/Validations/RuleRequired.swift; sourceTree = SOURCE_ROOT; }; 287E7D7F1D74E42D0065F4DE /* RuleRegExp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleRegExp.swift; path = Source/Validations/RuleRegExp.swift; sourceTree = SOURCE_ROOT; }; 287E7D811D74E64C0065F4DE /* RuleURL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleURL.swift; path = Source/Validations/RuleURL.swift; sourceTree = SOURCE_ROOT; }; 287E7D831D75F9C80065F4DE /* RuleEmail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleEmail.swift; path = Source/Validations/RuleEmail.swift; sourceTree = SOURCE_ROOT; }; 287E7D8F1D7601850065F4DE /* RuleLength.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleLength.swift; path = Source/Validations/RuleLength.swift; sourceTree = SOURCE_ROOT; }; 28B12FD41BA0E83C00F27A23 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Source/Assets.xcassets; sourceTree = SOURCE_ROOT; }; 28B1D7711C7F90C900605EB3 /* BaseEurekaTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = BaseEurekaTests.swift; path = Tests/BaseEurekaTests.swift; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 28B1D7721C7F90C900605EB3 /* CallbacksTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CallbacksTests.swift; path = Tests/CallbacksTests.swift; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 28B1D7731C7F90C900605EB3 /* CollectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CollectionTests.swift; path = Tests/CollectionTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D7741C7F90C900605EB3 /* DateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DateTests.swift; path = Tests/DateTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D7751C7F90C900605EB3 /* FormValuesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FormValuesTests.swift; path = Tests/FormValuesTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D7761C7F90C900605EB3 /* HelperMethodTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HelperMethodTests.swift; path = Tests/HelperMethodTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D7771C7F90C900605EB3 /* HiddenRowsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HiddenRowsTests.swift; path = Tests/HiddenRowsTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D7781C7F90C900605EB3 /* OperatorsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OperatorsTest.swift; path = Tests/OperatorsTest.swift; sourceTree = SOURCE_ROOT; }; 28B1D7791C7F90C900605EB3 /* RowByTagTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RowByTagTests.swift; path = Tests/RowByTagTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D77A1C7F90C900605EB3 /* RowCallbackTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RowCallbackTests.swift; path = Tests/RowCallbackTests.swift; sourceTree = SOURCE_ROOT; }; 28B1D77B1C7F90C900605EB3 /* SetValuesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SetValuesTests.swift; path = Tests/SetValuesTests.swift; sourceTree = SOURCE_ROOT; }; 28DB372F1CE4F864009E46BA /* Protocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Protocols.swift; path = Common/Protocols.swift; sourceTree = ""; }; 28EE0FDD1D5E889F00B91340 /* Validation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validation.swift; sourceTree = ""; }; 28EEBFE41C7E1B3B00300699 /* CellType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CellType.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 28EEBFEB1C7E224200300699 /* PresenterRowType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenterRowType.swift; sourceTree = ""; }; 28EEBFED1C7E231C00300699 /* RowControllerType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowControllerType.swift; sourceTree = ""; }; 28EEBFEF1C7E23F400300699 /* Row.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Row.swift; sourceTree = ""; }; 28EEBFF11C7E23FA00300699 /* RowType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowType.swift; sourceTree = ""; }; 28EEBFF31C7E240000300699 /* RowProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowProtocols.swift; sourceTree = ""; }; 28EEBFF51C7E240B00300699 /* InlineRowType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InlineRowType.swift; sourceTree = ""; }; 28EEBFF71C7E241200300699 /* SelectableRowType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableRowType.swift; sourceTree = ""; }; 28EEBFFA1C7E25C500300699 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; 28EEBFFC1C7E25F100300699 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Cell.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 397300091DF0E2A000080E4D /* PickerInputRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerInputRow.swift; sourceTree = ""; }; 49ADC7F81C8A83240073952B /* StepperRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperRow.swift; sourceTree = ""; }; 51729DEB1B9A4F5E004A00EB /* Eureka.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Eureka.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51729DF01B9A4F5E004A00EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51729DF51B9A4F5E004A00EB /* EurekaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EurekaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51729DFC1B9A4F5E004A00EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Tests/Info.plist; sourceTree = ""; }; 51729E661B9A5FA5004A00EB /* Eureka.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Eureka.h; path = Source/Eureka.h; sourceTree = SOURCE_ROOT; }; 798C5AD91EF1E35600A21F52 /* SwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActions.swift; sourceTree = ""; }; 8FCCF8EE20A24085004793A0 /* DoublePickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoublePickerRow.swift; sourceTree = ""; }; 8FCCF8F020A32613004793A0 /* TriplePickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriplePickerRow.swift; sourceTree = ""; }; 8FCCF92220A473E7004793A0 /* DoublePickerInputRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoublePickerInputRow.swift; sourceTree = ""; }; 8FCCF92420A4794B004793A0 /* TriplePickerInputRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriplePickerInputRow.swift; sourceTree = ""; }; 9EFC727B291D0369004840A9 /* ResultBuildersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultBuildersTests.swift; sourceTree = ""; }; 9EFC727D291D05EA004840A9 /* ResultBuilders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultBuilders.swift; sourceTree = ""; }; B244E6531FE1C94F0026B944 /* AlertOptionsRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AlertOptionsRow.swift; path = Common/AlertOptionsRow.swift; sourceTree = ""; }; B257FE2D1EC0F66900043911 /* RowsInsertionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RowsInsertionTests.swift; path = Tests/RowsInsertionTests.swift; sourceTree = SOURCE_ROOT; }; B2A401151EC0BA140042EDF0 /* SectionsInsertionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SectionsInsertionTests.swift; path = Tests/SectionsInsertionTests.swift; sourceTree = SOURCE_ROOT; }; E110A13F2BC9D08200FFD81D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FA56B6AE1DBAD38900407C94 /* RuleEqualsToRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuleEqualsToRow.swift; path = Source/Validations/RuleEqualsToRow.swift; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 51729DE71B9A4F5E004A00EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 51729DF21B9A4F5E004A00EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 51729DF61B9A4F5E004A00EB /* Eureka.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2859CD901C7CF1FD0002982F /* Core */ = { isa = PBXGroup; children = ( 2859CDE81C7DD78E0002982F /* BaseRow.swift */, 28EEBFFC1C7E25F100300699 /* Cell.swift */, 28EEBFE41C7E1B3B00300699 /* CellType.swift */, 2859CD921C7CF1FD0002982F /* Core.swift */, 2859CDBD1C7D137D0002982F /* Form.swift */, 2859CDC31C7D19C50002982F /* HeaderFooterView.swift */, 2859CD931C7CF1FD0002982F /* Helpers.swift */, 28EEBFF51C7E240B00300699 /* InlineRowType.swift */, 2859CDC51C7D1A2E0002982F /* NavigationAccessoryView.swift */, 28EEBFFA1C7E25C500300699 /* Operators.swift */, 28EEBFEB1C7E224200300699 /* PresenterRowType.swift */, 9EFC727D291D05EA004840A9 /* ResultBuilders.swift */, 28EEBFEF1C7E23F400300699 /* Row.swift */, 28EEBFED1C7E231C00300699 /* RowControllerType.swift */, 28EEBFF31C7E240000300699 /* RowProtocols.swift */, 28EEBFF11C7E23FA00300699 /* RowType.swift */, 28EEBFF71C7E241200300699 /* SelectableRowType.swift */, 2859CDBF1C7D138D0002982F /* Section.swift */, 2859CE231C7E141B0002982F /* SelectableSection.swift */, 798C5AD91EF1E35600A21F52 /* SwipeActions.swift */, 28EE0FDD1D5E889F00B91340 /* Validation.swift */, ); name = Core; path = Source/Core; sourceTree = SOURCE_ROOT; }; 2859CD971C7CF2020002982F /* Rows */ = { isa = PBXGroup; children = ( 2859CE1A1C7E04250002982F /* SelectableRows */, 2859CE0F1C7E01C70002982F /* Controllers */, 2859CDB21C7D0E210002982F /* Common */, 2859CDAE1C7D0D990002982F /* ActionSheetRow.swift */, 2859CDB01C7D0E010002982F /* AlertRow.swift */, 2859CDA21C7CF4050002982F /* ButtonRow.swift */, 2859CD991C7CF2020002982F /* ButtonRowWithPresent.swift */, 2859CDA41C7CF51A0002982F /* CheckRow.swift */, 2859CDFE1C7DF9A70002982F /* DateRow.swift */, 2859CDF41C7DF2740002982F /* DateInlineRow.swift */, 2859CDFC1C7DF9430002982F /* DatePickerRow.swift */, 8FCCF92220A473E7004793A0 /* DoublePickerInputRow.swift */, 8FCCF8EE20A24085004793A0 /* DoublePickerRow.swift */, 2859CE061C7DFBFA0002982F /* FieldsRow.swift */, 2859CDA01C7CF3A10002982F /* LabelRow.swift */, 2859CE1F1C7E092B0002982F /* MultipleSelectorRow.swift */, 2859CDB51C7D0EC60002982F /* PickerRow.swift */, 397300091DF0E2A000080E4D /* PickerInputRow.swift */, 2859CDB71C7D0FE70002982F /* PickerInlineRow.swift */, 2859CE1D1C7E08300002982F /* PopoverSelectorRow.swift */, 2859CDEC1C7DF0680002982F /* PushRow.swift */, 2859CDA81C7D0B3C0002982F /* SegmentedRow.swift */, 2859CD9A1C7CF2020002982F /* SliderRow.swift */, 49ADC7F81C8A83240073952B /* StepperRow.swift */, 2859CDA61C7CF5530002982F /* SwitchRow.swift */, 2859CDAC1C7D0BCA0002982F /* TextAreaRow.swift */, 8FCCF92420A4794B004793A0 /* TriplePickerInputRow.swift */, 8FCCF8F020A32613004793A0 /* TriplePickerRow.swift */, ); name = Rows; path = Source/Rows; sourceTree = SOURCE_ROOT; }; 2859CDB21C7D0E210002982F /* Common */ = { isa = PBXGroup; children = ( 2859CDF01C7DF1620002982F /* FieldRow.swift */, 2859CDB31C7D0E4B0002982F /* OptionsRow.swift */, B244E6531FE1C94F0026B944 /* AlertOptionsRow.swift */, 2859CDBB1C7D12930002982F /* SelectorRow.swift */, 2859CDEE1C7DF0F70002982F /* DateFieldRow.swift */, 2859CDF21C7DF22D0002982F /* DateInlineFieldRow.swift */, 2859CE0A1C7E003B0002982F /* DecimalFormatter.swift */, 2859CE211C7E13470002982F /* GenericMultipleSelectorRow.swift */, 28DB372F1CE4F864009E46BA /* Protocols.swift */, ); name = Common; sourceTree = ""; }; 2859CE0F1C7E01C70002982F /* Controllers */ = { isa = PBXGroup; children = ( 2859CE101C7E01EE0002982F /* SelectorViewController.swift */, 2859CE141C7E02710002982F /* SelectorAlertController.swift */, 2859CE121C7E02310002982F /* MultipleSelectorViewController.swift */, ); name = Controllers; sourceTree = ""; }; 2859CE1A1C7E04250002982F /* SelectableRows */ = { isa = PBXGroup; children = ( 2859CE1B1C7E04630002982F /* ListCheckRow.swift */, ); name = SelectableRows; sourceTree = ""; }; 287E7D7C1D62552C0065F4DE /* Validations */ = { isa = PBXGroup; children = ( 287E7D7D1D625A260065F4DE /* RuleRequired.swift */, 287E7D7F1D74E42D0065F4DE /* RuleRegExp.swift */, 287E7D831D75F9C80065F4DE /* RuleEmail.swift */, 287E7D811D74E64C0065F4DE /* RuleURL.swift */, 280FBE5C1D87360900900064 /* RuleRange.swift */, 287E7D8F1D7601850065F4DE /* RuleLength.swift */, 280FBE5E1D873CCE00900064 /* RuleClosure.swift */, FA56B6AE1DBAD38900407C94 /* RuleEqualsToRow.swift */, ); path = Validations; sourceTree = ""; }; 51729DE11B9A4F5E004A00EB = { isa = PBXGroup; children = ( 51729DED1B9A4F5E004A00EB /* Source */, 51729DF91B9A4F5E004A00EB /* Tests */, 51729DEC1B9A4F5E004A00EB /* Products */, ); sourceTree = ""; }; 51729DEC1B9A4F5E004A00EB /* Products */ = { isa = PBXGroup; children = ( 51729DEB1B9A4F5E004A00EB /* Eureka.framework */, 51729DF51B9A4F5E004A00EB /* EurekaTests.xctest */, ); name = Products; sourceTree = ""; }; 51729DED1B9A4F5E004A00EB /* Source */ = { isa = PBXGroup; children = ( 2859CD901C7CF1FD0002982F /* Core */, 2859CD971C7CF2020002982F /* Rows */, 287E7D7C1D62552C0065F4DE /* Validations */, 51729E661B9A5FA5004A00EB /* Eureka.h */, E110A13F2BC9D08200FFD81D /* PrivacyInfo.xcprivacy */, 51729DF01B9A4F5E004A00EB /* Info.plist */, 28B12FD41BA0E83C00F27A23 /* Assets.xcassets */, ); path = Source; sourceTree = ""; }; 51729DF91B9A4F5E004A00EB /* Tests */ = { isa = PBXGroup; children = ( 287A142D1D8A07B700FFE6EB /* SelectableSectionTests.swift */, 28B1D7711C7F90C900605EB3 /* BaseEurekaTests.swift */, 28B1D7721C7F90C900605EB3 /* CallbacksTests.swift */, 28B1D7731C7F90C900605EB3 /* CollectionTests.swift */, 28B1D7741C7F90C900605EB3 /* DateTests.swift */, 28B1D7751C7F90C900605EB3 /* FormValuesTests.swift */, 28B1D7761C7F90C900605EB3 /* HelperMethodTests.swift */, 28B1D7771C7F90C900605EB3 /* HiddenRowsTests.swift */, 28B1D7781C7F90C900605EB3 /* OperatorsTest.swift */, 9EFC727B291D0369004840A9 /* ResultBuildersTests.swift */, 28B1D7791C7F90C900605EB3 /* RowByTagTests.swift */, 28B1D77A1C7F90C900605EB3 /* RowCallbackTests.swift */, 28B1D77B1C7F90C900605EB3 /* SetValuesTests.swift */, 287A142A1D89DF1E00FFE6EB /* ValidationsTests.swift */, 285BA4481E81864E0034EE92 /* MultivaluedSectionTests.swift */, 51729DFC1B9A4F5E004A00EB /* Info.plist */, B2A401151EC0BA140042EDF0 /* SectionsInsertionTests.swift */, B257FE2D1EC0F66900043911 /* RowsInsertionTests.swift */, ); path = Tests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 51729DE81B9A4F5E004A00EB /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 51729E671B9A5FA5004A00EB /* Eureka.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 51729DEA1B9A4F5E004A00EB /* Eureka */ = { isa = PBXNativeTarget; buildConfigurationList = 51729DFF1B9A4F5E004A00EB /* Build configuration list for PBXNativeTarget "Eureka" */; buildPhases = ( 51729DE61B9A4F5E004A00EB /* Sources */, 51729DE71B9A4F5E004A00EB /* Frameworks */, 51729DE81B9A4F5E004A00EB /* Headers */, 51729DE91B9A4F5E004A00EB /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Eureka; productName = Eureka; productReference = 51729DEB1B9A4F5E004A00EB /* Eureka.framework */; productType = "com.apple.product-type.framework"; }; 51729DF41B9A4F5E004A00EB /* EurekaTests */ = { isa = PBXNativeTarget; buildConfigurationList = 51729E021B9A4F5E004A00EB /* Build configuration list for PBXNativeTarget "EurekaTests" */; buildPhases = ( 51729DF11B9A4F5E004A00EB /* Sources */, 51729DF21B9A4F5E004A00EB /* Frameworks */, 51729DF31B9A4F5E004A00EB /* Resources */, ); buildRules = ( ); dependencies = ( 51729DF81B9A4F5E004A00EB /* PBXTargetDependency */, ); name = EurekaTests; productName = EurekaTests; productReference = 51729DF51B9A4F5E004A00EB /* EurekaTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 51729DE21B9A4F5E004A00EB /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; LastUpgradeCheck = 1400; ORGANIZATIONNAME = Xmartlabs; TargetAttributes = { 51729DEA1B9A4F5E004A00EB = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; 51729DF41B9A4F5E004A00EB = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = 51729DE51B9A4F5E004A00EB /* Build configuration list for PBXProject "Eureka" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 51729DE11B9A4F5E004A00EB; productRefGroup = 51729DEC1B9A4F5E004A00EB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 51729DEA1B9A4F5E004A00EB /* Eureka */, 51729DF41B9A4F5E004A00EB /* EurekaTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 51729DE91B9A4F5E004A00EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( E110A1402BC9D08200FFD81D /* PrivacyInfo.xcprivacy in Resources */, 28B12FD51BA0E83C00F27A23 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 51729DF31B9A4F5E004A00EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 51729DE61B9A4F5E004A00EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 28EEC0251C7E3B2200300699 /* PickerInlineRow.swift in Sources */, 28EEC0331C7E64AB00300699 /* PopoverSelectorRow.swift in Sources */, 28EEBFF81C7E241200300699 /* SelectableRowType.swift in Sources */, 28EEC0201C7E3AD200300699 /* SegmentedRow.swift in Sources */, 28EEC01F1C7E3AC800300699 /* LabelRow.swift in Sources */, 287E7D7E1D625A260065F4DE /* RuleRequired.swift in Sources */, 28EEC0191C7E3A3D00300699 /* SelectorAlertController.swift in Sources */, 28EEBFEE1C7E231C00300699 /* RowControllerType.swift in Sources */, 28EE46A91C7F685900EFF4A2 /* PushRow.swift in Sources */, 28EEC00F1C7E398D00300699 /* FieldRow.swift in Sources */, 28EEC02F1C7E49BF00300699 /* SelectorViewController.swift in Sources */, 28EEC0031C7E390300300699 /* DecimalFormatter.swift in Sources */, 28EE0FDE1D5E889F00B91340 /* Validation.swift in Sources */, 2859CD951C7CF1FD0002982F /* Core.swift in Sources */, 2859CDBE1C7D137D0002982F /* Form.swift in Sources */, 280FBE5F1D873CCE00900064 /* RuleClosure.swift in Sources */, 28EEC0001C7E36EB00300699 /* SwitchRow.swift in Sources */, 3973000A1DF0E2A000080E4D /* PickerInputRow.swift in Sources */, 287E7D901D7601850065F4DE /* RuleLength.swift in Sources */, 8FCCF8EF20A24085004793A0 /* DoublePickerRow.swift in Sources */, 28EEC02A1C7E3B5A00300699 /* MultipleSelectorRow.swift in Sources */, 28EEC0231C7E3B1900300699 /* AlertRow.swift in Sources */, 28EEC0221C7E3B1300300699 /* ActionSheetRow.swift in Sources */, 28EEBFEC1C7E224200300699 /* PresenterRowType.swift in Sources */, 28EEC0211C7E3AD700300699 /* TextAreaRow.swift in Sources */, 28EEBFFD1C7E25F100300699 /* Cell.swift in Sources */, 28EEC0321C7E64A400300699 /* DatePickerRow.swift in Sources */, FA56B6AF1DBAD38900407C94 /* RuleEqualsToRow.swift in Sources */, 28EEBFE81C7E1D7E00300699 /* CellType.swift in Sources */, 8FCCF92320A473E7004793A0 /* DoublePickerInputRow.swift in Sources */, 28EEC0241C7E3B1D00300699 /* PickerRow.swift in Sources */, 28EEC0281C7E3B4B00300699 /* FieldsRow.swift in Sources */, 2859CD961C7CF1FD0002982F /* Helpers.swift in Sources */, 287E7D821D74E64C0065F4DE /* RuleURL.swift in Sources */, 28EEBFF21C7E23FA00300699 /* RowType.swift in Sources */, 9EFC727F291D05FB004840A9 /* ResultBuilders.swift in Sources */, 287E7D801D74E42D0065F4DE /* RuleRegExp.swift in Sources */, 28EEC0101C7E399500300699 /* DateInlineFieldRow.swift in Sources */, 2859CDC61C7D1A2E0002982F /* NavigationAccessoryView.swift in Sources */, 49ADC7F91C8A83240073952B /* StepperRow.swift in Sources */, B244E6541FE1C94F0026B944 /* AlertOptionsRow.swift in Sources */, 28EE46AC1C7F712300EFF4A2 /* DateInlineRow.swift in Sources */, 28EEC01C1C7E3A7400300699 /* ButtonRowWithPresent.swift in Sources */, 798C5ADA1EF1E35600A21F52 /* SwipeActions.swift in Sources */, 28EEBFF41C7E240000300699 /* RowProtocols.swift in Sources */, 28EEBFFE1C7E281F00300699 /* CheckRow.swift in Sources */, 2859CDC41C7D19C50002982F /* HeaderFooterView.swift in Sources */, 28EEC00C1C7E396000300699 /* SelectorRow.swift in Sources */, 280FBE5D1D87360900900064 /* RuleRange.swift in Sources */, 28EEC0161C7E3A3200300699 /* ListCheckRow.swift in Sources */, 28EEC01D1C7E3A8600300699 /* SliderRow.swift in Sources */, 28EEC00B1C7E395700300699 /* OptionsRow.swift in Sources */, 28EEC0021C7E37BD00300699 /* ButtonRow.swift in Sources */, 28EE46A81C7F5D5200EFF4A2 /* SelectableSection.swift in Sources */, 28EEBFF61C7E240B00300699 /* InlineRowType.swift in Sources */, 287E7D841D75F9C80065F4DE /* RuleEmail.swift in Sources */, 8FCCF8F120A32613004793A0 /* TriplePickerRow.swift in Sources */, 28EEBFFB1C7E25C500300699 /* Operators.swift in Sources */, 28DB37301CE4F864009E46BA /* Protocols.swift in Sources */, 2859CDC01C7D138D0002982F /* Section.swift in Sources */, 28EEC0151C7E39C100300699 /* DateRow.swift in Sources */, 2859CDE91C7DD78E0002982F /* BaseRow.swift in Sources */, 8FCCF92520A4794B004793A0 /* TriplePickerInputRow.swift in Sources */, 28EEBFF01C7E23F400300699 /* Row.swift in Sources */, 28EEC0121C7E399500300699 /* GenericMultipleSelectorRow.swift in Sources */, 28EEC0181C7E3A3D00300699 /* MultipleSelectorViewController.swift in Sources */, 28EEC00E1C7E398A00300699 /* DateFieldRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 51729DF11B9A4F5E004A00EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 28B1D7881C7F911900605EB3 /* CallbacksTests.swift in Sources */, 28B1D7891C7F911900605EB3 /* CollectionTests.swift in Sources */, 28B1D78C1C7F911900605EB3 /* HelperMethodTests.swift in Sources */, 28B1D78B1C7F911900605EB3 /* FormValuesTests.swift in Sources */, B2A401161EC0BA140042EDF0 /* SectionsInsertionTests.swift in Sources */, 9EFC727C291D0369004840A9 /* ResultBuildersTests.swift in Sources */, 28B1D7871C7F911900605EB3 /* BaseEurekaTests.swift in Sources */, 28B1D7911C7F911900605EB3 /* SetValuesTests.swift in Sources */, 285BA44A1E8187480034EE92 /* MultivaluedSectionTests.swift in Sources */, 28B1D7901C7F911900605EB3 /* RowCallbackTests.swift in Sources */, 28B1D78E1C7F911900605EB3 /* OperatorsTest.swift in Sources */, 28B1D78D1C7F911900605EB3 /* HiddenRowsTests.swift in Sources */, 287A14331D8A07ED00FFE6EB /* SelectableSectionTests.swift in Sources */, B257FE2E1EC0F66900043911 /* RowsInsertionTests.swift in Sources */, 28B1D78A1C7F911900605EB3 /* DateTests.swift in Sources */, 287A142B1D89DF1E00FFE6EB /* ValidationsTests.swift in Sources */, 28B1D78F1C7F911900605EB3 /* RowByTagTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 51729DF81B9A4F5E004A00EB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 51729DEA1B9A4F5E004A00EB /* Eureka */; targetProxy = 51729DF71B9A4F5E004A00EB /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 51729DFD1B9A4F5E004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; 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_MISSING_NEWLINE = 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 = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 51729DFE1B9A4F5E004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = 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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_SWIFT3_OBJC_INFERENCE = Off; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 51729E001B9A4F5E004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 5.5.0; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.Eureka; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 51729E011B9A4F5E004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = YES; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MARKETING_VERSION = 5.5.0; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.Eureka; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 5.0; }; name = Release; }; 51729E031B9A4F5E004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.EurekaTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Debug; }; 51729E041B9A4F5E004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.EurekaTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 51729DE51B9A4F5E004A00EB /* Build configuration list for PBXProject "Eureka" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729DFD1B9A4F5E004A00EB /* Debug */, 51729DFE1B9A4F5E004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 51729DFF1B9A4F5E004A00EB /* Build configuration list for PBXNativeTarget "Eureka" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729E001B9A4F5E004A00EB /* Debug */, 51729E011B9A4F5E004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 51729E021B9A4F5E004A00EB /* Build configuration list for PBXNativeTarget "EurekaTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729E031B9A4F5E004A00EB /* Debug */, 51729E041B9A4F5E004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 51729DE21B9A4F5E004A00EB /* Project object */; } ================================================ FILE: Eureka.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Eureka.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Eureka.xcodeproj/xcshareddata/xcschemes/Eureka.xcscheme ================================================ ================================================ FILE: Eureka.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Eureka.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Example/EmojiCell.xib ================================================ ================================================ FILE: Example/Example/AppDelegate.swift ================================================ // AppDelegate.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { // Take a look at Main.storyboard return true } } ================================================ FILE: Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "appIcon20pt@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "appIcon20pt@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "appIcon29pt@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "appIcon29pt@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "appIcon29pt@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "appIcon40pt@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "appIcon40pt@3x.png", "scale" : "3x" }, { "idiom" : "iphone", "size" : "57x57", "scale" : "1x" }, { "idiom" : "iphone", "size" : "57x57", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "appIcon60pt@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "appIcon60pt@3x.png", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "50x50", "scale" : "1x" }, { "idiom" : "ipad", "size" : "50x50", "scale" : "2x" }, { "idiom" : "ipad", "size" : "72x72", "scale" : "1x" }, { "idiom" : "ipad", "size" : "72x72", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "appIcon76pt@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "appIcon76pt@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "appIcon83-5pt@2x.png", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/Eureka.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "Eureka@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/checkedDay.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "checkedDay.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/map_pin.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "map_pin@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "map_pin@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/plus_image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "plus_image@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "plus_image@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/selected.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "selectedCircle@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "selectedCircle@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/selectedRectangle.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "selectedRectangle@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "selectedRectangle@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/uncheckedDay.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "uncheckedDay.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/unselected.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "unselectedCircle@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "unselectedCircle@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Assets.xcassets/unselectedRectangle.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "filename" : "unselectedRectangle@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "unselectedRectangle@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Base.lproj/EurekaSectionHeader.xib ================================================ ================================================ FILE: Example/Example/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Example/Example/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/Example/Base.lproj/WeekDaysCell.xib ================================================ ================================================ FILE: Example/Example/Controllers/CustomCellsViewController.swift ================================================ // // CustomCellsViewController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka //MARK: Custom Cells Example class CustomCellsController : FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section() { var header = HeaderFooterView(.nibFile(name: "EurekaSectionHeader", bundle: nil)) header.onSetupView = { (view, section) -> () in view.imageView.alpha = 0; UIView.animate(withDuration: 2.0, animations: { [weak view] in view?.imageView.alpha = 1 }) view.layer.transform = CATransform3DMakeScale(0.9, 0.9, 1) UIView.animate(withDuration: 1.0, animations: { [weak view] in view?.layer.transform = CATransform3DIdentity }) } $0.header = header } +++ Section("WeekDay cell") <<< WeekDayRow(){ $0.value = [.monday, .wednesday, .friday] } <<< TextFloatLabelRow() { $0.title = "Float Label Row, type something to see.." } <<< IntFloatLabelRow() { $0.title = "Float Label Row, type something to see.." } } } class EurekaLogoViewNib: UIView { @IBOutlet weak var imageView: UIImageView! required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } } ================================================ FILE: Example/Example/Controllers/CustomDesignController.swift ================================================ // // CustomDesignController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class CustomDesignController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section() <<< SwitchRow() { $0.cellProvider = CellProvider(nibName: "SwitchCell", bundle: Bundle.main) }.cellSetup { (cell, row) in cell.height = { 67 } } <<< DatePickerRow() { $0.cellProvider = CellProvider(nibName: "DatePickerCell", bundle: Bundle.main) }.cellSetup { (cell, row) in cell.height = { 345 } } <<< TextRow() { $0.cellProvider = CellProvider(nibName: "TextCell", bundle: Bundle.main) }.cellSetup { (cell, row) in cell.height = { 199 } } .onChange { row in if let textView = row.cell.viewWithTag(99) as? UITextView { textView.text = row.cell.textField.text } } } } ================================================ FILE: Example/Example/Controllers/DisabledRowsExample.swift ================================================ // // DisabledRowsExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class DisabledRowsExample : FormViewController { override func viewDidLoad() { super.viewDidLoad() form = Section() <<< SegmentedRow("segments"){ $0.options = ["Enabled", "Disabled"] $0.value = "Disabled" } <<< TextRow(){ $0.title = "choose enabled, disable above..." $0.disabled = "$segments = 'Disabled'" } <<< SwitchRow("Disable Next Section?"){ $0.title = $0.tag $0.disabled = "$segments = 'Disabled'" } +++ Section() <<< TextRow() { $0.title = "Gonna be disabled soon.." $0.disabled = Eureka.Condition.function(["Disable Next Section?"], { (form) -> Bool in let row: SwitchRow! = form.rowBy(tag: "Disable Next Section?") return row.value ?? false }) } +++ Section() <<< SegmentedRow(){ $0.options = ["Always Disabled"] $0.disabled = true } } } ================================================ FILE: Example/Example/Controllers/FieldRowCustomizationController.swift ================================================ // // FieldRowCustomizationController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class FieldRowCustomizationController : FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section(header: "Default field rows", footer: "Rows with title have a right-aligned text field.\nRows without title have a left-aligned text field.\nBut this can be changed...") <<< NameRow() { $0.title = "Your name:" $0.placeholder = "(right alignment)" } .cellSetup { cell, row in cell.imageView?.image = UIImage(named: "plus_image") } <<< NameRow() { $0.placeholder = "Name (left alignment)" } .cellSetup { cell, row in cell.imageView?.image = UIImage(named: "plus_image") } +++ Section("Customized Alignment") <<< NameRow() { $0.title = "Your name:" }.cellUpdate { cell, row in cell.textField.textAlignment = .left cell.textField.placeholder = "(left alignment)" } <<< NameRow().cellUpdate { cell, row in cell.textField.textAlignment = .right cell.textField.placeholder = "Name (right alignment)" } +++ Section(header: "Customized Text field width", footer: "Eureka allows us to set up a specific UITextField width using textFieldPercentage property. In the section above we have also right aligned the textLabels.") <<< NameRow() { $0.title = "Title" $0.titlePercentage = 0.4 $0.placeholder = "textFieldPercentage = 0.6" } .cellUpdate { $1.cell.textField.textAlignment = .left $1.cell.textLabel?.textAlignment = .right } <<< NameRow() { $0.title = "Another Title" $0.titlePercentage = 0.4 $0.placeholder = "textFieldPercentage = 0.6" } .cellUpdate { $1.cell.textField.textAlignment = .left $1.cell.textLabel?.textAlignment = .right } <<< NameRow() { $0.title = "One more" $0.titlePercentage = 0.3 $0.placeholder = "textFieldPercentage = 0.7" } .cellUpdate { $1.cell.textField.textAlignment = .left $1.cell.textLabel?.textAlignment = .right } +++ Section("TextAreaRow") <<< TextAreaRow() { $0.placeholder = "TextAreaRow" $0.textAreaHeight = .dynamic(initialTextViewHeight: 110) } <<< TextAreaRow() { $0.value = "You also have scrollable read only textAreaRows! I have to write a big text so you will be able to scroll a lot and see that this row is scrollable. I think it is a good idea to insert a Lorem Ipsum here: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ac odio consectetur, faucibus elit at, congue dolor. Duis quis magna eu ante egestas laoreet. Vivamus ultricies tristique porttitor. Proin viverra sem non turpis molestie, volutpat facilisis justo rutrum. Nulla eget commodo ligula. Aliquam lobortis lobortis justo id fermentum. Sed sit amet elit eu ipsum ultricies porttitor et sed justo. Fusce id mi aliquam, iaculis odio ac, tempus sem. Aenean in eros imperdiet, euismod lacus vitae, mattis nulla. Praesent ornare sem vitae ornare efficitur. Nullam dictum tortor a tortor vestibulum pharetra. Donec sollicitudin varius fringilla. Praesent posuere fringilla tristique. Aliquam dapibus vel nisi in sollicitudin. In eu ligula arcu." $0.textAreaMode = .readOnly $0.textAreaHeight = .fixed(cellHeight: 110) } } } ================================================ FILE: Example/Example/Controllers/FormatterExample.swift ================================================ // // FormatterExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class FormatterExample : FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section("Number formatters") <<< DecimalRow(){ $0.useFormatterDuringInput = true $0.title = "Currency style" $0.value = 2015 let formatter = CurrencyFormatter() formatter.locale = .current formatter.numberStyle = .currency $0.formatter = formatter } <<< DecimalRow(){ $0.title = "Scientific style" $0.value = 2015 let formatter = NumberFormatter() formatter.locale = .current formatter.numberStyle = .scientific $0.formatter = formatter } <<< IntRow(){ $0.title = "Spell out style" $0.value = 2015 let formatter = NumberFormatter() formatter.locale = .current formatter.numberStyle = .spellOut $0.formatter = formatter } +++ Section("Date formatters") <<< DateRow(){ $0.title = "Short style" $0.value = Date() let formatter = DateFormatter() formatter.locale = .current formatter.dateStyle = .short $0.dateFormatter = formatter } <<< DateRow(){ $0.title = "Long style" $0.value = Date() let formatter = DateFormatter() formatter.locale = .current formatter.dateStyle = .long $0.dateFormatter = formatter } +++ Section("Other formatters") <<< DecimalRow(){ $0.title = "Energy: Jules to calories" $0.value = 100.0 let formatter = EnergyFormatter() $0.formatter = formatter } <<< IntRow(){ $0.title = "Weight: Kg to lb" $0.value = 1000 $0.formatter = MassFormatter() } } class CurrencyFormatter : NumberFormatter, FormatterProtocol { override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, range rangep: UnsafeMutablePointer?) throws { guard obj != nil else { return } var str = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "") if !string.isEmpty, numberStyle == .currency && !string.contains(currencySymbol) { // Check if the currency symbol is at the last index if let formattedNumber = self.string(from: 1), String(formattedNumber[formattedNumber.index(before: formattedNumber.endIndex)...]) == currencySymbol { // This means the user has deleted the currency symbol. We cut the last number and then add the symbol automatically str = String(str[.. UITextPosition { return textInput.position(from: position, offset:((newValue?.count ?? 0) - (oldValue?.count ?? 0))) ?? position } } } ================================================ FILE: Example/Example/Controllers/HiddenRowsExample.swift ================================================ // // HiddenRowsExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class HiddenRowsExample : FormViewController { override func viewDidLoad() { super.viewDidLoad() TextRow.defaultCellUpdate = { cell, row in cell.textLabel?.font = UIFont.italicSystemFont(ofSize: 12) } form = Section("What do you want to talk about:") <<< SegmentedRow("segments"){ $0.options = ["Sport", "Music", "Films"] $0.value = "Films" } +++ Section(){ $0.tag = "sport_s" $0.hidden = "$segments != 'Sport'" // .Predicate(NSPredicate(format: "$segments != 'Sport'")) } <<< TextRow(){ $0.title = "Which is your favourite soccer player?" } <<< TextRow(){ $0.title = "Which is your favourite coach?" } <<< TextRow(){ $0.title = "Which is your favourite team?" } +++ Section(){ $0.tag = "music_s" $0.hidden = "$segments != 'Music'" } <<< TextRow(){ $0.title = "Which music style do you like most?" } <<< TextRow(){ $0.title = "Which is your favourite singer?" } <<< TextRow(){ $0.title = "How many CDs have you got?" } +++ Section(){ $0.tag = "films_s" $0.hidden = "$segments != 'Films'" } <<< TextRow(){ $0.title = "Which is your favourite actor?" } <<< TextRow(){ $0.title = "Which is your favourite film?" } +++ Section() <<< SwitchRow("Show Next Row"){ $0.title = $0.tag } <<< SwitchRow("Show Next Section"){ $0.title = $0.tag $0.hidden = .function(["Show Next Row"], { form -> Bool in let row: RowOf! = form.rowBy(tag: "Show Next Row") return row.value ?? false == false }) } +++ Section(footer: "This section is shown only when 'Show Next Row' switch is enabled"){ $0.hidden = .function(["Show Next Section"], { form -> Bool in let row: RowOf! = form.rowBy(tag: "Show Next Section") return row.value ?? false == false }) } <<< TextRow() { $0.placeholder = "Gonna dissapear soon!!" } } } ================================================ FILE: Example/Example/Controllers/InlineRowsExample.swift ================================================ // // InlineRowsExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class InlineRowsController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form.inlineRowHideOptions = InlineRowHideOptions.AnotherInlineRowIsShown.union(.FirstResponderChanges) form +++ Section("Automatically Hide Inline Rows?") <<< SwitchRow() { $0.title = "Hides when another inline row is shown" $0.value = true } .onChange { [weak form] in if $0.value == true { form?.inlineRowHideOptions = form?.inlineRowHideOptions?.union(.AnotherInlineRowIsShown) } else { form?.inlineRowHideOptions = form?.inlineRowHideOptions?.subtracting(.AnotherInlineRowIsShown) } } <<< SwitchRow() { $0.title = "Hides when the First Responder changes" $0.value = true } .onChange { [weak form] in if $0.value == true { form?.inlineRowHideOptions = form?.inlineRowHideOptions?.union(.FirstResponderChanges) } else { form?.inlineRowHideOptions = form?.inlineRowHideOptions?.subtracting(.FirstResponderChanges) } } +++ Section() <<< DateInlineRow() { $0.title = "DateInlineRow" $0.value = Date() } <<< TimeInlineRow(){ $0.title = "TimeInlineRow" $0.value = Date() } <<< DateTimeInlineRow(){ $0.title = "DateTimeInlineRow" $0.value = Date() } <<< CountDownInlineRow(){ $0.title = "CountDownInlineRow" var dateComp = DateComponents() dateComp.hour = 18 dateComp.minute = 33 dateComp.timeZone = TimeZone.current $0.value = Calendar.current.date(from: dateComp) } +++ Section("Generic inline picker") <<< PickerInlineRow("PickerInlineRow") { (row : PickerInlineRow) -> Void in row.title = row.tag row.displayValueFor = { (rowValue: Date?) in return rowValue.map { "Year \(Calendar.current.component(.year, from: $0))" } } row.options = [] var date = Date() for _ in 1...10{ row.options.append(date) date = date.addingTimeInterval(60*60*24*365) } row.value = row.options[0] } } } ================================================ FILE: Example/Example/Controllers/ListSectionsController.swift ================================================ // // ListSectionsController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class ListSectionsController: FormViewController { override func viewDidLoad() { super.viewDidLoad() let continents = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] form +++ SelectableSection>() { section in section.header = HeaderFooterView(title: "Where do you live?") } for option in continents { form.last! <<< ImageCheckRow(option){ lrow in lrow.title = option lrow.selectableValue = option lrow.value = nil } } let oceans = ["Arctic", "Atlantic", "Indian", "Pacific", "Southern"] form +++ SelectableSection>("And which of the following oceans have you taken a bath in?", selectionType: .multipleSelection) for option in oceans { form.last! <<< ImageCheckRow(option){ lrow in lrow.title = option lrow.selectableValue = option lrow.value = nil }.cellSetup { cell, _ in cell.trueImage = UIImage(named: "selectedRectangle")! cell.falseImage = UIImage(named: "unselectedRectangle")! cell.accessoryType = .checkmark } } } override func valueHasBeenChanged(for row: BaseRow, oldValue: Any?, newValue: Any?) { if row.section === form[0] { print("Single Selection:\((row.section as! SelectableSection>).selectedRow()?.baseValue ?? "No row selected")") } else if row.section === form[1] { print("Mutiple Selection:\((row.section as! SelectableSection>).selectedRows().map({$0.baseValue}))") } } } ================================================ FILE: Example/Example/Controllers/MultivaluedExamples.swift ================================================ // // MultivaluedSectionsController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class MultivaluedSectionsController: FormViewController { override func viewDidLoad() { super.viewDidLoad() title = "Multivalued examples" form +++ Section("Multivalued examples") <<< ButtonRow(){ $0.title = "Multivalued Sections" $0.presentationMode = .segueName(segueName: "MultivaluedControllerSegue", onDismiss: nil) } <<< ButtonRow(){ $0.title = "Multivalued Only Reorder" $0.presentationMode = .segueName(segueName: "MultivaluedOnlyReorderControllerSegue", onDismiss: nil) } <<< ButtonRow(){ $0.title = "Multivalued Only Insert" $0.presentationMode = .segueName(segueName: "MultivaluedOnlyInsertControllerSegue", onDismiss: nil) } <<< ButtonRow(){ $0.title = "Multivalued Only Delete" $0.presentationMode = .segueName(segueName: "MultivaluedOnlyDeleteControllerSegue", onDismiss: nil) } } } class MultivaluedController: FormViewController { override func viewDidLoad() { super.viewDidLoad() title = "Multivalued Examples" form +++ MultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], header: "Multivalued TextField", footer: ".Insert multivaluedOption adds the 'Add New Tag' button row as last cell.") { $0.tag = "textfields" $0.addButtonProvider = { section in return ButtonRow(){ $0.title = "Add New Tag" }.cellUpdate { cell, row in cell.textLabel?.textAlignment = .left } } $0.multivaluedRowToInsertAt = { index in return NameRow() { $0.placeholder = "Tag Name" } } $0 <<< NameRow() { $0.placeholder = "Tag Name" } } +++ MultivaluedSection(multivaluedOptions: [.Insert, .Delete], header: "Multivalued ActionSheet Selector example", footer: ".Insert multivaluedOption adds a 'Add' button row as last cell.") { $0.tag = "options" $0.multivaluedRowToInsertAt = { index in return ActionSheetRow{ $0.title = "Tap to select.." $0.options = ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"] } } $0 <<< ActionSheetRow { $0.title = "Tap to select.." $0.options = ["Option 1", "Option 2", "Option 3", "Option 4", "Option 5"] } } +++ MultivaluedSection(multivaluedOptions: [.Insert, .Delete, .Reorder], header: "Multivalued Push Selector example") { $0.tag = "push" $0.multivaluedRowToInsertAt = { index in return PushRow{ $0.title = "Tap to select ;)..at \(index)" $0.options = ["Option 1", "Option 2", "Option 3"] } } $0 <<< PushRow { $0.title = "Tap to select ;).." $0.options = ["Option 1", "Option 2", "Option 3"] } } } @IBAction func save(_ sender: Any) { print("\(form.values())") } } class MultivaluedOnlyReorderController: FormViewController { override func viewDidLoad() { super.viewDidLoad() let secondsPerDay = 24 * 60 * 60 let list = ["Today", "Yesterday", "Before Yesterday"] form +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "Reordering Selectors") { $0 <<< PushRow { $0.title = "Tap to select ;).." $0.options = ["Option 1", "Option 2", "Option 3"] } <<< PushRow { $0.title = "Tap to select ;).." $0.options = ["Option 1", "Option 2", "Option 3"] } <<< PushRow { $0.title = "Tap to select ;).." $0.options = ["Option 1", "Option 2", "Option 3"] } <<< PushRow { $0.title = "Tap to select ;).." $0.options = ["Option 1", "Option 2", "Option 3"] } } +++ // Multivalued Section with inline rows - section set up to support only reordering MultivaluedSection(multivaluedOptions: .Reorder, header: "Reordering Inline Rows") { section in list.enumerated().forEach({ offset, string in let dateInlineRow = DateInlineRow(){ $0.value = Date(timeInterval: Double(-secondsPerDay) * Double(offset), since: Date()) $0.title = string } section <<< dateInlineRow }) } +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "Reordering Field Rows") <<< NameRow { $0.value = "Martin" } <<< NameRow { $0.value = "Mathias" } <<< NameRow { $0.value = "Agustin" } <<< NameRow { $0.value = "Enrique" } } } class MultivaluedOnlyInsertController: FormViewController { override func viewDidLoad() { super.viewDidLoad() title = "Multivalued Only Insert" form +++ MultivaluedSection(multivaluedOptions: .Insert) { sec in sec.addButtonProvider = { _ in return ButtonRow { $0.title = "Add Tag" }.cellUpdate { cell, row in cell.textLabel?.textAlignment = .left } } sec.multivaluedRowToInsertAt = { index in return TextRow { $0.placeholder = "Tag Name" } } sec.showInsertIconInAddButton = false } +++ MultivaluedSection(multivaluedOptions: .Insert, header: "Insert With Inline Cells") { $0.multivaluedRowToInsertAt = { index in return DateInlineRow { $0.title = "Date" $0.value = Date() } } } } } class MultivaluedOnlyDeleteController: FormViewController { @IBOutlet weak var editButton: UIBarButtonItem! override func viewDidLoad() { super.viewDidLoad() tableView.isEditing = false let nameList = ["family", "male", "female", "client"] let section = MultivaluedSection(multivaluedOptions: .Delete) for tag in nameList { section <<< TextRow { $0.placeholder = "Tag Name" $0.value = tag } } let section2 = MultivaluedSection(multivaluedOptions: .Delete, footer: "") for _ in 1..<4 { section2 <<< PickerInlineRow { $0.title = "Tap to select" $0.value = "client" $0.options = nameList } } editButton.title = tableView.isEditing ? "Done" : "Edit" editButton.target = self editButton.action = #selector(editPressed(sender:)) form +++ section +++ section2 } @objc func editPressed(sender: UIBarButtonItem){ tableView.setEditing(!tableView.isEditing, animated: true) editButton.title = tableView.isEditing ? "Done" : "Edit" } } ================================================ FILE: Example/Example/Controllers/NativeEventExample.swift ================================================ // // NativeEventExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class NativeEventNavigationController: UINavigationController, RowControllerType { var onDismissCallback : ((UIViewController) -> ())? } class NativeEventFormViewController : FormViewController { override func viewDidLoad() { super.viewDidLoad() initializeForm() navigationItem.leftBarButtonItem?.target = self navigationItem.leftBarButtonItem?.action = #selector(NativeEventFormViewController.cancelTapped(_:)) } private func initializeForm() { form +++ TextRow("Title").cellSetup { cell, row in cell.textField.placeholder = row.tag } <<< TextRow("Location").cellSetup { $1.cell.textField.placeholder = $0.row.tag } +++ SwitchRow("All-day") { $0.title = $0.tag }.onChange { [weak self] row in let startDate: DateTimeInlineRow! = self?.form.rowBy(tag: "Starts") let endDate: DateTimeInlineRow! = self?.form.rowBy(tag: "Ends") if row.value ?? false { startDate.dateFormatter?.dateStyle = .medium startDate.dateFormatter?.timeStyle = .none endDate.dateFormatter?.dateStyle = .medium endDate.dateFormatter?.timeStyle = .none } else { startDate.dateFormatter?.dateStyle = .short startDate.dateFormatter?.timeStyle = .short endDate.dateFormatter?.dateStyle = .short endDate.dateFormatter?.timeStyle = .short } startDate.updateCell() endDate.updateCell() startDate.inlineRow?.updateCell() endDate.inlineRow?.updateCell() } <<< DateTimeInlineRow("Starts") { $0.title = $0.tag $0.value = Date().addingTimeInterval(60*60*24) } .onChange { [weak self] row in let endRow: DateTimeInlineRow! = self?.form.rowBy(tag: "Ends") if row.value?.compare(endRow.value!) == .orderedDescending { endRow.value = Date(timeInterval: 60*60*24, since: row.value!) endRow.cell!.backgroundColor = .white endRow.updateCell() } } .onExpandInlineRow { [weak self] cell, row, inlineRow in inlineRow.cellUpdate() { cell, row in let allRow: SwitchRow! = self?.form.rowBy(tag: "All-day") if allRow.value ?? false { cell.datePicker.datePickerMode = .date } else { cell.datePicker.datePickerMode = .dateAndTime } } let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } <<< DateTimeInlineRow("Ends"){ $0.title = $0.tag $0.value = Date().addingTimeInterval(60*60*25) } .onChange { [weak self] row in let startRow: DateTimeInlineRow! = self?.form.rowBy(tag: "Starts") if row.value?.compare(startRow.value!) == .orderedAscending { row.cell!.backgroundColor = .red } else{ row.cell!.backgroundColor = .white } row.updateCell() } .onExpandInlineRow { [weak self] cell, row, inlineRow in inlineRow.cellUpdate { cell, dateRow in let allRow: SwitchRow! = self?.form.rowBy(tag: "All-day") if allRow.value ?? false { cell.datePicker.datePickerMode = .date } else { cell.datePicker.datePickerMode = .dateAndTime } } let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } form +++ PushRow("Repeat") { $0.title = $0.tag $0.options = RepeatInterval.allCases $0.value = .Never }.onPresent({ (_, vc) in vc.enableDeselection = false vc.dismissOnSelection = false }) form +++ PushRow() { $0.title = "Alert" $0.options = EventAlert.allCases $0.value = .Never } .onChange { [weak self] row in if row.value == .Never { if let second : PushRow = self?.form.rowBy(tag: "Another Alert"), let secondIndexPath = second.indexPath { row.section?.remove(at: secondIndexPath.row) } } else{ guard let _ : PushRow = self?.form.rowBy(tag: "Another Alert") else { let second = PushRow("Another Alert") { $0.title = $0.tag $0.value = .Never $0.options = EventAlert.allCases } let secondIndex = row.indexPath!.row + 1 row.section?.insert(second, at: secondIndex) return } } } form +++ PushRow("Show As") { $0.title = "Show As" $0.options = EventState.allCases } form +++ URLRow("URL") { $0.placeholder = "URL" } <<< TextAreaRow("notes") { $0.placeholder = "Notes" $0.textAreaHeight = .dynamic(initialTextViewHeight: 50) } } @objc func cancelTapped(_ barButtonItem: UIBarButtonItem) { (navigationController as? NativeEventNavigationController)?.onDismissCallback?(self) } enum RepeatInterval : String, CaseIterable, CustomStringConvertible { case Never = "Never" case Every_Day = "Every Day" case Every_Week = "Every Week" case Every_2_Weeks = "Every 2 Weeks" case Every_Month = "Every Month" case Every_Year = "Every Year" var description : String { return rawValue } } enum EventAlert : String, CaseIterable, CustomStringConvertible { case Never = "None" case At_time_of_event = "At time of event" case Five_Minutes = "5 minutes before" case FifTeen_Minutes = "15 minutes before" case Half_Hour = "30 minutes before" case One_Hour = "1 hour before" case Two_Hour = "2 hours before" case One_Day = "1 day before" case Two_Days = "2 days before" var description : String { return rawValue } } enum EventState : CaseIterable { case busy case free } } ================================================ FILE: Example/Example/Controllers/NavigationAccessoryController.swift ================================================ // // NavigationAccessoryController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class NavigationAccessoryController : FormViewController { var navigationOptionsBackup : RowNavigationOptions? override func viewDidLoad() { super.viewDidLoad() navigationOptions = RowNavigationOptions.Enabled.union(.SkipCanNotBecomeFirstResponderRow) navigationOptionsBackup = navigationOptions form = Section(header: "Settings", footer: "These settings change how the navigation accessory view behaves") <<< SwitchRow("set_none") { [weak self] in $0.title = "Navigation accessory view" $0.value = self?.navigationOptions != .Disabled }.onChange { [weak self] in if $0.value ?? false { self?.navigationOptions = self?.navigationOptionsBackup self?.form.rowBy(tag: "set_disabled")?.baseValue = self?.navigationOptions?.contains(.StopDisabledRow) self?.form.rowBy(tag: "set_skip")?.baseValue = self?.navigationOptions?.contains(.SkipCanNotBecomeFirstResponderRow) self?.form.rowBy(tag: "set_disabled")?.updateCell() self?.form.rowBy(tag: "set_skip")?.updateCell() } else { self?.navigationOptionsBackup = self?.navigationOptions self?.navigationOptions = .Disabled } } <<< CheckRow("set_disabled") { [weak self] in $0.title = "Stop at disabled row" $0.value = self?.navigationOptions?.contains(.StopDisabledRow) $0.hidden = "$set_none == false" // .Predicate(NSPredicate(format: "$set_none == false")) }.onChange { [weak self] row in if row.value ?? false { self?.navigationOptions = self?.navigationOptions?.union(.StopDisabledRow) } else{ self?.navigationOptions = self?.navigationOptions?.subtracting(.StopDisabledRow) } } <<< CheckRow("set_skip") { [weak self] in $0.title = "Skip non first responder view" $0.value = self?.navigationOptions?.contains(.SkipCanNotBecomeFirstResponderRow) $0.hidden = "$set_none == false" }.onChange { [weak self] row in if row.value ?? false { self?.navigationOptions = self?.navigationOptions?.union(.SkipCanNotBecomeFirstResponderRow) } else{ self?.navigationOptions = self?.navigationOptions?.subtracting(.SkipCanNotBecomeFirstResponderRow) } } +++ NameRow() { $0.title = "Your name:" } <<< PasswordRow() { $0.title = "Your password:" } +++ Section() <<< SegmentedRow() { $0.title = "Favourite food:" $0.options = [🐗, 🐖, 🐡, 🍐] } <<< PhoneRow() { $0.title = "Your phone number" } <<< URLRow() { $0.title = "Disabled" $0.disabled = true } <<< TextRow() { $0.title = "Your father's name"} <<< TextRow(){ $0.title = "Your mother's name"} } } ================================================ FILE: Example/Example/Controllers/PlainTableViewExample.swift ================================================ // // PlainTableViewExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class PlainTableViewStyleController : FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section() <<< TextRow() { $0.title = "Name" $0.value = "John Doe" } <<< TextRow() { $0.title = "Username" $0.value = "johndoe1" } <<< EmailRow() { $0.title = "Email Address" $0.value = "john@doe.com" } <<< PasswordRow() { $0.title = "Password" $0.value = "johndoe9876" } // Remove excess separator lines on non-existent cells tableView.tableFooterView = UIView() } } ================================================ FILE: Example/Example/Controllers/RowsExample.swift ================================================ // // RowsExample.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import CoreLocation import Eureka //Mark: RowsExampleViewController class RowsExampleViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() URLRow.defaultCellUpdate = { cell, row in cell.textField.textColor = .systemBlue } LabelRow.defaultCellUpdate = { cell, row in cell.detailTextLabel?.textColor = .systemOrange } CheckRow.defaultCellSetup = { cell, row in cell.tintColor = .systemOrange } DateRow.defaultRowInitializer = { row in row.minimumDate = Date() } form +++ Section() <<< LabelRow () { $0.title = "LabelRow" $0.value = "tap the row" } .onCellSelection { cell, row in row.title = (row.title ?? "") + " 🇺🇾 " row.reload() // or row.updateCell() } <<< DateRow() { $0.value = Date(); $0.title = "DateRow" } <<< CountDownInlineRow() { $0.value = Date(); $0.title = "CountDownInlineRow" } <<< CheckRow() { $0.title = "CheckRow" $0.value = true } <<< SwitchRow() { $0.title = "SwitchRow" $0.value = true } <<< SliderRow() { $0.title = "SliderRow" $0.value = 5.0 } .cellSetup { cell, row in cell.imageView?.image = #imageLiteral(resourceName: "selected") } <<< StepperRow() { $0.title = "StepperRow" $0.value = 1.0 }.cellSetup({ (cell, row) in cell.imageView?.image = #imageLiteral(resourceName: "selectedRectangle") }) +++ Section("SegmentedRow examples") <<< SegmentedRow() { $0.options = ["One", "Two", "Three"] } <<< SegmentedRow(){ $0.title = "Who are you?" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻 ] $0.value = 🍐 } <<< SegmentedRow(){ $0.title = "SegmentedRow" $0.options = ["One", "Two"] }.cellSetup { cell, row in cell.imageView?.image = UIImage(named: "plus_image") } <<< SegmentedRow(){ $0.options = ["One", "Two", "Three", "Four"] $0.value = "Three" }.cellSetup { cell, row in cell.imageView?.image = UIImage(named: "plus_image") } <<< SegmentedRow(){ let names = ["selected", "plus_image", "unselected"] $0.options = names.map { UIImage(named: $0)! } $0.value = $0.options?.last } +++ Section("Selectors Rows Examples") <<< ActionSheetRow() { $0.title = "ActionSheetRow" $0.selectorTitle = "Your favourite player?" $0.options = ["Diego Forlán", "Edinson Cavani", "Diego Lugano", "Luis Suarez"] $0.value = "Luis Suarez" } .onPresent { from, to in to.popoverPresentationController?.permittedArrowDirections = .up } <<< AlertRow() { $0.title = "AlertRow" $0.cancelTitle = "Dismiss" $0.selectorTitle = "Who is there?" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 }.onChange { row in print(row.value ?? "No Value") } .onPresent{ _, to in to.view.tintColor = .purple } <<< PushRow() { $0.title = "PushRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 $0.selectorTitle = "Choose an Emoji!" }.onPresent { from, to in to.dismissOnSelection = false to.dismissOnChange = false } <<< PushRow() { $0.title = "SectionedPushRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 $0.selectorTitle = "Choose an Emoji!" }.onPresent { from, to in to.dismissOnSelection = false to.dismissOnChange = false to.sectionKeyForValue = { option in switch option { case 💁🏻, 👦🏼: return "People" case 🐗, 🐼, 🐻: return "Animals" case 🍐: return "Food" default: return "" } } } <<< PushRow() { $0.title = "LazySectionedPushRow" $0.value = 👦🏼 $0.selectorTitle = "Choose a lazy Emoji!" $0.optionsProvider = .lazy({ (form, completion) in let activityView = UIActivityIndicatorView(style: .gray) form.tableView.backgroundView = activityView activityView.startAnimating() DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: { form.tableView.backgroundView = nil completion([💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻]) }) }) } .onPresent { from, to in to.sectionKeyForValue = { option -> String in switch option { case 💁🏻, 👦🏼: return "People" case 🐗, 🐼, 🐻: return "Animals" case 🍐: return "Food" default: return "" } } } <<< PushRow() { $0.title = "Custom Cell Push Row" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 $0.selectorTitle = "Choose an Emoji!" } .onPresent { from, to in to.selectableRowSetup = { row in row.cellProvider = CellProvider>(nibName: "EmojiCell", bundle: Bundle.main) } to.selectableRowCellUpdate = { cell, row in var detailText: String? switch row.selectableValue { case 💁🏻, 👦🏼: detailText = "Person" case 🐗, 🐼, 🐻: detailText = "Animal" case 🍐: detailText = "Food" default: detailText = "" } cell.detailTextLabel?.text = detailText } } if UIDevice.current.userInterfaceIdiom == .pad { let section = form.last! section <<< PopoverSelectorRow() { $0.title = "PopoverSelectorRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 💁🏻 $0.selectorTitle = "Choose an Emoji!" } } let section = form.last! section <<< LocationRow(){ $0.title = "LocationRow" $0.value = CLLocation(latitude: -34.9124, longitude: -56.1594) } <<< ImageRow(){ $0.title = "ImageRow" } <<< MultipleSelectorRow() { $0.title = "MultipleSelectorRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = [👦🏼, 🍐, 🐗] } .onPresent { from, to in to.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: from, action: #selector(RowsExampleViewController.multipleSelectorDone(_:))) } <<< MultipleSelectorRow() { $0.title = "SectionedMultipleSelectorRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = [👦🏼, 🍐, 🐗] } .onPresent { from, to in to.sectionKeyForValue = { option in switch option { case 💁🏻, 👦🏼: return "People" case 🐗, 🐼, 🐻: return "Animals" case 🍐: return "Food" default: return "" } } to.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: from, action: #selector(RowsExampleViewController.multipleSelectorDone(_:))) } <<< MultipleSelectorRow() { $0.title = "LazyMultipleSelectorRow" $0.value = [👦🏼, 🍐, 🐗] $0.optionsProvider = .lazy({ (form, completion) in let activityView = UIActivityIndicatorView(style: .gray) form.tableView.backgroundView = activityView activityView.startAnimating() DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: { form.tableView.backgroundView = nil completion([💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻]) }) }) }.onPresent { from, to in to.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: from, action: #selector(RowsExampleViewController.multipleSelectorDone(_:))) } form +++ Section("Generic picker") <<< PickerRow("Picker Row") { (row : PickerRow) -> Void in row.options = [] for i in 1...10{ row.options.append("option \(i)") } } <<< PickerInputRow("Picker Input Row"){ $0.title = "Options" $0.options = [] for i in 1...10{ $0.options.append("option \(i)") } $0.value = $0.options.first } <<< DoublePickerInlineRow() { $0.title = "2 Component picker" $0.firstOptions = { return ["a", "b", "c"]} $0.secondOptions = { _ in return [1, 2, 3]} } <<< TriplePickerInputRow() { $0.firstOptions = { return ["a", "b", "c"]} $0.secondOptions = { return [$0, $0 + $0, $0 + "-" + $0, "asd"]} $0.thirdOptions = { _,_ in return [1, 2, 3]} $0.title = "3 Component picker" } +++ Section("FieldRow examples") <<< TextRow() { $0.title = "TextRow" $0.placeholder = "Placeholder" } <<< DecimalRow() { $0.title = "DecimalRow" $0.value = 5 $0.formatter = DecimalFormatter() $0.useFormatterDuringInput = true //$0.useFormatterOnDidBeginEditing = true }.cellSetup { cell, _ in cell.textField.keyboardType = .numberPad } <<< URLRow() { $0.title = "URLRow" $0.value = URL(string: "http://xmartlabs.com") } <<< PhoneRow() { $0.title = "PhoneRow (disabled)" $0.value = "+598 9898983510" $0.disabled = true } <<< NameRow() { $0.title = "NameRow" } <<< PasswordRow() { $0.title = "PasswordRow" $0.value = "password" } <<< IntRow() { $0.title = "IntRow" $0.value = 2015 } <<< EmailRow() { $0.title = "EmailRow" $0.value = "a@b.com" } <<< TwitterRow() { $0.title = "TwitterRow" $0.value = "@xmartlabs" } <<< AccountRow() { $0.title = "AccountRow" $0.placeholder = "Placeholder" } <<< ZipCodeRow() { $0.title = "ZipCodeRow" $0.placeholder = "90210" } } @objc func multipleSelectorDone(_ item:UIBarButtonItem) { _ = navigationController?.popViewController(animated: true) } } ================================================ FILE: Example/Example/Controllers/SwipeActionsController.swift ================================================ // // SwipeActionsController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class SwipeActionsController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section(footer: "Eureka sets table.isEditing = true only if the form contains a MultivaluedSection. SwipeActions only work when isEditing = false, therefore you have to set that in ViewWillAppear. Both can't be used on the same form.") <<< LabelRow("Actions Right: iOS >= 7") { $0.title = $0.tag let moreAction = SwipeAction(style: .normal, title: "More", handler: { (action, row, completionHandler) in print("More") completionHandler?(true) }) let deleteAction = SwipeAction(style: .destructive, title: "Delete") { (action, row, completionHandler) in print("Delete") completionHandler?(true) } $0.trailingSwipe.actions = [deleteAction,moreAction] } <<< LabelRow("Actions Left & Right: iOS >= 11") { $0.title = $0.tag let moreAction = SwipeAction(style: .normal, title: "More") { (action, row, completionHandler) in print("More") completionHandler?(true) } let deleteAction = SwipeAction(style: .destructive, title: "Delete", handler: { (action, row, completionHandler) in print("Delete") completionHandler?(true) }) $0.trailingSwipe.actions = [deleteAction,moreAction] $0.trailingSwipe.performsFirstActionWithFullSwipe = true if #available(iOS 11,*) { let infoAction = SwipeAction(style: .normal, title: "Info", handler: { (action, row, completionHandler) in print("Info") completionHandler?(true) }) infoAction.actionBackgroundColor = .blue $0.leadingSwipe.actions = [infoAction] $0.leadingSwipe.performsFirstActionWithFullSwipe = true } } } } ================================================ FILE: Example/Example/Controllers/ValidationsController.swift ================================================ // // ValidationsController.swift // Example // // Created by Mathias Claassen on 3/15/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Eureka class ValidationsController: FormViewController { override func viewDidLoad() { super.viewDidLoad() LabelRow.defaultCellUpdate = { cell, row in cell.contentView.backgroundColor = .red cell.textLabel?.textColor = .white cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 13) cell.textLabel?.textAlignment = .right } TextRow.defaultCellUpdate = { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } form +++ Section(header: "Required Rule", footer: "Options: Validates on change") <<< TextRow() { $0.title = "Required Rule" $0.add(rule: RuleRequired()) $0.validationOptions = .validatesOnChange } +++ Section(header: "Email Rule, Required Rule", footer: "Options: Validates on change after blurred") <<< TextRow() { $0.title = "Email Rule" $0.add(rule: RuleRequired()) var ruleSet = RuleSet() ruleSet.add(rule: RuleRequired()) ruleSet.add(rule: RuleEmail()) $0.add(ruleSet: ruleSet) $0.validationOptions = .validatesOnChangeAfterBlurred } +++ Section(header: "URL Rule", footer: "Options: Validates on change") <<< URLRow() { $0.title = "URL Rule" $0.add(rule: RuleURL()) $0.validationOptions = .validatesOnChange } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } +++ Section(header: "MinLength 8 Rule, MaxLength 13 Rule", footer: "Options: Validates on blurred") <<< PasswordRow() { $0.title = "Password" $0.add(rule: RuleMinLength(minLength: 8)) $0.add(rule: RuleMaxLength(maxLength: 13)) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } +++ Section(header: "Should be GreaterThan 2 and SmallerThan 999", footer: "Options: Validates on blurred") <<< IntRow() { $0.title = "Range Rule" $0.add(rule: RuleGreaterThan(min: 2)) $0.add(rule: RuleSmallerThan(max: 999)) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } +++ Section(header: "Match field values", footer: "Options: Validates on blurred") <<< PasswordRow("password") { $0.title = "Password" } <<< PasswordRow() { $0.title = "Confirm Password" $0.add(rule: RuleEqualsToRow(form: form, tag: "password")) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } +++ Section(header: "More sophisticated validations UX using callbacks", footer: "") <<< TextRow() { $0.title = "Required Rule" $0.add(rule: RuleRequired()) $0.validationOptions = .validatesOnChange } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } <<< EmailRow() { $0.title = "Email Rule" $0.add(rule: RuleRequired()) $0.add(rule: RuleEmail()) $0.validationOptions = .validatesOnChangeAfterBlurred } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } <<< URLRow() { $0.title = "URL Rule" $0.add(rule: RuleURL()) $0.validationOptions = .validatesOnChange } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } <<< PasswordRow("password2") { $0.title = "Password" $0.add(rule: RuleMinLength(minLength: 8)) $0.add(rule: RuleMaxLength(maxLength: 13)) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } <<< PasswordRow() { $0.title = "Confirm Password" $0.add(rule: RuleEqualsToRow(form: form, tag: "password2")) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } <<< IntRow() { $0.title = "Range Rule" $0.add(rule: RuleGreaterThan(min: 2)) $0.add(rule: RuleSmallerThan(max: 999)) } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .red } } .onRowValidationChanged { cell, row in let rowIndex = row.indexPath!.row while row.section!.count > rowIndex + 1 && row.section?[rowIndex + 1] is LabelRow { row.section?.remove(at: rowIndex + 1) } if !row.isValid { for (index, validationMsg) in row.validationErrors.map({ $0.msg }).enumerated() { let labelRow = LabelRow() { $0.title = validationMsg $0.cell.height = { 30 } } let indexPath = row.indexPath!.row + index + 1 row.section?.insert(labelRow, at: indexPath) } } } +++ Section() <<< ButtonRow() { $0.title = "Tap to force form validation" } .onCellSelection { cell, row in row.section?.form?.validate() } } } ================================================ FILE: Example/Example/CustomCells.swift ================================================ // CustomCells.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit import MapKit import Eureka //MARK: WeeklyDayCell public enum WeekDay { case monday, tuesday, wednesday, thursday, friday, saturday, sunday } public class WeekDayCell : Cell>, CellType { @IBOutlet var sundayButton: UIButton! @IBOutlet var mondayButton: UIButton! @IBOutlet var tuesdayButton: UIButton! @IBOutlet var wednesdayButton: UIButton! @IBOutlet var thursdayButton: UIButton! @IBOutlet var fridayButton: UIButton! @IBOutlet var saturdayButton: UIButton! open override func setup() { height = { 60 } row.title = nil super.setup() selectionStyle = .none for subview in contentView.subviews { if let button = subview as? UIButton { button.setImage(UIImage(named: "checkedDay"), for: .selected) button.setImage(UIImage(named: "uncheckedDay"), for: .normal) button.adjustsImageWhenHighlighted = false imageTopTitleBottom(button) } } } open override func update() { row.title = nil super.update() let value = row.value mondayButton.isSelected = value?.contains(.monday) ?? false tuesdayButton.isSelected = value?.contains(.tuesday) ?? false wednesdayButton.isSelected = value?.contains(.wednesday) ?? false thursdayButton.isSelected = value?.contains(.thursday) ?? false fridayButton.isSelected = value?.contains(.friday) ?? false saturdayButton.isSelected = value?.contains(.saturday) ?? false sundayButton.isSelected = value?.contains(.sunday) ?? false mondayButton.alpha = row.isDisabled ? 0.6 : 1.0 tuesdayButton.alpha = mondayButton.alpha wednesdayButton.alpha = mondayButton.alpha thursdayButton.alpha = mondayButton.alpha fridayButton.alpha = mondayButton.alpha saturdayButton.alpha = mondayButton.alpha sundayButton.alpha = mondayButton.alpha } @IBAction func dayTapped(_ sender: UIButton) { dayTapped(sender, day: getDayFromButton(sender)) } private func getDayFromButton(_ button: UIButton) -> WeekDay{ switch button{ case sundayButton: return .sunday case mondayButton: return .monday case tuesdayButton: return .tuesday case wednesdayButton: return .wednesday case thursdayButton: return .thursday case fridayButton: return .friday default: return .saturday } } private func dayTapped(_ button: UIButton, day: WeekDay){ button.isSelected = !button.isSelected if button.isSelected{ row.value?.insert(day) } else{ _ = row.value?.remove(day) } } private func imageTopTitleBottom(_ button : UIButton){ guard let imageSize = button.imageView?.image?.size else { return } let spacing : CGFloat = 3.0 button.titleEdgeInsets = UIEdgeInsets(top: 0.0, left: -imageSize.width, bottom: -(imageSize.height + spacing), right: 0.0) guard let titleLabel = button.titleLabel, let title = titleLabel.text else { return } let titleSize = title.size(withAttributes: [.font: titleLabel.font!]) button.imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + spacing), left: 0, bottom: 0, right: -titleSize.width) } } //MARK: WeekDayRow public final class WeekDayRow: Row, RowType { required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil cellProvider = CellProvider(nibName: "WeekDaysCell") } } //MARK: FloatLabelCell public class _FloatLabelCell: Cell, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable { public var textField: UITextField! { return floatLabelTextField } required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } lazy public var floatLabelTextField: FloatLabelTextField = { [unowned self] in let floatTextField = FloatLabelTextField() floatTextField.translatesAutoresizingMaskIntoConstraints = false floatTextField.font = .preferredFont(forTextStyle: .body) floatTextField.titleFont = .boldSystemFont(ofSize: 11.0) floatTextField.clearButtonMode = .whileEditing return floatTextField }() open override func setup() { super.setup() height = { 55 } selectionStyle = .none contentView.addSubview(floatLabelTextField) floatLabelTextField.delegate = self floatLabelTextField.addTarget(self, action: #selector(_FloatLabelCell.textFieldDidChange(_:)), for: .editingChanged) contentView.addConstraints(layoutConstraints()) } open override func update() { super.update() textLabel?.text = nil detailTextLabel?.text = nil floatLabelTextField.attributedPlaceholder = NSAttributedString(string: row.title ?? "", attributes: [.foregroundColor: UIColor.lightGray]) floatLabelTextField.text = row.displayValueFor?(row.value) floatLabelTextField.isEnabled = !row.isDisabled floatLabelTextField.titleTextColour = .lightGray floatLabelTextField.alpha = row.isDisabled ? 0.6 : 1 } open override func cellCanBecomeFirstResponder() -> Bool { return !row.isDisabled && floatLabelTextField.canBecomeFirstResponder } open override func cellBecomeFirstResponder(withDirection direction: Direction) -> Bool { return floatLabelTextField.becomeFirstResponder() } open override func cellResignFirstResponder() -> Bool { return floatLabelTextField.resignFirstResponder() } private func layoutConstraints() -> [NSLayoutConstraint] { let views = ["floatLabeledTextField": floatLabelTextField] let metrics = ["vMargin":8.0] return NSLayoutConstraint.constraints(withVisualFormat: "H:|-[floatLabeledTextField]-|", options: .alignAllLastBaseline, metrics: metrics, views: views) + NSLayoutConstraint.constraints(withVisualFormat: "V:|-(vMargin)-[floatLabeledTextField]-(vMargin)-|", options: .alignAllLastBaseline, metrics: metrics, views: views) } @objc public func textFieldDidChange(_ textField : UITextField){ guard let textValue = textField.text else { row.value = nil return } if let fieldRow = row as? FormatterConformance, let formatter = fieldRow.formatter { if fieldRow.useFormatterDuringInput { let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T if var selStartPos = textField.selectedTextRange?.start { let oldVal = textField.text textField.text = row.displayValueFor?(row.value) if let f = formatter as? FormatterProtocol { selStartPos = f.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) } textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos) } return } } else { let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T } return } } guard !textValue.isEmpty else { row.value = nil return } guard let newValue = T.init(string: textValue) else { return } row.value = newValue } //Mark: Helpers private func displayValue(useFormatter: Bool) -> String? { guard let v = row.value else { return nil } if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) } return String(describing: v) } //MARK: TextFieldDelegate public func textFieldDidBeginEditing(_ textField: UITextField) { formViewController()?.beginEditing(of: self) if let fieldRowConformance = row as? FormatterConformance, let _ = fieldRowConformance.formatter, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput { textField.text = displayValue(useFormatter: true) } else { textField.text = displayValue(useFormatter: false) } } public func textFieldDidEndEditing(_ textField: UITextField) { formViewController()?.endEditing(of: self) formViewController()?.textInputDidEndEditing(textField, cell: self) textFieldDidChange(textField) textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) } } public class TextFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .default textField?.autocapitalizationType = .sentences textField?.keyboardType = .default } } public class IntFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .default textField?.autocapitalizationType = .none textField?.keyboardType = .numberPad } } public class PhoneFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.keyboardType = .phonePad } } public class NameFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .words textField.keyboardType = .asciiCapable } } public class EmailFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .no textField?.autocapitalizationType = .none textField?.keyboardType = .emailAddress } } public class PasswordFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .no textField?.autocapitalizationType = .none textField?.keyboardType = .asciiCapable textField?.isSecureTextEntry = true } } public class DecimalFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.keyboardType = .decimalPad } } public class URLFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .no textField?.autocapitalizationType = .none textField?.keyboardType = .URL } } public class TwitterFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .no textField?.autocapitalizationType = .none textField?.keyboardType = .twitter } } public class AccountFloatLabelCell : _FloatLabelCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func setup() { super.setup() textField?.autocorrectionType = .no textField?.autocapitalizationType = .none textField?.keyboardType = .asciiCapable } } //MARK: FloatLabelRow open class FloatFieldRow: FormatteableRow where Cell: BaseCell, Cell: TextFieldCell { public required init(tag: String?) { super.init(tag: tag) } } public final class TextFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class IntFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class DecimalFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class URLFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class TwitterFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class AccountFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class PasswordFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class NameFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class EmailFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } public final class PhoneFloatLabelRow: FloatFieldRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } //MARK: LocationRow public final class LocationRow: OptionsRow>, PresenterRowType, RowType { public typealias PresenterRow = MapViewController /// Defines how the view controller will be presented, pushed, etc. public var presentationMode: PresentationMode? /// Will be called before the presentation occurs. public var onPresentCallback: ((FormViewController, PresenterRow) -> Void)? public required init(tag: String?) { super.init(tag: tag) presentationMode = .show(controllerProvider: ControllerProvider.callback { return MapViewController(){ _ in } }, onDismiss: { vc in _ = vc.navigationController?.popViewController(animated: true) }) displayValueFor = { guard let location = $0 else { return "" } let fmt = NumberFormatter() fmt.maximumFractionDigits = 4 fmt.minimumFractionDigits = 4 let latitude = fmt.string(from: NSNumber(value: location.coordinate.latitude))! let longitude = fmt.string(from: NSNumber(value: location.coordinate.longitude))! return "\(latitude), \(longitude)" } } /** Extends `didSelect` method */ public override func customDidSelect() { super.customDidSelect() guard let presentationMode = presentationMode, !isDisabled else { return } if let controller = presentationMode.makeController() { controller.row = self controller.title = selectorTitle ?? controller.title onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } } /** Prepares the pushed row setting its title and completion callback. */ public override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as? PresenterRow else { return } rowVC.title = selectorTitle ?? rowVC.title rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback onPresentCallback?(cell.formViewController()!, rowVC) rowVC.row = self } } public class MapViewController : UIViewController, TypedRowControllerType, MKMapViewDelegate { public var row: RowOf! public var onDismissCallback: ((UIViewController) -> ())? lazy var mapView : MKMapView = { [unowned self] in let v = MKMapView(frame: self.view.bounds) v.autoresizingMask = [.flexibleWidth, .flexibleHeight] return v }() lazy var pinView: UIImageView = { [unowned self] in let v = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) v.image = UIImage(named: "map_pin", in: Bundle(for: MapViewController.self), compatibleWith: nil) v.image = v.image?.withRenderingMode(.alwaysTemplate) v.tintColor = self.view.tintColor v.backgroundColor = .clear v.clipsToBounds = true v.contentMode = .scaleAspectFit v.isUserInteractionEnabled = false return v }() let width: CGFloat = 10.0 let height: CGFloat = 5.0 lazy var ellipse: UIBezierPath = { [unowned self] in let ellipse = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.width, height: self.height)) return ellipse }() lazy var ellipsisLayer: CAShapeLayer = { [unowned self] in let layer = CAShapeLayer() layer.bounds = CGRect(x: 0, y: 0, width: self.width, height: self.height) layer.path = self.ellipse.cgPath layer.fillColor = UIColor.gray.cgColor layer.fillRule = .nonZero layer.lineCap = .butt layer.lineDashPattern = nil layer.lineDashPhase = 0.0 layer.lineJoin = .miter layer.lineWidth = 1.0 layer.miterLimit = 10.0 layer.strokeColor = UIColor.gray.cgColor return layer }() required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) } convenience public init(_ callback: ((UIViewController) -> ())?){ self.init(nibName: nil, bundle: nil) onDismissCallback = callback } public override func viewDidLoad() { super.viewDidLoad() view.addSubview(mapView) mapView.delegate = self mapView.addSubview(pinView) mapView.layer.insertSublayer(ellipsisLayer, below: pinView.layer) let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(MapViewController.tappedDone(_:))) button.title = "Done" navigationItem.rightBarButtonItem = button if let value = row.value { let region = MKCoordinateRegion(center: value.coordinate, latitudinalMeters: 400, longitudinalMeters: 400) mapView.setRegion(region, animated: true) } else{ mapView.showsUserLocation = true } updateTitle() } public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let center = mapView.convert(mapView.centerCoordinate, toPointTo: pinView) pinView.center = CGPoint(x: center.x, y: center.y - (pinView.bounds.height/2)) ellipsisLayer.position = center } @objc func tappedDone(_ sender: UIBarButtonItem){ let target = mapView.convert(ellipsisLayer.position, toCoordinateFrom: mapView) row.value = CLLocation(latitude: target.latitude, longitude: target.longitude) onDismissCallback?(self) } func updateTitle(){ let fmt = NumberFormatter() fmt.maximumFractionDigits = 4 fmt.minimumFractionDigits = 4 let latitude = fmt.string(from: NSNumber(value: mapView.centerCoordinate.latitude))! let longitude = fmt.string(from: NSNumber(value: mapView.centerCoordinate.longitude))! title = "\(latitude), \(longitude)" } public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { ellipsisLayer.transform = CATransform3DMakeScale(0.5, 0.5, 1) UIView.animate(withDuration: 0.2, animations: { [weak self] in self?.pinView.center = CGPoint(x: self!.pinView.center.x, y: self!.pinView.center.y - 10) }) } public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { ellipsisLayer.transform = CATransform3DIdentity UIView.animate(withDuration: 0.2, animations: { [weak self] in self?.pinView.center = CGPoint(x: self!.pinView.center.x, y: self!.pinView.center.y + 10) }) updateTitle() } } public final class ImageCheckRow: Row>, SelectableRowType, RowType { public var selectableValue: T? required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil } } public class ImageCheckCell : Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// Image for selected state lazy public var trueImage: UIImage = { return UIImage(named: "selected")! }() /// Image for unselected state lazy public var falseImage: UIImage = { return UIImage(named: "unselected")! }() public override func update() { super.update() checkImageView?.image = row.value != nil ? trueImage : falseImage checkImageView?.sizeToFit() } /// Image view to render images. If `accessoryType` is set to `checkmark` /// will create a new `UIImageView` and set it as `accessoryView`. /// Otherwise returns `self.imageView`. open var checkImageView: UIImageView? { guard accessoryType == .checkmark else { return self.imageView } guard let accessoryView = accessoryView else { let imageView = UIImageView() self.accessoryView = imageView return imageView } return accessoryView as? UIImageView } public override func setup() { super.setup() accessoryType = .none } public override func didSelect() { row.reload() row.select() row.deselect() } } class EmojiCell: ListCheckCell {} ================================================ FILE: Example/Example/CustomDesign/DatePickerCell.xib ================================================ ================================================ FILE: Example/Example/CustomDesign/SwitchCell.xib ================================================ ================================================ FILE: Example/Example/CustomDesign/TextCell.xib ================================================ ================================================ FILE: Example/Example/CustomRows/ImageRow/ImagePickerController.swift ================================================ // ImagePickerController.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Eureka /// Selector Controller used to pick an image open class ImagePickerController : UIImagePickerController, TypedRowControllerType, UIImagePickerControllerDelegate, UINavigationControllerDelegate { /// The row that pushed or presented this controller public var row: RowOf! /// A closure to be called when the controller disappears. public var onDismissCallback : ((UIViewController) -> ())? open override func viewDidLoad() { super.viewDidLoad() delegate = self } open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { (row as? ImageRow)?.imageURL = info[.referenceURL] as? URL row.value = info[.originalImage] as? UIImage onDismissCallback?(self) } open func imagePickerControllerDidCancel(_ picker: UIImagePickerController){ onDismissCallback?(self) } } ================================================ FILE: Example/Example/CustomRows/ImageRow/ImageRow.swift ================================================ // ImageRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Eureka public struct ImageRowSourceTypes : OptionSet { public let rawValue: Int public var imagePickerControllerSourceTypeRawValue: Int { return self.rawValue >> 1 } public init(rawValue: Int) { self.rawValue = rawValue } init(_ sourceType: UIImagePickerController.SourceType) { self.init(rawValue: 1 << sourceType.rawValue) } public static let PhotoLibrary = ImageRowSourceTypes(.photoLibrary) public static let Camera = ImageRowSourceTypes(.camera) public static let SavedPhotosAlbum = ImageRowSourceTypes(.savedPhotosAlbum) public static let All: ImageRowSourceTypes = [Camera, PhotoLibrary, SavedPhotosAlbum] } extension ImageRowSourceTypes { // MARK: Helpers var localizedString: String { switch self { case ImageRowSourceTypes.Camera: return "Take photo" case ImageRowSourceTypes.PhotoLibrary: return "Photo Library" case ImageRowSourceTypes.SavedPhotosAlbum: return "Saved Photos" default: return "" } } } public enum ImageClearAction { case no case yes(style: UIAlertAction.Style) } //MARK: Row open class _ImageRow: OptionsRow, PresenterRowType where Cell: BaseCell, Cell.Value == UIImage { public typealias PresenterRow = ImagePickerController /// Defines how the view controller will be presented, pushed, etc. open var presentationMode: PresentationMode? /// Will be called before the presentation occurs. open var onPresentCallback: ((FormViewController, PresenterRow) -> Void)? open var sourceTypes: ImageRowSourceTypes open internal(set) var imageURL: URL? open var clearAction = ImageClearAction.yes(style: .destructive) private var _sourceType = UIImagePickerController.SourceType.camera public required init(tag: String?) { sourceTypes = .All super.init(tag: tag) presentationMode = .presentModally(controllerProvider: ControllerProvider.callback { return ImagePickerController() }, onDismiss: { [weak self] vc in self?.select() vc.dismiss(animated: true) }) self.displayValueFor = nil } // copy over the existing logic from the SelectorRow func displayImagePickerController(_ sourceType: UIImagePickerController.SourceType) { if let presentationMode = presentationMode, !isDisabled { if let controller = presentationMode.makeController(){ controller.row = self controller.sourceType = sourceType onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) } else { _sourceType = sourceType presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) } } } /// Extends `didSelect` method /// Selecting the Image Row cell will open a popup to choose where to source the photo from, /// based on the `sourceTypes` configured and the available sources. open override func customDidSelect() { guard !isDisabled else { super.customDidSelect() return } deselect() var availableSources: ImageRowSourceTypes = [] if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { let _ = availableSources.insert(.PhotoLibrary) } if UIImagePickerController.isSourceTypeAvailable(.camera) { let _ = availableSources.insert(.Camera) } if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) { let _ = availableSources.insert(.SavedPhotosAlbum) } sourceTypes.formIntersection(availableSources) if sourceTypes.isEmpty { super.customDidSelect() guard let presentationMode = presentationMode else { return } if let controller = presentationMode.makeController() { controller.row = self controller.title = selectorTitle ?? controller.title onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } return } // Now that we know the number of sources aren't empty, let the user select the source let sourceActionSheet = UIAlertController(title: nil, message: selectorTitle, preferredStyle: .actionSheet) guard let tableView = cell.formViewController()?.tableView else { fatalError() } if let popView = sourceActionSheet.popoverPresentationController { popView.sourceView = tableView popView.sourceRect = tableView.convert(cell.accessoryView?.frame ?? cell.contentView.frame, from: cell) } createOptionsForAlertController(sourceActionSheet) if case .yes(let style) = clearAction, value != nil { let clearPhotoOption = UIAlertAction(title: NSLocalizedString("Clear Photo", comment: ""), style: style, handler: { [weak self] _ in self?.value = nil self?.imageURL = nil self?.updateCell() }) sourceActionSheet.addAction(clearPhotoOption) } if sourceActionSheet.actions.count == 1 { if let imagePickerSourceType = UIImagePickerController.SourceType(rawValue: sourceTypes.imagePickerControllerSourceTypeRawValue) { displayImagePickerController(imagePickerSourceType) } } else { let cancelOption = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler:nil) sourceActionSheet.addAction(cancelOption) if let presentingViewController = cell.formViewController() { presentingViewController.present(sourceActionSheet, animated: true) } } } /** Prepares the pushed row setting its title and completion callback. */ open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as? PresenterRow else { return } rowVC.title = selectorTitle ?? rowVC.title rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback onPresentCallback?(cell.formViewController()!, rowVC) rowVC.row = self rowVC.sourceType = _sourceType } open override func customUpdateCell() { super.customUpdateCell() cell.accessoryType = .none cell.editingAccessoryView = .none if let image = self.value { let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) imageView.contentMode = .scaleAspectFill imageView.image = image imageView.clipsToBounds = true cell.accessoryView = imageView cell.editingAccessoryView = imageView } else { cell.accessoryView = nil cell.editingAccessoryView = nil } } } extension _ImageRow { //MARK: Helpers func createOptionForAlertController(_ alertController: UIAlertController, sourceType: ImageRowSourceTypes) { guard let pickerSourceType = UIImagePickerController.SourceType(rawValue: sourceType.imagePickerControllerSourceTypeRawValue), sourceTypes.contains(sourceType) else { return } let option = UIAlertAction(title: NSLocalizedString(sourceType.localizedString, comment: ""), style: .default, handler: { [weak self] _ in self?.displayImagePickerController(pickerSourceType) }) alertController.addAction(option) } func createOptionsForAlertController(_ alertController: UIAlertController) { createOptionForAlertController(alertController, sourceType: .Camera) createOptionForAlertController(alertController, sourceType: .PhotoLibrary) createOptionForAlertController(alertController, sourceType: .SavedPhotosAlbum) } } /// A selector row where the user can pick an image public final class ImageRow : _ImageRow>, RowType { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Example/Example/Helpers/FloatLabelTextField.swift ================================================ // // FloatLabelTextField.swift // FloatLabelFields // // Created by Fahim Farook on 28/11/14. // Copyright (c) 2014 RookSoft Ltd. All rights reserved. // // Original Concept by Matt D. Smith // http://dribbble.com/shots/1254439--GIF-Mobile-Form-Interaction?list=users // // Objective-C version by Jared Verdi // https://github.com/jverdi/JVFloatLabeledTextField // import UIKit @IBDesignable public class FloatLabelTextField: UITextField { let animationDuration = 0.3 var title = UILabel() // MARK:- Properties override public var accessibilityLabel:String! { get { if text?.isEmpty ?? true { return title.text } else { return text } } set { self.accessibilityLabel = newValue } } override public var placeholder:String? { didSet { title.text = placeholder title.sizeToFit() } } override public var attributedPlaceholder:NSAttributedString? { didSet { title.text = attributedPlaceholder?.string title.sizeToFit() } } var titleFont: UIFont = .systemFont(ofSize: 12.0) { didSet { title.font = titleFont title.sizeToFit() } } @IBInspectable var hintYPadding:CGFloat = 0.0 @IBInspectable var titleYPadding:CGFloat = 0.0 { didSet { var r = title.frame r.origin.y = titleYPadding title.frame = r } } @IBInspectable var titleTextColour:UIColor = .gray { didSet { if !isFirstResponder { title.textColor = titleTextColour } } } @IBInspectable var titleActiveTextColour:UIColor! { didSet { if isFirstResponder { title.textColor = titleActiveTextColour } } } // MARK:- Init required public init?(coder aDecoder:NSCoder) { super.init(coder:aDecoder) setup() } override init(frame:CGRect) { super.init(frame:frame) setup() } // MARK:- Overrides override public func layoutSubviews() { super.layoutSubviews() setTitlePositionForTextAlignment() let isResp = isFirstResponder if isResp && !(text?.isEmpty ?? true) { title.textColor = titleActiveTextColour } else { title.textColor = titleTextColour } // Should we show or hide the title label? if text?.isEmpty ?? true { // Hide hideTitle(isResp) } else { // Show showTitle(isResp) } } override public func textRect(forBounds bounds:CGRect) -> CGRect { var r = super.textRect(forBounds: bounds) if !(text?.isEmpty ?? true){ var top = ceil(title.font.lineHeight + hintYPadding) top = min(top, maxTopInset()) r = r.inset(by: UIEdgeInsets(top: top, left: 0.0, bottom: 0.0, right: 0.0)) } return r.integral } override public func editingRect(forBounds bounds:CGRect) -> CGRect { var r = super.editingRect(forBounds: bounds) if !(text?.isEmpty ?? true) { var top = ceil(title.font.lineHeight + hintYPadding) top = min(top, maxTopInset()) r = r.inset(by: UIEdgeInsets(top: top, left: 0.0, bottom: 0.0, right: 0.0)) } return r.integral } override public func clearButtonRect(forBounds bounds:CGRect) -> CGRect { var r = super.clearButtonRect(forBounds: bounds) if !(text?.isEmpty ?? true) { var top = ceil(title.font.lineHeight + hintYPadding) top = min(top, maxTopInset()) r = CGRect(x:r.origin.x, y:r.origin.y + (top * 0.5), width:r.size.width, height:r.size.height) } return r.integral } // MARK:- Public Methods // MARK:- Private Methods private func setup() { borderStyle = .none titleActiveTextColour = tintColor // Set up title label title.alpha = 0.0 title.font = titleFont title.textColor = titleTextColour if let str = placeholder, !str.isEmpty { title.text = str title.sizeToFit() } self.addSubview(title) } private func maxTopInset()->CGFloat { return max(0, floor(bounds.size.height - (font?.lineHeight ?? 0) - 4.0)) } private func setTitlePositionForTextAlignment() { let r = textRect(forBounds: bounds) var x = r.origin.x if textAlignment == .center { x = r.origin.x + (r.size.width * 0.5) - title.frame.size.width } else if textAlignment == .right { x = r.origin.x + r.size.width - title.frame.size.width } title.frame = CGRect(x:x, y:title.frame.origin.y, width:title.frame.size.width, height:title.frame.size.height) } private func showTitle(_ animated:Bool) { let dur = animated ? animationDuration : 0 UIView.animate(withDuration: dur, delay:0, options: [.beginFromCurrentState, .curveEaseOut], animations:{ // Animation self.title.alpha = 1.0 var r = self.title.frame r.origin.y = self.titleYPadding self.title.frame = r }) } private func hideTitle(_ animated:Bool) { let dur = animated ? animationDuration : 0 UIView.animate(withDuration: dur, delay:0, options: [.beginFromCurrentState, .curveEaseIn], animations:{ // Animation self.title.alpha = 0.0 var r = self.title.frame r.origin.y = self.title.font.lineHeight + self.hintYPadding self.title.frame = r }) } } ================================================ FILE: Example/Example/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName Eureka CFBundlePackageType APPL CFBundleShortVersionString 5.0.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription ImageRow example NSPhotoLibraryUsageDescription Show ImageRow example UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown ================================================ FILE: Example/Example/ViewController.swift ================================================ // ViewController.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit import Eureka //MARK: HomeViewController class HomeViewController : FormViewController { override func viewDidLoad() { super.viewDidLoad() ImageRow.defaultCellUpdate = { cell, row in cell.accessoryView?.layer.cornerRadius = 17 cell.accessoryView?.frame = CGRect(x: 0, y: 0, width: 34, height: 34) } form +++ Section() { $0.header = HeaderFooterView(.class) } <<< ButtonRow("Rows") { $0.title = $0.tag $0.presentationMode = .segueName(segueName: "RowsExampleViewControllerSegue", onDismiss: nil) } <<< ButtonRow("Native iOS Event Form") { row in row.title = row.tag row.presentationMode = .segueName(segueName: "NativeEventsFormNavigationControllerSegue", onDismiss:{ vc in vc.dismiss(animated: true) }) } <<< ButtonRow("Accesory View Navigation") { (row: ButtonRow) in row.title = row.tag row.presentationMode = .segueName(segueName: "AccesoryViewControllerSegue", onDismiss: nil) } <<< ButtonRow("Custom Cells") { (row: ButtonRow) -> () in row.title = row.tag row.presentationMode = .segueName(segueName: "CustomCellsControllerSegue", onDismiss: nil) } <<< ButtonRow("Customization of rows with text input") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "FieldCustomizationControllerSegue", onDismiss: nil) } <<< ButtonRow("Hidden rows") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "HiddenRowsControllerSegue", onDismiss: nil) } <<< ButtonRow("Disabled rows") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "DisabledRowsControllerSegue", onDismiss: nil) } <<< ButtonRow("Formatters") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "FormattersControllerSegue", onDismiss: nil) } <<< ButtonRow("Inline rows") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "InlineRowsControllerSegue", onDismiss: nil) } <<< ButtonRow("List Sections") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "ListSectionsControllerSegue", onDismiss: nil) } <<< ButtonRow("Validations") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "ValidationsControllerSegue", onDismiss: nil) } <<< ButtonRow("Custom Design") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "CustomDesignControllerSegue", onDismiss: nil) } <<< ButtonRow("Multivalued Sections") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "MultivaluedSectionsControllerSegue", onDismiss: nil) } <<< ButtonRow("Swipe Actions") { (row: ButtonRow) -> Void in row.title = row.tag row.presentationMode = .segueName(segueName: "SwipeActionsControllerSegue", onDismiss: nil) } <<< ButtonRow("Plain Table View Style") { (row: ButtonRow) in row.title = row.tag row.presentationMode = .segueName(segueName: "PlainTableViewStyleViewControllerSegue", onDismiss: nil) } +++ Section() <<< ButtonRow() { (row: ButtonRow) -> Void in row.title = "About" } .onCellSelection { [weak self] (cell, row) in self?.showAlert() } } @IBAction func showAlert() { let alertController = UIAlertController(title: "OnCellSelection", message: "Button Row Action", preferredStyle: .alert) let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(defaultAction) present(alertController, animated: true) } } //MARK: Emoji typealias Emoji = String let 👦🏼 = "👦🏼", 🍐 = "🍐", 💁🏻 = "💁🏻", 🐗 = "🐗", 🐼 = "🐼", 🐻 = "🐻", 🐖 = "🐖", 🐡 = "🐡" class EurekaLogoView: UIView { override init(frame: CGRect) { super.init(frame: frame) let imageView = UIImageView(image: UIImage(named: "Eureka")) imageView.frame = CGRect(x: 0, y: 0, width: 320, height: 130) imageView.autoresizingMask = .flexibleWidth self.frame = CGRect(x: 0, y: 0, width: 320, height: 130) imageView.contentMode = .scaleAspectFit addSubview(imageView) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Example/UITests/FieldRowUITests.swift ================================================ // ExampleUITests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest class FieldRowUITests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. XCUIApplication().launch() // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testDecimalRowWithFormatterDuringInput() { // Use XCTAssert and related functions to verify your tests produce the correct results. let app = XCUIApplication() let tablesQuery = app.tables // navigte to Rows view controller tablesQuery.cells.staticTexts["Rows"].tap() // get Decimal row text field let decimalRowTextFieldElement = tablesQuery.cells.containing(.staticText, identifier:"DecimalRow").children(matching: .textField).element // chack initial value XCTAssertEqual(decimalRowTextFieldElement.value as? String, decimalFormatter.editingString(for: Double(5)), "Initial Decimal Row value is wrong, should be 5,00 or 5.00") // check that delete keyboard worlks properly decimalRowTextFieldElement.tap() let deleteKey = app.keys["Delete"] deleteKey.tap() XCTAssertEqual(decimalRowTextFieldElement.value as? String, decimalFormatter.editingString(for: Double(0.5)), "Decimal Row value is wrong, should be 0,50 or 0.50") // check empty value for _ in 1...2 { deleteKey.tap() } //should be 0,00 XCTAssertEqual(decimalRowTextFieldElement.value as? String, decimalFormatter.editingString(for: Double(0)), "Decimal Row value is wrong, should be 0,00 or 0.00") // check result when entering digits decimalRowTextFieldElement.typeText("5678") // Should be 56,78 XCTAssertEqual(decimalRowTextFieldElement.value as? String, decimalFormatter.editingString(for: Double(56.78)), "Decimal Row value is wrong, should be 56,78 or 56.78") // one more check... decimalRowTextFieldElement.typeText("9") // SHOULD BE 567,89 XCTAssertEqual(decimalRowTextFieldElement.value as? String, decimalFormatter.editingString(for: Double(567.89)), "Decimal Row value is wrong, should be 567,89 or 567.89") } func testIntRowWithFormatterButNotDUringInput() { let app = XCUIApplication() let tablesQuery = app.tables tablesQuery.cells.staticTexts["Rows"].tap() let textField = tablesQuery.cells.containing(.staticText, identifier:"IntRow").children(matching: .textField).element // chack initial value XCTAssertEqual(textField.value as? String, intFormatter.string(for: Int(2015)), "Initial Int Row value is wrong, should be 2,015 or 2.015") textField.tap() // should be 2015 XCTAssertEqual(textField.value as? String, "2015", "Editing Initial Int Row value is wrong, should be 2015") let deleteKey = app.keys["Delete"] deleteKey.tap() // sould be 201 XCTAssertEqual(textField.value as? String, intFormatter.string(for: Int(201)), "Int Row value is wrong, should be 201") deleteKey.tap() deleteKey.tap() deleteKey.tap() // should be empty XCTAssertEqual(textField.value as? String, "", "Int Row value is wrong, should be \"\"") textField.typeText("1") // should be 1 XCTAssertEqual(textField.value as? String, intFormatter.string(for: Int(1)), "Int Row value is wrong, should be 1") textField.typeText("2345") // regign firest responder app.toolbars.buttons["Done"].tap() // should be 12,345 XCTAssertEqual(textField.value as? String, intFormatter.string(for: Int(12345)), "Int Row value is wrong, should be 12,345 or 12.345") } //MARK: Helpers lazy var decimalFormatter: NumberFormatter = { let numberFormatter = NumberFormatter() numberFormatter.locale = Locale.current numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 2 return numberFormatter }() lazy var intFormatter: NumberFormatter = { let numberFormatter = NumberFormatter() numberFormatter.locale = Locale.current numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 0 numberFormatter.maximumFractionDigits = 0 return numberFormatter }() } ================================================ FILE: Example/UITests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: Example.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 2837E2A01BA3A5A800A1952C /* FloatLabelTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2837E29E1BA3A5A800A1952C /* FloatLabelTextField.swift */; }; 284D28B61E49121E00BB38E7 /* DatePickerCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 284D28B51E49121E00BB38E7 /* DatePickerCell.xib */; }; 284D28B81E4917A000BB38E7 /* SwitchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 284D28B71E4917A000BB38E7 /* SwitchCell.xib */; }; 284D28BA1E491FEF00BB38E7 /* TextCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 284D28B91E491FEF00BB38E7 /* TextCell.xib */; }; 285F2B441BB79B86005991BA /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 285F2B431BB79B86005991BA /* MapKit.framework */; }; 287A143F1D99A96100FFE6EB /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287A143D1D99A96100FFE6EB /* ImagePickerController.swift */; }; 287A14401D99A96100FFE6EB /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287A143E1D99A96100FFE6EB /* ImageRow.swift */; }; 28C777241BA14A52000ED936 /* Eureka.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51729E471B9A5841004A00EB /* Eureka.framework */; }; 28C777251BA14A52000ED936 /* Eureka.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51729E471B9A5841004A00EB /* Eureka.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 28CD97C21BB32F0E00AFDA67 /* EurekaSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28CD97C01BB32F0E00AFDA67 /* EurekaSectionHeader.xib */; }; 28E8554C1B9EA46F004E5189 /* FieldRowUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E8554A1B9EA400004E5189 /* FieldRowUITests.swift */; }; 51729E1E1B9A54F1004A00EB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51729E1D1B9A54F1004A00EB /* AppDelegate.swift */; }; 51729E201B9A54F1004A00EB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51729E1F1B9A54F1004A00EB /* ViewController.swift */; }; 51729E231B9A54F1004A00EB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51729E211B9A54F1004A00EB /* Main.storyboard */; }; 51729E251B9A54F1004A00EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51729E241B9A54F1004A00EB /* Assets.xcassets */; }; 51729E281B9A54F1004A00EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51729E261B9A54F1004A00EB /* LaunchScreen.storyboard */; }; 51729E6E1B9A6601004A00EB /* CustomCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51729E6D1B9A6601004A00EB /* CustomCells.swift */; }; 51729E711B9A660A004A00EB /* WeekDaysCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51729E6F1B9A660A004A00EB /* WeekDaysCell.xib */; }; 8F32504F205AAA93006AF0A6 /* RowsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F32504E205AAA93006AF0A6 /* RowsExample.swift */; }; 8F325051205AAB35006AF0A6 /* CustomCellsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325050205AAB35006AF0A6 /* CustomCellsViewController.swift */; }; 8F325053205AAB6F006AF0A6 /* FieldRowCustomizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325052205AAB6F006AF0A6 /* FieldRowCustomizationController.swift */; }; 8F325055205AABA6006AF0A6 /* NavigationAccessoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325054205AABA6006AF0A6 /* NavigationAccessoryController.swift */; }; 8F325057205AABDB006AF0A6 /* NativeEventExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325056205AABDB006AF0A6 /* NativeEventExample.swift */; }; 8F325059205AAC06006AF0A6 /* HiddenRowsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325058205AAC06006AF0A6 /* HiddenRowsExample.swift */; }; 8F32505B205AAC2A006AF0A6 /* DisabledRowsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F32505A205AAC2A006AF0A6 /* DisabledRowsExample.swift */; }; 8F32505D205AAC68006AF0A6 /* FormatterExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F32505C205AAC68006AF0A6 /* FormatterExample.swift */; }; 8F32505F205AAC8E006AF0A6 /* InlineRowsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F32505E205AAC8E006AF0A6 /* InlineRowsExample.swift */; }; 8F325061205AACB9006AF0A6 /* ListSectionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325060205AACB9006AF0A6 /* ListSectionsController.swift */; }; 8F325063205AACF4006AF0A6 /* ValidationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325062205AACF4006AF0A6 /* ValidationsController.swift */; }; 8F325065205AAD23006AF0A6 /* CustomDesignController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325064205AAD23006AF0A6 /* CustomDesignController.swift */; }; 8F325067205AAD7A006AF0A6 /* MultivaluedExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325066205AAD7A006AF0A6 /* MultivaluedExamples.swift */; }; 8F325069205AAF1E006AF0A6 /* SwipeActionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F325068205AAF1E006AF0A6 /* SwipeActionsController.swift */; }; 8F32506B205AAF66006AF0A6 /* PlainTableViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F32506A205AAF66006AF0A6 /* PlainTableViewExample.swift */; }; D4ACF9132074B52200AEA952 /* EmojiCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D4ACF9122074B52200AEA952 /* EmojiCell.xib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 28C777261BA14A52000ED936 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729E411B9A5841004A00EB /* Eureka.xcodeproj */; proxyType = 1; remoteGlobalIDString = 51729DEA1B9A4F5E004A00EB; remoteInfo = Eureka; }; 51729E2F1B9A54F1004A00EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729E121B9A54F1004A00EB /* Project object */; proxyType = 1; remoteGlobalIDString = 51729E191B9A54F1004A00EB; remoteInfo = Example; }; 51729E461B9A5841004A00EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729E411B9A5841004A00EB /* Eureka.xcodeproj */; proxyType = 2; remoteGlobalIDString = 51729DEB1B9A4F5E004A00EB; remoteInfo = Eureka; }; 51729E481B9A5841004A00EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729E411B9A5841004A00EB /* Eureka.xcodeproj */; proxyType = 2; remoteGlobalIDString = 51729DF51B9A4F5E004A00EB; remoteInfo = EurekaTests; }; BF4298981BA0997D001D107B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 51729E411B9A5841004A00EB /* Eureka.xcodeproj */; proxyType = 1; remoteGlobalIDString = 51729DEA1B9A4F5E004A00EB; remoteInfo = Eureka; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 28C777281BA14A52000ED936 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 28C777251BA14A52000ED936 /* Eureka.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 2837E29E1BA3A5A800A1952C /* FloatLabelTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FloatLabelTextField.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 284D28B51E49121E00BB38E7 /* DatePickerCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = DatePickerCell.xib; path = Example/CustomDesign/DatePickerCell.xib; sourceTree = ""; }; 284D28B71E4917A000BB38E7 /* SwitchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = SwitchCell.xib; path = Example/CustomDesign/SwitchCell.xib; sourceTree = ""; }; 284D28B91E491FEF00BB38E7 /* TextCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = TextCell.xib; path = Example/CustomDesign/TextCell.xib; sourceTree = ""; }; 285F2B431BB79B86005991BA /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 287A143D1D99A96100FFE6EB /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImagePickerController.swift; path = Example/CustomRows/ImageRow/ImagePickerController.swift; sourceTree = ""; }; 287A143E1D99A96100FFE6EB /* ImageRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageRow.swift; path = Example/CustomRows/ImageRow/ImageRow.swift; sourceTree = ""; }; 28CD97C11BB32F0E00AFDA67 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Example/Base.lproj/EurekaSectionHeader.xib; sourceTree = ""; }; 28E8554A1B9EA400004E5189 /* FieldRowUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FieldRowUITests.swift; path = Example/UITests/FieldRowUITests.swift; sourceTree = SOURCE_ROOT; }; 51729E1A1B9A54F1004A00EB /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51729E1D1B9A54F1004A00EB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Example/AppDelegate.swift; sourceTree = ""; }; 51729E1F1B9A54F1004A00EB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = ViewController.swift; path = Example/ViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 51729E221B9A54F1004A00EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 51729E241B9A54F1004A00EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Example/Assets.xcassets; sourceTree = ""; }; 51729E271B9A54F1004A00EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 51729E291B9A54F1004A00EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Example/Info.plist; sourceTree = ""; }; 51729E2E1B9A54F1004A00EB /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51729E341B9A54F1004A00EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Example/UITests/Info.plist; sourceTree = ""; }; 51729E411B9A5841004A00EB /* Eureka.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Eureka.xcodeproj; sourceTree = ""; }; 51729E6D1B9A6601004A00EB /* CustomCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CustomCells.swift; path = Example/CustomCells.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 51729E701B9A660A004A00EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Example/Base.lproj/WeekDaysCell.xib; sourceTree = ""; }; 8F32504E205AAA93006AF0A6 /* RowsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowsExample.swift; sourceTree = ""; }; 8F325050205AAB35006AF0A6 /* CustomCellsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCellsViewController.swift; sourceTree = ""; }; 8F325052205AAB6F006AF0A6 /* FieldRowCustomizationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRowCustomizationController.swift; sourceTree = ""; }; 8F325054205AABA6006AF0A6 /* NavigationAccessoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationAccessoryController.swift; sourceTree = ""; }; 8F325056205AABDB006AF0A6 /* NativeEventExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeEventExample.swift; sourceTree = ""; }; 8F325058205AAC06006AF0A6 /* HiddenRowsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenRowsExample.swift; sourceTree = ""; }; 8F32505A205AAC2A006AF0A6 /* DisabledRowsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledRowsExample.swift; sourceTree = ""; }; 8F32505C205AAC68006AF0A6 /* FormatterExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatterExample.swift; sourceTree = ""; }; 8F32505E205AAC8E006AF0A6 /* InlineRowsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineRowsExample.swift; sourceTree = ""; }; 8F325060205AACB9006AF0A6 /* ListSectionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSectionsController.swift; sourceTree = ""; }; 8F325062205AACF4006AF0A6 /* ValidationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationsController.swift; sourceTree = ""; }; 8F325064205AAD23006AF0A6 /* CustomDesignController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDesignController.swift; sourceTree = ""; }; 8F325066205AAD7A006AF0A6 /* MultivaluedExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultivaluedExamples.swift; sourceTree = ""; }; 8F325068205AAF1E006AF0A6 /* SwipeActionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsController.swift; sourceTree = ""; }; 8F32506A205AAF66006AF0A6 /* PlainTableViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainTableViewExample.swift; sourceTree = ""; }; D4ACF9122074B52200AEA952 /* EmojiCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiCell.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 51729E171B9A54F1004A00EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 285F2B441BB79B86005991BA /* MapKit.framework in Frameworks */, 28C777241BA14A52000ED936 /* Eureka.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 51729E2B1B9A54F1004A00EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2837E29D1BA3A5A800A1952C /* Helpers */ = { isa = PBXGroup; children = ( 2837E29E1BA3A5A800A1952C /* FloatLabelTextField.swift */, ); name = Helpers; path = Example/Helpers; sourceTree = ""; }; 284D28AE1E48C27300BB38E7 /* Custom Design */ = { isa = PBXGroup; children = ( 284D28B71E4917A000BB38E7 /* SwitchCell.xib */, 284D28B51E49121E00BB38E7 /* DatePickerCell.xib */, 284D28B91E491FEF00BB38E7 /* TextCell.xib */, ); name = "Custom Design"; sourceTree = ""; }; 285F2B451BB79BB7005991BA /* Supporting Files */ = { isa = PBXGroup; children = ( 285F2B431BB79B86005991BA /* MapKit.framework */, ); name = "Supporting Files"; sourceTree = ""; }; 287A143B1D99A91B00FFE6EB /* ImageRow */ = { isa = PBXGroup; children = ( 287A143D1D99A96100FFE6EB /* ImagePickerController.swift */, 287A143E1D99A96100FFE6EB /* ImageRow.swift */, ); name = ImageRow; sourceTree = ""; }; 28CD97BE1BB32EC800AFDA67 /* Custom Section Header */ = { isa = PBXGroup; children = ( 28CD97C01BB32F0E00AFDA67 /* EurekaSectionHeader.xib */, ); name = "Custom Section Header"; sourceTree = ""; }; 51729E111B9A54F1004A00EB = { isa = PBXGroup; children = ( 51729E1C1B9A54F1004A00EB /* Example */, 51729E311B9A54F1004A00EB /* UITests */, 51729E1B1B9A54F1004A00EB /* Products */, 285F2B451BB79BB7005991BA /* Supporting Files */, 51729E411B9A5841004A00EB /* Eureka.xcodeproj */, ); sourceTree = ""; }; 51729E1B1B9A54F1004A00EB /* Products */ = { isa = PBXGroup; children = ( 51729E1A1B9A54F1004A00EB /* Example.app */, 51729E2E1B9A54F1004A00EB /* ExampleUITests.xctest */, ); name = Products; sourceTree = ""; }; 51729E1C1B9A54F1004A00EB /* Example */ = { isa = PBXGroup; children = ( 8F32504C205AAA4D006AF0A6 /* Controllers */, 284D28AE1E48C27300BB38E7 /* Custom Design */, 28CD97BE1BB32EC800AFDA67 /* Custom Section Header */, 51729E6A1B9A657E004A00EB /* Custom Row and Cell */, 51729E1D1B9A54F1004A00EB /* AppDelegate.swift */, 51729E1F1B9A54F1004A00EB /* ViewController.swift */, 51729E211B9A54F1004A00EB /* Main.storyboard */, 51729E241B9A54F1004A00EB /* Assets.xcassets */, 51729E261B9A54F1004A00EB /* LaunchScreen.storyboard */, 51729E291B9A54F1004A00EB /* Info.plist */, ); path = Example; sourceTree = ""; }; 51729E311B9A54F1004A00EB /* UITests */ = { isa = PBXGroup; children = ( 28E8554A1B9EA400004E5189 /* FieldRowUITests.swift */, 51729E341B9A54F1004A00EB /* Info.plist */, ); name = UITests; path = Example/UITests; sourceTree = ""; }; 51729E421B9A5841004A00EB /* Products */ = { isa = PBXGroup; children = ( 51729E471B9A5841004A00EB /* Eureka.framework */, 51729E491B9A5841004A00EB /* EurekaTests.xctest */, ); name = Products; sourceTree = ""; }; 51729E6A1B9A657E004A00EB /* Custom Row and Cell */ = { isa = PBXGroup; children = ( 287A143B1D99A91B00FFE6EB /* ImageRow */, 2837E29D1BA3A5A800A1952C /* Helpers */, 51729E6F1B9A660A004A00EB /* WeekDaysCell.xib */, D4ACF9122074B52200AEA952 /* EmojiCell.xib */, 51729E6D1B9A6601004A00EB /* CustomCells.swift */, ); name = "Custom Row and Cell"; sourceTree = ""; }; 8F32504C205AAA4D006AF0A6 /* Controllers */ = { isa = PBXGroup; children = ( 8F325050205AAB35006AF0A6 /* CustomCellsViewController.swift */, 8F325064205AAD23006AF0A6 /* CustomDesignController.swift */, 8F32505A205AAC2A006AF0A6 /* DisabledRowsExample.swift */, 8F325052205AAB6F006AF0A6 /* FieldRowCustomizationController.swift */, 8F32505C205AAC68006AF0A6 /* FormatterExample.swift */, 8F325058205AAC06006AF0A6 /* HiddenRowsExample.swift */, 8F32505E205AAC8E006AF0A6 /* InlineRowsExample.swift */, 8F325060205AACB9006AF0A6 /* ListSectionsController.swift */, 8F325066205AAD7A006AF0A6 /* MultivaluedExamples.swift */, 8F325056205AABDB006AF0A6 /* NativeEventExample.swift */, 8F325054205AABA6006AF0A6 /* NavigationAccessoryController.swift */, 8F32506A205AAF66006AF0A6 /* PlainTableViewExample.swift */, 8F32504E205AAA93006AF0A6 /* RowsExample.swift */, 8F325068205AAF1E006AF0A6 /* SwipeActionsController.swift */, 8F325062205AACF4006AF0A6 /* ValidationsController.swift */, ); name = Controllers; path = Example/Controllers; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 51729E191B9A54F1004A00EB /* Example */ = { isa = PBXNativeTarget; buildConfigurationList = 51729E371B9A54F1004A00EB /* Build configuration list for PBXNativeTarget "Example" */; buildPhases = ( 51729E161B9A54F1004A00EB /* Sources */, 51729E171B9A54F1004A00EB /* Frameworks */, 51729E181B9A54F1004A00EB /* Resources */, 28C777281BA14A52000ED936 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( BF4298991BA0997D001D107B /* PBXTargetDependency */, 28C777271BA14A52000ED936 /* PBXTargetDependency */, ); name = Example; productName = Example; productReference = 51729E1A1B9A54F1004A00EB /* Example.app */; productType = "com.apple.product-type.application"; }; 51729E2D1B9A54F1004A00EB /* ExampleUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 51729E3A1B9A54F1004A00EB /* Build configuration list for PBXNativeTarget "ExampleUITests" */; buildPhases = ( 51729E2A1B9A54F1004A00EB /* Sources */, 51729E2B1B9A54F1004A00EB /* Frameworks */, 51729E2C1B9A54F1004A00EB /* Resources */, ); buildRules = ( ); dependencies = ( 51729E301B9A54F1004A00EB /* PBXTargetDependency */, ); name = ExampleUITests; productName = ExampleUITests; productReference = 51729E2E1B9A54F1004A00EB /* ExampleUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 51729E121B9A54F1004A00EB /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1400; ORGANIZATIONNAME = Xmartlabs; TargetAttributes = { 51729E191B9A54F1004A00EB = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; 51729E2D1B9A54F1004A00EB = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; TestTargetID = 51729E191B9A54F1004A00EB; }; }; }; buildConfigurationList = 51729E151B9A54F1004A00EB /* Build configuration list for PBXProject "Example" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 51729E111B9A54F1004A00EB; productRefGroup = 51729E1B1B9A54F1004A00EB /* Products */; projectDirPath = ""; projectReferences = ( { ProductGroup = 51729E421B9A5841004A00EB /* Products */; ProjectRef = 51729E411B9A5841004A00EB /* Eureka.xcodeproj */; }, ); projectRoot = ""; targets = ( 51729E191B9A54F1004A00EB /* Example */, 51729E2D1B9A54F1004A00EB /* ExampleUITests */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ 51729E471B9A5841004A00EB /* Eureka.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Eureka.framework; remoteRef = 51729E461B9A5841004A00EB /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 51729E491B9A5841004A00EB /* EurekaTests.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; path = EurekaTests.xctest; remoteRef = 51729E481B9A5841004A00EB /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ 51729E181B9A54F1004A00EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 51729E281B9A54F1004A00EB /* LaunchScreen.storyboard in Resources */, 284D28B81E4917A000BB38E7 /* SwitchCell.xib in Resources */, 284D28BA1E491FEF00BB38E7 /* TextCell.xib in Resources */, 51729E251B9A54F1004A00EB /* Assets.xcassets in Resources */, 51729E711B9A660A004A00EB /* WeekDaysCell.xib in Resources */, 284D28B61E49121E00BB38E7 /* DatePickerCell.xib in Resources */, 28CD97C21BB32F0E00AFDA67 /* EurekaSectionHeader.xib in Resources */, 51729E231B9A54F1004A00EB /* Main.storyboard in Resources */, D4ACF9132074B52200AEA952 /* EmojiCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 51729E2C1B9A54F1004A00EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 51729E161B9A54F1004A00EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 8F32506B205AAF66006AF0A6 /* PlainTableViewExample.swift in Sources */, 8F325069205AAF1E006AF0A6 /* SwipeActionsController.swift in Sources */, 51729E201B9A54F1004A00EB /* ViewController.swift in Sources */, 8F32505D205AAC68006AF0A6 /* FormatterExample.swift in Sources */, 51729E6E1B9A6601004A00EB /* CustomCells.swift in Sources */, 8F32505B205AAC2A006AF0A6 /* DisabledRowsExample.swift in Sources */, 8F325057205AABDB006AF0A6 /* NativeEventExample.swift in Sources */, 287A14401D99A96100FFE6EB /* ImageRow.swift in Sources */, 287A143F1D99A96100FFE6EB /* ImagePickerController.swift in Sources */, 8F325055205AABA6006AF0A6 /* NavigationAccessoryController.swift in Sources */, 8F325065205AAD23006AF0A6 /* CustomDesignController.swift in Sources */, 8F32504F205AAA93006AF0A6 /* RowsExample.swift in Sources */, 8F32505F205AAC8E006AF0A6 /* InlineRowsExample.swift in Sources */, 51729E1E1B9A54F1004A00EB /* AppDelegate.swift in Sources */, 2837E2A01BA3A5A800A1952C /* FloatLabelTextField.swift in Sources */, 8F325063205AACF4006AF0A6 /* ValidationsController.swift in Sources */, 8F325059205AAC06006AF0A6 /* HiddenRowsExample.swift in Sources */, 8F325053205AAB6F006AF0A6 /* FieldRowCustomizationController.swift in Sources */, 8F325051205AAB35006AF0A6 /* CustomCellsViewController.swift in Sources */, 8F325067205AAD7A006AF0A6 /* MultivaluedExamples.swift in Sources */, 8F325061205AACB9006AF0A6 /* ListSectionsController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 51729E2A1B9A54F1004A00EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 28E8554C1B9EA46F004E5189 /* FieldRowUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 28C777271BA14A52000ED936 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Eureka; targetProxy = 28C777261BA14A52000ED936 /* PBXContainerItemProxy */; }; 51729E301B9A54F1004A00EB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 51729E191B9A54F1004A00EB /* Example */; targetProxy = 51729E2F1B9A54F1004A00EB /* PBXContainerItemProxy */; }; BF4298991BA0997D001D107B /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Eureka; targetProxy = BF4298981BA0997D001D107B /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 28CD97C01BB32F0E00AFDA67 /* EurekaSectionHeader.xib */ = { isa = PBXVariantGroup; children = ( 28CD97C11BB32F0E00AFDA67 /* Base */, ); name = EurekaSectionHeader.xib; sourceTree = ""; }; 51729E211B9A54F1004A00EB /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 51729E221B9A54F1004A00EB /* Base */, ); name = Main.storyboard; path = Example; sourceTree = ""; }; 51729E261B9A54F1004A00EB /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 51729E271B9A54F1004A00EB /* Base */, ); name = LaunchScreen.storyboard; path = Example; sourceTree = ""; }; 51729E6F1B9A660A004A00EB /* WeekDaysCell.xib */ = { isa = PBXVariantGroup; children = ( 51729E701B9A660A004A00EB /* Base */, ); name = WeekDaysCell.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 51729E351B9A54F1004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; 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 = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 51729E361B9A54F1004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = 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_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_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; 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 = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; 51729E381B9A54F1004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Example/Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.Example; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 51729E391B9A54F1004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Example/Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.Example; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 51729E3B1B9A54F1004A00EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = Example/UITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.ExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Example; USES_XCTRUNNER = YES; }; name = Debug; }; 51729E3C1B9A54F1004A00EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = Example/UITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.xmartlabs.ExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Example; USES_XCTRUNNER = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 51729E151B9A54F1004A00EB /* Build configuration list for PBXProject "Example" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729E351B9A54F1004A00EB /* Debug */, 51729E361B9A54F1004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 51729E371B9A54F1004A00EB /* Build configuration list for PBXNativeTarget "Example" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729E381B9A54F1004A00EB /* Debug */, 51729E391B9A54F1004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 51729E3A1B9A54F1004A00EB /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( 51729E3B1B9A54F1004A00EB /* Debug */, 51729E3C1B9A54F1004A00EB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 51729E121B9A54F1004A00EB /* Project object */; } ================================================ FILE: Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 XMARTLABS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "Eureka", platforms: [.iOS(.v9)], products: [ .library(name: "Eureka", targets: ["Eureka"]) ], targets: [ .target( name: "Eureka", path: "Source" ), .testTarget( name: "EurekaTests", dependencies: ["Eureka"], path: "Tests" ) ] ) ================================================ FILE: README.md ================================================ ![Eureka: Elegant form builder in Swift](Eureka.png)

Build status Platform iOS Swift 5 compatible Carthage compatible CocoaPods compatible License: MIT codebeat badge

Made with ❤️ by [XMARTLABS](http://xmartlabs.com). This is the re-creation of [XLForm] in Swift. [简体中文](Documentation/README_CN.md) ## Overview
## Contents * [Requirements] * [Usage] + [How to create a Form] + [Getting row values] + [Operators] + [Using the callbacks] + [Section Header and Footer] + [Dynamically hide and show rows (or sections)] + [List sections] + [Multivalued sections] + [Validations] + [Swipe Actions] * [Custom rows] + [Basic custom rows] + [Custom inline rows] + [Custom presenter rows] * [Row catalog] * [Installation] * [FAQ] **For more information look at [our blog post] that introduces *Eureka*.** ## Requirements (for latest release) * Xcode 11+ * Swift 5.0+ ### Example project You can clone and run the Example project to see examples of most of Eureka's features.
## Usage ### How to create a form By extending `FormViewController` you can then simply add sections and rows to the `form` variable. ```swift import Eureka class MyFormViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form +++ Section("Section1") <<< TextRow(){ row in row.title = "Text Row" row.placeholder = "Enter text here" } <<< PhoneRow(){ $0.title = "Phone Row" $0.placeholder = "And numbers here" } +++ Section("Section2") <<< DateRow(){ $0.title = "Date Row" $0.value = Date(timeIntervalSinceReferenceDate: 0) } } } ``` In the example we create two sections with standard rows, the result is this:
Screenshot of Custom Cells
You could create a form by just setting up the `form` property by yourself without extending from `FormViewController` but this method is typically more convenient. #### Configuring the keyboard navigation accesory To change the behaviour of this you should set the navigation options of your controller. The `FormViewController` has a `navigationOptions` variable which is an enum and can have one or more of the following values: - **disabled**: no view at all - **enabled**: enable view at the bottom - **stopDisabledRow**: if the navigation should stop when the next row is disabled - **skipCanNotBecomeFirstResponderRow**: if the navigation should skip the rows that return false to `canBecomeFirstResponder()` The default value is `enabled & skipCanNotBecomeFirstResponderRow` To enable smooth scrolling to off-screen rows, enable it via the `animateScroll` property. By default, the `FormViewController` jumps immediately between rows when the user hits the next or previous buttons in the keyboard navigation accesory, including when the next row is off screen. To set the amount of space between the keyboard and the highlighted row following a navigation event, set the `rowKeyboardSpacing` property. By default, when the form scrolls to an offscreen view no space will be left between the top of the keyboard and the bottom of the row. ```swift class MyFormViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() form = ... // Enables the navigation accessory and stops navigation when a disabled row is encountered navigationOptions = RowNavigationOptions.Enabled.union(.StopDisabledRow) // Enables smooth scrolling on navigation to off-screen rows animateScroll = true // Leaves 20pt of space between the keyboard and the highlighted row after scrolling to an off screen row rowKeyboardSpacing = 20 } } ``` If you want to change the whole navigation accessory view, you will have to override the `navigationAccessoryView` variable in your subclass of `FormViewController`. ### Getting row values The `Row` object holds a ***value*** of a specific type. For example, a `SwitchRow` holds a `Bool` value, while a `TextRow` holds a `String` value. ```swift // Get the value of a single row let row: TextRow? = form.rowBy(tag: "MyRowTag") let value = row.value // Get the value of all rows which have a Tag assigned // The dictionary contains the 'rowTag':value pairs. let valuesDictionary = form.values() ``` ### Operators Eureka includes custom operators to make form creation easy: #### +++       Add a section ```swift form +++ Section() // Chain it to add multiple Sections form +++ Section("First Section") +++ Section("Another Section") // Or use it with rows and get a blank section for free form +++ TextRow() +++ TextRow() // Each row will be on a separate section ``` #### <<<       Insert a row ```swift form +++ Section() <<< TextRow() <<< DateRow() // Or implicitly create the Section form +++ TextRow() <<< DateRow() ``` #### +=        Append an array ```swift // Append Sections into a Form form += [Section("A"), Section("B"), Section("C")] // Append Rows into a Section section += [TextRow(), DateRow()] ``` ### Result builders Eureka includes result builders to make form creation easy: #### @SectionBuilder ```swift // Section + Section form = (Section("A") +++ { URLRow("UrlRow_f1") { $0.title = "Url" } if something { TwitterRow("TwitterRow_f2") { $0.title = "Twitter" } } else { TwitterRow("TwitterRow_f1") { $0.title = "Twitter" } } AccountRow("AccountRow_f1") { $0.title = "Account" } }) // Form + Section form +++ { if something { PhoneRow("PhoneRow_f1") { $0.title = "Phone" } } else { PhoneRow("PhoneRow_f2") { $0.title = "Phone" } } PasswordRow("PasswordRow_f1") { $0.title = "Password" } } ``` #### @FormBuilder ```swift @FormBuilder var form: Form { Section("Section A") { section in section.tag = "Section_A" } if true { Section("Section B") { section in section.tag = "Section_B" } } NameRow("NameRow_f1") { $0.title = "Name" } } ``` ### Using the callbacks Eureka includes callbacks to change the appearance and behavior of a row. #### Understanding Row and Cell A `Row` is an abstraction Eureka uses which holds a **value** and contains the view `Cell`. The `Cell` manages the view and subclasses `UITableViewCell`. Here is an example: ```swift let row = SwitchRow("SwitchRow") { row in // initializer row.title = "The title" }.onChange { row in row.title = (row.value ?? false) ? "The title expands when on" : "The title" row.updateCell() }.cellSetup { cell, row in cell.backgroundColor = .lightGray }.cellUpdate { cell, row in cell.textLabel?.font = .italicSystemFont(ofSize: 18.0) } ``` Screenshot of Disabled Row #### Callbacks list * **onChange()** Called when the value of a row changes. You might be interested in adjusting some parameters here or even make some other rows appear or disappear. * **onCellSelection()** Called each time the user taps on the row and it gets selected. Note that this will also get called for disabled rows so you should start your code inside this callback with something like `guard !row.isDisabled else { return }` * **cellSetup()** Called only once when the cell is first configured. Set permanent settings here. * **cellUpdate()** Called each time the cell appears on screen. You can change the appearance here using variables that may not be present on cellSetup(). * **onCellHighlightChanged()** Called whenever the cell or any subview become or resign the first responder. * **onRowValidationChanged()** Called whenever the the validation errors associated with a row changes. * **onExpandInlineRow()** Called before expanding the inline row. Applies to rows conforming `InlineRowType` protocol. * **onCollapseInlineRow()** Called before collapsing the inline row. Applies to rows conforming `InlineRowType` protocol. * **onPresent()** Called by a row just before presenting another view controller. Applies to rows conforming `PresenterRowType` protocol. Use it to set up the presented controller. ### Section Header and Footer You can set a title `String` or a custom `View` as the header or footer of a `Section`. #### String title ```swift Section("Title") Section(header: "Title", footer: "Footer Title") Section(footer: "Footer Title") ``` #### Custom view You can use a Custom View from a `.xib` file: ```swift Section() { section in var header = HeaderFooterView(.nibFile(name: "MyHeaderNibFile", bundle: nil)) // Will be called every time the header appears on screen header.onSetupView = { view, _ in // Commonly used to setup texts inside the view // Don't change the view hierarchy or size here! } section.header = header } ``` Or a custom `UIView` created programmatically ```swift Section(){ section in var header = HeaderFooterView(.class) header.height = {100} header.onSetupView = { view, _ in view.backgroundColor = .red } section.header = header } ``` Or just build the view with a Callback ```swift Section(){ section in section.header = { var header = HeaderFooterView(.callback({ let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) view.backgroundColor = .red return view })) header.height = { 100 } return header }() } ``` ### Dynamically hide and show rows (or sections) Screenshot of Hidden Rows In this case we are hiding and showing whole sections. To accomplish this each row has a `hidden` variable of optional type `Condition` which can be set using a function or `NSPredicate`. #### Hiding using a function condition Using the `function` case of `Condition`: ```swift Condition.function([String], (Form)->Bool) ``` The array of `String` to pass should contain the tags of the rows this row depends on. Each time the value of any of those rows changes the function is reevaluated. The function then takes the `Form` and returns a `Bool` indicating whether the row should be hidden or not. This the most powerful way of setting up the `hidden` property as it has no explicit limitations of what can be done. ```swift form +++ Section() <<< SwitchRow("switchRowTag"){ $0.title = "Show message" } <<< LabelRow(){ $0.hidden = Condition.function(["switchRowTag"], { form in return !((form.rowBy(tag: "switchRowTag") as? SwitchRow)?.value ?? false) }) $0.title = "Switch is on!" } ``` Screenshot of Hidden Rows ```swift public enum Condition { case function([String], (Form)->Bool) case predicate(NSPredicate) } ``` #### Hiding using an NSPredicate The `hidden` variable can also be set with a NSPredicate. In the predicate string you can reference values of other rows by their tags to determine if a row should be hidden or visible. This will only work if the values of the rows the predicate has to check are NSObjects (String and Int will work as they are bridged to their ObjC counterparts, but enums won't work). Why could it then be useful to use predicates when they are more limited? Well, they can be much simpler, shorter and readable than functions. Look at this example: ```swift $0.hidden = Condition.predicate(NSPredicate(format: "$switchTag == false")) ``` And we can write it even shorter since `Condition` conforms to `ExpressibleByStringLiteral`: ```swift $0.hidden = "$switchTag == false" ``` *Note: we will substitute the value of the row whose tag is 'switchTag' instead of '$switchTag'* For all of this to work, **all of the implicated rows must have a tag** as the tag will identify them. We can also hide a row by doing: ```swift $0.hidden = true ``` as `Condition` conforms to `ExpressibleByBooleanLiteral`. Not setting the `hidden` variable will leave the row always visible. If you manually set the hidden (or disabled) condition after the form has been displayed you may have to call `row.evaluateHidden()` to force Eureka to reevaluate the new condition. See [this FAQ section](https://github.com/xmartlabs/Eureka#row-does-not-update-after-changing-hidden-or-disabled-condition) for more info. ##### Sections For sections this works just the same. That means we can set up section `hidden` property to show/hide it dynamically. ##### Disabling rows To disable rows, each row has an `disabled` variable which is also an optional `Condition` type property. This variable also works the same as the `hidden` variable so that it requires the rows to have a tag. Note that if you want to disable a row permanently you can also set `disabled` variable to `true`. ### List Sections To display a list of options, Eureka includes a special section called `SelectableSection`. When creating one you need to pass the type of row to use in the options and the `selectionType`. The `selectionType` is an enum which can be either `multipleSelection` or `singleSelection(enableDeselection: Bool)` where the `enableDeselection` parameter determines if the selected rows can be deselected or not. ```swift form +++ SelectableSection>("Where do you live", selectionType: .singleSelection(enableDeselection: true)) let continents = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] for option in continents { form.last! <<< ListCheckRow(option){ listRow in listRow.title = option listRow.selectableValue = option listRow.value = nil } } ``` ##### What kind of rows can be used? To create such a section you have to create a row that conforms the `SelectableRowType` protocol. ```swift public protocol SelectableRowType : RowType { var selectableValue : Value? { get set } } ``` This `selectableValue` is where the value of the row will be permanently stored. The `value` variable will be used to determine if the row is selected or not, being 'selectableValue' if selected or nil otherwise. Eureka includes the `ListCheckRow` which is used for example. In the custom rows of the Examples project you can also find the `ImageCheckRow`. ##### Getting the selected rows To easily get the selected row/s of a `SelectableSection` there are two methods: `selectedRow()` and `selectedRows()` which can be called to get the selected row in case it is a `SingleSelection` section or all the selected rows if it is a `MultipleSelection` section. ##### Grouping options in sections Additionally you can setup list of options to be grouped by sections using following properties of `SelectorViewController`: - `sectionKeyForValue` - a closure that should return key for particular row value. This key is later used to break options by sections. - `sectionHeaderTitleForKey` - a closure that returns header title for a section for particular key. By default returns the key itself. - `sectionFooterTitleForKey` - a closure that returns footer title for a section for particular key. ### Multivalued Sections Eureka supports multiple values for a certain field (such as telephone numbers in a contact) by using Multivalued sections. It allows us to easily create insertable, deletable and reorderable sections. Screenshot of Multivalued Section #### How to create a multivalued section In order to create a multivalued section we have to use `MultivaluedSection` type instead of the regular `Section` type. `MultivaluedSection` extends `Section` and has some additional properties to configure multivalued section behavior. let's dive into a code example... ```swift form +++ MultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], header: "Multivalued TextField", footer: ".Insert adds a 'Add Item' (Add New Tag) button row as last cell.") { $0.addButtonProvider = { section in return ButtonRow(){ $0.title = "Add New Tag" } } $0.multivaluedRowToInsertAt = { index in return NameRow() { $0.placeholder = "Tag Name" } } $0 <<< NameRow() { $0.placeholder = "Tag Name" } } ``` Previous code snippet shows how to create a multivalued section. In this case we want to insert, delete and reorder rows as multivaluedOptions argument indicates. `addButtonProvider` allows us to customize the button row which inserts a new row when tapped and `multivaluedOptions` contains `.Insert` value. `multivaluedRowToInsertAt` closure property is called by Eureka each time a new row needs to be inserted. In order to provide the row to add into multivalued section we should set this property. Eureka passes the index as closure parameter. Notice that we can return any kind of row, even custom rows, even though in most cases multivalued section rows are of the same type. Eureka automatically adds a button row when we create a insertable multivalued section. We can customize how the this button row looks like as we explained before. `showInsertIconInAddButton` property indicates if plus button (insert style) should appear in the left of the button, true by default. There are some considerations we need to have in mind when creating insertable sections. Any row added to the insertable multivalued section should be placed above the row that Eureka automatically adds to insert new rows. This can be easily achieved by adding these additional rows to the section from inside the section's initializer closure (last parameter of section initializer) so then Eureka adds the adds insert button at the end of the section. #### Editing mode By default Eureka will set the tableView's `isEditing` to true only if there is a MultivaluedSection in the form. This will be done in `viewWillAppear` the first time a form is presented. For more information on how to use multivalued sections please take a look at Eureka example project which contains several usage examples. #### Custom add button If you want to use an add button which is not a `ButtonRow` then you can use `GenericMultivaluedSection`, where `AddButtonType` is the type of the row you want to use as add button. This is useful if you want to use a custom row to change the UI of the button. Example: ```swift GenericMultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], { $0.addButtonProvider = { section in return LabelRow(){ $0.title = "A Label row as add button" } } // ... } ``` ### Validations Eureka 2.0.0 introduces the much requested built-in validations feature. A row has a collection of `Rules` and a specific configuration that determines when validation rules should be evaluated. There are some rules provided by default, but you can also create new ones on your own. The provided rules are: * RuleRequired * RuleEmail * RuleURL * RuleGreaterThan, RuleGreaterOrEqualThan, RuleSmallerThan, RuleSmallerOrEqualThan * RuleMinLength, RuleMaxLength * RuleClosure Let's see how to set up the validation rules. ```swift override func viewDidLoad() { super.viewDidLoad() form +++ Section(header: "Required Rule", footer: "Options: Validates on change") <<< TextRow() { $0.title = "Required Rule" $0.add(rule: RuleRequired()) // This could also have been achieved using a closure that returns nil if valid, or a ValidationError otherwise. /* let ruleRequiredViaClosure = RuleClosure { rowValue in return (rowValue == nil || rowValue!.isEmpty) ? ValidationError(msg: "Field required!") : nil } $0.add(rule: ruleRequiredViaClosure) */ $0.validationOptions = .validatesOnChange } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .systemRed } } +++ Section(header: "Email Rule, Required Rule", footer: "Options: Validates on change after blurred") <<< TextRow() { $0.title = "Email Rule" $0.add(rule: RuleRequired()) $0.add(rule: RuleEmail()) $0.validationOptions = .validatesOnChangeAfterBlurred } .cellUpdate { cell, row in if !row.isValid { cell.titleLabel?.textColor = .systemRed } } ``` As you can see in the previous code snippet we can set up as many rules as we want in a row by invoking row's `add(rule:)` function. Row also provides `func remove(ruleWithIdentifier identifier: String)` to remove a rule. In order to use it we must assign an id to the rule after creating it. Sometimes the collection of rules we want to use on a row is the same we want to use on many other rows. In this case we can set up all validation rules using a `RuleSet` which is a collection of validation rules. ```swift var rules = RuleSet() rules.add(rule: RuleRequired()) rules.add(rule: RuleEmail()) let row = TextRow() { $0.title = "Email Rule" $0.add(ruleSet: rules) $0.validationOptions = .validatesOnChangeAfterBlurred } ``` Eureka allows us to specify when validation rules should be evaluated. We can do it by setting up `validationOptions` row's property, which can have the following values: * `.validatesOnChange` - Validates whenever a row value changes. * `.validatesOnBlur` - (Default value) validates right after the cell resigns first responder. Not applicable for all rows. * `.validatesOnChangeAfterBlurred` - Validates whenever the row value changes after it resigns first responder for the first time. * `.validatesOnDemand` - We should manually validate the row or form by invoking `validate()` method. If you want to validate the entire form (all the rows) you can manually invoke Form `validate()` method. #### How to get validation errors Each row has the `validationErrors` property that can be used to retrieve all validation errors. This property just holds the validation error list of the latest row validation execution, which means it doesn't evaluate the validation rules of the row. #### Note on types As expected, the Rules must use the same types as the Row object. Be extra careful to check the row type used. You might see a compiler error ("Incorrect arugment label in call (have 'rule:' expected 'ruleSet:')" that is not pointing to the problem when mixing types. ### Swipe Actions By using swipe actions we can define multiple `leadingSwipe` and `trailingSwipe` actions per row. As swipe actions depend on iOS system features, `leadingSwipe` is available on iOS 11.0+ only. Let's see how to define swipe actions. ```swift let row = TextRow() { let deleteAction = SwipeAction( style: .destructive, title: "Delete", handler: { (action, row, completionHandler) in //add your code here. //make sure you call the completionHandler once done. completionHandler?(true) }) deleteAction.image = UIImage(named: "icon-trash") $0.trailingSwipe.actions = [deleteAction] $0.trailingSwipe.performsFirstActionWithFullSwipe = true //please be aware: `leadingSwipe` is only available on iOS 11+ only let infoAction = SwipeAction( style: .normal, title: "Info", handler: { (action, row, completionHandler) in //add your code here. //make sure you call the completionHandler once done. completionHandler?(true) }) infoAction.actionBackgroundColor = .blue infoAction.image = UIImage(named: "icon-info") $0.leadingSwipe.actions = [infoAction] $0.leadingSwipe.performsFirstActionWithFullSwipe = true } ``` Swipe Actions need `tableView.isEditing` be set to `false`. Eureka will set this to `true` if there is a MultivaluedSection in the form (in the `viewWillAppear`). If you have both MultivaluedSections and swipe actions in the same form you should set `isEditing` according to your needs. ## Custom rows It is very common that you need a row that is different from those included in Eureka. If this is the case you will have to create your own row but this should not be difficult. You can read [this tutorial on how to create custom rows](https://blog.xmartlabs.com/2016/09/06/Eureka-custom-row-tutorial/) to get started. You might also want to have a look at [EurekaCommunity] which includes some extra rows ready to be added to Eureka. ### Basic custom rows To create a row with custom behaviour and appearance you'll probably want to create subclasses of `Row` and `Cell`. Remember that `Row` is the abstraction Eureka uses, while the `Cell` is the actual `UITableViewCell` in charge of the view. As the `Row` contains the `Cell`, both `Row` and `Cell` must be defined for the same **value** type. ```swift // Custom Cell with value type: Bool // The cell is defined using a .xib, so we can set outlets :) public class CustomCell: Cell, CellType { @IBOutlet weak var switchControl: UISwitch! @IBOutlet weak var label: UILabel! public override func setup() { super.setup() switchControl.addTarget(self, action: #selector(CustomCell.switchValueChanged), for: .valueChanged) } func switchValueChanged(){ row.value = switchControl.on row.updateCell() // Re-draws the cell which calls 'update' bellow } public override func update() { super.update() backgroundColor = (row.value ?? false) ? .white : .black } } // The custom Row also has the cell: CustomCell and its correspond value public final class CustomRow: Row, RowType { required public init(tag: String?) { super.init(tag: tag) // We set the cellProvider to load the .xib corresponding to our cell cellProvider = CellProvider(nibName: "CustomCell") } } ``` The result:
Screenshot of Disabled Row
Custom rows need to subclass `Row` and conform to `RowType` protocol. Custom cells need to subclass `Cell` and conform to `CellType` protocol. Just like the callbacks cellSetup and CellUpdate, the `Cell` has the setup and update methods where you can customize it. ### Custom inline rows An inline row is a specific type of row that shows dynamically a row below it, normally an inline row changes between an expanded and collapsed mode whenever the row is tapped. So to create an inline row we need 2 rows, the row that is "always" visible and the row that will expand/collapse. Another requirement is that the value type of these 2 rows must be the same. This means if one row holds a `String` value then the other must have a `String` value too. Once we have these 2 rows, we should make the top row type conform to `InlineRowType`. This protocol requires you to define an `InlineRow` typealias and a `setupInlineRow` function. The `InlineRow` type will be the type of the row that will expand/collapse. Take this as an example: ```swift class PickerInlineRow : Row> where T: Equatable { public typealias InlineRow = PickerRow open var options = [T]() required public init(tag: String?) { super.init(tag: tag) } public func setupInlineRow(_ inlineRow: InlineRow) { inlineRow.options = self.options inlineRow.displayValueFor = self.displayValueFor inlineRow.cell.height = { UITableViewAutomaticDimension } } } ``` The `InlineRowType` will also add some methods to your inline row: ```swift func expandInlineRow() func collapseInlineRow() func toggleInlineRow() ``` These methods should work fine but should you want to override them keep in mind that it is `toggleInlineRow` that has to call `expandInlineRow` and `collapseInlineRow`. Finally you must invoke `toggleInlineRow()` when the row is selected, for example overriding `customDidSelect`: ```swift public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } ``` ### Custom Presenter rows **Note:** *A Presenter row is a row that presents a new UIViewController.* To create a custom Presenter row you must create a class that conforms the `PresenterRowType` protocol. It is highly recommended to subclass `SelectorRow` as it does conform to that protocol and adds other useful functionality. The PresenterRowType protocol is defined as follows: ```swift public protocol PresenterRowType: TypedRowType { associatedtype PresentedControllerType : UIViewController, TypedRowControllerType /// Defines how the view controller will be presented, pushed, etc. var presentationMode: PresentationMode? { get set } /// Will be called before the presentation occurs. var onPresentCallback: ((FormViewController, PresentedControllerType) -> Void)? { get set } } ``` The onPresentCallback will be called when the row is about to present another view controller. This is done in the `SelectorRow` so if you do not subclass it you will have to call it yourself. The `presentationMode` is what defines how the controller is presented and which controller is presented. This presentation can be using a Segue identifier, a segue class, presenting a controller modally or pushing to a specific view controller. For example a CustomPushRow can be defined like this: Let's see an example.. ```swift /// Generic row type where a user must select a value among several options. open class SelectorRow: OptionsRow, PresenterRowType where Cell: BaseCell { /// Defines how the view controller will be presented, pushed, etc. open var presentationMode: PresentationMode>>? /// Will be called before the presentation occurs. open var onPresentCallback: ((FormViewController, SelectorViewController>) -> Void)? required public init(tag: String?) { super.init(tag: tag) } /** Extends `didSelect` method */ open override func customDidSelect() { super.customDidSelect() guard let presentationMode = presentationMode, !isDisabled else { return } if let controller = presentationMode.makeController() { controller.row = self controller.title = selectorTitle ?? controller.title onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } } /** Prepares the pushed row setting its title and completion callback. */ open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as Any as? SelectorViewController> else { return } rowVC.title = selectorTitle ?? rowVC.title rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback onPresentCallback?(cell.formViewController()!, rowVC) rowVC.row = self } } // SelectorRow conforms to PresenterRowType public final class CustomPushRow: SelectorRow>, RowType { public required init(tag: String?) { super.init(tag: tag) presentationMode = .show(controllerProvider: ControllerProvider.callback { return SelectorViewController(){ _ in } }, onDismiss: { vc in _ = vc.navigationController?.popViewController(animated: true) }) } } ``` ### Subclassing cells using the same row Sometimes we want to change the UI look of one of our rows but without changing the row type and all the logic associated to one row. There is currently one way to do this **if you are using cells that are instantiated from nib files**. Currently, none of Eureka's core rows are instantiated from nib files but some of the custom rows in [EurekaCommunity] are, in particular the [PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow) which was moved there. What you have to do is: * Create a nib file containing the cell you want to create. * Then set the class of the cell to be the existing cell you want to modify (if you want to change something more apart from pure UI then you should subclass that cell). Make sure the module of that class is correctly set * Connect the outlets to your class * Tell your row to use the new nib file. This is done by setting the `cellProvider` variable to use this nib. You should do this in the initialiser, either in each concrete instantiation or using the `defaultRowInitializer`. For example: ```swift <<< PostalAddressRow() { $0.cellProvider = CellProvider(nibName: "CustomNib", bundle: Bundle.main) } ``` You could also create a new row for this. In that case try to inherit from the same superclass as the row you want to change to inherit its logic. There are some things to consider when you do this: * If you want to see an example have a look at the [PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow) or the [CreditCardRow](https://github.com/EurekaCommunity/CreditCardRow) which have use a custom nib file in their examples. * If you get an error saying `Unknown class in Interface Builder file`, it might be that you have to instantiate that new type somewhere in your code to load it in the runtime. Calling `let t = YourClass.self` helped in my case. ## Row catalog ### Controls Rows
Label Row


Button Row


Check Row


Switch Row


Slider Row


Stepper Row


Text Area Row


### Field Rows These rows have a textfield on the right side of the cell. The difference between each one of them consists in a different capitalization, autocorrection and keyboard type configuration.
TextRow

NameRow

URLRow

IntRow

PhoneRow

PasswordRow

EmailRow

DecimalRow

TwitterRow

AccountRow

ZipCodeRow
All of the `FieldRow` subtypes above have a `formatter` property of type `NSFormatter` which can be set to determine how that row's value should be displayed. A custom formatter for numbers with two digits after the decimal mark is included with Eureka (`DecimalFormatter`). The Example project also contains a `CurrencyFormatter` which displays a number as currency according to the user's locale. By default, setting a row's `formatter` only affects how a value is displayed when it is not being edited. To also format the value while the row is being edited, set `useFormatterDuringInput` to `true` when initializing the row. Formatting the value as it is being edited may require updating the cursor position and Eureka provides the following protocol that your formatter should conform to in order to handle cursor position: ```swift public protocol FormatterProtocol { func getNewPosition(forPosition forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition } ``` Additionally, `FieldRow` subtypes have a `useFormatterOnDidBeginEditing` property. When using a `DecimalRow` with a formatter that allows decimal values and conforms to the user's locale (e.g. `DecimalFormatter`), if `useFormatterDuringInput` is `false`, `useFormatterOnDidBeginEditing` must be set to `true` so that the decimal mark in the value being edited matches the decimal mark on the keyboard. ### Date Rows Date Rows hold a Date and allow us to set up a new value through UIDatePicker control. The mode of the UIDatePicker and the way how the date picker view is shown is what changes between them.
Date Row
Picker shown in the keyboard.
Date Row (Inline)
The row expands.
Date Row (Picker)
The picker is always visible.
With those 3 styles (Normal, Inline & Picker), Eureka includes: + **DateRow** + **TimeRow** + **DateTimeRow** + **CountDownRow** ### Option Rows These are rows with a list of options associated from which the user must choose. ```swift <<< ActionSheetRow() { $0.title = "ActionSheetRow" $0.selectorTitle = "Pick a number" $0.options = ["One","Two","Three"] $0.value = "Two" // initially selected } ```
Alert Row

Will show an alert with the options to choose from.
ActionSheet Row

Will show an action sheet with the options to choose from.
Push Row

Will push to a new controller from where to choose options listed using Check rows.
Multiple Selector Row

Like PushRow but allows the selection of multiple options.
Segmented Row
Segmented Row (w/Title)
Picker Row

Presents options of a generic type through a picker view
(There is also Picker Inline Row)
### Built your own custom row? Let us know about it, we would be glad to mention it here. :) * **LocationRow** (Included as custom row in the example project) Screenshot of Location Row ## Installation #### CocoaPods [CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. Specify Eureka into your project's `Podfile`: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! pod 'Eureka' ``` Then run the following command: ```bash $ pod install ``` #### Swift Package Manager [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. After you set up your `Package.swift` manifest file, you can add Eureka as a dependency by adding it to the dependencies value of your `Package.swift`. dependencies: [ .package(url: "https://github.com/xmartlabs/Eureka.git", from: "5.5.0") ] #### Carthage [Carthage](https://github.com/Carthage/Carthage) is a simple, decentralized dependency manager for Cocoa. Specify Eureka into your project's `Cartfile`: ```ogdl github "xmartlabs/Eureka" ~> 5.5 ``` #### Manually as Embedded Framework * Clone Eureka as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command from your project root git folder. ```bash $ git submodule add https://github.com/xmartlabs/Eureka.git ``` * Open Eureka folder that was created by the previous git submodule command and drag the Eureka.xcodeproj into the Project Navigator of your application's Xcode project. * Select the Eureka.xcodeproj in the Project Navigator and verify the deployment target matches with your application deployment target. * Select your project in the Xcode Navigation and then select your application target from the sidebar. Next select the "General" tab and click on the + button under the "Embedded Binaries" section. * Select `Eureka.framework` and we are done! ## Getting involved * If you **want to contribute** please feel free to **submit pull requests**. * If you **have a feature request** please **open an issue**. * If you **found a bug** check older issues before submitting an issue. * If you **need help** or would like to **ask general question**, use [StackOverflow]. (Tag `eureka-forms`). **Before contribute check the [CONTRIBUTING](CONTRIBUTING.md) file for more info.** If you use **Eureka** in your app We would love to hear about it! Drop us a line on [twitter]. ## Authors * [Martin Barreto](https://github.com/mtnBarreto) ([@mtnBarreto](https://twitter.com/mtnBarreto)) * [Mathias Claassen](https://github.com/mats-claassen) ([@mClaassen26](https://twitter.com/mClaassen26)) ## FAQ #### How to change the text representation of the row value shown in the cell. Every row has the following property: ```swift /// Block variable used to get the String that should be displayed for the value of this row. public var displayValueFor: ((T?) -> String?)? = { return $0.map { String(describing: $0) } } ``` You can set `displayValueFor` according the string value you want to display. #### How to get a Row using its tag value We can get a particular row by invoking any of the following functions exposed by the `Form` class: ```swift public func rowBy(tag: String) -> RowOf? public func rowBy(tag: String) -> Row? public func rowBy(tag: String) -> BaseRow? ``` For instance: ```swift let dateRow : DateRow? = form.rowBy(tag: "dateRowTag") let labelRow: LabelRow? = form.rowBy(tag: "labelRowTag") let dateRow2: Row? = form.rowBy(tag: "dateRowTag") let labelRow2: BaseRow? = form.rowBy(tag: "labelRowTag") ``` #### How to get a Section using its tag value ```swift let section: Section? = form.sectionBy(tag: "sectionTag") ``` #### How to set the form values using a dictionary Invoking `setValues(values: [String: Any?])` which is exposed by `Form` class. For example: ```swift form.setValues(["IntRowTag": 8, "TextRowTag": "Hello world!", "PushRowTag": Company(name:"Xmartlabs")]) ``` Where `"IntRowTag"`, `"TextRowTag"`, `"PushRowTag"` are row tags (each one uniquely identifies a row) and `8`, `"Hello world!"`, `Company(name:"Xmartlabs")` are the corresponding row value to assign. The value type of a row must match with the value type of the corresponding dictionary value otherwise nil will be assigned. If the form was already displayed we have to reload the visible rows either by reloading the table view `tableView.reloadData()` or invoking `updateCell()` to each visible row. #### Row does not update after changing hidden or disabled condition After setting a condition, this condition is not automatically evaluated. If you want it to do so immediately you can call `.evaluateHidden()` or `.evaluateDisabled()`. This functions are just called when a row is added to the form and when a row it depends on changes. If the condition is changed when the row is being displayed then it must be reevaluated manually. #### onCellUnHighlight doesn't get called unless onCellHighlight is also defined Look at this [issue](https://github.com/xmartlabs/Eureka/issues/96). #### How to update a Section header/footer * Set up a new header/footer data .... ```swift section.header = HeaderFooterView(title: "Header title \(variable)") // use String interpolation //or var header = HeaderFooterView(.class) // most flexible way to set up a header using any view type header.height = { 60 } // height can be calculated header.onSetupView = { view, section in // each time the view is about to be displayed onSetupView is invoked. view.backgroundColor = .orange } section.header = header ``` * Reload the Section to perform the changes ```swift section.reload() ``` #### How to customize Selector and MultipleSelector option cells `selectableRowSetup`, `selectableRowCellUpdate` and `selectableRowCellSetup` properties are provided to be able to customize SelectorViewController and MultipleSelectorViewController selectable cells. ```swift let row = PushRow() { $0.title = "PushRow" $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] $0.value = 👦🏼 $0.selectorTitle = "Choose an Emoji!" }.onPresent { from, to in to.dismissOnSelection = false to.dismissOnChange = false to.selectableRowSetup = { row in row.cellProvider = CellProvider>(nibName: "EmojiCell", bundle: Bundle.main) } to.selectableRowCellUpdate = { cell, row in cell.textLabel?.text = "Text " + row.selectableValue! // customization cell.detailTextLabel?.text = "Detail " + row.selectableValue! } } ``` #### Don't want to use Eureka custom operators? As we've said `Form` and `Section` types conform to `MutableCollection` and `RangeReplaceableCollection`. A Form is a collection of Sections and a Section is a collection of Rows. `RangeReplaceableCollection` protocol extension provides many useful methods to modify collection. ```swift extension RangeReplaceableCollection { public mutating func append(_ newElement: Self.Element) public mutating func append(contentsOf newElements: S) where S : Sequence, Self.Element == S.Element public mutating func insert(_ newElement: Self.Element, at i: Self.Index) public mutating func insert(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element public mutating func remove(at i: Self.Index) -> Self.Element public mutating func removeSubrange(_ bounds: Range) public mutating func removeFirst(_ n: Int) public mutating func removeFirst() -> Self.Element public mutating func removeAll(keepingCapacity keepCapacity: Bool) public mutating func reserveCapacity(_ n: Self.IndexDistance) } ``` These methods are used internally to implement the custom operators as shown bellow: ```swift public func +++(left: Form, right: Section) -> Form { left.append(right) return left } public func +=(inout lhs: Form, rhs: C) where C.Element == Section { lhs.append(contentsOf: rhs) } public func <<<(left: Section, right: BaseRow) -> Section { left.append(right) return left } public func +=(inout lhs: Section, rhs: C) where C.Element == BaseRow { lhs.append(contentsOf: rhs) } ``` You can see how the rest of custom operators are implemented [here](https://github.com/xmartlabs/Eureka/blob/master/Source/Core/Operators.swift). It's up to you to decide if you want to use Eureka custom operators or not. #### How to set up your form from a storyboard The form is always displayed in a `UITableView`. You can set up your view controller in a storyboard and add a UITableView where you want it to be and then connect the outlet to FormViewController's `tableView` variable. This allows you to define a custom frame (possibly with constraints) for your form. All of this can also be done by programmatically changing frame, margins, etc. of the `tableView` of your FormViewController. [Introduction]: #introduction [Requirements]: #requirements [How to create a Form]: #how-to-create-a-form [Getting row values]: #getting-row-values [How to get the form values]: #how-to-get-the-form-values [Examples]: #examples [Usage]: #usage [Operators]: #operators [Rows]: #rows [Using the callbacks]: #using-the-callbacks [Section Header and Footer]: #section-header-and-footer [Custom rows]: #custom-rows [Basic custom rows]: #basic-custom-rows [Custom inline rows]: #custom-inline-rows [Custom presenter rows]: #custom-presenter-rows [How to create custom inline rows]: #how-to-create-custom-inline-rows [Custom rows catalog]: #custom-rows-catalog [Dynamically hide and show rows (or sections)]: #hide-show-rows [Implementing a custom Presenter row]: #custom-presenter-row [Extensibility]: #extensibility [Row catalog]: #row-catalog [Installation]: #installation [FAQ]: #faq [List sections]: #list-sections [Multivalued sections]: #multivalued-sections [Validations]: #validations [Swipe Actions]: #swipe-actions [CustomCellsController]: Example/Example/ViewController.swift [FormViewController]: Example/Source/Controllers.swift [XLForm]: https://github.com/xmartlabs/XLForm [DSL]: https://en.wikipedia.org/wiki/Domain-specific_language [StackOverflow]: http://stackoverflow.com/questions/tagged/eureka-forms [our blog post]: http://blog.xmartlabs.com/2015/09/29/Introducing-Eureka-iOS-form-library-written-in-pure-Swift/ [twitter]: https://twitter.com/xmartlabs [EurekaCommunity]: https://github.com/EurekaCommunity # Donate to Eureka So we can make Eureka even better!

[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HRMAH7WZ4QQ8E) # Change Log This can be found in the [CHANGELOG.md](CHANGELOG.md) file. ================================================ FILE: Source/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Source/Core/BaseRow.swift ================================================ // BaseRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class BaseRow: BaseRowType { var callbackOnChange: (() -> Void)? var callbackCellUpdate: (() -> Void)? var callbackCellSetup: Any? var callbackCellOnSelection: (() -> Void)? var callbackOnExpandInlineRow: Any? var callbackOnCollapseInlineRow: Any? var callbackOnCellHighlightChanged: (() -> Void)? var callbackOnRowValidationChanged: (() -> Void)? var _inlineRow: BaseRow? var _cachedOptionsData: Any? public var validationOptions: ValidationOptions = .validatesOnBlur // validation state public internal(set) var validationErrors = [ValidationError]() { didSet { guard validationErrors != oldValue else { return } RowDefaults.onRowValidationChanged["\(type(of: self))"]?(baseCell, self) callbackOnRowValidationChanged?() updateCell() } } public internal(set) var wasBlurred = false public internal(set) var wasChanged = false public var isValid: Bool { return validationErrors.isEmpty } public var isHighlighted: Bool = false /// The title will be displayed in the textLabel of the row. public var title: String? /// Parameter used when creating the cell for this row. public var cellStyle = UITableViewCell.CellStyle.value1 /// String that uniquely identifies a row. Must be unique among rows and sections. public var tag: String? /// The untyped cell associated to this row. public var baseCell: BaseCell! { return nil } /// The untyped value of this row. public var baseValue: Any? { set {} get { return nil } } open func validate(quietly: Bool = false) -> [ValidationError] { return [] } // Reset validation open func cleanValidationErrors() { validationErrors = [] } public static var estimatedRowHeight: CGFloat = 44.0 /// Condition that determines if the row should be disabled or not. public var disabled: Condition? { willSet { removeFromDisabledRowObservers() } didSet { addToDisabledRowObservers() } } /// Condition that determines if the row should be hidden or not. public var hidden: Condition? { willSet { removeFromHiddenRowObservers() } didSet { addToHiddenRowObservers() } } /// Returns if this row is currently disabled or not public var isDisabled: Bool { return disabledCache } /// Returns if this row is currently hidden or not public var isHidden: Bool { return hiddenCache } /// The section to which this row belongs. open weak var section: Section? public lazy var trailingSwipe = {[unowned self] in SwipeConfiguration(self)}() //needs the accessor because if marked directly this throws "Stored properties cannot be marked potentially unavailable with '@available'" private lazy var _leadingSwipe = {[unowned self] in SwipeConfiguration(self)}() @available(iOS 11,*) public var leadingSwipe: SwipeConfiguration{ get { return self._leadingSwipe } set { self._leadingSwipe = newValue } } public required init(tag: String? = nil) { self.tag = tag } /** Method that reloads the cell */ open func updateCell() {} /** Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. */ open func didSelect() {} open func prepare(for segue: UIStoryboardSegue) {} /** Helps to pick destination part of the cell after scrolling */ open var destinationScrollPosition: UITableView.ScrollPosition? = UITableView.ScrollPosition.bottom /** Returns the IndexPath where this row is in the current form. */ public final var indexPath: IndexPath? { guard let sectionIndex = section?.index, let rowIndex = section?.firstIndex(of: self) else { return nil } return IndexPath(row: rowIndex, section: sectionIndex) } var hiddenCache = false var disabledCache = false { willSet { if newValue && !disabledCache { baseCell.cellResignFirstResponder() } } } } extension BaseRow { /** Evaluates if the row should be hidden or not and updates the form accordingly */ public final func evaluateHidden() { guard let h = hidden, let form = section?.form else { return } switch h { case .function(_, let callback): hiddenCache = callback(form) case .predicate(let predicate): hiddenCache = predicate.evaluate(with: self, substitutionVariables: form.dictionaryValuesToEvaluatePredicate()) } if hiddenCache { section?.hide(row: self) } else { section?.show(row: self) } } /** Evaluates if the row should be disabled or not and updates it accordingly */ public final func evaluateDisabled() { guard let d = disabled, let form = section?.form else { return } switch d { case .function(_, let callback): disabledCache = callback(form) case .predicate(let predicate): disabledCache = predicate.evaluate(with: self, substitutionVariables: form.dictionaryValuesToEvaluatePredicate()) } updateCell() } final func wasAddedTo(section: Section) { self.section = section if let t = tag { assert(section.form?.rowsByTag[t] == nil, "Duplicate tag \(t)") self.section?.form?.rowsByTag[t] = self self.section?.form?.tagToValues[t] = baseValue != nil ? baseValue! : NSNull() } addToRowObservers() evaluateHidden() evaluateDisabled() } final func addToHiddenRowObservers() { guard let h = hidden else { return } switch h { case .function(let tags, _): section?.form?.addRowObservers(to: self, rowTags: tags, type: .hidden) case .predicate(let predicate): section?.form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .hidden) } } final func addToDisabledRowObservers() { guard let d = disabled else { return } switch d { case .function(let tags, _): section?.form?.addRowObservers(to: self, rowTags: tags, type: .disabled) case .predicate(let predicate): section?.form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .disabled) } } final func addToRowObservers() { addToHiddenRowObservers() addToDisabledRowObservers() } final func willBeRemovedFromForm() { (self as? BaseInlineRowType)?.collapseInlineRow() if let t = tag { section?.form?.rowsByTag[t] = nil section?.form?.tagToValues[t] = nil } removeFromRowObservers() } final func willBeRemovedFromSection() { willBeRemovedFromForm() section = nil } final func removeFromHiddenRowObservers() { guard let h = hidden else { return } switch h { case .function(let tags, _): section?.form?.removeRowObservers(from: self, rowTags: tags, type: .hidden) case .predicate(let predicate): section?.form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .hidden) } } final func removeFromDisabledRowObservers() { guard let d = disabled else { return } switch d { case .function(let tags, _): section?.form?.removeRowObservers(from: self, rowTags: tags, type: .disabled) case .predicate(let predicate): section?.form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .disabled) } } final func removeFromRowObservers() { removeFromHiddenRowObservers() removeFromDisabledRowObservers() } } extension BaseRow: Equatable, Hidable, Disableable {} extension BaseRow { public func reload(with rowAnimation: UITableView.RowAnimation = .none) { guard let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView, let indexPath = indexPath else { return } tableView.reloadRows(at: [indexPath], with: rowAnimation) } public func deselect(animated: Bool = true) { guard let indexPath = indexPath, let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView else { return } tableView.deselectRow(at: indexPath, animated: animated) } public func select(animated: Bool = false, scrollPosition: UITableView.ScrollPosition = .none) { guard let indexPath = indexPath, let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView else { return } tableView.selectRow(at: indexPath, animated: animated, scrollPosition: scrollPosition) } } public func == (lhs: BaseRow, rhs: BaseRow) -> Bool { return lhs === rhs } ================================================ FILE: Source/Core/Cell.swift ================================================ // Cell.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// Base class for the Eureka cells @objc(EurekaBaseCell) open class BaseCell: UITableViewCell, BaseCellType { /// Untyped row associated to this cell. public var baseRow: BaseRow! { return nil } /// Block that returns the height for this cell. public var height: (() -> CGFloat)? public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } public required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } /** Function that returns the FormViewController this cell belongs to. */ public func formViewController() -> FormViewController? { var responder: UIResponder? = self while responder != nil { if let formVC = responder as? FormViewController { return formVC } responder = responder?.next } return nil } open func setup() {} open func update() {} open func didSelect() {} /** If the cell can become first responder. By default returns false */ open func cellCanBecomeFirstResponder() -> Bool { return false } /** Called when the cell becomes first responder */ @discardableResult open func cellBecomeFirstResponder(withDirection: Direction = .down) -> Bool { return becomeFirstResponder() } /** Called when the cell resigns first responder */ @discardableResult open func cellResignFirstResponder() -> Bool { return resignFirstResponder() } } /// Generic class that represents the Eureka cells. open class Cell: BaseCell, TypedCellType where T: Equatable { public typealias Value = T /// The row associated to this cell public weak var row: RowOf! private var updatingCellForTintColorDidChange = false /// Returns the navigationAccessoryView if it is defined or calls super if not. override open var inputAccessoryView: UIView? { if let v = formViewController()?.inputAccessoryView(for: row) { return v } return super.inputAccessoryView } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } /** Function responsible for setting up the cell at creation time. */ open override func setup() { super.setup() } /** Function responsible for updating the cell each time it is reloaded. */ open override func update() { super.update() textLabel?.text = row.title if #available(iOS 13.0, *) { textLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label } else { textLabel?.textColor = row.isDisabled ? .gray : .black } detailTextLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText } /** Called when the cell was selected. */ open override func didSelect() {} override open var canBecomeFirstResponder: Bool { return false } open override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() if result { formViewController()?.beginEditing(of: self) } return result } open override func resignFirstResponder() -> Bool { let result = super.resignFirstResponder() if result { formViewController()?.endEditing(of: self) } return result } open override func tintColorDidChange() { super.tintColorDidChange() /* Protection from infinite recursion in case an update method changes the tintColor */ if !updatingCellForTintColorDidChange && row != nil { updatingCellForTintColorDidChange = true row.updateCell() updatingCellForTintColorDidChange = false } } /// The untyped row associated to this cell. public override var baseRow: BaseRow! { return row } } ================================================ FILE: Source/Core/CellType.swift ================================================ // CellType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: Cell Protocols public protocol BaseCellType : AnyObject { /// Method that will return the height of the cell var height : (() -> CGFloat)? { get } /** Method called once when creating a cell. Responsible for setting up the cell. */ func setup() /** Method called each time the cell is updated (e.g. 'cellForRowAtIndexPath' is called). Responsible for updating the cell. */ func update() /** Method called each time the cell is selected (tapped on by the user). */ func didSelect() /** Called when cell is about to become first responder - returns: If the cell should become first responder. */ func cellCanBecomeFirstResponder() -> Bool /** Method called when the cell becomes first responder */ func cellBecomeFirstResponder(withDirection: Direction) -> Bool /** Method called when the cell resigns first responder */ func cellResignFirstResponder() -> Bool /** A reference to the controller in which the cell is displayed. */ func formViewController () -> FormViewController? } public protocol TypedCellType: BaseCellType { associatedtype Value: Equatable /// The row associated to this cell. var row: RowOf! { get set } } public protocol CellType: TypedCellType {} ================================================ FILE: Source/Core/Core.swift ================================================ // Core.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: Row internal class RowDefaults { static var cellUpdate = [String: (BaseCell, BaseRow) -> Void]() static var cellSetup = [String: (BaseCell, BaseRow) -> Void]() static var onCellHighlightChanged = [String: (BaseCell, BaseRow) -> Void]() static var rowInitialization = [String: (BaseRow) -> Void]() static var onRowValidationChanged = [String: (BaseCell, BaseRow) -> Void]() static var rawCellUpdate = [String: Any]() static var rawCellSetup = [String: Any]() static var rawOnCellHighlightChanged = [String: Any]() static var rawRowInitialization = [String: Any]() static var rawOnRowValidationChanged = [String: Any]() } // MARK: FormCells public struct CellProvider where Cell: CellType { /// Nibname of the cell that will be created. public private (set) var nibName: String? /// Bundle from which to get the nib file. public private (set) var bundle: Bundle! public init() {} public init(nibName: String, bundle: Bundle? = nil) { self.nibName = nibName self.bundle = bundle ?? Bundle(for: Cell.self) } /** Creates the cell with the specified style. - parameter cellStyle: The style with which the cell will be created. - returns: the cell */ func makeCell(style: UITableViewCell.CellStyle) -> Cell { if let nibName = self.nibName { return bundle.loadNibNamed(nibName, owner: nil, options: nil)!.first as! Cell } return Cell.init(style: style, reuseIdentifier: nil) } } /** Enumeration that defines how a controller should be created. - Callback->VCType: Creates the controller inside the specified block - NibFile: Loads a controller from a nib file in some bundle - StoryBoard: Loads the controller from a Storyboard by its storyboard id */ public enum ControllerProvider { /** * Creates the controller inside the specified block */ case callback(builder: (() -> VCType)) /** * Loads a controller from a nib file in some bundle */ case nibFile(name: String, bundle: Bundle?) /** * Loads the controller from a Storyboard by its storyboard id */ case storyBoard(storyboardId: String, storyboardName: String, bundle: Bundle?) func makeController() -> VCType { switch self { case .callback(let builder): return builder() case .nibFile(let nibName, let bundle): return VCType.init(nibName: nibName, bundle:bundle ?? Bundle(for: VCType.self)) case .storyBoard(let storyboardId, let storyboardName, let bundle): let sb = UIStoryboard(name: storyboardName, bundle: bundle ?? Bundle(for: VCType.self)) return sb.instantiateViewController(withIdentifier: storyboardId) as! VCType } } } /** Defines how a controller should be presented. - Show?: Shows the controller with `showViewController(...)`. - PresentModally?: Presents the controller modally. - SegueName?: Performs the segue with the specified identifier (name). - SegueClass?: Performs a segue from a segue class. */ public enum PresentationMode { /** * Shows the controller, created by the specified provider, with `showViewController(...)`. */ case show(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) /** * Presents the controller, created by the specified provider, modally. */ case presentModally(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) /** * Performs the segue with the specified identifier (name). */ case segueName(segueName: String, onDismiss: ((UIViewController) -> Void)?) /** * Performs a segue from a segue class. */ case segueClass(segueClass: UIStoryboardSegue.Type, onDismiss: ((UIViewController) -> Void)?) case popover(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) public var onDismissCallback: ((UIViewController) -> Void)? { switch self { case .show(_, let completion): return completion case .presentModally(_, let completion): return completion case .segueName(_, let completion): return completion case .segueClass(_, let completion): return completion case .popover(_, let completion): return completion } } /** Present the view controller provided by PresentationMode. Should only be used from custom row implementation. - parameter viewController: viewController to present if it makes sense (normally provided by makeController method) - parameter row: associated row - parameter presentingViewController: form view controller */ public func present(_ viewController: VCType!, row: BaseRow, presentingController: FormViewController) { switch self { case .show(_, _): presentingController.show(viewController, sender: row) case .presentModally(_, _): presentingController.present(viewController, animated: true) case .segueName(let segueName, _): presentingController.performSegue(withIdentifier: segueName, sender: row) case .segueClass(let segueClass, _): let segue = segueClass.init(identifier: row.tag, source: presentingController, destination: viewController) presentingController.prepare(for: segue, sender: row) segue.perform() case .popover(_, _): guard let porpoverController = viewController.popoverPresentationController else { fatalError() } porpoverController.sourceView = porpoverController.sourceView ?? presentingController.tableView presentingController.present(viewController, animated: true) } } /** Creates the view controller specified by presentation mode. Should only be used from custom row implementation. - returns: the created view controller or nil depending on the PresentationMode type. */ public func makeController() -> VCType? { switch self { case .show(let controllerProvider, let completionCallback): let controller = controllerProvider.makeController() let completionController = controller as? RowControllerType if let callback = completionCallback { completionController?.onDismissCallback = callback } return controller case .presentModally(let controllerProvider, let completionCallback): let controller = controllerProvider.makeController() let completionController = controller as? RowControllerType if let callback = completionCallback { completionController?.onDismissCallback = callback } return controller case .popover(let controllerProvider, let completionCallback): let controller = controllerProvider.makeController() controller.modalPresentationStyle = .popover let completionController = controller as? RowControllerType if let callback = completionCallback { completionController?.onDismissCallback = callback } return controller default: return nil } } } /** * Protocol to be implemented by custom formatters. */ public protocol FormatterProtocol { func getNewPosition(forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition } // MARK: Predicate Machine enum ConditionType { case hidden, disabled } /** Enumeration that are used to specify the disbaled and hidden conditions of rows - Function: A function that calculates the result - Predicate: A predicate that returns the result */ public enum Condition { /** * Calculate the condition inside a block * * @param Array of tags of the rows this function depends on * @param Form->Bool The block that calculates the result * * @return If the condition is true or false */ case function([String], (Form)->Bool) /** * Calculate the condition using a NSPredicate * * @param NSPredicate The predicate that will be evaluated * * @return If the condition is true or false */ case predicate(NSPredicate) } extension Condition : ExpressibleByBooleanLiteral { /** Initialize a condition to return afixed boolean value always */ public init(booleanLiteral value: Bool) { self = Condition.function([]) { _ in return value } } } extension Condition : ExpressibleByStringLiteral { /** Initialize a Condition with a string that will be converted to a NSPredicate */ public init(stringLiteral value: String) { self = .predicate(NSPredicate(format: value)) } /** Initialize a Condition with a string that will be converted to a NSPredicate */ public init(unicodeScalarLiteral value: String) { self = .predicate(NSPredicate(format: value)) } /** Initialize a Condition with a string that will be converted to a NSPredicate */ public init(extendedGraphemeClusterLiteral value: String) { self = .predicate(NSPredicate(format: value)) } } // MARK: Errors /** Errors thrown by Eureka - duplicatedTag: When a section or row is inserted whose tag dows already exist - rowNotInSection: When a row was expected to be in a Section, but is not. */ public enum EurekaError: Error { case duplicatedTag(tag: String) case rowNotInSection(row: BaseRow) } //Mark: FormViewController /** * A protocol implemented by FormViewController */ public protocol FormViewControllerProtocol { var tableView: UITableView! { get } func beginEditing(of: Cell) func endEditing(of: Cell) func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation } /** * Navigation options for a form view controller. */ public struct RowNavigationOptions: OptionSet { private enum NavigationOptions: Int { case disabled = 0, enabled = 1, stopDisabledRow = 2, skipCanNotBecomeFirstResponderRow = 4 } public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue} private init(_ options: NavigationOptions ) { self.rawValue = options.rawValue } /// No navigation. public static let Disabled = RowNavigationOptions(.disabled) /// Full navigation. public static let Enabled = RowNavigationOptions(.enabled) /// Break navigation when next row is disabled. public static let StopDisabledRow = RowNavigationOptions(.stopDisabledRow) /// Break navigation when next row cannot become first responder. public static let SkipCanNotBecomeFirstResponderRow = RowNavigationOptions(.skipCanNotBecomeFirstResponderRow) } /** * Defines the configuration for the keyboardType of FieldRows. */ public struct KeyboardReturnTypeConfiguration { /// Used when the next row is available. public var nextKeyboardType = UIReturnKeyType.next /// Used if next row is not available. public var defaultKeyboardType = UIReturnKeyType.default public init() {} public init(nextKeyboardType: UIReturnKeyType, defaultKeyboardType: UIReturnKeyType) { self.nextKeyboardType = nextKeyboardType self.defaultKeyboardType = defaultKeyboardType } } /** * Options that define when an inline row should collapse. */ public struct InlineRowHideOptions: OptionSet { private enum _InlineRowHideOptions: Int { case never = 0, anotherInlineRowIsShown = 1, firstResponderChanges = 2 } public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue} private init(_ options: _InlineRowHideOptions ) { self.rawValue = options.rawValue } /// Never collapse automatically. Only when user taps inline row. public static let Never = InlineRowHideOptions(.never) /// Collapse qhen another inline row expands. Just one inline row will be expanded at a time. public static let AnotherInlineRowIsShown = InlineRowHideOptions(.anotherInlineRowIsShown) /// Collapse when first responder changes. public static let FirstResponderChanges = InlineRowHideOptions(.firstResponderChanges) } /// View controller that shows a form. @objc(EurekaFormViewController) open class FormViewController: UIViewController, FormViewControllerProtocol, FormDelegate { @IBOutlet public var tableView: UITableView! private lazy var _form: Form = { [weak self] in let form = Form() form.delegate = self return form }() public var form: Form { get { return _form } set { guard form !== newValue else { return } _form.delegate = nil tableView?.endEditing(false) _form = newValue _form.delegate = self if isViewLoaded { tableView?.reloadData() } } } /// Extra space to leave between between the row in focus and the keyboard open var rowKeyboardSpacing: CGFloat = 0 /// Enables animated scrolling on row navigation open var animateScroll = false /// The default scroll position on the focussed cell when keyboard appears open var defaultScrollPosition = UITableView.ScrollPosition.none /// Accessory view that is responsible for the navigation between rows private var navigationAccessoryView: (UIView & NavigationAccessory)! /// Custom Accesory View to be used as a replacement open var customNavigationAccessoryView: (UIView & NavigationAccessory)? { return nil } /// Defines the behaviour of the navigation between rows public var navigationOptions: RowNavigationOptions? public var tableViewStyle: UITableView.Style = .grouped public init(style: UITableView.Style) { super.init(nibName: nil, bundle: nil) tableViewStyle = style } public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func viewDidLoad() { super.viewDidLoad() navigationAccessoryView = customNavigationAccessoryView ?? NavigationAccessoryView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 44.0)) navigationAccessoryView.autoresizingMask = .flexibleWidth if tableView == nil { tableView = UITableView(frame: view.bounds, style: tableViewStyle) tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] tableView.cellLayoutMarginsFollowReadableWidth = false } if tableView.superview == nil { view.addSubview(tableView) } if tableView.delegate == nil { tableView.delegate = self } if tableView.dataSource == nil { tableView.dataSource = self } tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = BaseRow.estimatedRowHeight tableView.allowsSelectionDuringEditing = true } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) animateTableView = true let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? [] if !selectedIndexPaths.isEmpty { if #available(iOS 13.0, *) { if tableView.window != nil { tableView.reloadRows(at: selectedIndexPaths, with: .none) } } else { tableView.reloadRows(at: selectedIndexPaths, with: .none) } } selectedIndexPaths.forEach { tableView.selectRow(at: $0, animated: false, scrollPosition: .none) } let deselectionAnimation = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in selectedIndexPaths.forEach { self?.tableView.deselectRow(at: $0, animated: context.isAnimated) } } let reselection = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in if context.isCancelled { selectedIndexPaths.forEach { self?.tableView.selectRow(at: $0, animated: false, scrollPosition: .none) } } } if let coordinator = transitionCoordinator { coordinator.animate(alongsideTransition: deselectionAnimation, completion: reselection) } else { selectedIndexPaths.forEach { tableView.deselectRow(at: $0, animated: false) } } NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) if form.containsMultivaluedSection && (isBeingPresented || isMovingToParent) { tableView.setEditing(true, animated: false) } } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) } open override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) let baseRow = sender as? BaseRow baseRow?.prepare(for: segue) } /** Returns the navigation accessory view if it is enabled. Returns nil otherwise. */ open func inputAccessoryView(for row: BaseRow) -> UIView? { let options = navigationOptions ?? Form.defaultNavigationOptions guard options.contains(.Enabled) else { return nil } guard row.baseCell.cellCanBecomeFirstResponder() else { return nil} navigationAccessoryView.previousEnabled = nextRow(for: row, withDirection: .up) != nil navigationAccessoryView.doneClosure = { [weak self] in self?.navigationDone() } navigationAccessoryView.previousClosure = { [weak self] in self?.navigationPrevious() } navigationAccessoryView.nextClosure = { [weak self] in self?.navigationNext() } navigationAccessoryView.nextEnabled = nextRow(for: row, withDirection: .down) != nil return navigationAccessoryView } // MARK: FormViewControllerProtocol /** Called when a cell becomes first responder */ public final func beginEditing(of cell: Cell) { cell.row.isHighlighted = true cell.row.updateCell() RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row) cell.row.callbackOnCellHighlightChanged?() guard let _ = tableView, (form.inlineRowHideOptions ?? Form.defaultInlineRowHideOptions).contains(.FirstResponderChanges) else { return } let row = cell.baseRow let inlineRow = row?._inlineRow for row in form.allRows.filter({ $0 !== row && $0 !== inlineRow && $0._inlineRow != nil }) { if let inlineRow = row as? BaseInlineRowType { inlineRow.collapseInlineRow() } } } /** Called when a cell resigns first responder */ public final func endEditing(of cell: Cell) { cell.row.isHighlighted = false cell.row.wasBlurred = true RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row) cell.row.callbackOnCellHighlightChanged?() if cell.row.validationOptions.contains(.validatesOnBlur) || (cell.row.wasChanged && cell.row.validationOptions.contains(.validatesOnChangeAfterBlurred)) { cell.row.validate() } cell.row.updateCell() } /** Returns the animation for the insertion of the given rows. */ open func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .fade } /** Returns the animation for the deletion of the given rows. */ open func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .fade } /** Returns the animation for the reloading of the given rows. */ open func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation { return .automatic } /** Returns the animation for the insertion of the given sections. */ open func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .automatic } /** Returns the animation for the deletion of the given sections. */ open func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .automatic } /** Returns the animation for the reloading of the given sections. */ open func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation { return .automatic } // MARK: TextField and TextView Delegate open func textInputShouldBeginEditing(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputDidBeginEditing(_ textInput: UITextInput, cell: Cell) { if let row = cell.row as? KeyboardReturnHandler { let next = nextRow(for: cell.row, withDirection: .down) if let textField = textInput as? UITextField { textField.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ?? (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) : (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ?? Form.defaultKeyboardReturnType.defaultKeyboardType)) } else if let textView = textInput as? UITextView { textView.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ?? (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) : (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ?? Form.defaultKeyboardReturnType.defaultKeyboardType)) } } } open func textInputShouldEndEditing(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputDidEndEditing(_ textInput: UITextInput, cell: Cell) { } open func textInput(_ textInput: UITextInput, shouldChangeCharactersInRange range: NSRange, replacementString string: String, cell: Cell) -> Bool { return true } open func textInputShouldClear(_ textInput: UITextInput, cell: Cell) -> Bool { return true } open func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> Bool { if let nextRow = nextRow(for: cell.row, withDirection: .down) { if nextRow.baseCell.cellCanBecomeFirstResponder() { nextRow.baseCell.cellBecomeFirstResponder() return true } } tableView?.endEditing(true) return true } // MARK: FormDelegate open func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?) {} // MARK: UITableViewDelegate @objc open func tableView(_ tableView: UITableView, willBeginReorderingRowAtIndexPath indexPath: IndexPath) { // end editing if inline cell is first responder let row = form[indexPath] if let inlineRow = row as? BaseInlineRowType, row._inlineRow != nil { inlineRow.collapseInlineRow() } } // MARK: FormDelegate open func sectionsHaveBeenAdded(_ sections: [Section], at indexes: IndexSet) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.insertSections(indexes, with: insertAnimation(forSections: sections)) tableView?.endUpdates() } open func sectionsHaveBeenRemoved(_ sections: [Section], at indexes: IndexSet) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.deleteSections(indexes, with: deleteAnimation(forSections: sections)) tableView?.endUpdates() } open func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at indexes: IndexSet) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.reloadSections(indexes, with: reloadAnimation(oldSections: oldSections, newSections: newSections)) tableView?.endUpdates() } open func rowsHaveBeenAdded(_ rows: [BaseRow], at indexes: [IndexPath]) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.insertRows(at: indexes, with: insertAnimation(forRows: rows)) tableView?.endUpdates() } open func rowsHaveBeenRemoved(_ rows: [BaseRow], at indexes: [IndexPath]) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.deleteRows(at: indexes, with: deleteAnimation(forRows: rows)) tableView?.endUpdates() } open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at indexes: [IndexPath]) { guard animateTableView else { return } tableView?.beginUpdates() tableView?.reloadRows(at: indexes, with: reloadAnimation(oldRows: oldRows, newRows: newRows)) tableView?.endUpdates() } // MARK: Private var oldBottomInset: CGFloat? var animateTableView = false /** Calculates the height needed for a header or footer. */ fileprivate func height(specifiedHeight: (() -> CGFloat)?, sectionView: UIView?, sectionTitle: String?) -> CGFloat { if let height = specifiedHeight { return height() } if let sectionView = sectionView { let height = sectionView.bounds.height if height == 0 { return UITableView.automaticDimension } return height } if let sectionTitle = sectionTitle, sectionTitle != "" { return UITableView.automaticDimension } // Fix for iOS 11+. By returning 0, we ensure that no section header or // footer is shown when self-sizing is enabled (i.e. when // tableView.estimatedSectionHeaderHeight or tableView.estimatedSectionFooterHeight // == UITableView.automaticDimension). if tableView.style == .plain { return 0 } return UITableView.automaticDimension } } extension FormViewController : UITableViewDelegate { // MARK: UITableViewDelegate open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { return indexPath } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard tableView == self.tableView else { return } let row = form[indexPath] // row.baseCell.cellBecomeFirstResponder() may be cause InlineRow collapsed then section count will be changed. Use orignal indexPath will out of section's bounds. if !row.baseCell.cellCanBecomeFirstResponder() || !row.baseCell.cellBecomeFirstResponder() { self.tableView?.endEditing(true) } row.didSelect() } open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { guard tableView == self.tableView else { return tableView.rowHeight } let row = form[indexPath.section][indexPath.row] return row.baseCell.height?() ?? tableView.rowHeight } open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { guard tableView == self.tableView else { return tableView.estimatedRowHeight } let row = form[indexPath.section][indexPath.row] return row.baseCell.height?() ?? tableView.estimatedRowHeight } open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { return form[section].header?.viewForSection(form[section], type: .header) } open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return form[section].footer?.viewForSection(form[section], type:.footer) } open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return height(specifiedHeight: form[section].header?.height, sectionView: self.tableView(tableView, viewForHeaderInSection: section), sectionTitle: self.tableView(tableView, titleForHeaderInSection: section)) } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return height(specifiedHeight: form[section].footer?.height, sectionView: self.tableView(tableView, viewForFooterInSection: section), sectionTitle: self.tableView(tableView, titleForFooterInSection: section)) } open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { let row = form[indexPath] guard !row.isDisabled else { return false } if row.trailingSwipe.actions.count > 0 { return true } if #available(iOS 11,*), row.leadingSwipe.actions.count > 0 { return true } guard let section = form[indexPath.section] as? BaseMultivaluedSection else { return false } guard !(indexPath.row == section.count - 1 && section.multivaluedOptions.contains(.Insert) && section.showInsertIconInAddButton) else { return true } if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil { return false } return true } open func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { let row = form[indexPath] let section = row.section! if let _ = row.baseCell.findFirstResponder() { tableView.endEditing(true) } section.remove(at: indexPath.row) } else if editingStyle == .insert { guard var section = form[indexPath.section] as? BaseMultivaluedSection else { return } guard let multivaluedRowToInsertAt = section.multivaluedRowToInsertAt else { fatalError("Multivalued section multivaluedRowToInsertAt property must be set up") } let newRow = multivaluedRowToInsertAt(max(0, section.count - 1)) section.insert(newRow, at: max(0, section.count - 1)) DispatchQueue.main.async { tableView.isEditing = !tableView.isEditing tableView.isEditing = !tableView.isEditing } tableView.scrollToRow(at: IndexPath(row: section.count - 1, section: indexPath.section), at: .bottom, animated: true) if newRow.baseCell.cellCanBecomeFirstResponder() { newRow.baseCell.cellBecomeFirstResponder() } else if let inlineRow = newRow as? BaseInlineRowType { inlineRow.expandInlineRow() } } } open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { guard let section = form[indexPath.section] as? BaseMultivaluedSection, section.multivaluedOptions.contains(.Reorder) && section.count > 1 else { return false } if section.multivaluedOptions.contains(.Insert) && (section.count <= 2 || indexPath.row == (section.count - 1)) { return false } if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil { return false } return true } open func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { guard let section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return sourceIndexPath } guard sourceIndexPath.section == proposedDestinationIndexPath.section else { return sourceIndexPath } let destRow = form[proposedDestinationIndexPath] if destRow is BaseInlineRowType && destRow._inlineRow != nil { return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section) } if proposedDestinationIndexPath.row > 0 { let previousRow = form[IndexPath(row: proposedDestinationIndexPath.row - 1, section: proposedDestinationIndexPath.section)] if previousRow is BaseInlineRowType && previousRow._inlineRow != nil { return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section) } } if section.multivaluedOptions.contains(.Insert) && proposedDestinationIndexPath.row == section.count - 1 { return IndexPath(row: section.count - 2, section: sourceIndexPath.section) } return proposedDestinationIndexPath } open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { guard var section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return } if sourceIndexPath.row < section.count && destinationIndexPath.row < section.count && sourceIndexPath.row != destinationIndexPath.row { let sourceRow = form[sourceIndexPath] animateTableView = false section.remove(at: sourceIndexPath.row) section.insert(sourceRow, at: destinationIndexPath.row) animateTableView = true // update the accessory view let _ = inputAccessoryView(for: sourceRow) } } open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { guard let section = form[indexPath.section] as? BaseMultivaluedSection else { if form[indexPath].trailingSwipe.actions.count > 0 { return .delete } return .none } if section.multivaluedOptions.contains(.Insert) && indexPath.row == section.count - 1 { return section.showInsertIconInAddButton ? .insert : .none } if section.multivaluedOptions.contains(.Delete) { return .delete } return .none } open func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { return self.tableView(tableView, editingStyleForRowAt: indexPath) != .none } @available(iOS 11,*) open func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { guard !form[indexPath].leadingSwipe.actions.isEmpty else { return nil } return form[indexPath].leadingSwipe.contextualConfiguration } @available(iOS 11,*) open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { guard !form[indexPath].trailingSwipe.actions.isEmpty else { return nil } return form[indexPath].trailingSwipe.contextualConfiguration } @available(macCatalyst, deprecated: 13.1, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead") @available(iOS, deprecated: 13, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead") open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?{ guard let actions = form[indexPath].trailingSwipe.contextualActions as? [UITableViewRowAction], !actions.isEmpty else { return nil } return actions } } extension FormViewController : UITableViewDataSource { // MARK: UITableViewDataSource open func numberOfSections(in tableView: UITableView) -> Int { return form.count } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return form[section].count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { form[indexPath].updateCell() return form[indexPath].baseCell } open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return form[section].header?.title } open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return form[section].footer?.title } open func sectionIndexTitles(for tableView: UITableView) -> [String]? { return nil } open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { return 0 } } extension FormViewController : UIScrollViewDelegate { // MARK: UIScrollViewDelegate open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard let tableView = tableView, scrollView === tableView else { return } tableView.endEditing(true) } } extension FormViewController { // MARK: KeyBoard Notifications /** Called when the keyboard will appear. Adjusts insets of the tableView and scrolls it if necessary. */ @objc open func keyboardWillShow(_ notification: Notification) { guard let table = tableView, let cell = table.findFirstResponder()?.formCell() else { return } let keyBoardInfo = notification.userInfo! let endFrame = keyBoardInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue let keyBoardFrame = table.window!.convert(endFrame.cgRectValue, to: table.superview) var newBottomInset = table.frame.origin.y + table.frame.size.height - keyBoardFrame.origin.y + rowKeyboardSpacing if #available(iOS 11.0, *) { newBottomInset = newBottomInset - tableView.safeAreaInsets.bottom } var tableInsets = table.contentInset var scrollIndicatorInsets = table.scrollIndicatorInsets oldBottomInset = oldBottomInset ?? tableInsets.bottom if newBottomInset > oldBottomInset! { tableInsets.bottom = newBottomInset scrollIndicatorInsets.bottom = tableInsets.bottom UIView.beginAnimations(nil, context: nil) UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double)) UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!) table.contentInset = tableInsets table.scrollIndicatorInsets = scrollIndicatorInsets if let selectedRow = table.indexPath(for: cell) { if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 11 { let rect = table.rectForRow(at: selectedRow) table.scrollRectToVisible(rect, animated: animateScroll) } else { table.scrollToRow(at: selectedRow, at: defaultScrollPosition, animated: animateScroll) } } UIView.commitAnimations() } } /** Called when the keyboard will disappear. Adjusts insets of the tableView. */ @objc open func keyboardWillHide(_ notification: Notification) { guard let table = tableView, let oldBottom = oldBottomInset else { return } let keyBoardInfo = notification.userInfo! var tableInsets = table.contentInset var scrollIndicatorInsets = table.scrollIndicatorInsets tableInsets.bottom = oldBottom scrollIndicatorInsets.bottom = tableInsets.bottom oldBottomInset = nil UIView.beginAnimations(nil, context: nil) UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double)) UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!) table.contentInset = tableInsets table.scrollIndicatorInsets = scrollIndicatorInsets UIView.commitAnimations() } } public enum Direction { case up, down } extension FormViewController { // MARK: Navigation Methods @objc func navigationDone() { tableView?.endEditing(true) } @objc func navigationPrevious() { navigateTo(direction: .up) } @objc func navigationNext() { navigateTo(direction: .down) } open override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { var didHandleEvent = false for press in presses { guard let key = press.key, key.keyCode == .keyboardTab, !key.modifierFlags.contains(.command) else { continue } if key.modifierFlags.contains(.shift) { navigateTo(direction: .up) } else { navigateTo(direction: .down) } didHandleEvent = true } if !didHandleEvent { // Didn't handle this key press, so pass the event to the next responder. super.pressesBegan(presses, with: event) } } public func navigateTo(direction: Direction) { guard let currentCell = tableView?.findFirstResponder()?.formCell() else { return } guard let currentIndexPath = tableView?.indexPath(for: currentCell) else { return } guard let nextRow = nextRow(for: form[currentIndexPath], withDirection: direction) else { return } if nextRow.baseCell.cellCanBecomeFirstResponder() { tableView?.scrollToRow(at: nextRow.indexPath!, at: .none, animated: animateScroll) nextRow.baseCell.cellBecomeFirstResponder(withDirection: direction) } } func nextRow(for currentRow: BaseRow, withDirection direction: Direction) -> BaseRow? { let options = navigationOptions ?? Form.defaultNavigationOptions guard options.contains(.Enabled) else { return nil } guard let next = direction == .down ? form.nextRow(for: currentRow) : form.previousRow(for: currentRow) else { return nil } if next.isDisabled && options.contains(.StopDisabledRow) { return nil } if !next.baseCell.cellCanBecomeFirstResponder() && !next.isDisabled && !options.contains(.SkipCanNotBecomeFirstResponderRow) { return nil } if !next.isDisabled && next.baseCell.cellCanBecomeFirstResponder() { return next } return nextRow(for: next, withDirection:direction) } } extension FormViewControllerProtocol { // MARK: Helpers func makeRowVisible(_ row: BaseRow, destinationScrollPosition: UITableView.ScrollPosition? = .bottom) { guard let destinationScrollPosition = destinationScrollPosition else { return } guard let cell = row.baseCell, let indexPath = row.indexPath, let tableView = tableView else { return } if cell.window == nil || (tableView.contentOffset.y + tableView.frame.size.height <= cell.frame.origin.y + cell.frame.size.height) { tableView.scrollToRow(at: indexPath, at: destinationScrollPosition, animated: true) } } } ================================================ FILE: Source/Core/Form.swift ================================================ // Form.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// The delegate of the Eureka form. public protocol FormDelegate : AnyObject { func sectionsHaveBeenAdded(_ sections: [Section], at: IndexSet) func sectionsHaveBeenRemoved(_ sections: [Section], at: IndexSet) func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at: IndexSet) func rowsHaveBeenAdded(_ rows: [BaseRow], at: [IndexPath]) func rowsHaveBeenRemoved(_ rows: [BaseRow], at: [IndexPath]) func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: [IndexPath]) func valueHasBeenChanged(for row: BaseRow, oldValue: Any?, newValue: Any?) } // MARK: Form /// The class representing the Eureka form. public final class Form { /// Defines the default options of the navigation accessory view. public static var defaultNavigationOptions = RowNavigationOptions.Enabled.union(.SkipCanNotBecomeFirstResponderRow) /// The default options that define when an inline row will be hidden. Applies only when `inlineRowHideOptions` is nil. public static var defaultInlineRowHideOptions = InlineRowHideOptions.FirstResponderChanges.union(.AnotherInlineRowIsShown) /// The options that define when an inline row will be hidden. If nil then `defaultInlineRowHideOptions` are used public var inlineRowHideOptions: InlineRowHideOptions? /// Which `UIReturnKeyType` should be used by default. Applies only when `keyboardReturnType` is nil. public static var defaultKeyboardReturnType = KeyboardReturnTypeConfiguration() /// Which `UIReturnKeyType` should be used in this form. If nil then `defaultKeyboardReturnType` is used public var keyboardReturnType: KeyboardReturnTypeConfiguration? /// This form's delegate public weak var delegate: FormDelegate? public init() {} /** Returns the row at the given indexPath */ public subscript(indexPath: IndexPath) -> BaseRow { return self[indexPath.section][indexPath.row] } /** Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster */ public func rowBy(tag: String) -> RowOf? where T: Equatable{ let row: BaseRow? = rowBy(tag: tag) return row as? RowOf } /** Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster */ public func rowBy(tag: String) -> Row? where Row: RowType{ let row: BaseRow? = rowBy(tag: tag) return row as? Row } /** Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster */ public func rowBy(tag: String) -> BaseRow? { return rowsByTag[tag] } /** Returns the section whose tag is passed as parameter. */ public func sectionBy(tag: String) -> Section? { return kvoWrapper._allSections.filter({ $0.tag == tag }).first } /** Method used to get all the values of all the rows of the form. Only rows with tag are included. - parameter includeHidden: If the values of hidden rows should be included. - returns: A dictionary mapping the rows tag to its value. [tag: value] */ public func values(includeHidden: Bool = false) -> [String: Any?] { if includeHidden { return getValues(for: allRows.filter({ $0.tag != nil })) .merging(getValues(for: allSections.filter({ $0 is BaseMultivaluedSection && $0.tag != nil }) as? [BaseMultivaluedSection]), uniquingKeysWith: {(_, new) in new }) } return getValues(for: rows.filter({ $0.tag != nil })) .merging(getValues(for: allSections.filter({ $0 is BaseMultivaluedSection && $0.tag != nil }) as? [BaseMultivaluedSection]), uniquingKeysWith: {(_, new) in new }) } /** Set values to the rows of this form - parameter values: A dictionary mapping tag to value of the rows to be set. [tag: value] */ public func setValues(_ values: [String: Any?]) { for (key, value) in values { let row: BaseRow? = rowBy(tag: key) row?.baseValue = value } } /// The visible rows of this form public var rows: [BaseRow] { return flatMap { $0 } } /// All the rows of this form. Includes the hidden rows. public var allRows: [BaseRow] { return kvoWrapper._allSections.map({ $0.kvoWrapper._allRows }).flatMap { $0 } } /// All the sections of this form. Includes hidden sections. public var allSections: [Section] { return kvoWrapper._allSections } /** * Hides all the inline rows of this form. */ public func hideInlineRows() { for row in self.allRows { if let inlineRow = row as? BaseInlineRowType { inlineRow.collapseInlineRow() } } } // MARK: Private var rowObservers = [String: [ConditionType: [Taggable]]]() var rowsByTag = [String: BaseRow]() var tagToValues = [String: Any]() lazy var kvoWrapper: KVOWrapper = { [unowned self] in return KVOWrapper(form: self) }() } extension Form: Collection { public var startIndex: Int { return 0 } public var endIndex: Int { return kvoWrapper.sections.count } } extension Form: MutableCollection { // MARK: MutableCollectionType public subscript (_ position: Int) -> Section { get { return kvoWrapper.sections[position] as! Section } set { if position > kvoWrapper.sections.count { assertionFailure("Form: Index out of bounds") } if position < kvoWrapper.sections.count { let oldSection = kvoWrapper.sections[position] let oldSectionIndex = kvoWrapper._allSections.firstIndex(of: oldSection as! Section)! // Remove the previous section from the form kvoWrapper._allSections[oldSectionIndex].willBeRemovedFromForm() kvoWrapper._allSections[oldSectionIndex] = newValue } else { kvoWrapper._allSections.append(newValue) } kvoWrapper.sections[position] = newValue newValue.wasAddedTo(form: self) } } public func index(after i: Int) -> Int { return i+1 <= endIndex ? i+1 : endIndex } public func index(before i: Int) -> Int { return i > startIndex ? i-1 : startIndex } public var last: Section? { return reversed().first } } extension Form : RangeReplaceableCollection { // MARK: RangeReplaceableCollectionType public func append(_ formSection: Section) { kvoWrapper.sections.insert(formSection, at: kvoWrapper.sections.count) kvoWrapper._allSections.append(formSection) formSection.wasAddedTo(form: self) } public func append(contentsOf newElements: S) where S.Iterator.Element == Section { kvoWrapper.sections.addObjects(from: newElements.map { $0 }) kvoWrapper._allSections.append(contentsOf: newElements) for section in newElements { section.wasAddedTo(form: self) } } public func replaceSubrange( _ subRange: Range, with newElements: C ) where C.Iterator.Element == Section { for i in subRange.lowerBound..( _ subRange: Range, with newElements: C ) where C.Iterator.Element == Section { // Remove subrange in all sections for i in subRange.reversed() where kvoWrapper._allSections.count > i { let removed = kvoWrapper._allSections.remove(at: i) removed.willBeRemovedFromForm() } kvoWrapper._allSections.insert(contentsOf: newElements, at: indexForInsertion(at: subRange.lowerBound)) // Replace all visible sections by `kvoWrapper._allSections`, as hidden ones are being removed later anyway kvoWrapper.sections.replaceObjects( in: NSRange(location: 0, length: kvoWrapper.sections.count), withObjectsFrom: kvoWrapper._allSections ) for section in newElements { section.wasAddedTo(form: self) } } public func removeAll(keepingCapacity keepCapacity: Bool = false) { // not doing anything with capacity let sections = kvoWrapper._allSections kvoWrapper.removeAllSections() for section in sections { section.willBeRemovedFromForm() } } public func removeAll(where shouldBeRemoved: (Section) throws -> Bool) rethrows { let indices = try kvoWrapper._allSections.enumerated() .filter { try shouldBeRemoved($0.element)} .map { $0.offset } var removedSections = [Section]() for index in indices.reversed() { removedSections.append(kvoWrapper._allSections.remove(at: index)) } kvoWrapper.sections.removeObjects(in: removedSections) removedSections.forEach { $0.willBeRemovedFromForm() } } private func indexForInsertion(at index: Int) -> Int { guard index != 0 else { return 0 } let section = kvoWrapper.sections[index-1] if let i = kvoWrapper._allSections.firstIndex(of: section as! Section) { return i + 1 } return kvoWrapper._allSections.count } } extension Form { // MARK: Private Helpers class KVOWrapper: NSObject { @objc dynamic private var _sections = NSMutableArray() var sections: NSMutableArray { return mutableArrayValue(forKey: "_sections") } var _allSections = [Section]() private weak var form: Form? init(form: Form) { self.form = form super.init() addObserver(self, forKeyPath: "_sections", options: [.new, .old], context:nil) } deinit { removeObserver(self, forKeyPath: "_sections") _sections.removeAllObjects() _allSections.removeAll() } func removeAllSections() { _sections = [] _allSections.removeAll() } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let newSections = change?[NSKeyValueChangeKey.newKey] as? [Section] ?? [] let oldSections = change?[NSKeyValueChangeKey.oldKey] as? [Section] ?? [] guard let delegateValue = form?.delegate, let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey] else { return } guard keyPathValue == "_sections" else { return } switch (changeType as! NSNumber).uintValue { case NSKeyValueChange.setting.rawValue: if newSections.count == 0 { let indexSet = IndexSet(integersIn: 0.. [String: Any] { return tagToValues } func addRowObservers(to taggable: Taggable, rowTags: [String], type: ConditionType) { for rowTag in rowTags { if rowObservers[rowTag] == nil { rowObservers[rowTag] = Dictionary() } if let _ = rowObservers[rowTag]?[type] { if !rowObservers[rowTag]![type]!.contains(where: { $0 === taggable }) { rowObservers[rowTag]?[type]!.append(taggable) } } else { rowObservers[rowTag]?[type] = [taggable] } } } func removeRowObservers(from taggable: Taggable, rowTags: [String], type: ConditionType) { for rowTag in rowTags { guard let arr = rowObservers[rowTag]?[type], let index = arr.firstIndex(where: { $0 === taggable }) else { continue } rowObservers[rowTag]?[type]?.remove(at: index) if rowObservers[rowTag]?[type]?.isEmpty == true { rowObservers[rowTag] = nil } } } func nextRow(for row: BaseRow) -> BaseRow? { let allRows = rows guard let index = allRows.firstIndex(of: row) else { return nil } guard index < allRows.count - 1 else { return nil } return allRows[index + 1] } func previousRow(for row: BaseRow) -> BaseRow? { let allRows = rows guard let index = allRows.firstIndex(of: row) else { return nil } guard index > 0 else { return nil } return allRows[index - 1] } func hideSection(_ section: Section) { kvoWrapper.sections.remove(section) } func showSection(_ section: Section) { guard !kvoWrapper.sections.contains(section) else { return } guard var index = kvoWrapper._allSections.firstIndex(of: section) else { return } var formIndex = NSNotFound while formIndex == NSNotFound && index > 0 { index = index - 1 let previous = kvoWrapper._allSections[index] formIndex = kvoWrapper.sections.index(of: previous) } kvoWrapper.sections.insert(section, at: formIndex == NSNotFound ? 0 : formIndex + 1 ) } var containsMultivaluedSection: Bool { return kvoWrapper._allSections.contains { $0 is BaseMultivaluedSection } } func getValues(for rows: [BaseRow]) -> [String: Any?] { return rows.reduce([String: Any?]()) { var result = $0 result[$1.tag!] = $1.baseValue return result } } func getValues(for multivaluedSections: [BaseMultivaluedSection]?) -> [String: [Any?]] { return multivaluedSections?.reduce([String: [Any?]]()) { var result = $0 result[$1.tag!] = $1.values() return result } ?? [:] } } extension Form { @discardableResult public func validate(includeHidden: Bool = false, includeDisabled: Bool = true, quietly: Bool = false) -> [ValidationError] { let rowsWithHiddenFilter = includeHidden ? allRows : rows let rowsWithDisabledFilter = includeDisabled ? rowsWithHiddenFilter : rowsWithHiddenFilter.filter { $0.isDisabled != true } return rowsWithDisabledFilter.reduce([ValidationError]()) { res, row in var res = res res.append(contentsOf: row.validate(quietly: quietly)) return res } } // Reset rows validation public func cleanValidationErrors(){ allRows.forEach { $0.cleanValidationErrors() } } } ================================================ FILE: Source/Core/HeaderFooterView.swift ================================================ // HeaderFooterView.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /** Enumeration used to generate views for the header and footer of a section. - Class: Will generate a view of the specified class. - Callback->ViewType: Will generate the view as a result of the given closure. - NibFile: Will load the view from a nib file. */ public enum HeaderFooterProvider { /** * Will generate a view of the specified class. */ case `class` /** * Will generate the view as a result of the given closure. */ case callback(()->ViewType) /** * Will load the view from a nib file. */ case nibFile(name: String, bundle: Bundle?) internal func createView() -> ViewType { switch self { case .class: return ViewType() case .callback(let builder): return builder() case .nibFile(let nibName, let bundle): return (bundle ?? Bundle(for: ViewType.self)).loadNibNamed(nibName, owner: nil, options: nil)![0] as! ViewType } } } /** * Represents headers and footers of sections */ public enum HeaderFooterType { case header, footer } /** * Struct used to generate headers and footers either from a view or a String. */ public struct HeaderFooterView : ExpressibleByStringLiteral, HeaderFooterViewRepresentable { /// Holds the title of the view if it was set up with a String. public var title: String? /// Generates the view. public var viewProvider: HeaderFooterProvider? /// Closure called when the view is created. Useful to customize its appearance. public var onSetupView: ((_ view: ViewType, _ section: Section) -> Void)? /// A closure that returns the height for the header or footer view. public var height: (() -> CGFloat)? /** This method can be called to get the view corresponding to the header or footer of a section in a specific controller. - parameter section: The section from which to get the view. - parameter type: Either header or footer. - parameter controller: The controller from which to get that view. - returns: The header or footer of the specified section. */ public func viewForSection(_ section: Section, type: HeaderFooterType) -> UIView? { var view: ViewType? if type == .header { view = section.headerView as? ViewType ?? { let result = viewProvider?.createView() section.headerView = result return result }() } else { view = section.footerView as? ViewType ?? { let result = viewProvider?.createView() section.footerView = result return result }() } guard let v = view else { return nil } onSetupView?(v, section) return v } /** Initiates the view with a String as title */ public init?(title: String?) { guard let t = title else { return nil } self.init(stringLiteral: t) } /** Initiates the view with a view provider, ideal for customized headers or footers */ public init(_ provider: HeaderFooterProvider) { viewProvider = provider } /** Initiates the view with a String as title */ public init(unicodeScalarLiteral value: String) { self.title = value } /** Initiates the view with a String as title */ public init(extendedGraphemeClusterLiteral value: String) { self.title = value } /** Initiates the view with a String as title */ public init(stringLiteral value: String) { self.title = value } } extension UIView { func eurekaInvalidate() { setNeedsUpdateConstraints() updateConstraintsIfNeeded() setNeedsLayout() } } ================================================ FILE: Source/Core/Helpers.swift ================================================ // Helpers.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit extension UIView { public func findFirstResponder() -> UIView? { if isFirstResponder { return self } for subView in subviews { if let firstResponder = subView.findFirstResponder() { return firstResponder } } return nil } public func formCell() -> BaseCell? { if self is UITableViewCell { return self as? BaseCell } return superview?.formCell() } } extension NSPredicate { var predicateVars: [String] { var ret = [String]() if let compoundPredicate = self as? NSCompoundPredicate { for subPredicate in compoundPredicate.subpredicates where subPredicate is NSPredicate { ret.append(contentsOf: (subPredicate as! NSPredicate).predicateVars) } } else if let comparisonPredicate = self as? NSComparisonPredicate { ret.append(contentsOf: comparisonPredicate.leftExpression.expressionVars) ret.append(contentsOf: comparisonPredicate.rightExpression.expressionVars) } return ret } } extension NSExpression { var expressionVars: [String] { switch expressionType { case .function, .variable: let str = "\(self)" if let range = str.range(of: ".") { return [String(str[str.index(str.startIndex, offsetBy: 1).. Void) -> Self { callbackOnExpandInlineRow = callback return self } /** Sets a block to be executed when a row is collapsed. */ @discardableResult public func onCollapseInlineRow(_ callback: @escaping (Cell, Self, InlineRow) -> Void) -> Self { callbackOnCollapseInlineRow = callback return self } /// Returns the block that will be executed when this row expands public var onCollapseInlineRowCallback: ((Cell, Self, InlineRow) -> Void)? { return callbackOnCollapseInlineRow as! ((Cell, Self, InlineRow) -> Void)? } /// Returns the block that will be executed when this row collapses public var onExpandInlineRowCallback: ((Cell, Self, InlineRow) -> Void)? { return callbackOnExpandInlineRow as! ((Cell, Self, InlineRow) -> Void)? } public var isExpanded: Bool { return _inlineRow != nil } public var isCollapsed: Bool { return !isExpanded } } ================================================ FILE: Source/Core/NavigationAccessoryView.swift ================================================ // NavigationAccessoryView.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit public protocol NavigationAccessory { var doneClosure: (() -> ())? { get set } var nextClosure: (() -> ())? { get set } var previousClosure: (() -> ())? { get set } var previousEnabled: Bool { get set } var nextEnabled: Bool { get set } } /// Class for the navigation accessory view used in FormViewController @objc(EurekaNavigationAccessoryView) open class NavigationAccessoryView: UIToolbar, NavigationAccessory { open var previousButton: UIBarButtonItem! open var nextButton: UIBarButtonItem! open var doneButton: UIBarButtonItem! private var fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) private var flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) public override init(frame: CGRect) { super.init(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: 44.0)) autoresizingMask = .flexibleWidth fixedSpace.width = 22.0 initializeChevrons() initializeDoneButton() setItems([previousButton, fixedSpace, nextButton, flexibleSpace, doneButton], animated: false) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } private func drawChevron(pointingRight: Bool) -> UIImage? { // Hardcoded chevron size let width = 12 let height = 20 // Begin the image context, with which we are going to draw a chevron UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, 0.0) guard let context = UIGraphicsGetCurrentContext() else { return nil } defer { UIGraphicsEndImageContext() } // The chevron looks like > or <. This can be drawn with 3 points; the Y coordinates of the points // are independant of whether it is to be pointing left or right; the X coordinates will depend, as follows. // The 1s are to ensure that the point of the chevron does not sit exactly on the edge of the frame, which // would slightly truncate the point. let chevronPointXCoordinate, chevronTailsXCoordinate: Int if pointingRight { chevronPointXCoordinate = width - 1 chevronTailsXCoordinate = 1 } else { chevronPointXCoordinate = 1 chevronTailsXCoordinate = width - 1 } // Draw the lines and return the image context.setLineWidth(1.5) context.setLineCap(.square) context.strokeLineSegments(between: [ CGPoint(x: chevronTailsXCoordinate, y: 0), CGPoint(x: chevronPointXCoordinate, y: height / 2), CGPoint(x: chevronPointXCoordinate, y: height / 2), CGPoint(x: chevronTailsXCoordinate, y: height) ]) return UIGraphicsGetImageFromCurrentImageContext() } private func initializeChevrons() { var imageLeftChevron, imageRightChevron: UIImage? if #available(iOS 13.0, *) { // If we have access to SFSymbols, use the system chevron images, rather than faffing around with our own imageLeftChevron = UIImage(systemName: "chevron.left") imageRightChevron = UIImage(systemName: "chevron.right") } else { imageLeftChevron = drawChevron(pointingRight: false) imageRightChevron = drawChevron(pointingRight: true) } // RTL language support imageLeftChevron = imageLeftChevron?.imageFlippedForRightToLeftLayoutDirection() imageRightChevron = imageRightChevron?.imageFlippedForRightToLeftLayoutDirection() previousButton = UIBarButtonItem(image: imageLeftChevron, style: .plain, target: self, action: #selector(didTapPrevious)) nextButton = UIBarButtonItem(image: imageRightChevron, style: .plain, target: self, action: #selector(didTapNext)) } private func initializeDoneButton() { doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(didTapDone)) } open override func touchesBegan(_ touches: Set, with event: UIEvent?) {} public var doneClosure: (() -> ())? public var nextClosure: (() -> ())? public var previousClosure: (() -> ())? @objc private func didTapDone() { doneClosure?() } @objc private func didTapNext() { nextClosure?() } @objc private func didTapPrevious() { previousClosure?() } public var previousEnabled: Bool { get { return previousButton.isEnabled } set { previousButton.isEnabled = newValue } } public var nextEnabled: Bool { get { return nextButton.isEnabled } set { nextButton.isEnabled = newValue } } } ================================================ FILE: Source/Core/Operators.swift ================================================ // Operators.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation // MARK: Operators precedencegroup FormPrecedence { associativity: left higherThan: LogicalConjunctionPrecedence } precedencegroup SectionPrecedence { associativity: left higherThan: FormPrecedence } infix operator +++ : FormPrecedence /** Appends a section to a form - parameter left: the form - parameter right: the section to be appended - returns: the updated form */ @discardableResult public func +++ (left: Form, right: Section) -> Form { left.append(right) return left } /** Appends a section to a form - parameter left: the form - parameter right: the section to be appended - returns: the updated form */ #if swift(>=5.4) @discardableResult public func +++ (left: Form, @SectionBuilder right: () -> Section) -> Form { left +++ right() } #endif /** Appends a row to the last section of a form - parameter left: the form - parameter right: the row */ @discardableResult public func +++ (left: Form, right: BaseRow) -> Form { let section = Section() let _ = left +++ section <<< right return left } /** Creates a form with two sections - parameter left: the first section - parameter right: the second section - returns: the created form */ @discardableResult public func +++ (left: Section, right: Section) -> Form { let form = Form() let _ = form +++ left +++ right return form } /** Creates a form with two sections - parameter left: the first section - parameter right: the second section - returns: the created form */ #if swift(>=5.4) @discardableResult public func +++ (left: Section, @SectionBuilder right: () -> Section) -> Form { left +++ right() } #endif /** Appends the row wrapped in a new section - parameter left: a section of the form - parameter right: a row to be appended - returns: the form */ @discardableResult public func +++ (left: Section, right: BaseRow) -> Form { let section = Section() section <<< right return left +++ section } /** Creates a form with two sections, each containing one row. - parameter left: The row for the first section - parameter right: The row for the second section - returns: the created form */ @discardableResult public func +++ (left: BaseRow, right: BaseRow) -> Form { let form = Section() <<< left +++ Section() <<< right return form } infix operator <<< : SectionPrecedence /** Appends a row to a section. - parameter left: the section - parameter right: the row to be appended - returns: the section */ @discardableResult public func <<< (left: Section, right: BaseRow) -> Section { left.append(right) return left } /** Creates a section with two rows - parameter left: The first row - parameter right: The second row - returns: the created section */ @discardableResult public func <<< (left: BaseRow, right: BaseRow) -> Section { let section = Section() section <<< left <<< right return section } /** Appends a collection of rows to a section - parameter lhs: the section - parameter rhs: the rows to be appended */ public func += (lhs: inout Section, rhs: C) where C.Iterator.Element == BaseRow { lhs.append(contentsOf: rhs) } /** Appends a collection of section to a form - parameter lhs: the form - parameter rhs: the sections to be appended */ public func += (lhs: inout Form, rhs: C) where C.Iterator.Element == Section { lhs.append(contentsOf: rhs) } ================================================ FILE: Source/Core/PresenterRowType.swift ================================================ // PresenterRowType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /** * Protocol that every row that displays a new view controller must conform to. * This includes presenting or pushing view controllers. */ public protocol PresenterRowType: TypedRowType { associatedtype PresentedControllerType : UIViewController, TypedRowControllerType /// Defines how the view controller will be presented, pushed, etc. var presentationMode: PresentationMode? { get set } /// Will be called before the presentation occurs. var onPresentCallback: ((FormViewController, PresentedControllerType) -> Void)? { get set } } extension PresenterRowType { /** Sets a block to be executed when the row presents a view controller - parameter callback: the block - returns: this row */ @discardableResult public func onPresent(_ callback: ((FormViewController, PresentedControllerType) -> Void)?) -> Self { onPresentCallback = callback return self } } ================================================ FILE: Source/Core/ResultBuilders.swift ================================================ // ResultBuilders.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2022 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if swift(>=5.4) public protocol RowsProvider { var rows: [BaseRow] { get } } extension BaseRow: RowsProvider { public var rows: [BaseRow] { [self] } } extension Array: RowsProvider where Element == BaseRow { public var rows: [BaseRow] { self } } public protocol SectionsProvider { var sections: [Section] { get } } extension Section: SectionsProvider { public var sections: [Section] { [self] } } extension Array: SectionsProvider where Element == Section { public var sections: [Section] { self } } extension BaseRow: SectionsProvider { public var sections: [Section] { [.init([self])] } } @resultBuilder public struct SectionBuilder { public static func buildBlock(_ components: RowsProvider...) -> [BaseRow] { components.flatMap { $0.rows } } public static func buildFinalResult(_ components: [BaseRow]) -> Section { .init(components) } public static func buildEither(first components: [RowsProvider]) -> [BaseRow] { components.flatMap { $0.rows } } public static func buildEither(second components: [RowsProvider]) -> [BaseRow] { components.flatMap { $0.rows } } public static func buildOptional(_ components: [RowsProvider]?) -> [BaseRow] { components?.flatMap { $0.rows } ?? [] } public static func buildExpression(_ expression: RowsProvider?) -> [BaseRow] { expression.flatMap { $0.rows } ?? [] } } @resultBuilder public struct FormBuilder { public static func buildBlock(_ components: SectionsProvider...) -> [Section] { components.flatMap { $0.sections } } public static func buildFinalResult(_ components: [Section]) -> Form { .init(components) } public static func buildEither(first components: [SectionsProvider]) -> [Section] { components.flatMap { $0.sections } } public static func buildEither(second components: [SectionsProvider]) -> [Section] { components.flatMap { $0.sections } } public static func buildOptional(_ components: [SectionsProvider]?) -> [Section] { components?.flatMap { $0.sections } ?? [] } public static func buildExpression(_ expression: SectionsProvider?) -> [Section] { expression.flatMap { $0.sections } ?? [] } } #endif ================================================ FILE: Source/Core/Row.swift ================================================ // Row.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation open class RowOf: BaseRow where T: Equatable { private var _value: T? { didSet { guard _value != oldValue else { return } guard let form = section?.form else { return } if let delegate = form.delegate { delegate.valueHasBeenChanged(for: self, oldValue: oldValue, newValue: value) callbackOnChange?() } guard let t = tag else { return } form.tagToValues[t] = (value != nil ? value! : NSNull()) if let rowObservers = form.rowObservers[t]?[.hidden] { for rowObserver in rowObservers { (rowObserver as? Hidable)?.evaluateHidden() } } if let rowObservers = form.rowObservers[t]?[.disabled] { for rowObserver in rowObservers { (rowObserver as? Disableable)?.evaluateDisabled() } } } } /// The typed value of this row. open var value: T? { set (newValue) { _value = newValue guard let _ = section?.form else { return } wasChanged = true if validationOptions.contains(.validatesOnChange) || (wasBlurred && validationOptions.contains(.validatesOnChangeAfterBlurred)) || (!isValid && validationOptions != .validatesOnDemand) { validate() } } get { return _value } } /// The reset value of this row. Sets the value property to the value of this row on the resetValue method call. open var resetValue: T? /// The untyped value of this row. public override var baseValue: Any? { get { return value } set { value = newValue as? T } } /// Block variable used to get the String that should be displayed for the value of this row. public var displayValueFor: ((T?) -> String?)? = { return $0.map { String(describing: $0) } } public required init(tag: String?) { super.init(tag: tag) } public internal(set) var rules: [ValidationRuleHelper] = [] @discardableResult open override func validate(quietly: Bool = false) -> [ValidationError] { var vErrors = [ValidationError]() #if swift(>=4.1) vErrors = rules.compactMap { $0.validateFn(value) } #else vErrors = rules.flatMap { $0.validateFn(value) } #endif if (!quietly) { validationErrors = vErrors } return vErrors } /// Resets the value of the row. Setting it's value to it's reset value. public func resetRowValue() { value = resetValue } /// Add a Validation rule for the Row /// - Parameter rule: RuleType object to add public func add(rule: Rule) where T == Rule.RowValueType { let validFn: ((T?) -> ValidationError?) = { (val: T?) in return rule.isValid(value: val) } rules.append(ValidationRuleHelper(validateFn: validFn, rule: rule)) } /// Add a Validation rule set for the Row /// - Parameter ruleSet: RuleSet set of rules to add public func add(ruleSet: RuleSet) { rules.append(contentsOf: ruleSet.rules) } public func remove(ruleWithIdentifier identifier: String) { if let index = rules.firstIndex(where: { (validationRuleHelper) -> Bool in return validationRuleHelper.rule.id == identifier }) { rules.remove(at: index) } } public func removeAllRules() { validationErrors.removeAll() rules.removeAll() } } /// Generic class that represents an Eureka row. open class Row: RowOf, TypedRowType where Cell: BaseCell { /// Responsible for creating the cell for this row. public var cellProvider = CellProvider() /// The type of the cell associated to this row. public let cellType: Cell.Type! = Cell.self private var _cell: Cell! { didSet { RowDefaults.cellSetup["\(type(of: self))"]?(_cell, self) (callbackCellSetup as? ((Cell) -> Void))?(_cell) } } /// The cell associated to this row. public var cell: Cell! { return _cell ?? { let result = cellProvider.makeCell(style: self.cellStyle) result.row = self result.setup() _cell = result return _cell }() } /// The untyped cell associated to this row public override var baseCell: BaseCell { return cell } public required init(tag: String?) { super.init(tag: tag) } /** Method that reloads the cell */ override open func updateCell() { super.updateCell() cell.update() customUpdateCell() RowDefaults.cellUpdate["\(type(of: self))"]?(cell, self) callbackCellUpdate?() } /** Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. */ open override func didSelect() { super.didSelect() if !isDisabled { cell?.didSelect() } customDidSelect() callbackCellOnSelection?() } /** Will be called inside `didSelect` method of the row. Can be used to customize row selection from the definition of the row. */ open func customDidSelect() {} /** Will be called inside `updateCell` method of the row. Can be used to customize reloading a row from its definition. */ open func customUpdateCell() {} } ================================================ FILE: Source/Core/RowControllerType.swift ================================================ // RowControllerType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /** * Base protocol for view controllers presented by Eureka rows. */ public protocol RowControllerType: NSObjectProtocol { /// A closure to be called when the controller disappears. var onDismissCallback: ((UIViewController) -> Void)? { get set } } ================================================ FILE: Source/Core/RowProtocols.swift ================================================ // RowProtocols.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /** * Protocol that view controllers pushed or presented by a row should conform to. */ public protocol TypedRowControllerType: RowControllerType { associatedtype RowValue: Equatable /// The row that pushed or presented this controller var row: RowOf! { get set } } // MARK: Header Footer Protocols /** * Protocol used to set headers and footers to sections. * Can be set with a view or a String */ public protocol HeaderFooterViewRepresentable { /** This method can be called to get the view corresponding to the header or footer of a section in a specific controller. - parameter section: The section from which to get the view. - parameter type: Either header or footer. - parameter controller: The controller from which to get that view. - returns: The header or footer of the specified section. */ func viewForSection(_ section: Section, type: HeaderFooterType) -> UIView? /// If the header or footer of a section was created with a String then it will be stored in the title. var title: String? { get set } /// The height of the header or footer. var height: (() -> CGFloat)? { get set } } ================================================ FILE: Source/Core/RowType.swift ================================================ // RowType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit protocol Disableable: Taggable { func evaluateDisabled() var disabled: Condition? { get set } var isDisabled: Bool { get } } protocol Hidable: Taggable { func evaluateHidden() var hidden: Condition? { get set } var isHidden: Bool { get } } public protocol KeyboardReturnHandler: BaseRowType { var keyboardReturnType: KeyboardReturnTypeConfiguration? { get set } } public protocol Taggable: AnyObject { var tag: String? { get set } } public protocol BaseRowType: Taggable { /// The cell associated to this row. var baseCell: BaseCell! { get } /// The section to which this row belongs. var section: Section? { get } /// Parameter used when creating the cell for this row. var cellStyle: UITableViewCell.CellStyle { get set } /// The title will be displayed in the textLabel of the row. var title: String? { get set } /** Method that should re-display the cell */ func updateCell() /** Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. */ func didSelect() /** Typically we don't need to explicitly call this method since it is called by Eureka framework. It will validates the row if you invoke it. */ func validate(quietly: Bool) -> [ValidationError] } public protocol TypedRowType: BaseRowType { associatedtype Cell: BaseCell, TypedCellType /// The typed cell associated to this row. var cell: Cell! { get } /// The typed value this row stores. var value: Cell.Value? { get set } func add(rule: Rule) where Rule.RowValueType == Cell.Value func remove(ruleWithIdentifier: String) } /** * Protocol that every row type has to conform to. */ public protocol RowType: TypedRowType { init(_ tag: String?, _ initializer: (Self) -> Void) } extension RowType where Self: BaseRow { /** Default initializer for a row */ public init(_ tag: String? = nil, _ initializer: (Self) -> Void = { _ in }) { self.init(tag: tag) RowDefaults.rowInitialization["\(type(of: self))"]?(self) initializer(self) } } extension RowType where Self: BaseRow { /// The default block executed when the cell is updated. Applies to every row of this type. public static var defaultCellUpdate: ((Cell, Self) -> Void)? { set { if let newValue = newValue { let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in newValue(baseCell as! Cell, baseRow as! Self) } RowDefaults.cellUpdate["\(self)"] = wrapper RowDefaults.rawCellUpdate["\(self)"] = newValue } else { RowDefaults.cellUpdate["\(self)"] = nil RowDefaults.rawCellUpdate["\(self)"] = nil } } get { return RowDefaults.rawCellUpdate["\(self)"] as? ((Cell, Self) -> Void) } } /// The default block executed when the cell is created. Applies to every row of this type. public static var defaultCellSetup: ((Cell, Self) -> Void)? { set { if let newValue = newValue { let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in newValue(baseCell as! Cell, baseRow as! Self) } RowDefaults.cellSetup["\(self)"] = wrapper RowDefaults.rawCellSetup["\(self)"] = newValue } else { RowDefaults.cellSetup["\(self)"] = nil RowDefaults.rawCellSetup["\(self)"] = nil } } get { return RowDefaults.rawCellSetup["\(self)"] as? ((Cell, Self) -> Void) } } /// The default block executed when the cell becomes first responder. Applies to every row of this type. public static var defaultOnCellHighlightChanged: ((Cell, Self) -> Void)? { set { if let newValue = newValue { let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in newValue(baseCell as! Cell, baseRow as! Self) } RowDefaults.onCellHighlightChanged ["\(self)"] = wrapper RowDefaults.rawOnCellHighlightChanged["\(self)"] = newValue } else { RowDefaults.onCellHighlightChanged["\(self)"] = nil RowDefaults.rawOnCellHighlightChanged["\(self)"] = nil } } get { return RowDefaults.rawOnCellHighlightChanged["\(self)"] as? ((Cell, Self) -> Void) } } /// The default block executed to initialize a row. Applies to every row of this type. public static var defaultRowInitializer: ((Self) -> Void)? { set { if let newValue = newValue { let wrapper: (BaseRow) -> Void = { (baseRow: BaseRow) in newValue(baseRow as! Self) } RowDefaults.rowInitialization["\(self)"] = wrapper RowDefaults.rawRowInitialization["\(self)"] = newValue } else { RowDefaults.rowInitialization["\(self)"] = nil RowDefaults.rawRowInitialization["\(self)"] = nil } } get { return RowDefaults.rawRowInitialization["\(self)"] as? ((Self) -> Void) } } /// The default block executed to initialize a row. Applies to every row of this type. public static var defaultOnRowValidationChanged: ((Cell, Self) -> Void)? { set { if let newValue = newValue { let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in newValue(baseCell as! Cell, baseRow as! Self) } RowDefaults.onRowValidationChanged["\(self)"] = wrapper RowDefaults.rawOnRowValidationChanged["\(self)"] = newValue } else { RowDefaults.onRowValidationChanged["\(self)"] = nil RowDefaults.rawOnRowValidationChanged["\(self)"] = nil } } get { return RowDefaults.rawOnRowValidationChanged["\(self)"] as? ((Cell, Self) -> Void) } } /** Sets a block to be called when the value of this row changes. - returns: this row */ @discardableResult public func onChange(_ callback: @escaping (Self) -> Void) -> Self { callbackOnChange = { [weak self] in callback(self!) } return self } /** Sets a block to be called when the cell corresponding to this row is refreshed. - returns: this row */ @discardableResult public func cellUpdate(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { callbackCellUpdate = { [weak self] in callback(self!.cell, self!) } return self } /** Sets a block to be called when the cell corresponding to this row is created. - returns: this row */ @discardableResult public func cellSetup(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { callbackCellSetup = { [weak self] (cell: Cell) in callback(cell, self!) } return self } /** Sets a block to be called when the cell corresponding to this row is selected. - returns: this row */ @discardableResult public func onCellSelection(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { callbackCellOnSelection = { [weak self] in callback(self!.cell, self!) } return self } /** Sets a block to be called when the cell corresponding to this row becomes or resigns the first responder. - returns: this row */ @discardableResult public func onCellHighlightChanged(_ callback: @escaping (_ cell: Cell, _ row: Self) -> Void) -> Self { callbackOnCellHighlightChanged = { [weak self] in callback(self!.cell, self!) } return self } @discardableResult public func onRowValidationChanged(_ callback: @escaping (_ cell: Cell, _ row: Self) -> Void) -> Self { callbackOnRowValidationChanged = { [weak self] in callback(self!.cell, self!) } return self } } ================================================ FILE: Source/Core/Section.swift ================================================ // Section.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// The delegate of the Eureka sections. public protocol SectionDelegate: AnyObject { func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) func rowsHaveBeenRemoved(_ rows: [BaseRow], at: IndexSet) func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: IndexSet) } // MARK: Section extension Section : Equatable {} public func == (lhs: Section, rhs: Section) -> Bool { return lhs === rhs } extension Section : Hidable, SectionDelegate {} extension Section { public func reload(with rowAnimation: UITableView.RowAnimation = .none) { guard let tableView = (form?.delegate as? FormViewController)?.tableView, let index = index, index < tableView.numberOfSections else { return } tableView.reloadSections(IndexSet(integer: index), with: rowAnimation) } } extension Section { internal class KVOWrapper: NSObject { @objc dynamic private var _rows = NSMutableArray() var rows: NSMutableArray { return mutableArrayValue(forKey: "_rows") } var _allRows = [BaseRow]() private weak var section: Section? init(section: Section) { self.section = section super.init() addObserver(self, forKeyPath: "_rows", options: [.new, .old], context:nil) } deinit { removeObserver(self, forKeyPath: "_rows") _rows.removeAllObjects() _allRows.removeAll() } func removeAllRows() { _rows = [] _allRows.removeAll() } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let newRows = change![NSKeyValueChangeKey.newKey] as? [BaseRow] ?? [] let oldRows = change![NSKeyValueChangeKey.oldKey] as? [BaseRow] ?? [] guard let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey] else { return } let delegateValue = section?.form?.delegate guard keyPathValue == "_rows" else { return } switch (changeType as! NSNumber).uintValue { case NSKeyValueChange.setting.rawValue: if newRows.count == 0 { let indexSet = IndexSet(integersIn: 0..(tag: String) -> Row? { guard let index = kvoWrapper._allRows.firstIndex(where: { $0.tag == tag }) else { return nil } return kvoWrapper._allRows[index] as? Row } } /// The class representing the sections in a Eureka form. open class Section { /// The tag is used to uniquely identify a Section. Must be unique among sections and rows. public var tag: String? /// The form that contains this section public internal(set) weak var form: Form? /// The header of this section. public var header: HeaderFooterViewRepresentable? { willSet { headerView = nil } } /// The footer of this section public var footer: HeaderFooterViewRepresentable? { willSet { footerView = nil } } /// Index of this section in the form it belongs to. public var index: Int? { return form?.firstIndex(of: self) } /// Condition that determines if the section should be hidden or not. public var hidden: Condition? { willSet { removeFromRowObservers() } didSet { addToRowObservers() } } /// Returns if the section is currently hidden or not. public var isHidden: Bool { return hiddenCache } /// Returns all the rows in this section, including hidden rows. public var allRows: [BaseRow] { return kvoWrapper._allRows } public required init() {} #if swift(>=4.1) public required init(_ elements: S) where S: Sequence, S.Element == BaseRow { self.append(contentsOf: elements) } #endif public init(_ initializer: @escaping (Section) -> Void) { initializer(self) } public init(_ header: String?, _ initializer: @escaping (Section) -> Void = { _ in }) { if let header = header { self.header = HeaderFooterView(stringLiteral: header) } initializer(self) } public init(header: String?, footer: String?, _ initializer: (Section) -> Void = { _ in }) { if let header = header { self.header = HeaderFooterView(stringLiteral: header) } if let footer = footer { self.footer = HeaderFooterView(stringLiteral: footer) } initializer(self) } public init(footer: String?, _ initializer: (Section) -> Void = { _ in }) { if let footer = footer { self.footer = HeaderFooterView(stringLiteral: footer) } initializer(self) } // MARK: SectionDelegate /** * Delegate method called by the framework when one or more rows have been added to the section. */ open func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) {} /** * Delegate method called by the framework when one or more rows have been removed from the section. */ open func rowsHaveBeenRemoved(_ rows: [BaseRow], at: IndexSet) {} /** * Delegate method called by the framework when one or more rows have been replaced in the section. */ open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: IndexSet) {} // MARK: Private lazy var kvoWrapper: KVOWrapper = { [unowned self] in return KVOWrapper(section: self) }() var headerView: UIView? var footerView: UIView? var hiddenCache = false } extension Section: MutableCollection, BidirectionalCollection { // MARK: MutableCollectionType public var startIndex: Int { return 0 } public var endIndex: Int { return kvoWrapper.rows.count } public subscript (position: Int) -> BaseRow { get { if position >= kvoWrapper.rows.count { assertionFailure("Section: Index out of bounds") } return kvoWrapper.rows[position] as! BaseRow } set { if position > kvoWrapper.rows.count { assertionFailure("Section: Index out of bounds") } if position < kvoWrapper.rows.count { let oldRow = kvoWrapper.rows[position] let oldRowIndex = kvoWrapper._allRows.firstIndex(of: oldRow as! BaseRow)! // Remove the previous row from the form kvoWrapper._allRows[oldRowIndex].willBeRemovedFromSection() kvoWrapper._allRows[oldRowIndex] = newValue } else { kvoWrapper._allRows.append(newValue) } kvoWrapper.rows[position] = newValue newValue.wasAddedTo(section: self) } } public subscript (range: Range) -> ArraySlice { get { return kvoWrapper.rows.map { $0 as! BaseRow }[range] } set { replaceSubrange(range, with: newValue) } } public func index(after i: Int) -> Int { return i + 1 } public func index(before i: Int) -> Int { return i - 1 } } extension Section: RangeReplaceableCollection { // MARK: RangeReplaceableCollectionType public func append(_ formRow: BaseRow) { kvoWrapper.rows.insert(formRow, at: kvoWrapper.rows.count) kvoWrapper._allRows.append(formRow) formRow.wasAddedTo(section: self) } public func append(contentsOf newElements: S) where S.Iterator.Element == BaseRow { kvoWrapper.rows.addObjects(from: newElements.map { $0 }) kvoWrapper._allRows.append(contentsOf: newElements) for row in newElements { row.wasAddedTo(section: self) } } public func replaceSubrange(_ subrange: Range, with newElements: C) where C : Collection, C.Element == BaseRow { for i in subrange.lowerBound.. Bool) rethrows { let indices = try kvoWrapper._allRows.enumerated() .filter { try shouldBeRemoved($0.element)} .map { $0.offset } var removedRows = [BaseRow]() for index in indices.reversed() { removedRows.append(kvoWrapper._allRows.remove(at: index)) } kvoWrapper.rows.removeObjects(in: removedRows) removedRows.forEach { $0.willBeRemovedFromSection() } } @discardableResult public func remove(at position: Int) -> BaseRow { let row = kvoWrapper.rows.object(at: position) as! BaseRow row.willBeRemovedFromSection() kvoWrapper.rows.removeObject(at: position) if let index = kvoWrapper._allRows.firstIndex(of: row) { kvoWrapper._allRows.remove(at: index) } return row } private func indexForInsertion(at index: Int) -> Int { guard index != 0 else { return 0 } let row = kvoWrapper.rows[index-1] if let i = kvoWrapper._allRows.firstIndex(of: row as! BaseRow) { return i + 1 } return kvoWrapper._allRows.count } } extension Section /* Condition */ { // MARK: Hidden/Disable Engine /** Function that evaluates if the section should be hidden and updates it accordingly. */ public final func evaluateHidden() { if let h = hidden, let f = form { switch h { case .function(_, let callback): hiddenCache = callback(f) case .predicate(let predicate): hiddenCache = predicate.evaluate(with: self, substitutionVariables: f.dictionaryValuesToEvaluatePredicate()) } if hiddenCache { form?.hideSection(self) } else { form?.showSection(self) } } } /** Internal function called when this section was added to a form. */ func wasAddedTo(form: Form) { self.form = form addToRowObservers() evaluateHidden() for row in kvoWrapper._allRows { row.wasAddedTo(section: self) } } /** Internal function called to add this section to the observers of certain rows. Called when the hidden variable is set and depends on other rows. */ func addToRowObservers() { guard let h = hidden else { return } switch h { case .function(let tags, _): form?.addRowObservers(to: self, rowTags: tags, type: .hidden) case .predicate(let predicate): form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .hidden) } } /** Internal function called when this section was removed from a form. */ func willBeRemovedFromForm() { for row in kvoWrapper._allRows { row.willBeRemovedFromForm() } removeFromRowObservers() self.form = nil } /** Internal function called to remove this section from the observers of certain rows. Called when the hidden variable is changed. */ func removeFromRowObservers() { guard let h = hidden else { return } switch h { case .function(let tags, _): form?.removeRowObservers(from: self, rowTags: tags, type: .hidden) case .predicate(let predicate): form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .hidden) } } func hide(row: BaseRow) { row.baseCell.cellResignFirstResponder() (row as? BaseInlineRowType)?.collapseInlineRow() kvoWrapper.rows.remove(row) } func show(row: BaseRow) { guard !kvoWrapper.rows.contains(row) else { return } guard var index = kvoWrapper._allRows.firstIndex(of: row) else { return } var formIndex = NSNotFound while formIndex == NSNotFound && index > 0 { index = index - 1 let previous = kvoWrapper._allRows[index] formIndex = kvoWrapper.rows.index(of: previous) } kvoWrapper.rows.insert(row, at: formIndex == NSNotFound ? 0 : formIndex + 1) } } extension Section /* Helpers */ { /** * This method inserts a row after another row. * It is useful if you want to insert a row after a row that is currently hidden. Otherwise use `insert(at: Int)`. * It throws an error if the old row is not in this section. */ public func insert(row newRow: BaseRow, after previousRow: BaseRow) throws { guard let rowIndex = (kvoWrapper._allRows as [BaseRow]).firstIndex(of: previousRow) else { throw EurekaError.rowNotInSection(row: previousRow) } kvoWrapper._allRows.insert(newRow, at: index(after: rowIndex)) show(row: newRow) newRow.wasAddedTo(section: self) } } /** * Navigation options for a form view controller. */ public struct MultivaluedOptions: OptionSet { private enum Options: Int { case none = 0, insert = 1, delete = 2, reorder = 4 } public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue} private init(_ options: Options) { self.rawValue = options.rawValue } /// No multivalued. public static let None = MultivaluedOptions(.none) /// Allows user to insert rows. public static let Insert = MultivaluedOptions(.insert) /// Allows user to delete rows. public static let Delete = MultivaluedOptions(.delete) /// Allows user to reorder rows public static let Reorder = MultivaluedOptions(.reorder) } /// Base class for multivalued sections. Use one of the subclasses. open class BaseMultivaluedSection: Section { public var multivaluedOptions: MultivaluedOptions public var showInsertIconInAddButton = true public var multivaluedRowToInsertAt: ((Int) -> BaseRow)? public required init(multivaluedOptions: MultivaluedOptions = MultivaluedOptions.Insert.union(.Delete), header: String? = nil, footer: String? = nil, _ initializer: (BaseMultivaluedSection) -> Void = { _ in }) { self.multivaluedOptions = multivaluedOptions super.init(header: header, footer: footer, {section in initializer(section as! BaseMultivaluedSection) }) guard multivaluedOptions.contains(.Insert) else { return } initialize() } public required init() { self.multivaluedOptions = MultivaluedOptions.Insert.union(.Delete) super.init() initialize() } #if swift(>=4.1) public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { self.multivaluedOptions = MultivaluedOptions.Insert.union(.Delete) super.init(elements) initialize() } #endif func initialize() { // Overridden by subclasses } /** Method used to get all the values of the section. - returns: An Array mapping the row values. [value] */ public func values() -> [Any?] { return kvoWrapper._allRows.filter({ $0.baseValue != nil }).map({ $0.baseValue }) } } /// Generic multivalued section. Pass the type of the add button row as generic parameter. open class GenericMultivaluedSection: BaseMultivaluedSection where AddButtonType: BaseRow { public var addButtonProvider: ((GenericMultivaluedSection) -> AddButtonType)! public required init(multivaluedOptions: MultivaluedOptions = MultivaluedOptions.Insert.union(.Delete), header: String? = nil, footer: String? = nil, _ initializer: (GenericMultivaluedSection) -> Void = { _ in }) { super.init(multivaluedOptions: multivaluedOptions, header: header, footer: footer, {section in initializer(section as! GenericMultivaluedSection) }) } public required init() { super.init() } #if swift(>=4.1) public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { super.init(elements) } #endif override func initialize() { let addRow = addButtonProvider(self) addRow.onCellSelection { cell, row in guard !row.isDisabled else { return } guard let tableView = cell.formViewController()?.tableView, let indexPath = row.indexPath else { return } cell.formViewController()?.tableView(tableView, commit: .insert, forRowAt: indexPath) } self <<< addRow } } /** * Multivalued sections allows us to easily create insertable, deletable and reorderable sections. By using a multivalued section we can add multiple values for a certain field, such as telephone numbers in a contact. */ open class MultivaluedSection: GenericMultivaluedSection { override func initialize() { if addButtonProvider == nil { addButtonProvider = { _ in return ButtonRow { $0.title = "Add" $0.cellStyle = .value1 }.cellUpdate { cell, _ in cell.textLabel?.textAlignment = .left } } } super.initialize() } } ================================================ FILE: Source/Core/SelectableRowType.swift ================================================ // SelectableRowType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /** * Every row that shall be used in a SelectableSection must conform to this protocol. */ public protocol SelectableRowType: RowType { var selectableValue: Cell.Value? { get set } } ================================================ FILE: Source/Core/SelectableSection.swift ================================================ // SelectableSection.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: SelectableSection /** Defines how the selection works in a SelectableSection - MultipleSelection: Multiple options can be selected at once - SingleSelection: Only one selection at a time. Can additionally specify if deselection is enabled or not. */ public enum SelectionType { /** * Multiple options can be selected at once */ case multipleSelection /** * Only one selection at a time. Can additionally specify if deselection is enabled or not. */ case singleSelection(enableDeselection: Bool) } /** * Protocol to be implemented by selectable sections types. Enables easier customization */ public protocol SelectableSectionType: Collection { associatedtype SelectableRow: BaseRow, SelectableRowType /// Defines how the selection works (single / multiple selection) var selectionType: SelectionType { get set } /// A closure called when a row of this section is selected. var onSelectSelectableRow: ((SelectableRow.Cell, SelectableRow) -> Void)? { get set } func selectedRow() -> SelectableRow? func selectedRows() -> [SelectableRow] } extension SelectableSectionType where Element == BaseRow, Self: AnyObject { /** Returns the selected row of this section. Should be used if selectionType is SingleSelection */ public func selectedRow() -> SelectableRow? { return selectedRows().first } /** Returns the selected rows of this section. Should be used if selectionType is MultipleSelection */ public func selectedRows() -> [SelectableRow] { let selectedRows: [BaseRow] = self.filter { $0 is SelectableRow && $0.baseValue != nil } return selectedRows.map { $0 as! SelectableRow } } /** Internal function used to set up a collection of rows before they are added to the section */ func prepare(selectableRows rows: [BaseRow]) { for row in rows { if let row = row as? SelectableRow { row.onCellSelection { [weak self] cell, row in guard let s = self, !row.isDisabled else { return } switch s.selectionType { case .multipleSelection: row.value = row.value == nil ? row.selectableValue : nil case let .singleSelection(enableDeselection): s.forEach { guard $0.baseValue != nil && $0 != row && $0 is SelectableRow else { return } $0.baseValue = nil $0.updateCell() } // Check if row is not already selected if row.value == nil { row.value = row.selectableValue } else if enableDeselection { row.value = nil } } row.updateCell() s.onSelectSelectableRow?(cell, row) } } } } } /// A subclass of Section that serves to create a section with a list of selectable options. open class SelectableSection: Section, SelectableSectionType where Row: SelectableRowType, Row: BaseRow { public typealias SelectableRow = Row /// Defines how the selection works (single / multiple selection) public var selectionType = SelectionType.singleSelection(enableDeselection: true) /// A closure called when a row of this section is selected. public var onSelectSelectableRow: ((Row.Cell, Row) -> Void)? public override init(_ initializer: @escaping (SelectableSection) -> Void) { super.init({ _ in }) initializer(self) } public init(_ header: String?, selectionType: SelectionType, _ initializer: @escaping (SelectableSection) -> Void = { _ in }) { self.selectionType = selectionType super.init(header, { _ in }) initializer(self) } public init(header: String?, footer: String?, selectionType: SelectionType, _ initializer: @escaping (SelectableSection) -> Void = { _ in }) { self.selectionType = selectionType super.init(header: header, footer: footer, { _ in }) initializer(self) } public required init() { super.init() } #if swift(>=4.1) public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { super.init(elements) } #endif open override func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) { prepare(selectableRows: rows) } } ================================================ FILE: Source/Core/SwipeActions.swift ================================================ // // Swipe.swift // Eureka // // Created by Marco Betschart on 14.06.17. // Copyright © 2017 Xmartlabs. All rights reserved. // import Foundation import UIKit public typealias SwipeActionHandler = (SwipeAction, BaseRow, ((Bool) -> Void)?) -> Void public class SwipeAction: ContextualAction { let handler: SwipeActionHandler let style: Style public var actionBackgroundColor: UIColor? public var image: UIImage? public var title: String? @available (*, deprecated, message: "Use actionBackgroundColor instead") public var backgroundColor: UIColor? { get { return actionBackgroundColor } set { self.actionBackgroundColor = newValue } } public init(style: Style, title: String?, handler: @escaping SwipeActionHandler){ self.style = style self.title = title self.handler = handler } func contextualAction(forRow: BaseRow) -> ContextualAction { var action: ContextualAction if #available(iOS 11, *){ action = UIContextualAction(style: style.contextualStyle as! UIContextualAction.Style, title: title){ [weak self] action, view, completion -> Void in guard let strongSelf = self else{ return } strongSelf.handler(strongSelf, forRow) { shouldComplete in if #available(iOS 13, *) { // starting in iOS 13, completion handler is not removing the row automatically, so we need to remove it ourselves if shouldComplete && action.style == .destructive { forRow.section?.remove(at: forRow.indexPath!.row) } } completion(shouldComplete) } } } else { action = UITableViewRowAction(style: style.contextualStyle as! UITableViewRowAction.Style,title: title){ [weak self] (action, indexPath) -> Void in guard let strongSelf = self else{ return } strongSelf.handler(strongSelf, forRow) { _ in DispatchQueue.main.async { guard action.style == .destructive else { forRow.baseCell?.formViewController()?.tableView?.setEditing(false, animated: true) return } forRow.section?.remove(at: indexPath.row) } } } } if let color = self.actionBackgroundColor { action.actionBackgroundColor = color } if let image = self.image { action.image = image } return action } public enum Style { case normal case destructive var contextualStyle: ContextualStyle { if #available(iOS 11, *){ switch self{ case .normal: return UIContextualAction.Style.normal case .destructive: return UIContextualAction.Style.destructive } } else { switch self{ case .normal: return UITableViewRowAction.Style.normal case .destructive: return UITableViewRowAction.Style.destructive } } } } } public struct SwipeConfiguration { unowned var row: BaseRow init(_ row: BaseRow){ self.row = row } public var performsFirstActionWithFullSwipe = false public var actions: [SwipeAction] = [] } extension SwipeConfiguration { @available(iOS 11.0, *) var contextualConfiguration: UISwipeActionsConfiguration? { let contextualConfiguration = UISwipeActionsConfiguration(actions: self.contextualActions as! [UIContextualAction]) contextualConfiguration.performsFirstActionWithFullSwipe = self.performsFirstActionWithFullSwipe return contextualConfiguration } var contextualActions: [ContextualAction]{ return self.actions.map { $0.contextualAction(forRow: self.row) } } } protocol ContextualAction { var actionBackgroundColor: UIColor? { get set } var image: UIImage? { get set } var title: String? { get set } } extension UITableViewRowAction: ContextualAction { public var image: UIImage? { get { return nil } set { return } } public var actionBackgroundColor: UIColor? { get { return backgroundColor } set { self.backgroundColor = newValue } } } @available(iOS 11.0, *) extension UIContextualAction: ContextualAction { public var actionBackgroundColor: UIColor? { get { return backgroundColor } set { self.backgroundColor = newValue } } } public protocol ContextualStyle{} extension UITableViewRowAction.Style: ContextualStyle {} @available(iOS 11.0, *) extension UIContextualAction.Style: ContextualStyle {} ================================================ FILE: Source/Core/Validation.swift ================================================ // RowValidationType.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct ValidationError: Equatable { public let msg: String public init(msg: String) { self.msg = msg } } public func == (lhs: ValidationError, rhs: ValidationError) -> Bool { return lhs.msg == rhs.msg } public protocol BaseRuleType { var id: String? { get set } var validationError: ValidationError { get set } } public protocol RuleType: BaseRuleType { associatedtype RowValueType func isValid(value: RowValueType?) -> ValidationError? } public struct ValidationOptions: OptionSet { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } public static let validatesOnDemand = ValidationOptions(rawValue: 1 << 0) public static let validatesOnChange = ValidationOptions(rawValue: 1 << 1) public static let validatesOnBlur = ValidationOptions(rawValue: 1 << 2) public static let validatesOnChangeAfterBlurred = ValidationOptions(rawValue: 1 << 3) public static let validatesAlways: ValidationOptions = [.validatesOnChange, .validatesOnBlur] } public struct ValidationRuleHelper where T: Equatable { let validateFn: ((T?) -> ValidationError?) public let rule: BaseRuleType } public struct RuleSet { internal var rules: [ValidationRuleHelper] = [] public init() {} /// Add a validation Rule to a Row /// - Parameter rule: RuleType object typed to the same type of the Row.value public mutating func add(rule: Rule) where T == Rule.RowValueType { let validFn: ((T?) -> ValidationError?) = { (val: T?) in return rule.isValid(value: val) } rules.append(ValidationRuleHelper(validateFn: validFn, rule: rule)) } public mutating func remove(ruleWithIdentifier identifier: String) { if let index = rules.firstIndex(where: { (validationRuleHelper) -> Bool in return validationRuleHelper.rule.id == identifier }) { rules.remove(at: index) } } public mutating func removeAllRules() { rules.removeAll() } } ================================================ FILE: Source/Eureka.h ================================================ // Eureka.h // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #import //! Project version number for Eureka. FOUNDATION_EXPORT double EurekaVersionNumber; //! Project version string for Eureka. FOUNDATION_EXPORT const unsigned char EurekaVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Source/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Source/PrivacyInfo.xcprivacy ================================================ NSPrivacyCollectedDataTypes NSPrivacyCollectedDataType NSPrivacyCollectedDataTypeOtherUsageData NSPrivacyCollectedDataTypeLinked NSPrivacyCollectedDataTypeTracking NSPrivacyCollectedDataTypePurposes NSPrivacyCollectedDataTypePurposeAppFunctionality ================================================ FILE: Source/Rows/ActionSheetRow.swift ================================================ // ActionSheetRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class AlertSelectorCell : Cell, CellType where T: Equatable { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() accessoryType = .none editingAccessoryType = accessoryType selectionStyle = row.isDisabled ? .none : .default } open override func didSelect() { super.didSelect() row.deselect() } } open class _ActionSheetRow: AlertOptionsRow, PresenterRowType where Cell: BaseCell { public typealias ProviderType = SelectorAlertController<_ActionSheetRow> public var onPresentCallback: ((FormViewController, ProviderType) -> Void)? lazy public var presentationMode: PresentationMode? = { return .presentModally(controllerProvider: ControllerProvider.callback { [weak self] in let vc = SelectorAlertController<_ActionSheetRow>(title: self?.selectorTitle, message: nil, preferredStyle: .actionSheet) if let popView = vc.popoverPresentationController { guard let cell = self?.cell, let tableView = cell.formViewController()?.tableView else { fatalError() } popView.sourceView = tableView popView.sourceRect = tableView.convert(cell.detailTextLabel?.frame ?? cell.textLabel?.frame ?? cell.contentView.frame, from: cell) } vc.row = self return vc }, onDismiss: { [weak self] in $0.dismiss(animated: true) self?.cell?.formViewController()?.tableView?.reloadData() }) }() public required init(tag: String?) { super.init(tag: tag) } open override func customDidSelect() { super.customDidSelect() if let presentationMode = presentationMode, !isDisabled { if let controller = presentationMode.makeController() { controller.row = self onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) } } } } /// An options row where the user can select an option from an ActionSheet public final class ActionSheetRow: _ActionSheetRow>, RowType where T: Equatable { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/AlertRow.swift ================================================ // AlertRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class _AlertRow: AlertOptionsRow, PresenterRowType where Cell: BaseCell { public typealias PresentedController = SelectorAlertController<_AlertRow> open var onPresentCallback: ((FormViewController, PresentedController) -> Void)? lazy open var presentationMode: PresentationMode? = { return .presentModally(controllerProvider: ControllerProvider.callback { [weak self] in let vc = PresentedController(title: self?.selectorTitle, message: nil, preferredStyle: .alert) vc.row = self return vc }, onDismiss: { [weak self] in $0.dismiss(animated: true) self?.cell?.formViewController()?.tableView?.reloadData() }) }() public required init(tag: String?) { super.init(tag: tag) } open override func customDidSelect() { super.customDidSelect() if let presentationMode = presentationMode, !isDisabled { if let controller = presentationMode.makeController() { controller.row = self onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) } } } } /// An options row where the user can select an option from a modal Alert public final class AlertRow: _AlertRow>, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/ButtonRow.swift ================================================ // ButtonRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: ButtonCell open class ButtonCellOf: Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default accessoryType = .none editingAccessoryType = accessoryType textLabel?.textAlignment = .center textLabel?.textColor = tintColor.withAlphaComponent(row.isDisabled ? 0.3 : 1.0) } open override func didSelect() { super.didSelect() row.deselect() } } public typealias ButtonCell = ButtonCellOf // MARK: ButtonRow open class _ButtonRowOf : Row> { open var presentationMode: PresentationMode? required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil cellStyle = .default } open override func customDidSelect() { super.customDidSelect() if !isDisabled { if let presentationMode = presentationMode { if let controller = presentationMode.makeController() { presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } } } } open override func customUpdateCell() { super.customUpdateCell() let leftAligmnment = presentationMode != nil cell.textLabel?.textAlignment = leftAligmnment ? .left : .center cell.accessoryType = !leftAligmnment || isDisabled ? .none : .disclosureIndicator cell.editingAccessoryType = cell.accessoryType cell.textLabel?.textColor = !leftAligmnment ? cell.tintColor.withAlphaComponent(isDisabled ? 0.3 : 1.0) : nil } open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) (segue.destination as? RowControllerType)?.onDismissCallback = presentationMode?.onDismissCallback } } /// A generic row with a button. The action of this button can be anything but normally will push a new view controller public final class ButtonRowOf : _ButtonRowOf, RowType { public required init(tag: String?) { super.init(tag: tag) } } /// A row with a button and String value. The action of this button can be anything but normally will push a new view controller public typealias ButtonRow = ButtonRowOf ================================================ FILE: Source/Rows/ButtonRowWithPresent.swift ================================================ // Rows.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class _ButtonRowWithPresent: Row>, PresenterRowType where VCType: UIViewController { open var presentationMode: PresentationMode? open var onPresentCallback: ((FormViewController, VCType) -> Void)? required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil cellStyle = .default } open override func customUpdateCell() { super.customUpdateCell() let leftAligmnment = presentationMode != nil cell.textLabel?.textAlignment = leftAligmnment ? .left : .center cell.accessoryType = !leftAligmnment || isDisabled ? .none : .disclosureIndicator cell.editingAccessoryType = cell.accessoryType if !leftAligmnment { var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 cell.tintColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) cell.textLabel?.textColor = UIColor(red: red, green: green, blue: blue, alpha:isDisabled ? 0.3 : 1.0) } else { cell.textLabel?.textColor = nil } } open override func customDidSelect() { super.customDidSelect() if let presentationMode = presentationMode, !isDisabled { if let controller = presentationMode.makeController() { controller.row = self onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) } } } open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as? VCType else { return } if let callback = presentationMode?.onDismissCallback { rowVC.onDismissCallback = callback } rowVC.row = self onPresentCallback?(cell.formViewController()!, rowVC) } } // MARK: Rows /// A generic row with a button that presents a view controller when tapped public final class ButtonRowWithPresent : _ButtonRowWithPresent, RowType where VCType: UIViewController { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/CheckRow.swift ================================================ // CheckRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: CheckCell open class CheckCell: Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() accessoryType = row.value == true ? .checkmark : .none editingAccessoryType = accessoryType selectionStyle = .default if row.isDisabled { tintAdjustmentMode = .dimmed selectionStyle = .none } else { tintAdjustmentMode = .automatic } } open override func setup() { super.setup() accessoryType = .checkmark editingAccessoryType = accessoryType } open override func didSelect() { row.value = row.value ?? false ? false : true row.deselect() row.updateCell() } } // MARK: CheckRow open class _CheckRow: Row { required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil } } ///// Boolean row that has a checkmark as accessoryType public final class CheckRow: _CheckRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/Common/AlertOptionsRow.swift ================================================ // // AlertOptionsRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import Foundation open class AlertOptionsRow : OptionsRow, AlertOptionsProviderRow where Cell: BaseCell { typealias OptionsProviderType = OptionsProvider open var cancelTitle: String? required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/Common/DateFieldRow.swift ================================================ // DateFieldRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit public protocol DatePickerRowProtocol: AnyObject { var minimumDate: Date? { get set } var maximumDate: Date? { get set } var minuteInterval: Int? { get set } } open class DateCell: Cell, CellType { public var datePicker: UIDatePicker public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { datePicker = UIDatePicker() super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { datePicker = UIDatePicker() super.init(coder: aDecoder) } open override func setup() { super.setup() accessoryType = .none editingAccessoryType = .none datePicker.datePickerMode = datePickerMode() datePicker.addTarget(self, action: #selector(DateCell.datePickerValueDidChange(_:)), for: .valueChanged) #if swift(>=5.2) if #available(iOS 13.4, *) { datePicker.preferredDatePickerStyle = .wheels } #endif } deinit { datePicker.removeTarget(self, action: nil, for: .allEvents) } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default datePicker.setDate(row.value ?? Date(), animated: row is CountDownPickerRow) datePicker.minimumDate = (row as? DatePickerRowProtocol)?.minimumDate datePicker.maximumDate = (row as? DatePickerRowProtocol)?.maximumDate if let minuteIntervalValue = (row as? DatePickerRowProtocol)?.minuteInterval { datePicker.minuteInterval = minuteIntervalValue } if row.isHighlighted { textLabel?.textColor = tintColor } } open override func didSelect() { super.didSelect() row.deselect() } override open var inputView: UIView? { if let v = row.value { datePicker.setDate(v, animated:row is CountDownRow) } return datePicker } @objc(datePickerValueDidChange:) func datePickerValueDidChange(_ sender: UIDatePicker) { row.value = sender.date detailTextLabel?.text = row.displayValueFor?(row.value) } private func datePickerMode() -> UIDatePicker.Mode { switch row { case is DateRow: return .date case is TimeRow: return .time case is DateTimeRow: return .dateAndTime case is CountDownRow: return .countDownTimer default: return .date } } open override func cellCanBecomeFirstResponder() -> Bool { return canBecomeFirstResponder } override open var canBecomeFirstResponder: Bool { return !row.isDisabled } } open class _DateFieldRow: Row, DatePickerRowProtocol, NoValueDisplayTextConformance { /// The minimum value for this row's UIDatePicker open var minimumDate: Date? /// The maximum value for this row's UIDatePicker open var maximumDate: Date? /// The interval between options for this row's UIDatePicker open var minuteInterval: Int? /// The formatter for the date picked by the user open var dateFormatter: DateFormatter? open var noValueDisplayText: String? = nil required public init(tag: String?) { super.init(tag: tag) displayValueFor = { [unowned self] value in guard let val = value, let formatter = self.dateFormatter else { return nil } return formatter.string(from: val) } } } ================================================ FILE: Source/Rows/Common/DateInlineFieldRow.swift ================================================ // DateInlineFieldRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class DateInlineCell: Cell, CellType { public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() accessoryType = .none editingAccessoryType = .none } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default } open override func didSelect() { super.didSelect() row.deselect() } } open class _DateInlineFieldRow: Row, DatePickerRowProtocol, NoValueDisplayTextConformance { /// The minimum value for this row's UIDatePicker open var minimumDate: Date? /// The maximum value for this row's UIDatePicker open var maximumDate: Date? /// The interval between options for this row's UIDatePicker open var minuteInterval: Int? /// The formatter for the date picked by the user open var dateFormatter: DateFormatter? open var noValueDisplayText: String? required public init(tag: String?) { super.init(tag: tag) dateFormatter = DateFormatter() dateFormatter?.locale = Locale.current displayValueFor = { [unowned self] value in guard let val = value, let formatter = self.dateFormatter else { return nil } return formatter.string(from: val) } } } ================================================ FILE: Source/Rows/Common/DecimalFormatter.swift ================================================ // DecimalFormatter.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// A custom formatter for numbers with two digits after the decimal mark open class DecimalFormatter: NumberFormatter, FormatterProtocol { /// Creates the formatter with 2 Fraction Digits, Locale set to .current and .decimal NumberFormatter.Style public override init() { super.init() locale = Locale.current numberStyle = .decimal minimumFractionDigits = 2 maximumFractionDigits = 2 } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// Creates an NSNumber from the given String /// - Parameter obj: Pointer to NSNumber object to assign /// - Parameter string: String with number assumed to have the configured min. fraction digits. /// - Parameter rangep: Unused range parameter override open func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, range rangep: UnsafeMutablePointer?) throws { guard obj != nil else { return } let str = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "") // Recover the number from the string in a way that forces the formatter's fraction digits // numberWithoutDecimals / 10 ^ minimumFractionDigits obj?.pointee = NSNumber(value: (Double(str) ?? 0.0)/Double(pow(10.0, Double(minimumFractionDigits)))) } open func getNewPosition(forPosition position: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition { return textInput.position(from: position, offset:((newValue?.count ?? 0) - (oldValue?.count ?? 0))) ?? position } } ================================================ FILE: Source/Rows/Common/FieldRow.swift ================================================ // FieldRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit public protocol InputTypeInitiable { init?(string stringValue: String) } public protocol FieldRowConformance : FormatterConformance { var titlePercentage : CGFloat? { get set } var placeholder : String? { get set } var placeholderColor : UIColor? { get set } } extension Int: InputTypeInitiable { public init?(string stringValue: String) { self.init(stringValue, radix: 10) } } extension Float: InputTypeInitiable { public init?(string stringValue: String) { self.init(stringValue) } } extension String: InputTypeInitiable { public init?(string stringValue: String) { self.init(stringValue) } } extension URL: InputTypeInitiable {} extension Double: InputTypeInitiable { public init?(string stringValue: String) { self.init(stringValue) } } open class FormatteableRow: Row, FormatterConformance where Cell: BaseCell, Cell: TextInputCell { /// A formatter to be used to format the user's input open var formatter: Formatter? /// If the formatter should be used while the user is editing the text. open var useFormatterDuringInput = false open var useFormatterOnDidBeginEditing: Bool? public required init(tag: String?) { super.init(tag: tag) displayValueFor = { [unowned self] value in guard let v = value else { return nil } guard let formatter = self.formatter else { return String(describing: v) } if (self.cell.textInput as? UIView)?.isFirstResponder == true { return self.useFormatterDuringInput ? formatter.editingString(for: v) : String(describing: v) } return formatter.string(for: v) } } } open class FieldRow: FormatteableRow, FieldRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: TextFieldCell { /// Configuration for the keyboardReturnType of this row open var keyboardReturnType: KeyboardReturnTypeConfiguration? /// The percentage of the cell that should be occupied by the textField @available (*, deprecated, message: "Use titlePercentage instead") open var textFieldPercentage : CGFloat? { get { return titlePercentage.map { 1 - $0 } } set { titlePercentage = newValue.map { 1 - $0 } } } /// The percentage of the cell that should be occupied by the title (i.e. the titleLabel and optional imageView combined) open var titlePercentage: CGFloat? /// The placeholder for the textField open var placeholder: String? /// The textColor for the textField's placeholder open var placeholderColor: UIColor? public required init(tag: String?) { super.init(tag: tag) } } /** * Protocol for cells that contain a UITextField */ public protocol TextInputCell { var textInput: UITextInput { get } } public protocol TextFieldCell: TextInputCell { var textField: UITextField! { get } } extension TextFieldCell { public var textInput: UITextInput { return textField } } open class _FieldCell : Cell, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable { @IBOutlet public weak var textField: UITextField! @IBOutlet public weak var titleLabel: UILabel? fileprivate var observingTitleText = false private var awakeFromNibCalled = false open var dynamicConstraints = [NSLayoutConstraint]() private var calculatedTitlePercentage: CGFloat = 0.7 public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { let textField = UITextField() self.textField = textField textField.translatesAutoresizingMaskIntoConstraints = false super.init(style: style, reuseIdentifier: reuseIdentifier) setupTitleLabel() contentView.addSubview(titleLabel!) contentView.addSubview(textField) NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } guard me.observingTitleText else { return } me.titleLabel?.removeObserver(me, forKeyPath: "text") me.observingTitleText = false } NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } guard !me.observingTitleText else { return } me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil) me.observingTitleText = true } NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in self?.setupTitleLabel() self?.setNeedsUpdateConstraints() } } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func awakeFromNib() { super.awakeFromNib() awakeFromNibCalled = true } deinit { textField?.delegate = nil textField?.removeTarget(self, action: nil, for: .allEvents) guard !awakeFromNibCalled else { return } if observingTitleText { titleLabel?.removeObserver(self, forKeyPath: "text") } imageView?.removeObserver(self, forKeyPath: "image") NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) } open override func setup() { super.setup() selectionStyle = .none if !awakeFromNibCalled { titleLabel?.addObserver(self, forKeyPath: "text", options: [.new, .old], context: nil) observingTitleText = true imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil) } textField.addTarget(self, action: #selector(_FieldCell.textFieldDidChange(_:)), for: .editingChanged) if let titleLabel = titleLabel { // Make sure the title takes over most of the empty space so that the text field starts editing at the back. let priority = UILayoutPriority(rawValue: titleLabel.contentHuggingPriority(for: .horizontal).rawValue + 1) textField.setContentHuggingPriority(priority, for: .horizontal) } } open override func update() { super.update() detailTextLabel?.text = nil if !awakeFromNibCalled { if let title = row.title { switch row.cellStyle { case .subtitle: textField.textAlignment = .left textField.clearButtonMode = .whileEditing default: textField.textAlignment = title.isEmpty ? .left : .right textField.clearButtonMode = title.isEmpty ? .whileEditing : .never } } else { textField.textAlignment = .left textField.clearButtonMode = .whileEditing } } else { textLabel?.text = nil titleLabel?.text = row.title if #available(iOS 13.0, *) { titleLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label } else { titleLabel?.textColor = row.isDisabled ? .gray : .black } } textField.delegate = self textField.text = row.displayValueFor?(row.value) textField.isEnabled = !row.isDisabled if #available(iOS 13.0, *) { textField.textColor = row.isDisabled ? .tertiaryLabel : .label } else { textField.textColor = row.isDisabled ? .gray : .black } textField.font = .preferredFont(forTextStyle: .body) if let placeholder = (row as? FieldRowConformance)?.placeholder { if let color = (row as? FieldRowConformance)?.placeholderColor { textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color]) } else { textField.placeholder = (row as? FieldRowConformance)?.placeholder } } if row.isHighlighted { titleLabel?.textColor = tintColor } } open override func cellCanBecomeFirstResponder() -> Bool { return !row.isDisabled && textField?.canBecomeFirstResponder == true } open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool { return textField.becomeFirstResponder() } open override func cellResignFirstResponder() -> Bool { return textField.resignFirstResponder() } open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let obj = object as AnyObject? if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], ((obj === titleLabel && keyPathValue == "text") || (obj === imageView && keyPathValue == "image")) && (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue { setNeedsUpdateConstraints() updateConstraintsIfNeeded() } } // MARK: Helpers open func customConstraints() { guard !awakeFromNibCalled else { return } contentView.removeConstraints(dynamicConstraints) dynamicConstraints = [] switch row.cellStyle { case .subtitle: var views: [String: AnyObject] = ["textField": textField] if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { views["titleLabel"] = titleLabel dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-3-[textField]-|", options: .alignAllLeading, metrics: nil, views: views) titleLabel.setContentHuggingPriority( UILayoutPriority(textField.contentHuggingPriority(for: .vertical).rawValue + 1), for: .vertical) dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: textField, attribute: .centerX, multiplier: 1, constant: 0)) } else { dynamicConstraints.append(NSLayoutConstraint(item: textField!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) } if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-|", options: [], metrics: nil, views: views) dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) } else { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) } } else { if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-|", options: [], metrics: nil, views: views) dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: [], metrics: nil, views: views) } else { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views) } } default: var views: [String: AnyObject] = ["textField": textField] dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textField]-|", options: .alignAllLastBaseline, metrics: nil, views: views) if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { views["titleLabel"] = titleLabel dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-|", options: .alignAllLastBaseline, metrics: nil, views: views) dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: textField, attribute: .centerY, multiplier: 1, constant: 0)) } if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views) dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, toItem: contentView, attribute: .width, multiplier: calculatedTitlePercentage, constant: 0.0)) } else { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) } } else { if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views) dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .width, relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, toItem: contentView, attribute: .width, multiplier: calculatedTitlePercentage, constant: 0.0)) } else { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views) } } } contentView.addConstraints(dynamicConstraints) } open override func updateConstraints() { customConstraints() super.updateConstraints() } @objc open func textFieldDidChange(_ textField: UITextField) { guard textField.markedTextRange == nil else { return } guard let textValue = textField.text else { row.value = nil return } guard let fieldRow = row as? FieldRowConformance, let formatter = fieldRow.formatter else { row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) return } if fieldRow.useFormatterDuringInput { let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) defer { unsafePointer.deallocate() } let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T guard var selStartPos = textField.selectedTextRange?.start else { return } let oldVal = textField.text textField.text = row.displayValueFor?(row.value) selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) ?? selStartPos textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos) return } } else { let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) defer { unsafePointer.deallocate() } let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T } else { row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) } } } // MARK: Helpers private func setupTitleLabel() { titleLabel = self.textLabel titleLabel?.translatesAutoresizingMaskIntoConstraints = false titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) titleLabel?.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .horizontal) } private func displayValue(useFormatter: Bool) -> String? { guard let v = row.value else { return nil } if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) } return String(describing: v) } // MARK: TextFieldDelegate open func textFieldDidBeginEditing(_ textField: UITextField) { formViewController()?.beginEditing(of: self) formViewController()?.textInputDidBeginEditing(textField, cell: self) if let fieldRowConformance = row as? FormatterConformance, let _ = fieldRowConformance.formatter, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput { textField.text = displayValue(useFormatter: true) } else { textField.text = displayValue(useFormatter: false) } } open func textFieldDidEndEditing(_ textField: UITextField) { formViewController()?.endEditing(of: self) formViewController()?.textInputDidEndEditing(textField, cell: self) textFieldDidChange(textField) textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) } open func textFieldShouldReturn(_ textField: UITextField) -> Bool { return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true } open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return formViewController()?.textInput(textField, shouldChangeCharactersInRange:range, replacementString:string, cell: self) ?? true } open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true } open func textFieldShouldClear(_ textField: UITextField) -> Bool { return formViewController()?.textInputShouldClear(textField, cell: self) ?? true } open func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true } open override func layoutSubviews() { super.layoutSubviews() guard let row = (row as? FieldRowConformance) else { return } defer { // As titleLabel is the textLabel, iOS may re-layout without updating constraints, for example: // swiping, showing alert or actionsheet from the same section. // thus we need forcing update to use customConstraints() setNeedsUpdateConstraints() updateConstraintsIfNeeded() } guard let titlePercentage = row.titlePercentage else { return } var targetTitleWidth = bounds.size.width * titlePercentage if let imageView = imageView, let _ = imageView.image, let titleLabel = titleLabel { var extraWidthToSubtract = titleLabel.frame.minX - imageView.frame.minX // Left-to-right interface layout if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft { extraWidthToSubtract = imageView.frame.maxX - titleLabel.frame.maxX } targetTitleWidth -= extraWidthToSubtract } calculatedTitlePercentage = targetTitleWidth / contentView.bounds.size.width } } ================================================ FILE: Source/Rows/Common/GenericMultipleSelectorRow.swift ================================================ // GenericMultipleSelectorRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// Generic options selector row that allows multiple selection. open class GenericMultipleSelectorRow: Row, PresenterRowType, NoValueDisplayTextConformance, OptionsProviderRow where Cell: BaseCell, Cell.Value == Set { public typealias PresentedController = MultipleSelectorViewController> /// Defines how the view controller will be presented, pushed, etc. open var presentationMode: PresentationMode? /// Will be called before the presentation occurs. open var onPresentCallback: ((FormViewController, PresentedController) -> Void)? /// Title to be displayed for the options open var selectorTitle: String? open var noValueDisplayText: String? /// Options from which the user will choose open var optionsProvider: OptionsProvider? required public init(tag: String?) { super.init(tag: tag) displayValueFor = { (rowValue: Set?) in return rowValue?.map({ String(describing: $0) }).sorted().joined(separator: ", ") } presentationMode = .show(controllerProvider: ControllerProvider.callback { return MultipleSelectorViewController>() }, onDismiss: { vc in let _ = vc.navigationController?.popViewController(animated: true) }) } /** Extends `didSelect` method */ open override func customDidSelect() { super.customDidSelect() guard let presentationMode = presentationMode, !isDisabled else { return } if let controller = presentationMode.makeController() { controller.row = self controller.title = selectorTitle ?? controller.title onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } } /** Prepares the pushed row setting its title and completion callback. */ open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as Any as? PresentedController else { return } rowVC.title = selectorTitle ?? rowVC.title rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback onPresentCallback?(cell.formViewController()!, rowVC) rowVC.row = self } } ================================================ FILE: Source/Rows/Common/OptionsRow.swift ================================================ // OptionsRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation open class OptionsRow : Row, NoValueDisplayTextConformance, OptionsProviderRow where Cell: BaseCell { open var optionsProvider: OptionsProvider? open var selectorTitle: String? open var noValueDisplayText: String? required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/Common/Protocols.swift ================================================ // Protocols.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public protocol FormatterConformance: AnyObject { var formatter: Formatter? { get set } var useFormatterDuringInput: Bool { get set } var useFormatterOnDidBeginEditing: Bool? { get set } } public protocol NoValueDisplayTextConformance: AnyObject { var noValueDisplayText: String? { get set } } ================================================ FILE: Source/Rows/Common/SelectorRow.swift ================================================ // SelectorRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class PushSelectorCell : Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() accessoryType = .disclosureIndicator editingAccessoryType = accessoryType selectionStyle = row.isDisabled ? .none : .default } } /// Generic row type where a user must select a value among several options. open class SelectorRow: OptionsRow, PresenterRowType where Cell: BaseCell { /// Defines how the view controller will be presented, pushed, etc. open var presentationMode: PresentationMode>>? /// Will be called before the presentation occurs. open var onPresentCallback: ((FormViewController, SelectorViewController>) -> Void)? required public init(tag: String?) { super.init(tag: tag) } /** Extends `didSelect` method */ open override func customDidSelect() { super.customDidSelect() guard let presentationMode = presentationMode, !isDisabled else { return } if let controller = presentationMode.makeController() { controller.row = self controller.title = selectorTitle ?? controller.title onPresentCallback?(cell.formViewController()!, controller) presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) } else { presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) } } /** Prepares the pushed row setting its title and completion callback. */ open override func prepare(for segue: UIStoryboardSegue) { super.prepare(for: segue) guard let rowVC = segue.destination as Any as? SelectorViewController> else { return } rowVC.title = selectorTitle ?? rowVC.title rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback onPresentCallback?(cell.formViewController()!, rowVC) rowVC.row = self } } ================================================ FILE: Source/Rows/Controllers/MultipleSelectorViewController.swift ================================================ // MultipleSelectorViewController.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// Selector Controller that enables multiple selection open class _MultipleSelectorViewController : FormViewController, TypedRowControllerType where Row: BaseRow, Row.Cell.Value == OptionsRow.OptionsProviderType.Option, OptionsRow.OptionsProviderType.Option: Hashable { /// The row that pushed or presented this controller public var row: RowOf>! public var selectableRowSetup: ((_ row: Row) -> Void)? public var selectableRowCellSetup: ((_ cell: Row.Cell, _ row: Row) -> Void)? public var selectableRowCellUpdate: ((_ cell: Row.Cell, _ row: Row) -> Void)? /// A closure to be called when the controller disappears. public var onDismissCallback: ((UIViewController) -> Void)? /// A closure that should return key for particular row value. /// This key is later used to break options by sections. public var sectionKeyForValue: ((Row.Cell.Value) -> (String))? /// A closure that returns header title for a section for particular key. /// By default returns the key itself. public var sectionHeaderTitleForKey: ((String) -> String?)? = { $0 } /// A closure that returns footer title for a section for particular key. public var sectionFooterTitleForKey: ((String) -> String?)? public var optionsProviderRow: OptionsRow { return row as! OptionsRow } override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } convenience public init(_ callback: ((UIViewController) -> Void)?) { self.init(nibName: nil, bundle: nil) onDismissCallback = callback } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func viewDidLoad() { super.viewDidLoad() setupForm() } open func setupForm() { optionsProviderRow.optionsProvider?.options(for: self) { [weak self] (options: [OptionsRow.OptionsProviderType.Option]?) in guard let strongSelf = self, let options = options else { return } strongSelf.optionsProviderRow.cachedOptionsData = options strongSelf.setupForm(with: options) } } open func setupForm(with options: [OptionsRow.OptionsProviderType.Option]) { if let optionsBySections = optionsBySections(with: options) { for (sectionKey, options) in optionsBySections { form +++ section(with: options, header: sectionHeaderTitleForKey?(sectionKey), footer: sectionFooterTitleForKey?(sectionKey)) } } else { form +++ section(with: options, header: row.title, footer: nil) } } open func optionsBySections(with options: [OptionsRow.OptionsProviderType.Option]) -> [(String, [Row.Cell.Value])]? { guard let sectionKeyForValue = sectionKeyForValue else { return nil } let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in var reduced = reduced let key = sectionKeyForValue(option) var items = reduced[key] ?? [] items.append(option) reduced[key] = items return reduced } return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 }) } func section(with options: [OptionsRow.OptionsProviderType.Option], header: String?, footer: String?) -> SelectableSection { let section = SelectableSection(header: header ?? "", footer: footer ?? "", selectionType: .multipleSelection) { section in section.onSelectSelectableRow = { [weak self] _, selectableRow in var newValue: Set = self?.row.value ?? [] if let selectableValue = selectableRow.value { newValue.insert(selectableValue) } else { newValue.remove(selectableRow.selectableValue!) } self?.row.value = newValue } } for option in options { section <<< Row.init { lrow in lrow.title = String(describing: option) lrow.selectableValue = option lrow.value = self.row.value?.contains(option) ?? false ? option : nil self.selectableRowSetup?(lrow) }.cellSetup { [weak self] cell, row in self?.selectableRowCellSetup?(cell, row) }.cellUpdate { [weak self] cell, row in self?.selectableRowCellUpdate?(cell, row) } } return section } } open class MultipleSelectorViewController: _MultipleSelectorViewController, OptionsRow> where OptionsRow.OptionsProviderType.Option: Hashable{ override public init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } } ================================================ FILE: Source/Rows/Controllers/SelectorAlertController.swift ================================================ // SelectorAlertController.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /// Specific type, Responsible for the options passed to a selector alert view controller public protocol AlertOptionsProviderRow: OptionsProviderRow { var cancelTitle: String? { get set } } /// Selector UIAlertController open class SelectorAlertController: UIAlertController, TypedRowControllerType where AlertOptionsRow.OptionsProviderType.Option == AlertOptionsRow.Cell.Value, AlertOptionsRow: BaseRow { /// The row that pushed or presented this controller public var row: RowOf! @available(*, deprecated, message: "Use AlertOptionsRow.cancelTitle instead.") public var cancelTitle = NSLocalizedString("Cancel", comment: "") /// A closure to be called when the controller disappears. public var onDismissCallback: ((UIViewController) -> Void)? /// Options provider to use to get available options. /// If not set will use synchronous data provider built with `row.dataProvider.arrayData`. // public var optionsProvider: OptionsProvider? public var optionsProviderRow: AlertOptionsRow { return row as Any as! AlertOptionsRow } override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } convenience public init(_ callback: ((UIViewController) -> Void)?) { self.init() onDismissCallback = callback } open override func viewDidLoad() { super.viewDidLoad() guard let options = optionsProviderRow.options else { return } let cancelTitle = optionsProviderRow.cancelTitle ?? NSLocalizedString("Cancel", comment: "") addAction(UIAlertAction(title: cancelTitle, style: .cancel, handler: nil)) for option in options { addAction(UIAlertAction(title: row.displayValueFor?(option), style: .default, handler: { [weak self] _ in self?.row.value = option self?.onDismissCallback?(self!) })) } } } ================================================ FILE: Source/Rows/Controllers/SelectorViewController.swift ================================================ // SelectorViewController.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit /** * Responsible for the options passed to a selector view controller */ public protocol OptionsProviderRow: TypedRowType { associatedtype OptionsProviderType: OptionsProviderConformance var optionsProvider: OptionsProviderType? { get set } var cachedOptionsData: [OptionsProviderType.Option]? { get set } } extension OptionsProviderRow where Self: BaseRow { public var options: [OptionsProviderType.Option]? { set (newValue){ let optProvider = OptionsProviderType.init(array: newValue) optionsProvider = optProvider } get { return self.cachedOptionsData ?? optionsProvider?.optionsArray } } public var cachedOptionsData: [OptionsProviderType.Option]? { get { return self._cachedOptionsData as? [OptionsProviderType.Option] } set { self._cachedOptionsData = newValue } } } public protocol OptionsProviderConformance: ExpressibleByArrayLiteral { associatedtype Option: Equatable init(array: [Option]?) func options(for selectorViewController: FormViewController, completion: @escaping ([Option]?) -> Void) var optionsArray: [Option]? { get } } /// Provider of selectable options. public enum OptionsProvider: OptionsProviderConformance { /// Synchronous provider that provides array of options it was initialized with case array([T]?) /// Provider that uses closure it was initialized with to provide options. Can be synchronous or asynchronous. case lazy((FormViewController, @escaping ([T]?) -> Void) -> Void) public init(array: [T]?) { self = .array(array) } public init(arrayLiteral elements: T...) { self = .array(elements) } public func options(for selectorViewController: FormViewController, completion: @escaping ([T]?) -> Void) { switch self { case let .array(array): completion(array) case let .lazy(fetch): fetch(selectorViewController, completion) } } public var optionsArray: [T]?{ switch self { case let .array(arrayData): return arrayData default: return nil } } } open class _SelectorViewController: FormViewController, TypedRowControllerType where Row: BaseRow, Row.Cell.Value == OptionsRow.OptionsProviderType.Option { /// The row that pushed or presented this controller public var row: RowOf! public var enableDeselection = true public var dismissOnSelection = true public var dismissOnChange = true public var selectableRowSetup: ((_ row: Row) -> Void)? public var selectableRowCellUpdate: ((_ cell: Row.Cell, _ row: Row) -> Void)? public var selectableRowCellSetup: ((_ cell: Row.Cell, _ row: Row) -> Void)? /// A closure to be called when the controller disappears. public var onDismissCallback: ((UIViewController) -> Void)? /// A closure that should return key for particular row value. /// This key is later used to break options by sections. public var sectionKeyForValue: ((Row.Cell.Value) -> (String))? /// A closure that returns header title for a section for particular key. /// By default returns the key itself. public var sectionHeaderTitleForKey: ((String) -> String?)? = { $0 } /// A closure that returns footer title for a section for particular key. public var sectionFooterTitleForKey: ((String) -> String?)? public var optionsProviderRow: OptionsRow { return row as! OptionsRow } override public init(style: UITableView.Style) { super.init(style: style) } override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } convenience public init(_ callback: ((UIViewController) -> Void)?) { self.init(nibName: nil, bundle: nil) onDismissCallback = callback } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func viewDidLoad() { super.viewDidLoad() setupForm() } open func setupForm() { let optProvider = optionsProviderRow.optionsProvider optProvider?.options(for: self) { [weak self] (options: [Row.Cell.Value]?) in guard let strongSelf = self, let options = options else { return } strongSelf.optionsProviderRow.cachedOptionsData = options strongSelf.setupForm(with: options) } } open func setupForm(with options: [Row.Cell.Value]) { if let optionsBySections = optionsBySections(with: options) { for (sectionKey, options) in optionsBySections { form +++ section(with: options, header: sectionHeaderTitleForKey?(sectionKey), footer: sectionFooterTitleForKey?(sectionKey)) } } else { form +++ section(with: options, header: nil, footer: nil) } } func optionsBySections(with options: [Row.Cell.Value]) -> [(String, [Row.Cell.Value])]? { guard let sectionKeyForValue = sectionKeyForValue else { return nil } let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in var reduced = reduced let key = sectionKeyForValue(option) reduced[key] = (reduced[key] ?? []) + [option] return reduced } return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 }) } func section(with options: [Row.Cell.Value], header: String?, footer: String?) -> SelectableSection { let section = SelectableSection(header: header, footer: footer, selectionType: .singleSelection(enableDeselection: enableDeselection)) { section in section.onSelectSelectableRow = { [weak self] _, row in let changed = self?.row.value != row.value self?.row.value = row.value if let form = row.section?.form { for section in form where section !== row.section && section is SelectableSection { let section = section as Any as! SelectableSection if let selectedRow = section.selectedRow(), selectedRow !== row { selectedRow.value = nil selectedRow.updateCell() } } } if self?.dismissOnSelection == true || (changed && self?.dismissOnChange == true) { self?.onDismissCallback?(self!) } } } for option in options { section <<< Row.init(String(describing: option)) { lrow in lrow.title = self.row.displayValueFor?(option) lrow.selectableValue = option lrow.value = self.row.value == option ? option : nil self.selectableRowSetup?(lrow) }.cellSetup { [weak self] cell, row in self?.selectableRowCellSetup?(cell, row) }.cellUpdate { [weak self] cell, row in self?.selectableRowCellUpdate?(cell, row) } } return section } } /// Selector Controller (used to select one option among a list) open class SelectorViewController: _SelectorViewController, OptionsRow> { } ================================================ FILE: Source/Rows/DateInlineRow.swift ================================================ // DateInliuneRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit extension DatePickerRowProtocol { func configureInlineRow(_ inlineRow: DatePickerRowProtocol) { inlineRow.minimumDate = minimumDate inlineRow.maximumDate = maximumDate inlineRow.minuteInterval = minuteInterval } func configurePickerStyle(_ cell: DatePickerCell, _ mode: UIDatePicker.Mode = .dateAndTime) { cell.datePicker.datePickerMode = mode // For Xcode 11.4 and above #if swift(>=5.2) if #available(iOS 14.0, *) { #if swift(>=5.3) && !(os(OSX) || (os(iOS) && targetEnvironment(macCatalyst))) cell.datePicker.preferredDatePickerStyle = .inline #else cell.datePicker.preferredDatePickerStyle = .wheels #endif } else if #available(iOS 13.4, *) { cell.datePicker.preferredDatePickerStyle = .wheels } #endif } } open class _DateInlineRow: _DateInlineFieldRow { public typealias InlineRow = DatePickerRow public required init(tag: String?) { super.init(tag: tag) dateFormatter?.timeStyle = .none dateFormatter?.dateStyle = .medium } open func setupInlineRow(_ inlineRow: DatePickerRow) { configureInlineRow(inlineRow) configurePickerStyle(inlineRow.cell, .date) } } open class _TimeInlineRow: _DateInlineFieldRow { public typealias InlineRow = TimePickerRow public required init(tag: String?) { super.init(tag: tag) dateFormatter?.timeStyle = .short dateFormatter?.dateStyle = .none } open func setupInlineRow(_ inlineRow: TimePickerRow) { configureInlineRow(inlineRow) configurePickerStyle(inlineRow.cell, .time) } } open class _DateTimeInlineRow: _DateInlineFieldRow { public typealias InlineRow = DateTimePickerRow public required init(tag: String?) { super.init(tag: tag) dateFormatter?.timeStyle = .short dateFormatter?.dateStyle = .short } open func setupInlineRow(_ inlineRow: DateTimePickerRow) { configureInlineRow(inlineRow) configurePickerStyle(inlineRow.cell) } } open class _CountDownInlineRow: _DateInlineFieldRow { public typealias InlineRow = CountDownPickerRow public required init(tag: String?) { super.init(tag: tag) displayValueFor = { guard let date = $0 else { return nil } let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: date) return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .full)?.replacingOccurrences(of: ",", with: "") } } public func setupInlineRow(_ inlineRow: CountDownPickerRow) { configureInlineRow(inlineRow) } } /// A row with an Date as value where the user can select a date from an inline picker view. public final class DateInlineRow_: _DateInlineRow, RowType, InlineRowType { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } } public typealias DateInlineRow = DateInlineRow_ /// A row with an Date as value where the user can select date and time from an inline picker view. public final class DateTimeInlineRow_: _DateTimeInlineRow, RowType, InlineRowType { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } } public typealias DateTimeInlineRow = DateTimeInlineRow_ /// A row with an Date as value where the user can select a time from an inline picker view. public final class TimeInlineRow_: _TimeInlineRow, RowType, InlineRowType { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } } public typealias TimeInlineRow = TimeInlineRow_ ///// A row with an Date as value where the user can select hour and minute as a countdown timer in an inline picker view. public final class CountDownInlineRow_: _CountDownInlineRow, RowType, InlineRowType { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } } public typealias CountDownInlineRow = CountDownInlineRow_ ================================================ FILE: Source/Rows/DatePickerRow.swift ================================================ // DateRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class DatePickerCell: Cell, CellType { @IBOutlet weak public var datePicker: UIDatePicker! public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { let datePicker = UIDatePicker() self.datePicker = datePicker self.datePicker.translatesAutoresizingMaskIntoConstraints = false super.init(style: style, reuseIdentifier: reuseIdentifier) self.contentView.addSubview(self.datePicker) self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": self.datePicker!])) self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": self.datePicker!])) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() selectionStyle = .none accessoryType = .none editingAccessoryType = .none height = { UITableView.automaticDimension } datePicker.datePickerMode = datePickerMode() datePicker.addTarget(self, action: #selector(DatePickerCell.datePickerValueDidChange(_:)), for: .valueChanged) if datePicker.datePickerMode != .countDownTimer { #if swift(>=5.2) if #available(iOS 14.0, *) { #if swift(>=5.3) && !(os(OSX) || (os(iOS) && targetEnvironment(macCatalyst))) datePicker.preferredDatePickerStyle = .inline #else datePicker.preferredDatePickerStyle = .wheels #endif } else if #available(iOS 13.4, *) { datePicker.preferredDatePickerStyle = .wheels } #endif } } deinit { datePicker?.removeTarget(self, action: nil, for: .allEvents) } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default datePicker.isUserInteractionEnabled = !row.isDisabled detailTextLabel?.text = nil textLabel?.text = nil datePicker.setDate(row.value ?? Date(), animated: row is CountDownPickerRow) datePicker.minimumDate = (row as? DatePickerRowProtocol)?.minimumDate datePicker.maximumDate = (row as? DatePickerRowProtocol)?.maximumDate if let minuteIntervalValue = (row as? DatePickerRowProtocol)?.minuteInterval { datePicker.minuteInterval = minuteIntervalValue } } @objc(datePickerValueDidChange:) func datePickerValueDidChange(_ sender: UIDatePicker) { row?.value = sender.date // workaround for UIDatePicker bug when it doesn't trigger "value changed" event after trying to pick 00:00 value // for details see this comment: https://stackoverflow.com/questions/20181980/uidatepicker-bug-uicontroleventvaluechanged-after-hitting-minimum-internal#comment56681891_20204225 if sender.datePickerMode == .countDownTimer && sender.countDownDuration == TimeInterval(sender.minuteInterval * 60) { datePicker.countDownDuration = sender.countDownDuration } } private func datePickerMode() -> UIDatePicker.Mode { switch row { case is DatePickerRow: return .date case is TimePickerRow: return .time case is DateTimePickerRow: return .dateAndTime case is CountDownPickerRow: return .countDownTimer default: return .date } } } open class _DatePickerRow: Row, DatePickerRowProtocol { open var minimumDate: Date? open var maximumDate: Date? open var minuteInterval: Int? required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil } } /// A row with an Date as value where the user can select a date directly. public final class DatePickerRow: _DatePickerRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select a time directly. public final class TimePickerRow: _DatePickerRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select date and time directly. public final class DateTimePickerRow: _DatePickerRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select hour and minute as a countdown timer. public final class CountDownPickerRow: _DatePickerRow, RowType { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/DateRow.swift ================================================ // DateRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation open class _DateRow: _DateFieldRow { required public init(tag: String?) { super.init(tag: tag) dateFormatter = DateFormatter() dateFormatter?.timeStyle = .none dateFormatter?.dateStyle = .medium dateFormatter?.locale = Locale.current } } open class _TimeRow: _DateFieldRow { required public init(tag: String?) { super.init(tag: tag) dateFormatter = DateFormatter() dateFormatter?.timeStyle = .short dateFormatter?.dateStyle = .none dateFormatter?.locale = Locale.current } } open class _DateTimeRow: _DateFieldRow { required public init(tag: String?) { super.init(tag: tag) dateFormatter = DateFormatter() dateFormatter?.timeStyle = .short dateFormatter?.dateStyle = .short dateFormatter?.locale = Locale.current } } open class _CountDownRow: _DateFieldRow { required public init(tag: String?) { super.init(tag: tag) displayValueFor = { [unowned self] value in guard let val = value else { return nil } if let formatter = self.dateFormatter { return formatter.string(from: val) } let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: val) return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .full)?.replacingOccurrences(of: ",", with: "") } } } /// A row with an Date as value where the user can select a date from a picker view. public final class DateRow: _DateRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select a time from a picker view. public final class TimeRow: _TimeRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select date and time from a picker view. public final class DateTimeRow: _DateTimeRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row with an Date as value where the user can select hour and minute as a countdown timer in a picker view. public final class CountDownRow: _CountDownRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/DoublePickerInputRow.swift ================================================ // // DoublePickerInputRow.swift // Eureka // // Created by Mathias Claassen on 5/10/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Foundation import UIKit open class DoublePickerInputCell : _PickerInputCell> where A: Equatable, B: Equatable { private var pickerRow: _DoublePickerInputRow! { return row as? _DoublePickerInputRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() if let selectedValue = pickerRow.value, let indexA = pickerRow.firstOptions().firstIndex(of: selectedValue.a), let indexB = pickerRow.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b) { picker.selectRow(indexA, inComponent: 0, animated: true) picker.selectRow(indexB, inComponent: 1, animated: true) } } open override func numberOfComponents(in pickerView: UIPickerView) -> Int { return 2 } open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return component == 0 ? pickerRow.firstOptions().count : pickerRow.secondOptions(pickerRow.selectedFirst()).count } open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { if component == 0 { return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) } else { return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) } } open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { if component == 0 { let a = pickerRow.firstOptions()[row] if let value = pickerRow.value { guard value.a != a else { return } if pickerRow.secondOptions(a).contains(value.b) { pickerRow.value = Tuple(a: a, b: value.b) pickerView.reloadComponent(1) update() return } else { pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) } } else { pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) } pickerView.reloadComponent(1) pickerView.selectRow(0, inComponent: 1, animated: true) } else { let a = pickerRow.selectedFirst() pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[row]) } update() } } open class _DoublePickerInputRow : Row>, NoValueDisplayTextConformance { open var noValueDisplayText: String? = nil /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = {_ in []} /// Modify the displayed values for the first picker row. public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } /// Modify the displayed values for the second picker row. public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } required public init(tag: String?) { super.init(tag: tag) } func selectedFirst() -> A { return value?.a ?? firstOptions()[0] } } /// A generic row where the user can pick an option from a picker view displayed in the keyboard area public final class DoublePickerInputRow: _DoublePickerInputRow, RowType where A: Equatable, B: Equatable { required public init(tag: String?) { super.init(tag: tag) self.displayValueFor = { [weak self] tuple in guard let tuple = tuple else { return self?.noValueDisplayText } return String(describing: tuple.a) + ", " + String(describing: tuple.b) } } } ================================================ FILE: Source/Rows/DoublePickerRow.swift ================================================ // // MultiplePickerRow.swift // Eureka // // Created by Mathias Claassen on 5/8/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Foundation import UIKit public struct Tuple { public let a: A public let b: B public init(a: A, b: B) { self.a = a self.b = b } } extension Tuple: Equatable {} public func == (lhs: Tuple, rhs: Tuple) -> Bool { return lhs.a == rhs.a && lhs.b == rhs.b } // MARK: MultiplePickerCell open class DoublePickerCell : _PickerCell> where A: Equatable, B: Equatable { private var pickerRow: _DoublePickerRow? { return row as? _DoublePickerRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() if let selectedValue = pickerRow?.value, let indexA = pickerRow?.firstOptions().firstIndex(of: selectedValue.a), let indexB = pickerRow?.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b) { picker.selectRow(indexA, inComponent: 0, animated: true) picker.selectRow(indexB, inComponent: 1, animated: true) } } open override func numberOfComponents(in pickerView: UIPickerView) -> Int { return 2 } open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { guard let pickerRow = pickerRow else { return 0 } return component == 0 ? pickerRow.firstOptions().count : pickerRow.secondOptions(pickerRow.selectedFirst()).count } open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { guard let pickerRow = pickerRow else { return "" } if component == 0 { return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) } else { return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) } } open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { guard let pickerRow = pickerRow else { return } if component == 0 { let a = pickerRow.firstOptions()[row] if let value = pickerRow.value { guard value.a != a else { return } if pickerRow.secondOptions(a).contains(value.b) { pickerRow.value = Tuple(a: a, b: value.b) pickerView.reloadComponent(1) return } else { pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) } } else { pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) } pickerView.reloadComponent(1) pickerView.selectRow(0, inComponent: 1, animated: true) } else { let a = pickerRow.selectedFirst() pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[row]) } } } // MARK: PickerRow open class _DoublePickerRow : Row> where A: Equatable, B: Equatable { /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = {_ in []} /// Modify the displayed values for the first picker row. public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } /// Modify the displayed values for the second picker row. public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } required public init(tag: String?) { super.init(tag: tag) } func selectedFirst() -> A { return value?.a ?? firstOptions()[0] } } /// A generic row where the user can pick an option from a picker view public final class DoublePickerRow: _DoublePickerRow, RowType where A: Equatable, B: Equatable { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/FieldsRow.swift ================================================ // FieldsRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class TextCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .default textField.autocapitalizationType = .sentences textField.keyboardType = .default } } open class IntCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .default textField.autocapitalizationType = .none textField.keyboardType = .numberPad } } open class PhoneCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.keyboardType = .phonePad if #available(iOS 10,*) { textField.textContentType = .telephoneNumber } } } open class NameCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .words textField.keyboardType = .asciiCapable if #available(iOS 10,*) { textField.textContentType = .name } } } open class EmailCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .emailAddress if #available(iOS 10,*) { textField.textContentType = .emailAddress } } } open class PasswordCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .asciiCapable textField.isSecureTextEntry = true if let textLabel = textLabel { textField.setContentHuggingPriority(textLabel.contentHuggingPriority(for: .horizontal) - 1, for: .horizontal) } if #available(iOS 11,*) { textField.textContentType = .password } } } open class DecimalCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.keyboardType = .decimalPad } } open class URLCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .URL if #available(iOS 10,*) { textField.textContentType = .URL } } } open class TwitterCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .twitter } } open class AccountCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .asciiCapable if #available(iOS 11,*) { textField.textContentType = .username } } } open class ZipCodeCell: _FieldCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() textField.autocorrectionType = .no textField.autocapitalizationType = .allCharacters textField.keyboardType = .numbersAndPunctuation if #available(iOS 10,*) { textField.textContentType = .postalCode } } } open class _TextRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _IntRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) let numberFormatter = NumberFormatter() numberFormatter.locale = Locale.current numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 0 formatter = numberFormatter } } open class _PhoneRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _NameRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _EmailRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _PasswordRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _DecimalRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) let numberFormatter = NumberFormatter() numberFormatter.locale = Locale.current numberFormatter.numberStyle = .decimal numberFormatter.minimumFractionDigits = 2 formatter = numberFormatter } } open class _URLRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _TwitterRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _AccountRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } open class _ZipCodeRow: FieldRow { public required init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter arbitrary text. public final class TextRow: _TextRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter names. Biggest difference to TextRow is that it autocapitalization is set to Words. public final class NameRow: _NameRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter secure text. public final class PasswordRow: _PasswordRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter an email address. public final class EmailRow: _EmailRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter a twitter username. public final class TwitterRow: _TwitterRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter a simple account username. public final class AccountRow: _AccountRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter a zip code. public final class ZipCodeRow: _ZipCodeRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row where the user can enter an integer number. public final class IntRow: _IntRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row where the user can enter a decimal number. public final class DecimalRow: _DecimalRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A row where the user can enter an URL. The value of this row will be a URL. public final class URLRow: _URLRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } /// A String valued row where the user can enter a phone number. public final class PhoneRow: _PhoneRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/LabelRow.swift ================================================ // LabelRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: LabelCell open class LabelCellOf: Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() selectionStyle = .none } } public typealias LabelCell = LabelCellOf // MARK: LabelRow open class _LabelRow: Row { required public init(tag: String?) { super.init(tag: tag) } } /// Simple row that can show title and value but is not editable by user. public final class LabelRow: _LabelRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/MultipleSelectorRow.swift ================================================ // MultipleSelectorRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation open class _MultipleSelectorRow: GenericMultipleSelectorRow where Cell: BaseCell, Cell.Value == Set { public required init(tag: String?) { super.init(tag: tag) } } /// A selector row where the user can pick several options from a pushed view controller public final class MultipleSelectorRow : _MultipleSelectorRow>>, RowType { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/PickerInlineRow.swift ================================================ // PickerInlineRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class PickerInlineCell : Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() accessoryType = .none editingAccessoryType = .none } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default } open override func didSelect() { super.didSelect() row.deselect() } } // MARK: PickerInlineRow open class _PickerInlineRow : Row>, NoValueDisplayTextConformance where T: Equatable { public typealias InlineRow = PickerRow open var options = [T]() open var noValueDisplayText: String? required public init(tag: String?) { super.init(tag: tag) } } /// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically public final class PickerInlineRow : _PickerInlineRow, RowType, InlineRowType where T: Equatable { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } public func setupInlineRow(_ inlineRow: InlineRow) { inlineRow.options = self.options inlineRow.displayValueFor = self.displayValueFor inlineRow.cell.height = { UITableView.automaticDimension } } } open class _DoublePickerInlineRow : Row>>, NoValueDisplayTextConformance where A: Equatable, B: Equatable { public typealias InlineRow = DoublePickerRow /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = { _ in [] } /// Modify the displayed values for the first picker row. public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } /// Modify the displayed values for the second picker row. public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } public var noValueDisplayText: String? required public init(tag: String?) { super.init(tag: tag) self.displayValueFor = { [weak self] tuple in if let tuple = tuple { return String(describing: tuple.a) + ", " + String(describing: tuple.b) } return self?.noValueDisplayText } } } /// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically public final class DoublePickerInlineRow : _DoublePickerInlineRow, RowType, InlineRowType where A: Equatable, B: Equatable { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } public func setupInlineRow(_ inlineRow: InlineRow) { inlineRow.firstOptions = firstOptions inlineRow.secondOptions = secondOptions inlineRow.displayValueForFirstRow = displayValueForFirstRow inlineRow.displayValueForSecondRow = displayValueForSecondRow inlineRow.displayValueFor = self.displayValueFor inlineRow.cell.height = { UITableView.automaticDimension } } } open class _TriplePickerInlineRow : Row>>, NoValueDisplayTextConformance where A: Equatable, B: Equatable, C: Equatable { public typealias InlineRow = TriplePickerRow /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = { _ in [] } /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} open var noValueDisplayText: String? required public init(tag: String?) { super.init(tag: tag) self.displayValueFor = { [weak self] tuple in if let tuple = tuple { return String(describing: tuple.a) + ", " + String(describing: tuple.b) + ", " + String(describing: tuple.c) } return self?.noValueDisplayText } } } /// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically public final class TriplePickerInlineRow : _TriplePickerInlineRow, RowType, InlineRowType where A: Equatable, B: Equatable, C: Equatable { required public init(tag: String?) { super.init(tag: tag) onExpandInlineRow { cell, row, _ in let color = cell.detailTextLabel?.textColor row.onCollapseInlineRow { cell, _, _ in cell.detailTextLabel?.textColor = color } cell.detailTextLabel?.textColor = cell.tintColor } } public override func customDidSelect() { super.customDidSelect() if !isDisabled { toggleInlineRow() } } public func setupInlineRow(_ inlineRow: InlineRow) { inlineRow.firstOptions = firstOptions inlineRow.secondOptions = secondOptions inlineRow.thirdOptions = thirdOptions inlineRow.displayValueFor = self.displayValueFor inlineRow.cell.height = { UITableView.automaticDimension } } } ================================================ FILE: Source/Rows/PickerInputRow.swift ================================================ // PickerInputRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: PickerInputCell open class _PickerInputCell : Cell, CellType, UIPickerViewDataSource, UIPickerViewDelegate where T: Equatable { lazy public var picker: UIPickerView = { let picker = UIPickerView() picker.translatesAutoresizingMaskIntoConstraints = false return picker }() fileprivate var pickerInputRow: _PickerInputRow? { return row as? _PickerInputRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() accessoryType = .none editingAccessoryType = .none picker.delegate = self picker.dataSource = self } deinit { picker.delegate = nil picker.dataSource = nil } open override func update() { super.update() selectionStyle = row.isDisabled ? .none : .default if row.title?.isEmpty == false { detailTextLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText } else { textLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText detailTextLabel?.text = nil } if #available(iOS 13.0, *) { textLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label } else { textLabel?.textColor = row.isDisabled ? .gray : .black } if row.isHighlighted { textLabel?.textColor = tintColor } picker.reloadAllComponents() } open override func didSelect() { super.didSelect() row.deselect() } open override var inputView: UIView? { return picker } open override func cellCanBecomeFirstResponder() -> Bool { return canBecomeFirstResponder } override open var canBecomeFirstResponder: Bool { return !row.isDisabled } open func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } open func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return pickerInputRow?.options.count ?? 0 } open func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return pickerInputRow?.displayValueFor?(pickerInputRow?.options[row]) } open func pickerView(_ pickerView: UIPickerView, didSelectRow rowNumber: Int, inComponent component: Int) { if let picker = pickerInputRow, picker.options.count > rowNumber { picker.value = picker.options[rowNumber] update() } } } open class PickerInputCell: _PickerInputCell where T: Equatable { public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() if let selectedValue = pickerInputRow?.value, let index = pickerInputRow?.options.firstIndex(of: selectedValue) { picker.selectRow(index, inComponent: 0, animated: true) } } } // MARK: PickerInputRow open class _PickerInputRow : Row>, NoValueDisplayTextConformance where T: Equatable { open var noValueDisplayText: String? = nil open var options = [T]() required public init(tag: String?) { super.init(tag: tag) } } /// A generic row where the user can pick an option from a picker view displayed in the keyboard area public final class PickerInputRow: _PickerInputRow, RowType where T: Equatable { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/PickerRow.swift ================================================ // PickerRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: PickerCell open class _PickerCell : Cell, CellType, UIPickerViewDataSource, UIPickerViewDelegate where T: Equatable { @IBOutlet public weak var picker: UIPickerView! fileprivate var pickerRow: _PickerRow? { return row as? _PickerRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { let pickerView = UIPickerView() self.picker = pickerView self.picker?.translatesAutoresizingMaskIntoConstraints = false super.init(style: style, reuseIdentifier: reuseIdentifier) self.contentView.addSubview(pickerView) self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": pickerView])) self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": pickerView])) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() accessoryType = .none editingAccessoryType = .none height = { UITableView.automaticDimension } picker.delegate = self picker.dataSource = self } open override func update() { super.update() textLabel?.text = nil detailTextLabel?.text = nil picker.reloadAllComponents() } deinit { picker?.delegate = nil picker?.dataSource = nil } open var pickerTextAttributes: [NSAttributedString.Key: Any]? open func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } open func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return pickerRow?.options.count ?? 0 } open func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return pickerRow?.displayValueFor?(pickerRow?.options[row]) } open func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { if let picker = pickerRow, !picker.options.isEmpty { picker.value = picker.options[row] } } open func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { guard let pickerTextAttributes = pickerTextAttributes, let text = self.pickerView(pickerView, titleForRow: row, forComponent: component) else { return nil } return NSAttributedString(string: text, attributes: pickerTextAttributes) } } open class PickerCell : _PickerCell where T: Equatable { required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } open override func update() { super.update() if let selectedValue = pickerRow?.value, let index = pickerRow?.options.firstIndex(of: selectedValue) { picker.selectRow(index, inComponent: 0, animated: true) } } } // MARK: PickerRow open class _PickerRow : Row> where T: Equatable { open var options = [T]() required public init(tag: String?) { super.init(tag: tag) } } /// A generic row where the user can pick an option from a picker view public final class PickerRow: _PickerRow, RowType where T: Equatable { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/PopoverSelectorRow.swift ================================================ // PopoverSelectorRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class _PopoverSelectorRow : SelectorRow where Cell: BaseCell { public required init(tag: String?) { super.init(tag: tag) onPresentCallback = { [weak self] (_, viewController) -> Void in guard let porpoverController = viewController.popoverPresentationController, let tableView = self?.baseCell.formViewController()?.tableView, let cell = self?.cell else { fatalError() } porpoverController.sourceView = tableView porpoverController.sourceRect = tableView.convert(cell.detailTextLabel?.frame ?? cell.textLabel?.frame ?? cell.contentView.frame, from: cell) } presentationMode = .popover(controllerProvider: ControllerProvider.callback { return SelectorViewController> { _ in } }, onDismiss: { [weak self] in $0.dismiss(animated: true) self?.reload() }) } open override func didSelect() { deselect() super.didSelect() } } public final class PopoverSelectorRow : _PopoverSelectorRow>, RowType { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/PushRow.swift ================================================ // PushRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class _PushRow: SelectorRow where Cell: BaseCell { public required init(tag: String?) { super.init(tag: tag) presentationMode = .show(controllerProvider: ControllerProvider.callback { return SelectorViewController> { _ in } }, onDismiss: { vc in let _ = vc.navigationController?.popViewController(animated: true) }) } } /// A selector row where the user can pick an option from a pushed view controller public final class PushRow : _PushRow>, RowType { public required init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/SegmentedRow.swift ================================================ // SegmentedRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: SegmentedCell open class SegmentedCell : Cell, CellType { @IBOutlet public weak var segmentedControl: UISegmentedControl! @IBOutlet public weak var titleLabel: UILabel? private var dynamicConstraints = [NSLayoutConstraint]() fileprivate var observingTitleText = false private var awakeFromNibCalled = false required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) let segmentedControl = UISegmentedControl() segmentedControl.translatesAutoresizingMaskIntoConstraints = false segmentedControl.setContentHuggingPriority(UILayoutPriority(rawValue: 250), for: .horizontal) self.segmentedControl = segmentedControl self.titleLabel = self.textLabel self.titleLabel?.translatesAutoresizingMaskIntoConstraints = false self.titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) self.titleLabel?.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } guard me.observingTitleText else { return } me.titleLabel?.removeObserver(me, forKeyPath: "text") me.observingTitleText = false } NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } guard !me.observingTitleText else { return } me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil) me.observingTitleText = true } NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in self?.titleLabel = self?.textLabel self?.setNeedsUpdateConstraints() } contentView.addSubview(titleLabel!) contentView.addSubview(segmentedControl) titleLabel?.addObserver(self, forKeyPath: "text", options: [.old, .new], context: nil) observingTitleText = true imageView?.addObserver(self, forKeyPath: "image", options: [.old, .new], context: nil) contentView.addConstraint(NSLayoutConstraint(item: segmentedControl, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func awakeFromNib() { super.awakeFromNib() awakeFromNibCalled = true } deinit { segmentedControl.removeTarget(self, action: nil, for: .allEvents) if !awakeFromNibCalled { if observingTitleText { titleLabel?.removeObserver(self, forKeyPath: "text") } imageView?.removeObserver(self, forKeyPath: "image") NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) } } open override func setup() { super.setup() selectionStyle = .none segmentedControl.addTarget(self, action: #selector(SegmentedCell.valueChanged), for: .valueChanged) } open override func update() { super.update() detailTextLabel?.text = nil updateSegmentedControl() segmentedControl.selectedSegmentIndex = selectedIndex() ?? UISegmentedControl.noSegment segmentedControl.isEnabled = !row.isDisabled } @objc (segmentedValueDidChange) func valueChanged() { row.value = (row as! OptionsRow).options?[segmentedControl.selectedSegmentIndex] } open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let obj = object as AnyObject? if let changeType = change, let _ = keyPath, ((obj === titleLabel && keyPath == "text") || (obj === imageView && keyPath == "image")) && (changeType[NSKeyValueChangeKey.kindKey] as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue, !awakeFromNibCalled { setNeedsUpdateConstraints() updateConstraintsIfNeeded() } } func updateSegmentedControl() { segmentedControl.removeAllSegments() (row as! OptionsRow).options?.reversed().forEach { if let image = $0 as? UIImage { segmentedControl.insertSegment(with: image, at: 0, animated: false) } else { segmentedControl.insertSegment(withTitle: row.displayValueFor?($0) ?? "", at: 0, animated: false) } } } open override func updateConstraints() { guard !awakeFromNibCalled else { super.updateConstraints() return } contentView.removeConstraints(dynamicConstraints) dynamicConstraints = [] var views: [String: AnyObject] = ["segmentedControl": segmentedControl] var hasImageView = false var hasTitleLabel = false if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView hasImageView = true } if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { views["titleLabel"] = titleLabel hasTitleLabel = true dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) } dynamicConstraints.append(NSLayoutConstraint(item: segmentedControl!, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: contentView, attribute: .width, multiplier: 0.3, constant: 0.0)) if hasImageView && hasTitleLabel { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[segmentedControl]-|", options: [], metrics: nil, views: views) } else if hasImageView && !hasTitleLabel { dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-[segmentedControl]-|", options: [], metrics: nil, views: views) } else if !hasImageView && hasTitleLabel { dynamicConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[segmentedControl]-|", options: .alignAllCenterY, metrics: nil, views: views) } else { dynamicConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[segmentedControl]-|", options: .alignAllCenterY, metrics: nil, views: views) } contentView.addConstraints(dynamicConstraints) super.updateConstraints() } func selectedIndex() -> Int? { guard let value = row.value else { return nil } return (row as! OptionsRow).options?.firstIndex(of: value) } } // MARK: SegmentedRow /// An options row where the user can select an option from an UISegmentedControl public final class SegmentedRow: OptionsRow>, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/SelectableRows/ListCheckRow.swift ================================================ // ListCheckRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit open class ListCheckCell : Cell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() accessoryType = row.value != nil ? .checkmark : .none editingAccessoryType = accessoryType selectionStyle = .default if row.isDisabled { tintAdjustmentMode = .dimmed selectionStyle = .none } else { tintAdjustmentMode = .automatic } } open override func setup() { super.setup() accessoryType = .checkmark editingAccessoryType = accessoryType } open override func didSelect() { row.deselect() row.updateCell() } } public final class ListCheckRow: Row>, SelectableRowType, RowType where T: Equatable { public var selectableValue: T? required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil } } ================================================ FILE: Source/Rows/SliderRow.swift ================================================ // SliderRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import UIKit /// The cell of the SliderRow open class SliderCell: Cell, CellType { private var awakeFromNibCalled = false @IBOutlet open weak var titleLabel: UILabel! @IBOutlet open weak var valueLabel: UILabel! @IBOutlet open weak var slider: UISlider! open var formatter: NumberFormatter? public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .value1, reuseIdentifier: reuseIdentifier) NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } if me.shouldShowTitle { me.titleLabel = me.textLabel me.valueLabel = me.detailTextLabel me.setNeedsUpdateConstraints() } } } deinit { guard !awakeFromNibCalled else { return } NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) awakeFromNibCalled = true } open override func setup() { super.setup() if !awakeFromNibCalled { let title = textLabel textLabel?.translatesAutoresizingMaskIntoConstraints = false textLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) self.titleLabel = title let value = detailTextLabel value?.translatesAutoresizingMaskIntoConstraints = false value?.setContentHuggingPriority(UILayoutPriority(500), for: .horizontal) value?.adjustsFontSizeToFitWidth = true value?.minimumScaleFactor = 0.5 self.valueLabel = value let slider = UISlider() slider.translatesAutoresizingMaskIntoConstraints = false slider.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) self.slider = slider if shouldShowTitle { contentView.addSubview(titleLabel) } if !sliderRow.shouldHideValue { contentView.addSubview(valueLabel) } contentView.addSubview(slider) setNeedsUpdateConstraints() } selectionStyle = .none slider.minimumValue = 0 slider.maximumValue = 10 slider.addTarget(self, action: #selector(SliderCell.valueChanged), for: .valueChanged) } open override func update() { super.update() titleLabel.text = row.title titleLabel.isHidden = !shouldShowTitle valueLabel.text = row.displayValueFor?(row.value ?? slider.minimumValue) valueLabel.isHidden = sliderRow.shouldHideValue slider.value = row.value ?? slider.minimumValue slider.isEnabled = !row.isDisabled } @objc (sliderValueDidChange) func valueChanged() { let roundedValue: Float let steps = Float(sliderRow.steps) if steps > 0 { let stepValue = round((slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) * steps) let stepAmount = (slider.maximumValue - slider.minimumValue) / steps roundedValue = stepValue * stepAmount + self.slider.minimumValue } else { roundedValue = slider.value } row.value = roundedValue row.updateCell() } var shouldShowTitle: Bool { return row?.title?.isEmpty == false } private var sliderRow: SliderRow { return row as! SliderRow } open override func updateConstraints() { customConstraints() super.updateConstraints() } open var dynamicConstraints = [NSLayoutConstraint]() open func customConstraints() { guard !awakeFromNibCalled else { return } contentView.removeConstraints(dynamicConstraints) dynamicConstraints = [] var views: [String : Any] = ["titleLabel": titleLabel!, "slider": slider!, "valueLabel": valueLabel!] let metrics = ["spacing": 15.0] valueLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) let title = shouldShowTitle ? "[titleLabel]-spacing-" : "" let value = !sliderRow.shouldHideValue ? "-[valueLabel]" : "" if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[imageView]-(15)-\(title)[slider]\(value)-|", options: .alignAllCenterY, metrics: metrics, views: views) imageView.translatesAutoresizingMaskIntoConstraints = false dynamicConstraints.append(contentsOf: hContraints) } else { let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-\(title)[slider]\(value)-|", options: .alignAllCenterY, metrics: metrics, views: views) dynamicConstraints.append(contentsOf: hContraints) } let vContraint = NSLayoutConstraint(item: slider!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0) dynamicConstraints.append(vContraint) contentView.addConstraints(dynamicConstraints) } } /// A row that displays a UISlider. If there is a title set then the title and value will appear above the UISlider. public final class SliderRow: Row, RowType { public var steps: UInt = 20 public var shouldHideValue = false required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/StepperRow.swift ================================================ // // StepperRow.swift // Eureka // // Created by Andrew Holt on 3/4/16. // Copyright © 2016 Xmartlabs. All rights reserved. // import UIKit // MARK: StepperCell open class StepperCell: Cell, CellType { @IBOutlet open weak var stepper: UIStepper! @IBOutlet open weak var valueLabel: UILabel! @IBOutlet open weak var titleLabel: UILabel! private var awakeFromNibCalled = false required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .value1, reuseIdentifier: reuseIdentifier) NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in guard let me = self else { return } if me.shouldShowTitle { me.titleLabel = me.textLabel me.setNeedsUpdateConstraints() } } } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) awakeFromNibCalled = true } open override func setup() { super.setup() if !awakeFromNibCalled { let title = textLabel textLabel?.translatesAutoresizingMaskIntoConstraints = false textLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) self.titleLabel = title let stepper = UIStepper() stepper.translatesAutoresizingMaskIntoConstraints = false stepper.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) self.stepper = stepper if shouldShowTitle { contentView.addSubview(titleLabel) } setupValueLabel() contentView.addSubview(stepper) setNeedsUpdateConstraints() } selectionStyle = .none stepper.addTarget(self, action: #selector(StepperCell.valueChanged), for: .valueChanged) } open func setupValueLabel() { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.setContentHuggingPriority(UILayoutPriority(500), for: .horizontal) label.adjustsFontSizeToFitWidth = true label.minimumScaleFactor = 0.5 self.valueLabel = label contentView.addSubview(valueLabel) } open override func update() { super.update() detailTextLabel?.text = nil stepper.isEnabled = !row.isDisabled titleLabel.isHidden = !shouldShowTitle stepper.value = row.value ?? 0 stepper.alpha = row.isDisabled ? 0.3 : 1.0 valueLabel?.textColor = tintColor valueLabel?.alpha = row.isDisabled ? 0.3 : 1.0 valueLabel?.text = row.displayValueFor?(row.value) } @objc(stepperValueDidChange) func valueChanged() { row.value = stepper.value row.updateCell() } var shouldShowTitle: Bool { return row?.title?.isEmpty == false } private var stepperRow: StepperRow { return row as! StepperRow } deinit { stepper.removeTarget(self, action: nil, for: .allEvents) guard !awakeFromNibCalled else { return } NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) } open override func updateConstraints() { customConstraints() super.updateConstraints() } open var dynamicConstraints = [NSLayoutConstraint]() open func customConstraints() { guard !awakeFromNibCalled else { return } contentView.removeConstraints(dynamicConstraints) dynamicConstraints = [] var views: [String : Any] = ["titleLabel": titleLabel!, "stepper": stepper!, "valueLabel": valueLabel!] let metrics = ["spacing": 15.0] valueLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) let title = shouldShowTitle ? "[titleLabel]-(>=15@250)-" : "" if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[imageView]-(15)-\(title)[valueLabel]-[stepper]-|", options: .alignAllCenterY, metrics: metrics, views: views) imageView.translatesAutoresizingMaskIntoConstraints = false dynamicConstraints.append(contentsOf: hContraints) } else { let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-\(title)[valueLabel]-[stepper]-|", options: .alignAllCenterY, metrics: metrics, views: views) dynamicConstraints.append(contentsOf: hContraints) } let vContraint = NSLayoutConstraint(item: stepper!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0) dynamicConstraints.append(vContraint) contentView.addConstraints(dynamicConstraints) } } // MARK: StepperRow open class _StepperRow: Row { required public init(tag: String?) { super.init(tag: tag) displayValueFor = { value in guard let value = value else { return nil } return DecimalFormatter().string(from: NSNumber(value: value)) } } } /// Double row that has a UIStepper as accessoryType public final class StepperRow: _StepperRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/SwitchRow.swift ================================================ // SwitchRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // MARK: SwitchCell open class SwitchCell: Cell, CellType { @IBOutlet public weak var switchControl: UISwitch! required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) let switchC = UISwitch() switchControl = switchC accessoryView = switchControl editingAccessoryView = accessoryView } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func setup() { super.setup() selectionStyle = .none switchControl.addTarget(self, action: #selector(SwitchCell.valueChanged), for: .valueChanged) } deinit { switchControl?.removeTarget(self, action: nil, for: .allEvents) } open override func update() { super.update() switchControl.isOn = row.value ?? false switchControl.isEnabled = !row.isDisabled } @objc (switchValueDidChange) func valueChanged() { row.value = switchControl?.isOn ?? false } } // MARK: SwitchRow open class _SwitchRow: Row { required public init(tag: String?) { super.init(tag: tag) displayValueFor = nil } } /// Boolean row that has a UISwitch as accessoryType public final class SwitchRow: _SwitchRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/TextAreaRow.swift ================================================ // TextAreaRow.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit // TODO: Temporary workaround for Xcode 10 beta #if swift(>=4.2) import UIKit.UIGeometry extension UIEdgeInsets { static let zero = UIEdgeInsets() } #endif public enum TextAreaHeight { case fixed(cellHeight: CGFloat) case dynamic(initialTextViewHeight: CGFloat) } public enum TextAreaMode { case normal case readOnly } protocol TextAreaConformance: FormatterConformance { var placeholder: String? { get set } var textAreaHeight: TextAreaHeight { get set } var titlePercentage: CGFloat? { get set} } /** * Protocol for cells that contain a UITextView */ public protocol AreaCell: TextInputCell { var textView: UITextView! { get } } extension AreaCell { public var textInput: UITextInput { return textView } } open class _TextAreaCell : Cell, UITextViewDelegate, AreaCell where T: Equatable, T: InputTypeInitiable { @IBOutlet public weak var textView: UITextView! @IBOutlet public weak var placeholderLabel: UILabel? private var awakeFromNibCalled = false required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) let textView: UITextView if #available(iOS 16, *) { textView = UITextView(usingTextLayoutManager: false) } else { textView = UITextView() } self.textView = textView textView.translatesAutoresizingMaskIntoConstraints = false textView.keyboardType = .default textView.font = .preferredFont(forTextStyle: .body) textView.textContainer.lineFragmentPadding = 0 textView.textContainerInset = UIEdgeInsets.zero textView.backgroundColor = .clear contentView.addSubview(textView) let placeholderLabel = UILabel() self.placeholderLabel = placeholderLabel placeholderLabel.translatesAutoresizingMaskIntoConstraints = false placeholderLabel.numberOfLines = 0 if #available(iOS 13.0, *) { placeholderLabel.textColor = UIColor.tertiaryLabel } else { placeholderLabel.textColor = UIColor(white: 0, alpha: 0.22) } placeholderLabel.font = textView.font contentView.addSubview(placeholderLabel) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func awakeFromNib() { super.awakeFromNib() awakeFromNibCalled = true } open var dynamicConstraints = [NSLayoutConstraint]() open override func setup() { super.setup() let textAreaRow = row as! TextAreaConformance switch textAreaRow.textAreaHeight { case .dynamic(_): height = { UITableView.automaticDimension } textView.isScrollEnabled = false case .fixed(let cellHeight): height = { cellHeight } } textView.delegate = self selectionStyle = .none if !awakeFromNibCalled { imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil) } setNeedsUpdateConstraints() } deinit { textView?.delegate = nil if !awakeFromNibCalled { imageView?.removeObserver(self, forKeyPath: "image") } } open override func update() { super.update() textLabel?.text = nil detailTextLabel?.text = nil textView.isEditable = !row.isDisabled if #available(iOS 13.0, *) { textView.textColor = row.isDisabled ? .tertiaryLabel : .label } else { textView.textColor = row.isDisabled ? .gray : .black } textView.text = row.displayValueFor?(row.value) placeholderLabel?.text = (row as? TextAreaConformance)?.placeholder placeholderLabel?.isHidden = textView.text.count != 0 } open override func cellCanBecomeFirstResponder() -> Bool { return !row.isDisabled && textView?.canBecomeFirstResponder == true } open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool { // workaround to solve https://github.com/xmartlabs/Eureka/issues/887 UIKit issue textView?.perform(#selector(UITextView.becomeFirstResponder), with: nil, afterDelay: 0.0) return true } open override func cellResignFirstResponder() -> Bool { return textView?.resignFirstResponder() ?? true } open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { let obj = object as AnyObject? if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], obj === imageView && keyPathValue == "image" && (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue, !awakeFromNibCalled { setNeedsUpdateConstraints() updateConstraintsIfNeeded() } } //Mark: Helpers private func displayValue(useFormatter: Bool) -> String? { guard let v = row.value else { return nil } if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { return textView?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) } return String(describing: v) } // MARK: TextFieldDelegate open func textViewDidBeginEditing(_ textView: UITextView) { formViewController()?.beginEditing(of: self) formViewController()?.textInputDidBeginEditing(textView, cell: self) if let textAreaConformance = (row as? TextAreaConformance), let _ = textAreaConformance.formatter, textAreaConformance.useFormatterOnDidBeginEditing ?? textAreaConformance.useFormatterDuringInput { textView.text = self.displayValue(useFormatter: true) } else { textView.text = self.displayValue(useFormatter: false) } } open func textViewDidEndEditing(_ textView: UITextView) { formViewController()?.endEditing(of: self) formViewController()?.textInputDidEndEditing(textView, cell: self) textViewDidChange(textView) textView.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) } open func textViewDidChange(_ textView: UITextView) { if let textAreaConformance = row as? TextAreaConformance, case .dynamic = textAreaConformance.textAreaHeight, let tableView = formViewController()?.tableView { let currentOffset = tableView.contentOffset UIView.performWithoutAnimation { tableView.beginUpdates() tableView.endUpdates() } tableView.setContentOffset(currentOffset, animated: false) } placeholderLabel?.isHidden = textView.text.count != 0 guard let textValue = textView.text else { row.value = nil return } guard let formatterRow = row as? FormatterConformance, let formatter = formatterRow.formatter else { row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) return } if formatterRow.useFormatterDuringInput { let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T guard var selStartPos = textView.selectedTextRange?.start else { return } let oldVal = textView.text textView.text = row.displayValueFor?(row.value) selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textView, oldValue: oldVal, newValue: textView.text) ?? selStartPos textView.selectedTextRange = textView.textRange(from: selStartPos, to: selStartPos) return } } else { let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) let errorDesc: AutoreleasingUnsafeMutablePointer? = nil if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { row.value = value.pointee as? T } } } open func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { return formViewController()?.textInput(textView, shouldChangeCharactersInRange: range, replacementString: text, cell: self) ?? true } open func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { if let textAreaRow = self.row as? _TextAreaRow, textAreaRow.textAreaMode == .readOnly { return false } return formViewController()?.textInputShouldBeginEditing(textView, cell: self) ?? true } open func textViewShouldEndEditing(_ textView: UITextView) -> Bool { return formViewController()?.textInputShouldEndEditing(textView, cell: self) ?? true } open override func updateConstraints() { customConstraints() super.updateConstraints() } open func customConstraints() { guard !awakeFromNibCalled else { return } contentView.removeConstraints(dynamicConstraints) dynamicConstraints = [] var views: [String: AnyObject] = ["textView": textView, "label": placeholderLabel!] dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[label]", options: [], metrics: nil, views: views)) if let textAreaConformance = row as? TextAreaConformance, case .dynamic(let initialTextViewHeight) = textAreaConformance.textAreaHeight { dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView(>=initialHeight@800)]-|", options: [], metrics: ["initialHeight": initialTextViewHeight], views: views)) } else { dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView]-|", options: [], metrics: nil, views: views)) } if let imageView = imageView, let _ = imageView.image { views["imageView"] = imageView dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textView]-|", options: [], metrics: nil, views: views)) dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[label]-|", options: [], metrics: nil, views: views)) } else if let titlePercentage = (row as? TextAreaConformance)?.titlePercentage, titlePercentage > 0.0 { textView.textAlignment = .right dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[textView]-|", options: [], metrics: nil, views: views) let sideSpaces = (layoutMargins.right + layoutMargins.left) dynamicConstraints.append(NSLayoutConstraint(item: textView!, attribute: .width, relatedBy: .equal, toItem: contentView, attribute: .width, multiplier: 1 - titlePercentage, constant: -sideSpaces)) } else { dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textView]-|", options: [], metrics: nil, views: views)) dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", options: [], metrics: nil, views: views)) } contentView.addConstraints(dynamicConstraints) } } open class TextAreaCell: _TextAreaCell, CellType { required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } } open class AreaRow: FormatteableRow, TextAreaConformance where Cell: BaseCell, Cell: AreaCell { open var placeholder: String? open var textAreaHeight = TextAreaHeight.fixed(cellHeight: 110) open var textAreaMode = TextAreaMode.normal /// The percentage of the cell that should be occupied by the remaining space to the left of the textArea. This is equivalent to the space occupied by a title in FieldRow, making the textArea aligned to fieldRows using the same titlePercentage. This behavior works only if the cell does not contain an image, due to its automatically set constraints in the cell. open var titlePercentage: CGFloat? public required init(tag: String?) { super.init(tag: tag) } } open class _TextAreaRow: AreaRow { required public init(tag: String?) { super.init(tag: tag) } } /// A row with a UITextView where the user can enter large text. public final class TextAreaRow: _TextAreaRow, RowType { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Rows/TriplePickerInputRow.swift ================================================ // // TriplePickerInputRow.swift // Eureka // // Created by Mathias Claassen on 5/10/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Foundation import UIKit open class TriplePickerInputCell : _PickerInputCell> where A: Equatable, B: Equatable, C: Equatable { private var pickerRow: _TriplePickerInputRow! { return row as? _TriplePickerInputRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() if let selectedValue = pickerRow.value, let indexA = pickerRow.firstOptions().firstIndex(of: selectedValue.a), let indexB = pickerRow.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b), let indexC = pickerRow.thirdOptions(selectedValue.a, selectedValue.b).firstIndex(of: selectedValue.c){ picker.selectRow(indexA, inComponent: 0, animated: true) picker.selectRow(indexB, inComponent: 1, animated: true) picker.selectRow(indexC, inComponent: 2, animated: true) } } open override func numberOfComponents(in pickerView: UIPickerView) -> Int { return 3 } open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { if component == 0 { return pickerRow.firstOptions().count } else if component == 1 { return pickerRow.secondOptions(pickerRow.selectedFirst()).count } else { return pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond()).count } } open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { if component == 0 { return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) } else if component == 1 { return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) } else { return pickerRow.displayValueForThirdRow(pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond())[row]) } } open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { if component == 0 { let a = pickerRow.firstOptions()[row] if let value = pickerRow.value { guard value.a != a else { return } let b: B = pickerRow.secondOptions(a).contains(value.b) ? value.b : pickerRow.secondOptions(a)[0] let c: C = pickerRow.thirdOptions(a, b).contains(value.c) ? value.c : pickerRow.thirdOptions(a, b)[0] pickerRow.value = Tuple3(a: a, b: b, c: c) pickerView.reloadComponent(1) pickerView.reloadComponent(2) if b != value.b { pickerView.selectRow(0, inComponent: 1, animated: true) } if c != value.c { pickerView.selectRow(0, inComponent: 2, animated: true) } } else { let b = pickerRow.secondOptions(a)[0] pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) pickerView.reloadComponent(1) pickerView.reloadComponent(2) pickerView.selectRow(0, inComponent: 1, animated: true) pickerView.selectRow(0, inComponent: 2, animated: true) } } else if component == 1 { let a = pickerRow.selectedFirst() let b = pickerRow.secondOptions(a)[row] if let value = pickerRow.value { guard value.b != b else { return } if pickerRow.thirdOptions(a, b).contains(value.c) { pickerRow.value = Tuple3(a: a, b: b, c: value.c) pickerView.reloadComponent(2) update() return } else { pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) } } else { pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) } pickerView.reloadComponent(2) pickerView.selectRow(0, inComponent: 2, animated: true) } else { let a = pickerRow.selectedFirst() let b = pickerRow.selectedSecond() pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[row]) } update() } } open class _TriplePickerInputRow : Row>, NoValueDisplayTextConformance { open var noValueDisplayText: String? = nil /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = {_ in []} /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} /// Modify the displayed values for the first picker row. public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } /// Modify the displayed values for the second picker row. public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } /// Modify the displayed values for the third picker row. public var displayValueForThirdRow: ((C) -> (String)) = { c in return String(describing: c) } required public init(tag: String?) { super.init(tag: tag) } func selectedFirst() -> A { return value?.a ?? firstOptions()[0] } func selectedSecond() -> B { return value?.b ?? secondOptions(selectedFirst())[0] } } /// A generic row where the user can pick an option from a picker view displayed in the keyboard area public final class TriplePickerInputRow: _TriplePickerInputRow, RowType where A: Equatable, B: Equatable, C: Equatable { required public init(tag: String?) { super.init(tag: tag) self.displayValueFor = { [weak self] tuple in guard let tuple = tuple else { return self?.noValueDisplayText } return String(describing: tuple.a) + ", " + String(describing: tuple.b) + ", " + String(describing: tuple.c) } } } ================================================ FILE: Source/Rows/TriplePickerRow.swift ================================================ // // TriplePickerRow.swift // Eureka // // Created by Mathias Claassen on 5/9/18. // Copyright © 2018 Xmartlabs. All rights reserved. // import Foundation import UIKit public struct Tuple3 { public let a: A public let b: B public let c: C public init(a: A, b: B, c: C) { self.a = a self.b = b self.c = c } } extension Tuple3: Equatable {} public func == (lhs: Tuple3, rhs: Tuple3) -> Bool { return lhs.a == rhs.a && lhs.b == rhs.b && lhs.c == rhs.c } // MARK: MultiplePickerCell open class TriplePickerCell : _PickerCell> where A: Equatable, B: Equatable, C: Equatable { private var pickerRow: _TriplePickerRow? { return row as? _TriplePickerRow } public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } open override func update() { super.update() if let selectedValue = pickerRow?.value, let indexA = pickerRow?.firstOptions().firstIndex(of: selectedValue.a), let indexB = pickerRow?.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b), let indexC = pickerRow?.thirdOptions(selectedValue.a, selectedValue.b).firstIndex(of: selectedValue.c) { picker.selectRow(indexA, inComponent: 0, animated: true) picker.selectRow(indexB, inComponent: 1, animated: true) picker.selectRow(indexC, inComponent: 2, animated: true) } } open override func numberOfComponents(in pickerView: UIPickerView) -> Int { return 3 } open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { guard let pickerRow = pickerRow else { return 0 } if component == 0 { return pickerRow.firstOptions().count } else if component == 1 { return pickerRow.secondOptions(pickerRow.selectedFirst()).count } else { return pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond()).count } } open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { guard let pickerRow = pickerRow else { return "" } if component == 0 { return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) } else if component == 1 { return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) } else { return pickerRow.displayValueForThirdRow(pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond())[row]) } } open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { guard let pickerRow = pickerRow else { return } if component == 0 { let a = pickerRow.firstOptions()[row] if let value = pickerRow.value { guard value.a != a else { return } let b: B = pickerRow.secondOptions(a).contains(value.b) ? value.b : pickerRow.secondOptions(a)[0] let c: C = pickerRow.thirdOptions(a, b).contains(value.c) ? value.c : pickerRow.thirdOptions(a, b)[0] pickerRow.value = Tuple3(a: a, b: b, c: c) pickerView.reloadComponent(1) pickerView.reloadComponent(2) if b != value.b { pickerView.selectRow(0, inComponent: 1, animated: true) } if c != value.c { pickerView.selectRow(0, inComponent: 2, animated: true) } } else { let b = pickerRow.secondOptions(a)[0] pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) pickerView.reloadComponent(1) pickerView.reloadComponent(2) pickerView.selectRow(0, inComponent: 1, animated: true) pickerView.selectRow(0, inComponent: 2, animated: true) } } else if component == 1 { let a = pickerRow.selectedFirst() let b = pickerRow.secondOptions(a)[row] if let value = pickerRow.value { guard value.b != b else { return } if pickerRow.thirdOptions(a, b).contains(value.c) { pickerRow.value = Tuple3(a: a, b: b, c: value.c) pickerView.reloadComponent(2) return } else { pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) } } else { pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) } pickerView.reloadComponent(2) pickerView.selectRow(0, inComponent: 2, animated: true) } else { let a = pickerRow.selectedFirst() let b = pickerRow.selectedSecond() pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[row]) } } } // MARK: PickerRow open class _TriplePickerRow : Row> where A: Equatable, B: Equatable, C: Equatable { /// Options for first component. Will be called often so should be O(1) public var firstOptions: (() -> [A]) = {[]} /// Options for second component given the selected value from the first component. Will be called often so should be O(1) public var secondOptions: ((A) -> [B]) = {_ in []} /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} /// Modify the displayed values for the first picker row. public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } /// Modify the displayed values for the second picker row. public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } /// Modify the displayed values for the third picker row. public var displayValueForThirdRow: ((C) -> (String)) = { c in return String(describing: c) } required public init(tag: String?) { super.init(tag: tag) } func selectedFirst() -> A { return value?.a ?? firstOptions()[0] } func selectedSecond() -> B { return value?.b ?? secondOptions(selectedFirst())[0] } } /// A generic row where the user can pick an option from a picker view public final class TriplePickerRow: _TriplePickerRow, RowType where A: Equatable, B: Equatable, C: Equatable { required public init(tag: String?) { super.init(tag: tag) } } ================================================ FILE: Source/Validations/RuleClosure.swift ================================================ // RuleClosure.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct RuleClosure: RuleType { public var id: String? public var validationError: ValidationError public var closure: (T?) -> ValidationError? public func isValid(value: T?) -> ValidationError? { return closure(value) } public init(validationError: ValidationError = ValidationError(msg: "Field validation fails.."), id: String? = nil, closure: @escaping ((T?) -> ValidationError?)) { self.validationError = validationError self.closure = closure self.id = id } } ================================================ FILE: Source/Validations/RuleEmail.swift ================================================ // RuleEmail.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public class RuleEmail: RuleRegExp { public init(msg: String = "Field value should be a valid email!", id: String? = nil) { super.init(regExpr: RegExprPattern.EmailAddress.rawValue, allowsEmpty: true, msg: msg, id: id) } } ================================================ FILE: Source/Validations/RuleEqualsToRow.swift ================================================ // RuleRequire.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct RuleEqualsToRow: RuleType { public init(form: Form, tag: String, msg: String = "Fields don't match!", id: String? = nil) { self.validationError = ValidationError(msg: msg) self.form = form self.tag = tag self.row = nil self.id = id } public init(row: RowOf, msg: String = "Fields don't match!", id: String? = nil) { self.validationError = ValidationError(msg: msg) self.form = nil self.tag = nil self.row = row self.id = id } public var id: String? public var validationError: ValidationError public weak var form: Form? public var tag: String? public weak var row: RowOf? public func isValid(value: T?) -> ValidationError? { let rowAux: RowOf = row ?? form!.rowBy(tag: tag!)! return rowAux.value == value ? nil : validationError } } ================================================ FILE: Source/Validations/RuleLength.swift ================================================ // RuleLength.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct RuleMinLength: RuleType { let min: UInt public var id: String? public var validationError: ValidationError public init(minLength: UInt, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must have at least \(minLength) characters" min = minLength validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: String?) -> ValidationError? { guard let value = value, !value.isEmpty else { return nil } return value.count < Int(min) ? validationError : nil } } public struct RuleMaxLength: RuleType { let max: UInt public var id: String? public var validationError: ValidationError public init(maxLength: UInt, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must have less than \(maxLength) characters" max = maxLength validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: String?) -> ValidationError? { guard let value = value, !value.isEmpty else { return nil } return value.count > Int(max) ? validationError : nil } } public struct RuleExactLength: RuleType { let length: UInt public var id: String? public var validationError: ValidationError public init(exactLength: UInt, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must have exactly \(exactLength) characters" length = exactLength validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: String?) -> ValidationError? { guard let value = value, !value.isEmpty else { return nil } return value.count != Int(length) ? validationError : nil } } ================================================ FILE: Source/Validations/RuleRange.swift ================================================ // RuleRange.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct RuleGreaterThan: RuleType { let min: T public var id: String? public var validationError: ValidationError public init(min: T, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must be greater than \(min)" self.min = min self.validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: T?) -> ValidationError? { guard let val = value else { return nil } guard val > min else { return validationError } return nil } } public struct RuleGreaterOrEqualThan: RuleType { let min: T public var id: String? public var validationError: ValidationError public init(min: T, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must be greater or equals than \(min)" self.min = min self.validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: T?) -> ValidationError? { guard let val = value else { return nil } guard val >= min else { return validationError } return nil } } public struct RuleSmallerThan: RuleType { let max: T public var id: String? public var validationError: ValidationError public init(max: T, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must be smaller than \(max)" self.max = max self.validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: T?) -> ValidationError? { guard let val = value else { return nil } guard val < max else { return validationError } return nil } } public struct RuleSmallerOrEqualThan: RuleType { let max: T public var id: String? public var validationError: ValidationError public init(max: T, msg: String? = nil, id: String? = nil) { let ruleMsg = msg ?? "Field value must be smaller or equals than \(max)" self.max = max self.validationError = ValidationError(msg: ruleMsg) self.id = id } public func isValid(value: T?) -> ValidationError? { guard let val = value else { return nil } guard val <= max else { return validationError } return nil } } ================================================ FILE: Source/Validations/RuleRegExp.swift ================================================ // RegexRule.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public enum RegExprPattern: String { case EmailAddress = "^[_A-Za-z0-9-+!?#$%'`*/=~^{}|]+(\\.[_A-Za-z0-9-+!?#$%'`*/=~^{}|]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z‌​]{2,})$" case URL = "^(?:(?:http|https)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$" case ContainsNumber = ".*\\d.*" case ContainsCapital = "^.*?[A-Z].*?$" case ContainsLowercase = "^.*?[a-z].*?$" } open class RuleRegExp: RuleType { public var regExpr: String = "" public var id: String? public var validationError: ValidationError public var allowsEmpty = true public init(regExpr: String, allowsEmpty: Bool = true, msg: String = "Invalid field value!", id: String? = nil) { self.validationError = ValidationError(msg: msg) self.regExpr = regExpr self.allowsEmpty = allowsEmpty self.id = id } public func isValid(value: String?) -> ValidationError? { if let value = value, !value.isEmpty { let predicate = NSPredicate(format: "SELF MATCHES %@", regExpr) guard predicate.evaluate(with: value) else { return validationError } return nil } else if !allowsEmpty { return validationError } return nil } } ================================================ FILE: Source/Validations/RuleRequired.swift ================================================ // RuleRequire.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public struct RuleRequired: RuleType { public init(msg: String = "Field required!", id: String? = nil) { self.validationError = ValidationError(msg: msg) self.id = id } public var id: String? public var validationError: ValidationError public func isValid(value: T?) -> ValidationError? { if let str = value as? String { return str.isEmpty ? validationError : nil } return value != nil ? nil : validationError } } ================================================ FILE: Source/Validations/RuleURL.swift ================================================ // RuleURL.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import UIKit public struct RuleURL: RuleType { public init(allowsEmpty: Bool = true, requiresProtocol: Bool = false, msg: String = "Field value must be an URL!", id: String? = nil) { validationError = ValidationError(msg: msg) self.allowsEmpty = allowsEmpty self.requiresProtocol = requiresProtocol self.id = id } public var id: String? public var allowsEmpty = true public var requiresProtocol = false public var validationError: ValidationError public func isValid(value: URL?) -> ValidationError? { if let value = value, value.absoluteString.isEmpty == false { let predicate = NSPredicate(format:"SELF MATCHES %@", RegExprPattern.URL.rawValue) guard predicate.evaluate(with: value.absoluteString) else { return validationError } return nil } else if !allowsEmpty { return validationError } return nil } } ================================================ FILE: Tests/BaseEurekaTests.swift ================================================ // BaseEurekaTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class BaseEurekaTests: XCTestCase { var dateForm = Form() var fieldForm = Form() var shortForm = Form() var manySectionsForm = Form() var formVC = FormViewController() override func setUp() { super.setUp() // load the view to test the cells formVC.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) formVC.tableView?.frame = formVC.view.frame // Create a Date section containing one date row of each type and some extra rows that use minimumDate, maximumDate and minuteInterval restrictions dateForm +++ Section("Date Section") <<< DateRow("DateRow_d1") { $0.title = "Date"; $0.value = Date() } <<< DateTimeRow("DateTimeRow_d1") { $0.title = "DateTime"; $0.value = Date() } <<< TimeRow("TimeRow_d1") { $0.title = "Time"; $0.value = Date() } <<< CountDownRow("CountDownRow_d1") { $0.title = "CountDown"; $0.value = Date() } <<< DateRow("MinDateRow_d1") { $0.title = "Date(min)"; $0.value = Date(); $0.minimumDate = $0.value?.addingTimeInterval(-60*60*24) } <<< DateRow("MaxDateRow_d1") { $0.title = "Date(max)"; $0.value = Date(); $0.maximumDate = $0.value?.addingTimeInterval(60*60*24) } <<< DateRow("MinMaxDateRow_d1") { $0.title = "Date(min/max)"; $0.value = Date(); $0.minimumDate = $0.value?.addingTimeInterval(-60*60*24); $0.maximumDate = $0.value?.addingTimeInterval(60*60*24) } <<< DateRow("IntervalDateRow_d1") { $0.title = "Date(interval)"; $0.value = Date(); $0.minuteInterval = 15 } shortForm +++ Section("short") <<< NameRow("NameRow_s1") { $0.title = "Name" } <<< IntRow("IntRow_s1") { $0.title = "Age" } fieldForm +++ Section("Field Section") <<< TextRow("TextRow_f1") { $0.title = "Text" } <<< NameRow("NameRow_f1") { $0.title = "Name" } <<< EmailRow("EmailRow_f1") { $0.title = "Email" } <<< PhoneRow("PhoneRow_f1") { $0.title = "Phone" } <<< PasswordRow("PasswordRow_f1") { $0.title = "Password" } <<< URLRow("UrlRow_f1") { $0.title = "Url" } <<< IntRow("IntRow_f1") { $0.title = "Int" } <<< DecimalRow("DecimalRow_f1") { $0.title = "Decimal" } <<< TwitterRow("TwitterRow_f1") { $0.title = "Twitter" } <<< AccountRow("AccountRow_f1") { $0.title = "Account" } <<< ZipCodeRow("ZipCodeRow_f1") { $0.title = "Zip Code" } manySectionsForm = Section("Section A") +++ Section("Section B") +++ Section("Section C") +++ Section("Section D") +++ Section("Section E") +++ Section("Section F") } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() dateForm = Form() shortForm = Form() fieldForm = Form() manySectionsForm = Form() formVC = FormViewController() } func testTearUp() { XCTAssertEqual(dateForm.count, 1) XCTAssertEqual(dateForm[0].count, 8) XCTAssertEqual(shortForm[0].count, 2) XCTAssertEqual(fieldForm.count, 1) XCTAssertEqual(fieldForm[0].count, 11) XCTAssertEqual(manySectionsForm.count, 6) XCTAssertEqual(manySectionsForm[0].count, 0) } } public class MyFormDelegate: FormDelegate { public var valuesChanged = 0 public var rowsAdded = 0 public var sectionsAdded = 0 public var rowsRemoved = 0 public var sectionsRemoved = 0 public var rowsReplacedIn = 0 public var sectionsReplacedIn = 0 public var rowsReplacedOut = 0 public var sectionsReplacedOut = 0 public func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?) { valuesChanged += 1 } public func sectionsHaveBeenAdded(_ sections: [Section], at: IndexSet) { sectionsAdded += sections.count } public func sectionsHaveBeenRemoved(_ sections: [Section], at: IndexSet) { sectionsRemoved += sections.count } public func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at: IndexSet) { sectionsReplacedIn += newSections.count sectionsReplacedOut += oldSections.count } public func rowsHaveBeenAdded(_ rows: [BaseRow], at: [IndexPath]) { rowsAdded += rows.count } public func rowsHaveBeenRemoved(_ rows: [BaseRow], at: [IndexPath]) { rowsRemoved += rows.count } public func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: [IndexPath]) { rowsReplacedIn += newRows.count rowsReplacedOut += oldRows.count } } ================================================ FILE: Tests/CallbacksTests.swift ================================================ // CallbacksTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class CallbacksTests: XCTestCase { var formVC = FormViewController() override func setUp() { super.setUp() // load the view to test the cells formVC.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) formVC.tableView?.frame = formVC.view.frame } override func tearDown() { super.tearDown() } func testOnChange() { onChangeTest(row:TextRow(), value: "text") onChangeTest(row:IntRow(), value: 33) onChangeTest(row:DecimalRow(), value: 35.7) onChangeTest(row:URLRow(), value: URL(string: "http://xmartlabs.com")!) onChangeTest(row:DateRow(), value: Date().addingTimeInterval(100)) onChangeTest(row:DateInlineRow(), value: Date().addingTimeInterval(100)) onChangeTest(row:PopoverSelectorRow(), value: "text") onChangeTest(row:SliderRow(), value: 5.0) onChangeTest(row:StepperRow(), value: 2.5) onChangeTest(row:PickerInputRow(), value: "Option 2") } func testCellSetup() { cellSetupTest(row:TextRow()) cellSetupTest(row:IntRow()) cellSetupTest(row:DecimalRow()) cellSetupTest(row:URLRow()) cellSetupTest(row:DateRow()) cellSetupTest(row:DateInlineRow()) cellSetupTest(row:PopoverSelectorRow()) cellSetupTest(row:SliderRow()) cellSetupTest(row:StepperRow()) cellSetupTest(row:PickerInputRow()) } func testCellUpdate() { cellUpdateTest(row:TextRow()) cellUpdateTest(row:IntRow()) cellUpdateTest(row:DecimalRow()) cellUpdateTest(row:URLRow()) cellUpdateTest(row:DateRow()) cellUpdateTest(row:DateInlineRow()) cellUpdateTest(row:PopoverSelectorRow()) cellUpdateTest(row:SliderRow()) cellUpdateTest(row:StepperRow()) cellUpdateTest(row:PickerInputRow()) } func testDefaultCellSetup() { defaultCellSetupTest(row:TextRow()) defaultCellSetupTest(row:IntRow()) defaultCellSetupTest(row:DecimalRow()) defaultCellSetupTest(row:URLRow()) defaultCellSetupTest(row:DateRow()) defaultCellSetupTest(row:DateInlineRow()) defaultCellSetupTest(row:PopoverSelectorRow()) defaultCellSetupTest(row:SliderRow()) defaultCellSetupTest(row:StepperRow()) defaultCellSetupTest(row:PickerInputRow()) } func testDefaultCellUpdate() { defaultCellUpdateTest(row: TextRow()) defaultCellUpdateTest(row: IntRow()) defaultCellUpdateTest(row: DecimalRow()) defaultCellUpdateTest(row: URLRow()) defaultCellUpdateTest(row: DateRow()) defaultCellUpdateTest(row: DateInlineRow()) defaultCellUpdateTest(row: PopoverSelectorRow()) defaultCellUpdateTest(row: SliderRow()) defaultCellUpdateTest(row: StepperRow()) defaultCellUpdateTest(row: PickerInputRow()) } func testDefaultInitializers() { defaultInitializerTest(row: TextRow()) defaultInitializerTest(row: IntRow()) defaultInitializerTest(row: DecimalRow()) defaultInitializerTest(row: URLRow()) defaultInitializerTest(row: DateRow()) defaultInitializerTest(row: DateInlineRow()) defaultInitializerTest(row: PopoverSelectorRow()) defaultInitializerTest(row: SliderRow()) defaultInitializerTest(row: StepperRow()) defaultInitializerTest(row: PickerInputRow()) } func testOnRowValidationChenged() { onRowValidationTests(row: TextRow(), value: "Eureka!") onRowValidationTests(row: IntRow(), value: 33) onRowValidationTests(row: DecimalRow(), value: 35.7) onRowValidationTests(row: URLRow(), value: URL(string: "http://xmartlabs.com")!) onRowValidationTests(row: DateRow(), value: Date().addingTimeInterval(100)) onRowValidationTests(row: DateInlineRow(), value: Date().addingTimeInterval(100)) onRowValidationTests(row: PopoverSelectorRow(), value: "text") onRowValidationTests(row: SliderRow(), value: 5.0) onRowValidationTests(row: StepperRow(), value: 2.5) onRowValidationTests(row: TimeInlineRow(), value: Date()) onRowValidationTests(row: PickerInputRow(), value: "Hi!!") } private func onChangeTest(row:Row, value:Value) where Row: BaseRow, Row: RowType, Value == Row.Cell.Value { var invoked = false row.onChange { _ in invoked = true } formVC.form +++ Section() <<< row row.value = value XCTAssertTrue(invoked) } private func cellSetupTest(row:Row) where Row: BaseRow, Row : RowType, Value == Row.Cell.Value { var invoked = false row.cellSetup { _, _ in invoked = true } formVC.form +++ Section() <<< row let _ = row.cell // laod cell XCTAssertTrue(invoked) } private func cellUpdateTest(row:Row) where Row: BaseRow, Row : RowType, Value == Row.Cell.Value { var invoked = false row.cellUpdate { _, _ in invoked = true } formVC.form +++ Section() <<< row let _ = formVC.tableView(formVC.tableView!, cellForRowAt: row.indexPath!) // should invoke cell update XCTAssertTrue(invoked) } func onRowValidationTests(row:Row, value:Value) where Row: BaseRow, Row: RowType, Value == Row.Cell.Value { var invoked = false row.validationOptions = ValidationOptions.validatesOnChange row.add(rule: RuleClosure { _ in return ValidationError(msg: "Validation Error") }) row.onRowValidationChanged { _, _ in invoked = true } formVC.form +++ Section() <<< row row.value = value XCTAssertTrue(invoked) } private func defaultInitializerTest(row:Row) where Row: BaseRow, Row : RowType { var invoked = false Row.defaultRowInitializer = { row in invoked = true } formVC.form +++ Row.init { _ in } XCTAssertTrue(invoked) } private func defaultCellSetupTest(row:Row) where Row: BaseRow, Row: RowType { var invoked = false Row.defaultCellSetup = { cell, row in invoked = true } formVC.form +++ row let _ = row.cell // laod cell XCTAssertTrue(invoked) } private func defaultCellUpdateTest(row:Row) where Row: BaseRow, Row : RowType { var invoked = false Row.defaultCellUpdate = { cell, row in invoked = true } formVC.form +++ row let _ = formVC.tableView(formVC.tableView!, cellForRowAt: row.indexPath!) // should invoke cell update XCTAssertTrue(invoked) } } ================================================ FILE: Tests/CollectionTests.swift ================================================ // CollectionTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class CollectionTests: BaseEurekaTests { func testSectionRangeReplaceableCollectionTypeConformance() { // test if the collection function work as expected fieldForm[0].replaceSubrange(Range(3...6), with: [CheckRow("check1_ctx")]) // replacing 4 rows with 1 XCTAssertEqual(fieldForm[0].count, 8) // fieldform had 10 rows prior to replacing fieldForm[0][4] = CheckRow("check2_ctx") // replacing 5th row XCTAssertEqual(fieldForm[0].count, 8) let rows: [BaseRow] = fieldForm[0].filter({ $0 is CheckRow }) XCTAssertEqual(rows.count, 2) // Do I have 2 CheckRows?? fieldForm[0].append(contentsOf: [CheckRow("check3_ctx"), CheckRow("check4_ctx"), CheckRow("check5_ctx")]) // is the same as fieldForm[0] += [...] XCTAssertEqual(fieldForm[0].count, 11) } func testFormRangeReplaceableCollectionTypeConformance() { // test if the collection function work as expected manySectionsForm.replaceSubrange(2..<5, with: [Section("Out of order"), Section()]) // replacing 3 rows with 2 XCTAssertEqual(manySectionsForm.count, 5) // fieldform had 10 rows prior to replacing manySectionsForm[3] = Section("There is no order anyway") // replacing 4th row XCTAssertEqual(manySectionsForm.count, 5) let sections: [Section] = manySectionsForm.filter({ $0.header?.title?.contains("order") ?? false}) XCTAssertEqual(sections.count, 2) manySectionsForm.append(contentsOf: [Section("1"), Section("2")]) // is the same as fieldForm[0] += [...] XCTAssertEqual(manySectionsForm.count, 7) } func testDelegateFunctions() { // Test operators let form = Form() let delegate = MyFormDelegate() form.delegate = delegate form +++ Section("A") // addsection + 1 form +++ TextRow("textrow1_ctx") { $0.value = " "} // addsection + 1 form +++ Section("C") <<< TextRow("textrow2_ctx") <<< TextRow("textrow3_ctx") // addsection + 1 XCTAssertEqual(delegate.sectionsAdded, 3) XCTAssertEqual(delegate.rowsAdded, 0) form[0][0] = TextRow("textrow6_ctx") // addrow + 1 form[1][0].baseValue = "a" // valueschanged + 1 XCTAssertEqual(delegate.valuesChanged, 1) XCTAssertEqual(delegate.sectionsAdded, 3) XCTAssertEqual(delegate.rowsAdded, 1) form[2][1] = TextRow("textrow7_ctx") // replacerowIn+1, replacerowOut+1, form.replaceSubrange(Range(0...1), with: [Section("replaced in")]) // replacesectionOut+1, sectionremoved+1, replacesectionin+1 XCTAssertEqual(delegate.sectionsRemoved, 1) form[1].removeAll() // rowsremoved + 2 form.removeAll() // sectionsremoved + 2 //Test delegate XCTAssertEqual(delegate.valuesChanged, 1) XCTAssertEqual(delegate.sectionsAdded, 3) XCTAssertEqual(delegate.rowsAdded, 1) XCTAssertEqual(delegate.sectionsRemoved, 3) XCTAssertEqual(delegate.rowsRemoved, 2) XCTAssertEqual(delegate.rowsReplacedIn, 1) XCTAssertEqual(delegate.rowsReplacedOut, 1) XCTAssertEqual(delegate.sectionsReplacedIn, 1) XCTAssertEqual(delegate.sectionsReplacedOut, 1) } } ================================================ FILE: Tests/DateTests.swift ================================================ // DateTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class DateTests: BaseEurekaTests { override func setUp() { super.setUp() formVC.form = dateForm } func testMinMax() { let minRow: DateRow! = formVC.form.rowBy(tag: "MinDateRow_d1") let maxRow: DateRow! = formVC.form.rowBy(tag: "MaxDateRow_d1") let minMaxRow: DateRow! = formVC.form.rowBy(tag: "MinMaxDateRow_d1") XCTAssertNotNil(minRow.indexPath) XCTAssertNotNil(maxRow.indexPath) XCTAssertNotNil(minMaxRow.indexPath) // make sure cellSetup is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: minRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: maxRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: minMaxRow.indexPath!) //make sure cell update is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: minRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: maxRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: minMaxRow.indexPath!) XCTAssertNil(minRow.cell.datePicker.maximumDate) XCTAssertEqual(minRow.cell.datePicker.minimumDate, minRow.value?.addingTimeInterval(-60*60*24)) XCTAssertNil(maxRow.cell.datePicker.minimumDate) XCTAssertEqual(maxRow.cell.datePicker.maximumDate, maxRow.value?.addingTimeInterval(60*60*24)) XCTAssertNotNil(minMaxRow.cell.datePicker.minimumDate) XCTAssertEqual(minMaxRow.cell.datePicker.maximumDate, minMaxRow.cell.datePicker.minimumDate!.addingTimeInterval(2*60*60*24)) } func testInterval() { let row: DateRow? = formVC.form.rowBy(tag: "IntervalDateRow_d1") XCTAssertNotNil(row?.indexPath) // make sure cellSetup is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: row!.indexPath!) //make sure cell update is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: row!.indexPath!) XCTAssertEqual(row?.cell.datePicker.minuteInterval, 15) } } ================================================ FILE: Tests/FormValuesTests.swift ================================================ // FormValuesTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class FormValuesTests: BaseEurekaTests { override func setUp() { super.setUp() } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testFormValues() { let form = Form() let textRow = TextRow("RowTag") form +++ Section() <<< textRow // check that the row is included in the values dictionary XCTAssertEqual(form.values(includeHidden: true).count, 1) // check that the row value is nil XCTAssertTrue(form.values(includeHidden: true)["RowTag"]! == nil) // change the value and check that the new value appears textRow.value = "Hi!" XCTAssertTrue(form.values(includeHidden: true)["RowTag"] is String) XCTAssertEqual(form.values(includeHidden: true)["RowTag"] as? String, "Hi!") let textRowWithoutTag = TextRow() form +++ Section() <<< textRowWithoutTag XCTAssertEqual(form.values(includeHidden: true).count, 1) XCTAssertTrue(form.values(includeHidden: true)["RowTag"] is String) let textRowInvisible = TextRow("InvisibleRowTag") textRowInvisible.hidden = true form +++ Section() <<< textRowInvisible XCTAssertEqual(form.values(includeHidden: true).count, 2) XCTAssertTrue(form.values(includeHidden: true)["RowTag"] is String) XCTAssertEqual(form.allRows.count, 3) XCTAssertEqual(form.rows.count, 2) XCTAssertEqual(form.values().count, 1) XCTAssertEqual(form.values(includeHidden: true).count, 2) } func testIncludeHiddenFormValues() { let form = Form() let textRow = TextRow("RowTag") form +++ Section() <<< textRow // check that the row is included in the values dictionary XCTAssertEqual(form.values().count, 1) // check that the row value is nil XCTAssertTrue(form.values()["RowTag"]! == nil) // change the value and check that the new value appears textRow.value = "Hi!" XCTAssertTrue(form.values()["RowTag"] is String) XCTAssertEqual(form.values()["RowTag"] as? String, "Hi!") let textRowWithoutTag = TextRow() form +++ Section() <<< textRowWithoutTag XCTAssertEqual(form.values().count, 1) XCTAssertTrue(form.values()["RowTag"] is String) let textRowInvisible = TextRow("InvisibleRowTag") textRowInvisible.hidden = true textRowInvisible.value = "Bye!" form +++ Section() <<< textRowInvisible XCTAssertEqual(form.values(includeHidden: true).count, 2) XCTAssertTrue(form.values(includeHidden: true)["RowTag"] is String) XCTAssertTrue(form.values(includeHidden: true)["InvisibleRowTag"] is String) XCTAssertEqual(form.allRows.count, 3) XCTAssertEqual(form.rows.count, 2) XCTAssertEqual(form.values().count, 1) XCTAssertEqual(form.values(includeHidden: true).count, 2) } } ================================================ FILE: Tests/HelperMethodTests.swift ================================================ // HelperMethodTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class HelperMethodTests: BaseEurekaTests { func testRowByTag() { // Tests rowBy(tag: ) method let urlRow: URLRow? = fieldForm.rowBy(tag: "UrlRow_f1") XCTAssertNotNil(urlRow) let phoneRow: PhoneRow? = fieldForm.rowBy(tag: "phone") XCTAssertNil(phoneRow) } func testRowSequenceMethods() { // Tests the nextRowForRow() and the previousRowForRow() methods let form = fieldForm + shortForm + dateForm let row6 = form.nextRow(for: form[0][5]) XCTAssertEqual(row6, form[0][6]) XCTAssertEqual(row6, form.rowBy(tag: "IntRow_f1") as? IntRow) let row_5_and_6: ArraySlice = form[0][5...6] XCTAssertEqual(row_5_and_6[5], form[0][5]) XCTAssertEqual(row_5_and_6[6], form[0][6]) let row10n = form.nextRow(for: form[0][8]) let rownil = form.nextRow(for: form[2][7]) XCTAssertEqual(row10n, form[0][9]) XCTAssertNil(rownil) let row10p = form.previousRow(for: form[0][10]) let rowNilP = form.previousRow(for: form[0][0]) XCTAssertEqual(row10n, row10p) XCTAssertNil(rowNilP) XCTAssertNotNil(form.nextRow(for: form[1][1])) XCTAssertEqual(form[1][1], form.previousRow(for: form.nextRow(for: form[1][1])!)) } func testAllRowsMethod() { // Tests the allRows() method let form = fieldForm + shortForm + dateForm XCTAssertEqual(form.rows.count, 21) XCTAssertEqual(form.rows[12], shortForm[0][1]) XCTAssertEqual(form.rows[20], form.rowBy(tag: "IntervalDateRow_d1") as? DateRow) } func testAllRowsWrappedByTagMethod() { // Tests the allRows() method let form = fieldForm + shortForm + dateForm let rows = form.dictionaryValuesToEvaluatePredicate() XCTAssertEqual(rows.count, 21) } func testDisabledRows() { // Tests that a row set up as disabled can not become firstResponder let checkRow = CheckRow("check") { $0.disabled = true } let switchRow = SwitchRow("switch") { $0.disabled = true } let segmentedRow = SegmentedRow("segments") { $0.disabled = true; $0.options = ["a", "b"] } let intRow = IntRow("int") { $0.disabled = true } formVC.form +++ checkRow <<< switchRow <<< segmentedRow <<< intRow checkRow.updateCell() XCTAssertTrue(checkRow.cell.selectionStyle == .none) switchRow.updateCell() XCTAssertNotNil(switchRow.cell.switchControl) XCTAssertFalse(switchRow.cell.switchControl!.isEnabled) segmentedRow.updateCell() XCTAssertFalse(segmentedRow.cell.segmentedControl.isEnabled) intRow.updateCell() XCTAssertFalse(intRow.cell.cellCanBecomeFirstResponder()) } func testAllSectionsMethod() { let form = manySectionsForm let sections = form let allSections = form.allSections XCTAssertEqual(sections.count, 6) XCTAssertEqual(sections.count, allSections.count) let section1 = form[0] let section4 = form[3] form.hideSection(section1) form.hideSection(section4) XCTAssertEqual(sections.count, 4) XCTAssertEqual(allSections.count, 6) } } ================================================ FILE: Tests/HiddenRowsTests.swift ================================================ // HiddenRowsTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class HiddenRowsTests: BaseEurekaTests { var form: Form! let row10 = IntRow("int1_hrt") { $0.hidden = "$IntRow_s1 > 23" } let row11 = TextRow("txt1_hrt") { $0.hidden = .function(["NameRow_s1"], { form in if let r1: NameRow = form.rowBy(tag: "NameRow_s1") { return r1.value?.contains(" is ") ?? false } return false }) } let sec2 = Section("Whatsoever") { $0.tag = "s3_hrt" $0.hidden = "$NameRow_s1 contains 'God'" } let row20 = TextRow("txt2_hrt") { $0.hidden = .function(["IntRow_s1", "NameRow_s1"], { form in if let r1: IntRow = form.rowBy(tag: "IntRow_s1"), let r2: NameRow = form.rowBy(tag: "NameRow_s1") { return r1.value == 88 || r2.value?.hasSuffix("real") ?? false } return false }) } let inlineDateRow21 = DateInlineRow { $0.hidden = "$IntRow_s1 > 23" } override func setUp() { super.setUp() form = shortForm +++ row10 <<< row11 +++ sec2 <<< row20 <<< inlineDateRow21 } func testAddRowToObserver() { let intDep = form.rowObservers["IntRow_s1"]?[.hidden] let nameDep = form.rowObservers["NameRow_s1"]?[.hidden] // make sure we can unwrap XCTAssertNotNil(intDep) XCTAssertNotNil(nameDep) // test rowObservers XCTAssertEqual(intDep!.count, 3) XCTAssertEqual(nameDep!.count, 3) XCTAssertTrue(intDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(intDep!.contains(where: { $0.tag == "int1_hrt" })) XCTAssertFalse(intDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt1_hrt" })) XCTAssertFalse(nameDep!.contains(where: { $0.tag == "int1_hrt" })) //This should not change when some rows hide ... form[0][0].baseValue = "God is real" form[0][1].baseValue = 88 //check everything is still the same XCTAssertEqual(intDep!.count, 3) XCTAssertEqual(nameDep!.count, 3) XCTAssertTrue(intDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(intDep!.contains(where: { $0.tag == "int1_hrt" })) XCTAssertFalse(intDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt1_hrt" })) XCTAssertFalse(nameDep!.contains(where: { $0.tag == "int1_hrt" })) // ...nor if they reappear form[0][0].baseValue = "blah blah blah" form[0][1].baseValue = 1 //check everything is still the same XCTAssertEqual(intDep!.count, 3) XCTAssertEqual(nameDep!.count, 3) XCTAssertTrue(intDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(intDep!.contains(where: { $0.tag == "int1_hrt" })) XCTAssertFalse(intDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt2_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "s3_hrt" })) XCTAssertTrue(nameDep!.contains(where: { $0.tag == "txt1_hrt" })) XCTAssertFalse(nameDep!.contains(where: { $0.tag == "int1_hrt" })) // Test a condition with nil let newRow = TextRow("new_row") { $0.hidden = "$txt1_hrt == nil" } form.last! <<< newRow XCTAssertTrue(newRow.hiddenCache) } func testItemsByTag() { // test that all rows and sections with tag are there XCTAssertEqual(form.rowBy(tag: "NameRow_s1"), form[0][0]) XCTAssertEqual(form.rowBy(tag: "IntRow_s1"), form[0][1]) XCTAssertEqual(form.rowBy(tag: "int1_hrt"), row10) XCTAssertEqual(form.rowBy(tag: "txt1_hrt"), row11) XCTAssertEqual(form.sectionBy(tag: "s3_hrt"), sec2) XCTAssertEqual(form.rowBy(tag: "txt2_hrt"), row20) // check that these are all in there XCTAssertEqual(form.rowsByTag.count, 5) // what happens after hiding the rows? Let's cause havoc form[0][0].baseValue = "God is real" form[0][1].baseValue = 88 // we still want the same results here XCTAssertEqual(form.rowBy(tag: "NameRow_s1"), form[0][0]) XCTAssertEqual(form.rowBy(tag: "IntRow_s1"), form[0][1]) XCTAssertEqual(form.rowBy(tag: "int1_hrt"), row10) XCTAssertEqual(form.rowBy(tag: "txt1_hrt"), row11) XCTAssertEqual(form.sectionBy(tag: "s3_hrt"), sec2) XCTAssertEqual(form.rowBy(tag: "txt2_hrt"), row20) XCTAssertEqual(form.rowsByTag.count, 5) // and let them come up again form[0][0].baseValue = "blah blah" form[0][1].baseValue = 1 // we still want the same results here XCTAssertEqual(form.rowsByTag["NameRow_s1"], form[0][0]) XCTAssertEqual(form.rowsByTag["IntRow_s1"], form[0][1]) XCTAssertEqual(form.rowsByTag["int1_hrt"], row10) XCTAssertEqual(form.rowsByTag["txt1_hrt"], row11) XCTAssertEqual(form.sectionBy(tag: "s3_hrt"), sec2) XCTAssertEqual(form.rowsByTag["txt2_hrt"], row20) XCTAssertEqual(form.rowsByTag.count, 5) } func testCorrectValues() { //initial empty values (none is hidden) XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(sec2.count, 2) // false values form[0][0].baseValue = "Hi there" form[0][1].baseValue = 15 XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(sec2.count, 2) // hide 'int1_hrt' row form[0][1].baseValue = 24 XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 1) XCTAssertEqual(sec2.count, 1) XCTAssertEqual(form[1][0].tag, "txt1_hrt") // hide 'txt1_hrt' and 'txt2_hrt' form[0][0].baseValue = " is real" XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 0) XCTAssertEqual(sec2.count, 0) // let the last section disappear form[0][0].baseValue = "God is real" XCTAssertEqual(form.count, 2) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 0) // and see if they come back to live form[0][0].baseValue = "blah" form[0][1].baseValue = 2 XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(sec2.count, 2) } func testInlineRows() { //initial empty values (none is hidden) XCTAssertEqual(sec2.count, 2) // change dependency value form[0][1].baseValue = 25 XCTAssertEqual(sec2.count, 1) // change dependency value form[0][1].baseValue = 10 XCTAssertEqual(sec2.count, 2) //hide inline row when expanded inlineDateRow21.expandInlineRow() // check that the row is expanded XCTAssertEqual(sec2.count, 3) // hide expanded inline row form[0][1].baseValue = 25 XCTAssertEqual(sec2.count, 1) // make inline row visible again form[0][1].baseValue = 10 XCTAssertEqual(sec2.count, 2) } func testHiddenSections() { let s1 = Section { $0.hidden = "$NameRow_s1 contains 'hello'" $0.tag = "s1_ths" } let s2 = Section { $0.hidden = "$NameRow_s1 contains 'morning'" $0.tag = "s2_ths" } form.insert(s1, at: 1) form.insert(s2, at: 3) /* what we should have here shortForm (1 section) s1 { row10, row11 } s2 sec2 (2 rows) */ XCTAssertEqual(form.count, 5) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 0) XCTAssertEqual(form[2].count, 2) XCTAssertEqual(form[3].count, 0) XCTAssertEqual(form[4].count, 2) form[0][0].baseValue = "hello, good morning!" XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(form[2].count, 2) form[0][0].baseValue = "whatever" XCTAssertEqual(form.count, 5) XCTAssertEqual(form[1].tag, "s1_ths") XCTAssertEqual(form[3].tag, "s2_ths") XCTAssertEqual(form[4].tag, "s3_hrt") } func testInsertionIndex() { let r1 = CheckRow("check1_tii_hrt") { $0.hidden = "$NameRow_s1 contains 'morning'" } let r2 = CheckRow("check2_tii_hrt") { $0.hidden = "$NameRow_s1 contains 'morning'" } let r3 = CheckRow("check3_tii_hrt") { $0.hidden = "$NameRow_s1 contains 'good'" } let r4 = CheckRow("check4_tii_hrt") { $0.hidden = "$NameRow_s1 contains 'good'" } form[0].insert(r1, at: 1) form[1].insert(contentsOf: [r2, r3], at: 0) //test correct insert XCTAssertEqual(form[0].count, 3) XCTAssertEqual(form[0][1].tag, "check1_tii_hrt") XCTAssertEqual(form[1].count, 4) XCTAssertEqual(form[1][0].tag, "check2_tii_hrt") XCTAssertEqual(form[1][1].tag, "check3_tii_hrt") // hide these rows form[0][0].baseValue = "hello, good morning!" // insert another row form[1].insert(r4, at: 1) XCTAssertEqual(form[1].count, 2) // all inserted rows should be hidden XCTAssertEqual(form[1][0].tag, "int1_hrt") XCTAssertEqual(form[1][1].tag, "txt1_hrt") form[0][0].baseValue = "whatever" // we inserted r4 at index 1 but there were two rows hidden before it as well so it shall be at index 3 XCTAssertEqual(form[1].count, 5) XCTAssertEqual(form[1][0].tag, "check2_tii_hrt") XCTAssertEqual(form[1][1].tag, "check3_tii_hrt") XCTAssertEqual(form[1][2].tag, "int1_hrt") XCTAssertEqual(form[1][3].tag, "check4_tii_hrt") XCTAssertEqual(form[1][4].tag, "txt1_hrt") form[0][0].baseValue = "hello, good morning!" //check that hidden rows get removed as well form[1].removeAll() //inserting 2 rows at the end, deleting 1 form[2].replaceSubrange(1..<2, with: [r2, r4]) XCTAssertEqual(form[1].count, 0) XCTAssertEqual(form[2].count, 1) XCTAssertEqual(form[2][0].tag, "txt2_hrt") form[0][0].baseValue = "whatever" XCTAssertEqual(form[2].count, 3) XCTAssertEqual(form[2][0].tag, "txt2_hrt") XCTAssertEqual(form[2][1].tag, "check2_tii_hrt") XCTAssertEqual(form[2][2].tag, "check4_tii_hrt") } } ================================================ FILE: Tests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 4.3.1 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: Tests/MultivaluedSectionTests.swift ================================================ // MultivaluedSectionTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2017 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class MultivaluedSectionTests: XCTestCase { var formVC = FormViewController() override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. formVC = FormViewController() formVC.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) formVC.tableView?.frame = formVC.view.frame } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testHeaders() { let headerSection = MultivaluedSection(multivaluedOptions: .Insert, header: "Header Text", footer: nil) { _ in } XCTAssertEqual(headerSection.header!.title, "Header Text") XCTAssertNil(headerSection.footer) } func testFooters() { let footerSection = MultivaluedSection(multivaluedOptions: .Insert, header: nil, footer: "Footer Text") { _ in } XCTAssertEqual(footerSection.footer!.title, "Footer Text") XCTAssertNil(footerSection.header) } func testAddButton() { let section = MultivaluedSection(multivaluedOptions: .Insert, header: "", footer: "") { _ in // just an empty closure } XCTAssertEqual(section.count, 1) let section2 = MultivaluedSection(multivaluedOptions: .Reorder, header: "", footer: "") { _ in // just an empty closure } XCTAssertEqual(section2.count, 0) } func testDelegateMethods() { let form = Form() let section = MultivaluedSection(multivaluedOptions: .Insert, header: "", footer: "") { section in section.tag = "textrows" section <<< TextRow() { $0.value = "text" } } form +++ section formVC.form = form // values XCTAssertEqual(form.values().keys.count, 1) XCTAssertEqual(form.values()["textrows"] as! [String], ["text"]) // canEditRowAt XCTAssertTrue(formVC.tableView(formVC.tableView, canEditRowAt: IndexPath(item: 0, section: 0))) form +++ Section() <<< TextRow() XCTAssertFalse(formVC.tableView(formVC.tableView, canEditRowAt: IndexPath(item: 0, section: 1))) // editingStyleForRowAt XCTAssertEqual(formVC.tableView(formVC.tableView, editingStyleForRowAt: IndexPath(item: 1, section: 0)), UITableViewCell.EditingStyle.insert) XCTAssertEqual(formVC.tableView(formVC.tableView, editingStyleForRowAt: IndexPath(item: 0, section: 0)), UITableViewCell.EditingStyle.none) XCTAssertEqual(formVC.tableView(formVC.tableView, editingStyleForRowAt: IndexPath(item: 0, section: 1)), UITableViewCell.EditingStyle.none) form +++ MultivaluedSection(multivaluedOptions: .Delete, header: "", footer: "") { _ in } <<< TextRow() XCTAssertEqual(formVC.tableView(formVC.tableView, editingStyleForRowAt: IndexPath(item: 0, section: 2)), UITableViewCell.EditingStyle.delete) // shouldIndentWhileEditingRowAt XCTAssertFalse(formVC.tableView(formVC.tableView, shouldIndentWhileEditingRowAt: IndexPath(item: 0, section: 0))) XCTAssertTrue(formVC.tableView(formVC.tableView, shouldIndentWhileEditingRowAt: IndexPath(item: 1, section: 0))) XCTAssertFalse(formVC.tableView(formVC.tableView, shouldIndentWhileEditingRowAt: IndexPath(item: 0, section: 1))) XCTAssertTrue(formVC.tableView(formVC.tableView, shouldIndentWhileEditingRowAt: IndexPath(item: 0, section: 2))) // canMoveRowAt XCTAssertFalse(formVC.tableView(formVC.tableView, canMoveRowAt: IndexPath(item: 0, section: 0))) XCTAssertFalse(formVC.tableView(formVC.tableView, canMoveRowAt: IndexPath(item: 1, section: 0))) form +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "", footer: "") { _ in } <<< TextRow() XCTAssertFalse(formVC.tableView(formVC.tableView, canMoveRowAt: IndexPath(item: 0, section: 3))) form.last! <<< TextRow() XCTAssertTrue(formVC.tableView(formVC.tableView, canMoveRowAt: IndexPath(item: 0, section: 3))) XCTAssertTrue(formVC.tableView(formVC.tableView, canMoveRowAt: IndexPath(item: 1, section: 3))) } } ================================================ FILE: Tests/OperatorsTest.swift ================================================ // OperatorsTest.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class OperatorsTest: BaseEurekaTests { func testOperators() { // test the operators var form = Form() form +++ TextRow("textrow1_ctx") <<< TextRow("textrow2_ctx") form = form + (TextRow("textrow3_ctx") <<< TextRow("textrow4_ctx") +++ TextRow("textrow5_ctx") <<< TextRow("textrow6_ctx")) + (TextRow("textrow7_ctx") +++ TextRow("textrow8_ctx")) XCTAssertEqual(form.count, 5) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(form[2].count, 2) form +++ IntRow("introw1_ctx") form +++ IntRow("introw2_ctx") <<< IntRow("introw3_ctx") <<< IntRow("introw4_ctx") // form: // text1 // text2 // ----- // text3 // text4 // ----- // text5 // text6 // ----- // text7 // ----- // text8 // ----- // int1 // ---- // int2 // int3 // int4 XCTAssertEqual(form.count, 7) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(form[2].count, 2) XCTAssertEqual(form[3].count, 1) XCTAssertEqual(form[4].count, 1) XCTAssertEqual(form[5].count, 1) XCTAssertEqual(form[6].count, 3) } } ================================================ FILE: Tests/ResultBuildersTests.swift ================================================ // ResultBuildersTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2022 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class ResultBuildersTests: BaseEurekaTests { #if swift(>=5.4) @SectionBuilder var section1: Section { NameRow("NameRow_f1") { $0.title = "Name" } if true { IntRow("IntRow_f1") { $0.title = "Int" } } DecimalRow("DecimalRow_f1") { $0.title = "Decimal" } } @FormBuilder var form: Form { Section("Section A") { section in section.tag = "Section_A" } if true { Section("Section B") { section in section.tag = "Section_B" } } NameRow("NameRow_f1") { $0.title = "Name" } } #endif private var checkBuildEither = false private var checkBuildExpressionBaseRowOptional = false func testSectionBuilder() { #if swift(>=5.4) setupManySectionsForm() addMoreItemsToManySectionsForm() XCTAssertNotNil(manySectionsForm.rowBy(tag: "NameRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "IntRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "DecimalRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "UrlRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "TwitterRow_f1")) XCTAssertNil(manySectionsForm.rowBy(tag: "TwitterRow_f2")) XCTAssertNil(manySectionsForm.rowBy(tag: "EmailRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "AccountRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "PhoneRow_f1")) XCTAssertNil(manySectionsForm.rowBy(tag: "PhoneRow_f2")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "ZipCodeRow_f1")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "PasswordRow_f1")) #endif } private func setupManySectionsForm() { checkBuildEither = false checkBuildExpressionBaseRowOptional = true manySectionsForm = (section1 +++ { URLRow("UrlRow_f1") { $0.title = "Url" } if checkBuildEither { TwitterRow("TwitterRow_f2") { $0.title = "Twitter" } } else { TwitterRow("TwitterRow_f1") { $0.title = "Twitter" } } if checkBuildExpressionBaseRowOptional { nil } else { EmailRow("EmailRow_f1") { $0.title = "Email" } } AccountRow("AccountRow_f1") { $0.title = "Account" } }) } private func addMoreItemsToManySectionsForm() { checkBuildEither.toggle() checkBuildExpressionBaseRowOptional.toggle() manySectionsForm +++ { if checkBuildEither { PhoneRow("PhoneRow_f1") { $0.title = "Phone" } } else { PhoneRow("PhoneRow_f2") { $0.title = "Phone" } } if checkBuildExpressionBaseRowOptional { nil } else { ZipCodeRow("ZipCodeRow_f1") { $0.title = "Zip Code" } } PasswordRow("PasswordRow_f1") { $0.title = "Password" } } } func testFormBuilder() { #if swift(>=5.4) manySectionsForm = form XCTAssertNotNil(manySectionsForm.sectionBy(tag: "Section_A")) XCTAssertNotNil(manySectionsForm.sectionBy(tag: "Section_B")) XCTAssertNotNil(manySectionsForm.rowBy(tag: "NameRow_f1")) #endif } } ================================================ FILE: Tests/RowByTagTests.swift ================================================ // RowByTagTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class RowByTagTests: XCTestCase { var form: Form! override func setUp() { super.setUp() form = Form() let section = Section() form +++ section section <<< LabelRow("LabelRow") section <<< ButtonRow("ButtonRow") section <<< ActionSheetRow("ActionSheetRow") section <<< AlertRow("AlertRow") section <<< PushRow("PushRow") } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testRowByTag() { let labelRow: LabelRow? = form.rowBy(tag: "LabelRow") XCTAssertNotNil(labelRow) let buttonRow: ButtonRow? = form.rowBy(tag: "ButtonRow") XCTAssertNotNil(buttonRow) let actionSheetRow: ActionSheetRow? = form.rowBy(tag: "ActionSheetRow") XCTAssertNotNil(actionSheetRow) let alertRow: AlertRow? = form.rowBy(tag: "AlertRow") XCTAssertNotNil(alertRow) let pushRow: PushRow? = form.rowBy(tag: "PushRow") XCTAssertNotNil(pushRow) } } ================================================ FILE: Tests/RowCallbackTests.swift ================================================ // RowCallbackTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class RowCallbackTests: BaseEurekaTests { override func setUp() { super.setUp() formVC.form = Form() +++ Section("something") <<< CheckRow("row1").cellSetup { cell, _ in cell.textLabel?.text = "checkrow + Setup" cell.backgroundColor = .red } <<< IntRow("row2").cellUpdate({ cell, _ in cell.textLabel?.text = "introw" cell.textLabel?.font = UIFont(name: "Baskerville-Italic", size: 20) }) <<< TextRow("row3").cellSetup({ cell, _ in cell.textLabel?.text = "aftersetup" }).cellUpdate({ cell, _ in cell.textLabel?.text = "afterupdate" cell.textLabel?.font = UIFont(name: "Baskerville-Italic", size: 20) }) } func testTableViewNotNil() { XCTAssertNotNil(formVC.tableView) } func testOnChange() { // Test onChange callback let chk = CheckRow("row1") { $0.title = "check"; $0.value = false } formVC.form = Form() +++ Section("something") <<< chk <<< IntRow("row2") { $0.title = "int"; $0.value = 1 } .onChange { [weak chk] row in chk?.value = ((row.value! % 2) == 0) } let intRow = formVC.form[0][1] intRow.baseValue = 2 XCTAssertEqual(chk.value, true) formVC.form[0][1].baseValue = 3 XCTAssertEqual(chk.value, false) } func testCellSetupAndUpdate() { let chkRow: CheckRow! = formVC.form.rowBy(tag: "row1") let intRow: IntRow! = formVC.form.rowBy(tag: "row2") let textRow: TextRow! = formVC.form.rowBy(tag: "row3") // check that they all have indexPath XCTAssertNotNil(chkRow.indexPath) XCTAssertNotNil(intRow.indexPath) XCTAssertNotNil(textRow.indexPath) // make sure cellSetup is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: intRow.indexPath!) XCTAssertEqual(chkRow.cell.textLabel?.text, "checkrow + Setup") XCTAssertEqual(textRow.cell.textLabel?.text, "aftersetup") let _ = formVC.tableView(formVC.tableView!, cellForRowAt: textRow.indexPath!) XCTAssertEqual(textRow.cell.textLabel?.text, "afterupdate") let _ = formVC.tableView(formVC.tableView!, cellForRowAt: chkRow.indexPath!) //make sure cell update is called for each cell let _ = formVC.tableView(formVC.tableView!, cellForRowAt: chkRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: intRow.indexPath!) let _ = formVC.tableView(formVC.tableView!, cellForRowAt: textRow.indexPath!) XCTAssertEqual(chkRow?.cell.textLabel?.text, chkRow?.title) XCTAssertEqual(intRow?.cell.textLabel?.text, "introw") XCTAssertEqual(textRow?.cell.textLabel?.text, "afterupdate") XCTAssertEqual(chkRow?.cell.backgroundColor, .red) XCTAssertEqual(intRow?.cell.textLabel?.font, UIFont(name: "Baskerville-Italic", size: 20)) XCTAssertEqual(textRow?.cell.textLabel?.font, UIFont(name: "Baskerville-Italic", size: 20)) } } ================================================ FILE: Tests/RowsInsertionTests.swift ================================================ // // RowsInsertionTests.swift // Eureka // // Created by Miguel Revetria on 5/8/17. // Copyright © 2017 Xmartlabs. All rights reserved. // import XCTest @testable import Eureka class RowsInsertionTests: XCTestCase { func testAppendingRows() { let form = Form() let section = Section("section_01") form.append(section) section.append(NameRow(tag: "row_01")) section.append(NameRow(tag: "row_02")) section.append(NameRow(tag: "row_03")) hideAndShowRows(form: form, expectedTags: ["row_01", "row_02", "row_03"]) } func testAppendingRowsWithCustomOperator() { let form = Form() let section = Section("section_01") form +++ section <<< NameRow(tag: "row_01") <<< NameRow(tag: "row_02") <<< NameRow(tag: "row_03") hideAndShowRows(form: form, expectedTags: ["row_01", "row_02", "row_03"]) } func testInsertingRowsWithSubscript() { let form = Form() let section = Section("section_01") form[0] = section form[0][0] = NameRow(tag: "row_01") form[0][1] = NameRow(tag: "row_02") form[0][2] = NameRow(tag: "row_03") hideAndShowRows(form: form, expectedTags: ["row_01", "row_02", "row_03"]) } func testMovingAppendedRows() { let formInit: () -> Form = { let form = Form() let section = Section() form.append(section) section.append(NameRow(tag: "tag_01")) section.append(NameRow(tag: "tag_02")) section.append(NameRow(tag: "tag_03")) return form } let append: (Section, BaseRow) -> Void = { section, row in section.append(row) } movingRows(formInit: formInit, sectionAppend: append) } func testMovingAppendedRowsWithCustomOperator() { let formInit: () -> Form = { let form = Form() let section = Section() form +++ section section <<< NameRow(tag: "tag_01") section <<< NameRow(tag: "tag_02") section <<< NameRow(tag: "tag_03") return form } let append: (Section, BaseRow) -> Void = { section, row in section <<< row } movingRows(formInit: formInit, sectionAppend: append) } func testMovingInsertedRowsWithSubscript() { let formInit: () -> Form = { let form = Form() let section = Section() form[0] = section section[0] = NameRow(tag: "tag_01") section[1] = NameRow(tag: "tag_02") section[2] = NameRow(tag: "tag_03") return form } let append: (Section, BaseRow) -> Void = { section, row in section[section.count] = row } movingRows(formInit: formInit, sectionAppend: append) } func testReplacingAppendedRows() { let form = Form() let section = Section() form.append(section) section.append(NameRow(tag: "tag_01")) section.append(NameRow(tag: "tag_02")) section.append(NameRow(tag: "tag_03")) replaceRows(form: form) } func testReplacingAppendedRowsWithCustomOperator() { let form = Form() let section = Section() form +++ section <<< NameRow(tag: "tag_01") <<< NameRow(tag: "tag_02") <<< NameRow(tag: "tag_03") replaceRows(form: form) } func testReplacingInsertedRowsWithSubscript() { let form = Form() let section = Section() form[0] = section form[0][0] = NameRow(tag: "tag_01") form[0][1] = NameRow(tag: "tag_02") form[0][2] = NameRow(tag: "tag_03") replaceRows(form: form) } func testReplacingSubrangeForAppendedRows() { let form = Form() let section = Section() form.append(section) section.append(NameRow(tag: "tag_01")) section.append(NameRow(tag: "tag_02")) section.append(NameRow(tag: "tag_03")) replaceSectionSubranges(form: form) } func testReplacingSubrangeForAppendedRowsWithCustomOperator() { let form = Form() let section = Section() form +++ section <<< NameRow(tag: "tag_01") <<< NameRow(tag: "tag_02") <<< NameRow(tag: "tag_03") replaceSectionSubranges(form: form) } func testReplacingSubrangeForInsertedRowsWithSubscript() { let form = Form() let section = Section() form[0] = section form[0][0] = NameRow(tag: "tag_01") form[0][1] = NameRow(tag: "tag_02") form[0][2] = NameRow(tag: "tag_03") replaceSectionSubranges(form: form) } private func hideAndShowRows(form: Form, expectedTags tags: [String]) { form.first?.forEach { row in XCTAssertNotNil(row.section) } XCTAssertEqual(form[0].count, 3) var tag1 = form[0][0].tag! var tag2 = form[0][1].tag! var tag3 = form[0][2].tag! XCTAssertEqual(tag1, tags[0]) XCTAssertEqual(tag2, tags[1]) XCTAssertEqual(tag3, tags[2]) var row = form[0][1] row.hidden = true row.evaluateHidden() XCTAssertNotNil(row.section) XCTAssertEqual(form[0].count, 2) XCTAssertEqual(form.allRows.count, 3) tag1 = form[0][0].tag! tag3 = form[0][1].tag! XCTAssertEqual(tag1, tags[0]) XCTAssertEqual(tag3, tags[2]) row = form[0][0] row.hidden = true row.evaluateHidden() XCTAssertNotNil(row.section) XCTAssertEqual(form[0].count, 1) XCTAssertEqual(form.allRows.count, 3) tag3 = form[0][0].tag! XCTAssertEqual(tag3, tags[2]) row = form[0][0] row.hidden = true row.evaluateHidden() XCTAssertNotNil(row.section) XCTAssertEqual(form[0].count, 0) XCTAssertEqual(form.allRows.count, 3) form.allRows .map { $0.tag! } .enumerated() .forEach { ind, tag in XCTAssertEqual(tag, tags[ind]) } form.allRows[1].hidden = false form.allRows[1].evaluateHidden() XCTAssertEqual(form[0].count, 1) XCTAssertEqual(form.allRows.count, 3) tag2 = form[0][0].tag! XCTAssertEqual(tag2, tags[1]) } private func movingRows(formInit: () -> Form, sectionAppend: (Section, BaseRow) -> Void) { var form = formInit() var tmp = form[0].remove(at: 0) XCTAssertNil(tmp.section) sectionAppend(form[0], tmp) XCTAssertNotNil(tmp.section) hideAndShowRows(form: form, expectedTags: ["tag_02", "tag_03", "tag_01"]) form = formInit() tmp = form[0].remove(at: 1) XCTAssertNil(tmp.section) sectionAppend(form[0], tmp) XCTAssertNotNil(tmp.section) hideAndShowRows(form: form, expectedTags: ["tag_01", "tag_03", "tag_02"]) form = formInit() tmp = form[0].remove(at: 2) XCTAssertNil(tmp.section) sectionAppend(form[0], tmp) XCTAssertNotNil(tmp.section) hideAndShowRows(form: form, expectedTags: ["tag_01", "tag_02", "tag_03"]) } private func replaceRows(form: Form) { let section = form.first! for ind in 0.. Form = { let form = Form() form.append(Section("section_01")) form.append(Section("section_02")) form.append(Section("section_03")) return form } let append: (Form, Section) -> Void = { form, section in form.append(section) } movingSections(formInit: formInit, formAppend: append) } func testMovingAppendedSectionsWithCustomOperator() { let formInit: () -> Form = { let form = Form() form +++ Section("section_01") form +++ Section("section_02") form +++ Section("section_03") return form } let append: (Form, Section) -> Void = { form, section in form +++ section } movingSections(formInit: formInit, formAppend: append) } func testMovingInsertedSectionsWithSubscript() { let formInit: () -> Form = { let form = Form() form[0] = Section("section_01") form[1] = Section("section_02") form[2] = Section("section_03") return form } let append: (Form, Section) -> Void = { form, section in form[form.count] = section } movingSections(formInit: formInit, formAppend: append) } func testReplacingAppendedSections() { let form = Form() form.append(Section("section_01")) form.append(Section("section_02")) form.append(Section("section_03")) replaceSections(form: form) } func testReplacingAppendedSectionsWithCustomOperator() { let form = Form() form +++ Section("section_01") form +++ Section("section_02") form +++ Section("section_03") replaceSections(form: form) } func testReplacingInsertedSectionsWithSubscript() { let form = Form() form[0] = Section("section_01") form[1] = Section("section_02") form[2] = Section("section_03") replaceSections(form: form) } func testReplacingSubrangeForAppendedSections() { let form = Form() form.append(Section("section_01")) form.append(Section("section_02")) form.append(Section("section_03")) replaceSectionSubranges(form: form) } func testReplacingSubrangeForAppendedSectionsWithCustomOperator() { let form = Form() form +++ Section("section_01") form +++ Section("section_02") form +++ Section("section_03") replaceSectionSubranges(form: form) } func testReplacingSubrangeForInsertedSectionsWithSubscript() { let form = Form() form[0] = Section("section_01") form[1] = Section("section_02") form[2] = Section("section_03") replaceSectionSubranges(form: form) } func testInsertingAfterRow() { let form = Form() form[0] = Section("section_01") form[0] <<< TextRow("a") <<< TextRow("b") { $0.hidden = true } <<< TextRow("d") { $0.hidden = true } <<< TextRow("e") let bRow = form.rowBy(tag: "b") XCTAssertNotNil(bRow) try? form[0].insert(row: TextRow("c"), after: bRow!) let cRow = form.rowBy(tag: "c") XCTAssertNotNil(cRow) XCTAssertEqual(form[0].count, 3) // a, c, e XCTAssertEqual(form.allRows.count, 5) // a, b, c, d, e XCTAssertEqual(form[0][1], cRow) } func testInsertingAfterRowAtEnd() { let form = Form() form[0] = Section("section_01") form[0] <<< TextRow("a") { $0.hidden = true } let aRow = form.rowBy(tag: "a") XCTAssertNotNil(aRow) try? form[0].insert(row: TextRow("b") { $0.hidden = true }, after: aRow!) let bRow = form.rowBy(tag: "b") XCTAssertNotNil(bRow) XCTAssertEqual(form[0].count, 0) XCTAssertEqual(form.allRows.count, 2) // a, b try? form[0].insert(row: TextRow("c"), after: aRow!) let cRow = form.rowBy(tag: "c") XCTAssertNotNil(cRow) XCTAssertEqual(form[0].count, 1) // c XCTAssertEqual(form.allRows.count, 3) // a, c, b XCTAssertEqual(form.allRows[0], aRow) XCTAssertEqual(form.allRows[1], cRow) XCTAssertEqual(form.allRows[2], bRow) } func testDeletingRows() { let form = Form() let section = Section("section_01") form.append(section) section.append(NameRow(tag: "row_01")) section.append(NameRow(tag: "row_2")) section.append(NameRow("row_03") { $0.hidden = true }) section.append(NameRow("row_04") { $0.hidden = true }) section.removeAll(where: { row in row.tag?.hasPrefix("row_0") ?? false }) XCTAssertNotNil(form.rowBy(tag: "row_2")) XCTAssertEqual(form.allRows.count, 1) } func testDeletingSections() { let form = Form() form +++ Section("section_0") +++ Section("section_1") { $0.hidden = true } +++ Section("section_22") +++ Section("section_32") form.removeAll(where: { section in section.header?.title?.hasSuffix("2") ?? false }) XCTAssertEqual(form.allSections.count, 2) } func testReplaceAllSection() { let form = Form() +++ Section("section1") { $0.hidden = true } +++ Section("section2") +++ Section("section3") form.replaceSubrangeInAllSections(Range(uncheckedBounds: (lower: 0, upper: 2)), with: [Section("section0") { $0.hidden = true }]) XCTAssertEqual(form.allSections.count, 2) XCTAssertEqual(form.count, 1) XCTAssertEqual(form[0].header?.title, "section3") XCTAssertEqual(form.allSections[0].header?.title, "section0") XCTAssertEqual(form.allSections[1].header?.title, "section3") } private func hideAndShowSections(form: Form, expectedTitles titles: [String]) { // Doesn't matter how rows were added to the form (using append, +++ or subscript index) // next must work XCTAssertEqual(form.count, 3) var title1 = form[0].header!.title! var title2 = form[1].header!.title! var title3 = form[2].header!.title! XCTAssertEqual(title1, titles[0]) XCTAssertEqual(title2, titles[1]) XCTAssertEqual(title3, titles[2]) var sect = form[1] sect.hidden = true sect.evaluateHidden() XCTAssertNotNil(sect.form) XCTAssertEqual(form.count, 2) XCTAssertEqual(form.allSections.count, 3) title1 = form[0].header!.title! title3 = form[1].header!.title! XCTAssertEqual(title1, titles[0]) XCTAssertEqual(title3, titles[2]) sect = form[0] sect.hidden = true sect.evaluateHidden() XCTAssertNotNil(sect.form) XCTAssertEqual(form.count, 1) XCTAssertEqual(form.allSections.count, 3) title3 = form[0].header!.title! XCTAssertEqual(title3, titles[2]) sect = form[0] sect.hidden = true sect.evaluateHidden() XCTAssertNotNil(sect.form) XCTAssertEqual(form.count, 0) XCTAssertEqual(form.allSections.count, 3) form.allSections .map { $0.header!.title! } .enumerated() .forEach { ind, title in XCTAssertEqual(title, titles[ind]) } form.allSections[1].hidden = false form.allSections[1].evaluateHidden() XCTAssertEqual(form.count, 1) XCTAssertEqual(form.allSections.count, 3) title2 = form[0].header!.title! XCTAssertEqual(title2, titles[1]) } private func movingSections(formInit: () -> Form, formAppend: (Form, Section) -> Void) { var form = formInit() var tmp = form.remove(at: 0) XCTAssertNil(tmp.form) formAppend(form, tmp) XCTAssertNotNil(tmp.form) hideAndShowSections(form: form, expectedTitles: ["section_02", "section_03", "section_01"]) form = formInit() tmp = form.remove(at: 1) XCTAssertNil(tmp.form) formAppend(form, tmp) XCTAssertNotNil(tmp.form) hideAndShowSections(form: form, expectedTitles: ["section_01", "section_03", "section_02"]) form = formInit() tmp = form.remove(at: 2) XCTAssertNil(tmp.form) formAppend(form, tmp) XCTAssertNotNil(tmp.form) hideAndShowSections(form: form, expectedTitles: ["section_01", "section_02", "section_03"]) } private func replaceSections(form: Form) { for ind in 0..> { _ in } for option in continents { form.last! <<< ListCheckRow(option) { lrow in lrow.title = option lrow.selectableValue = option lrow.value = nil } } form +++ SelectableSection>("And which of the following oceans have you taken a bath in?", selectionType: .multipleSelection) for option in oceans { form.last! <<< ListCheckRow(option) { lrow in lrow.title = option lrow.selectableValue = option } } form +++ SelectableSection>("", selectionType: .singleSelection(enableDeselection: false)) for option in oceans { form.last! <<< ListCheckRow("\(option)2") { lrow in lrow.title = option lrow.selectableValue = option } } formVC.form = form // load the view to test the cells formVC.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) formVC.tableView?.frame = formVC.view.frame } override func tearDown() { super.tearDown() } func testSections() { formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 1, section: 0)) formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 1, section: 1)) formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 3, section: 1)) let value1 = (formVC.form[0] as! SelectableSection>).selectedRow()?.baseValue let value2 = (formVC.form[1] as! SelectableSection>).selectedRows().map {$0.baseValue} XCTAssertEqual(value1 as! String, "Antarctica") XCTAssertTrue(value2.count == 2) XCTAssertEqual((value2[0] as! String), "Atlantic") XCTAssertEqual((value2[1] as! String), "Pacific") //Now deselect One of the multiple selection section and change the value of the first section formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 6, section: 0)) formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 1, section: 1)) let value3 = (formVC.form[0] as! SelectableSection>).selectedRow()?.baseValue let selectedRows = (formVC.form[1] as! SelectableSection>).selectedRows() let value4 = selectedRows.map { $0.baseValue } XCTAssertEqual(value3 as! String, "South America") XCTAssertTrue(value4.count == 1) XCTAssertEqual((value4[0] as! String), "Pacific") } func testDeselectionDisabled() { formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 0, section: 2)) var value1 = (formVC.form[2] as! SelectableSection>).selectedRow()?.baseValue XCTAssertEqual(value1 as? String, "Arctic") // now try deselecting one of each and see that nothing changes. formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 0, section: 2)) value1 = (formVC.form[2] as! SelectableSection>).selectedRow()?.baseValue XCTAssertEqual(value1 as? String, "Arctic") // But we can change the value in the first section formVC.tableView(formVC.tableView!, didSelectRowAt: IndexPath(row: 2, section: 2)) value1 = (formVC.form[2] as! SelectableSection>).selectedRow()?.baseValue XCTAssertEqual(value1 as? String, "Indian") } func testSectionedSections() { let selectorViewController = SelectorViewController>(nibName: nil, bundle: nil) selectorViewController.row = PushRow { row in row.options = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] } enum Hemisphere: Int { case west, east, none } selectorViewController.sectionKeyForValue = { option in switch option { case "Africa", "Asia", "Australia", "Europe": return String(Hemisphere.west.rawValue) case "North America", "South America": return String(Hemisphere.east.rawValue) default: return String(Hemisphere.none.rawValue) } } selectorViewController.sectionHeaderTitleForKey = { key in switch Hemisphere(rawValue: Int(key)!)! { case .west: return "West hemisphere" case .east: return "East hemisphere" case .none: return "" } } selectorViewController.sectionFooterTitleForKey = { key in switch Hemisphere(rawValue: Int(key)!)! { case .west: return "West hemisphere" case .east: return "East hemisphere" case .none: return "" } } selectorViewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) selectorViewController.tableView?.frame = selectorViewController.view.frame let form = selectorViewController.form XCTAssertEqual(form.count, 3) XCTAssertEqual(form[0].count, 4) XCTAssertEqual(form[1].count, 2) XCTAssertEqual(form[2].count, 1) XCTAssertEqual(form[0].header?.title, "West hemisphere") XCTAssertEqual(form[1].header?.title, "East hemisphere") XCTAssertEqual(form[2].header?.title, "") XCTAssertEqual(form[0].footer?.title, "West hemisphere") XCTAssertEqual(form[1].footer?.title, "East hemisphere") XCTAssertEqual(form[2].footer?.title, "") XCTAssertEqual(form[0].compactMap({ ($0 as! ListCheckRow).selectableValue }), ["Africa", "Asia", "Australia", "Europe"]) XCTAssertEqual(form[1].compactMap({ ($0 as! ListCheckRow).selectableValue }), ["North America", "South America"]) XCTAssertEqual(form[2].compactMap({ ($0 as! ListCheckRow).selectableValue }), ["Antarctica"]) } func testLazyOptionsProvider() { let selectorViewController = SelectorViewController>(nibName: nil, bundle: nil) let row = PushRow() selectorViewController.row = row let options = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] let optionsFetched = expectation(description: "Fetched options") row.optionsProvider = .lazy({ form, completion in DispatchQueue.main.async { completion(options) optionsFetched.fulfill() } }) let form = selectorViewController.form XCTAssertEqual(form.count, 0) selectorViewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 3000) selectorViewController.tableView?.frame = selectorViewController.view.frame waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(row.options ?? [], options) XCTAssertEqual(form.count, 1) XCTAssertEqual(form[0].count, options.count) XCTAssertEqual(form[0].compactMap({ ($0 as! ListCheckRow).selectableValue }), options) } } ================================================ FILE: Tests/SetValuesTests.swift ================================================ // SetValuesTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class SetValuesTests: XCTestCase { var form: Form! override func setUp() { super.setUp() form = Form() let section = Section() form +++ section section <<< IntRow("IntRow") section <<< TextRow("TextRow") section <<< ActionSheetRow("ActionSheetRow") section <<< AlertRow("AlertRow") section <<< PushRow("PushRow") } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testSetValues() { let intRowValue: Int = 4 let intRowNilValue: Int? = nil let textRowValue = "textRow value!!!" let actionSheetRowValue = "ActionSheetRow value!!!" let alertRowValue: Int = 33 let pushRowValue: Float = 0.7 form.setValues(["IntRow": intRowValue, "TextRow": textRowValue, "ActionSheetRow": actionSheetRowValue, "AlertRow": alertRowValue, "PushRow": pushRowValue, "No existing tag": 2.0]) let intRow: IntRow? = form.rowBy(tag: "IntRow") XCTAssertEqual(intRow?.value, intRowValue) let textRow: TextRow? = form.rowBy(tag: "TextRow") XCTAssertEqual(textRow?.value, textRowValue) let actionSheetRow: ActionSheetRow? = form.rowBy(tag: "ActionSheetRow") XCTAssertEqual(actionSheetRow?.value, actionSheetRowValue) let alertRow: AlertRow? = form.rowBy(tag: "AlertRow") XCTAssertNotNil(alertRow) XCTAssertEqual(alertRow!.value, alertRowValue) var pushRow: PushRow? = form.rowBy(tag: "PushRow") XCTAssertNotNil(pushRow) XCTAssertEqual(pushRow!.value, pushRowValue) form.setValues(["PushRow": Float(1.0), "No existing tag": 2.0]) pushRow = form.rowBy(tag: "PushRow") XCTAssertNotNil(pushRow) XCTAssertEqual(pushRow!.value, Float(1.0)) form.setValues(["IntRow": intRowNilValue]) XCTAssertNil(intRow?.value) } // The reset value stores the default value of the row. The reset value should not change along with the value property, and when you reset the row value, it should set the value property to the reset value. func testSetAndResetValues() { let intValue: Int = 4 let intDefaultvalue: Int = 0 let stringValue = "String Value!" let stringDefaultValue = "String Default Value!" let floatValue: Float = 0.7 let floatDefaultValue: Float = 1.4 let intRow: IntRow? = form.rowBy(tag: "IntRow") XCTAssertNotNil(intRow) intRow?.resetValue = intDefaultvalue XCTAssertEqual(intRow?.resetValue, intDefaultvalue) intRow?.value = intValue intRow?.resetRowValue() XCTAssertEqual(intRow?.value, intDefaultvalue) let textRow: TextRow? = form.rowBy(tag: "TextRow") XCTAssertNotNil(textRow) textRow?.resetValue = stringDefaultValue XCTAssertEqual(textRow?.resetValue, stringDefaultValue) textRow?.value = stringValue textRow?.resetRowValue() XCTAssertEqual(textRow?.value, stringDefaultValue) let actionSheetRow: ActionSheetRow? = form.rowBy(tag: "ActionSheetRow") XCTAssertNotNil(actionSheetRow) actionSheetRow?.resetValue = stringDefaultValue XCTAssertEqual(actionSheetRow?.resetValue, stringDefaultValue) actionSheetRow?.value = stringValue actionSheetRow?.resetRowValue() XCTAssertEqual(actionSheetRow?.value, stringDefaultValue) let alertRow: AlertRow? = form.rowBy(tag: "AlertRow") XCTAssertNotNil(alertRow) alertRow?.resetValue = intDefaultvalue XCTAssertEqual(alertRow?.resetValue, intDefaultvalue) alertRow?.value = intValue alertRow?.resetRowValue() XCTAssertEqual(alertRow?.value, intDefaultvalue) let pushRow: PushRow? = form.rowBy(tag: "PushRow") XCTAssertNotNil(pushRow) pushRow?.resetValue = floatDefaultValue XCTAssertEqual(pushRow?.resetValue, floatDefaultValue) pushRow?.value = floatValue pushRow?.resetRowValue() XCTAssertEqual(pushRow?.value, floatDefaultValue) } } ================================================ FILE: Tests/ValidationsTests.swift ================================================ // ValidationsTests.swift // Eureka ( https://github.com/xmartlabs/Eureka ) // // Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import XCTest @testable import Eureka class ValidationsTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testadd() { let textRow = TextRow() textRow.add(rule: RuleRequired()) textRow.add(rule: RuleEmail()) XCTAssertEqual(textRow.rules.count, 2) textRow.removeAllRules() XCTAssertEqual(textRow.rules.count, 0) } func testRuleSet() { var ruleSet = RuleSet() ruleSet.add(rule: RuleRequired()) ruleSet.add(rule: RuleEmail()) XCTAssertEqual(ruleSet.rules.count, 2) let textRow = TextRow() textRow.add(ruleSet: ruleSet) XCTAssertEqual(textRow.rules.count, 2) } func testRemoveRules() { let textRow = TextRow() textRow.add(rule: RuleRequired()) textRow.add(rule: RuleEmail()) XCTAssertEqual(textRow.rules.count, 2) textRow.removeAllRules() XCTAssertEqual(textRow.rules.count, 0) var requiredRule = RuleRequired() requiredRule.id = "required_rule_id" textRow.add(rule: requiredRule) textRow.add(rule: RuleEmail()) XCTAssertEqual(textRow.rules.count, 2) textRow.remove(ruleWithIdentifier: "required_rule_id") XCTAssertEqual(textRow.rules.count, 1) } func testBlurred() { let formVC = FormViewController(style: .grouped) let row = TextRow() row.add(rule: RuleRequired()) XCTAssertFalse(row.wasBlurred) formVC.form +++ row formVC.endEditing(of: row.cell) XCTAssertTrue(row.wasBlurred) } func testUsed() { let formVC = FormViewController(style: .grouped) let row = TextRow() row.add(rule: RuleRequired()) XCTAssertFalse(row.wasChanged) row.value = "Hi!" XCTAssertFalse(row.wasChanged) // because it's not added in the form yet formVC.form +++ row row.value = "Eureka!" XCTAssertTrue(row.wasChanged) // because it was added to the form } func testRequired() { let textRow = TextRow() textRow.add(rule: RuleRequired()) textRow.value = nil XCTAssertTrue(textRow.validate().count == 1, "errors collection must contains Required rule error") textRow.value = "Hi1" XCTAssertTrue(textRow.validate().count == 0, "errors collection must not contains Required rule error") } func testRuleEmail() { let emailRule = RuleEmail() XCTAssertNil(emailRule.isValid(value: nil)) XCTAssertNil(emailRule.isValid(value: "")) XCTAssertNil(emailRule.isValid(value: "a@b.com")) XCTAssertNil(emailRule.isValid(value: "a!omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a?omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a#omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a$omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a%omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a'omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a`omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a*omalley@b.com")) XCTAssertNil(emailRule.isValid(value: "a/omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a=omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a~omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a^omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a/omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a{omally@b.com")) XCTAssertNil(emailRule.isValid(value: "a}omally@b.com")) XCTAssertNotNil(emailRule.isValid(value: "abc")) XCTAssertNotNil(emailRule.isValid(value: "abc.com")) XCTAssertNotNil(emailRule.isValid(value: "abc@assa")) XCTAssertNotNil(emailRule.isValid(value: "a&b@b.com")) XCTAssertNotNil(emailRule.isValid(value: "a[omalley@b.com")) XCTAssertNotNil(emailRule.isValid(value: "a]omalley@b.com")) } func testMaxLengthRule() { let maxLengthRule = RuleMaxLength(maxLength: 10) XCTAssertNil(maxLengthRule.isValid(value: nil)) XCTAssertNil(maxLengthRule.isValid(value: "123456789")) XCTAssertNotNil(maxLengthRule.isValid(value:"12345678910")) } func testMinLengthRule() { let minLengthRule = RuleMinLength(minLength: 5) XCTAssertNil(minLengthRule.isValid(value: nil)) XCTAssertNil(minLengthRule.isValid(value: "12345")) XCTAssertNotNil(minLengthRule.isValid(value:"1234")) } func testExactLengthRule() { let exactLengthRule = RuleExactLength(exactLength: 3) XCTAssertNil(exactLengthRule.isValid(value: nil)) XCTAssertNil(exactLengthRule.isValid(value: "123")) XCTAssertNotNil(exactLengthRule.isValid(value:"1234")) } func testRuleURL() { let urlRule = RuleURL() XCTAssertNil(urlRule.isValid(value: nil)) XCTAssertNil(urlRule.isValid(value: URL(string: ""))) XCTAssertNil(urlRule.isValid(value: URL(string: "http://example.com"))) XCTAssertNil(urlRule.isValid(value: URL(string: "http://to.co"))) XCTAssertNil(urlRule.isValid(value: URL(string: "https://example.com/path/to/file.ext?key=value#location"))) XCTAssertNil(urlRule.isValid(value: URL(string: "https://example.com:8080/path/to/file.ext?key=value#location"))) XCTAssertNil(urlRule.isValid(value: URL(string: "https://localhost"))) XCTAssertNil(urlRule.isValid(value: URL(string: "https://localhost:8080"))) XCTAssertNotNil(urlRule.isValid(value: URL(string: "example.com"))) XCTAssertNotNil(urlRule.isValid(value: URL(string: "www.example.com"))) XCTAssertNotNil(urlRule.isValid(value: URL(string: "http://"))) } }