├── .deepsource.toml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── ThunderTable.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── ThunderTable.xcscheme │ └── ThunderTableTests.xcscheme ├── ThunderTable ├── ApplicationLoadingIndicatorManager.swift ├── DeclarativeSection.swift ├── DeclarativeTableViewController.swift ├── DefaultTableViewCell.swift ├── DefaultTableViewCell.xib ├── Extensions.swift ├── GCPlaceholderTextView.m ├── ImageController.swift ├── ImageView.swift ├── IndexPath+SafeSection.swift ├── Info.plist ├── InputDatePickerRow.swift ├── InputDatePickerViewCell.swift ├── InputDatePickerViewCell.xib ├── InputInlineDatePickerViewCell.swift ├── InputInlineDatePickerViewCell.xib ├── InputPickerRow.swift ├── InputPickerViewCell.swift ├── InputPickerViewCell.xib ├── InputSliderRow.swift ├── InputSliderViewCell.swift ├── InputSliderViewCell.xib ├── InputSwitchRow.swift ├── InputSwitchViewCell.swift ├── InputSwitchViewCell.xib ├── InputTableRow.swift ├── InputTextFieldRow.swift ├── InputTextFieldViewCell.swift ├── InputTextFieldViewCell.xib ├── InputTextViewCell.swift ├── InputTextViewCell.xib ├── InputTextViewRow.swift ├── ScrollOffsetManagable.swift ├── ScrollOffsetManager.swift ├── SubtitleTableViewCell.swift ├── SubtitleTableViewCell.xib ├── TableImageViewCell.swift ├── TableImageViewCell.xib ├── TableRow+Actions.swift ├── TableRow.swift ├── TableSection.swift ├── TableViewCell.swift ├── TableViewCell.xib ├── TableViewController+Collection.swift ├── TableViewController.swift ├── Theme.swift ├── Value1TableViewCell.swift ├── Value1TableViewCell.xib ├── Value2TableViewCell.swift └── Value2TableViewCell.xib ├── ThunderTableDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── logo.imageset │ │ ├── Contents.json │ │ ├── Logo.png │ │ ├── Logo@2x.png │ │ └── Logo@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Cells │ ├── CollectionTableViewCell.swift │ ├── CollectionTableViewCell.xib │ ├── CollectionViewCell.swift │ ├── ContactTableViewCell.swift │ └── ContactTableViewCell.xib ├── Info.plist ├── Models │ ├── CNContact+Row.swift │ ├── CNContact+Section.swift │ └── CollectionRow.swift └── ViewController.swift └── ThunderTableTests ├── Info.plist └── ThunderTableTests.swift /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "secrets" 5 | 6 | [[analyzers]] 7 | name = "swift" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | 33 | Carthage/Build 34 | ThunderTable.framework.zip 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | xcode_project: ThunderTable.xcodeproj # path to your xcodeproj folder 3 | osx_image: xcode13 4 | env: 5 | global: 6 | - LC_CTYPE=en_US.UTF-8 7 | - LANG=en_US.UTF-8 8 | matrix: 9 | include: 10 | - xcode_scheme: ThunderTable 11 | xcode_destination: platform=iOS Simulator,OS=15.0,name=iPhone 13 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behaviour that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behaviour by participants include: 24 | 25 | * The use of sexualised language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behaviour and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behaviour. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviours that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported by contacting the project team at [ios@3sidedcube.com](ios@3sidedcube.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # _"I want to contribute to ThunderTable!"_ 2 | 3 | 👋🏻 Hi there, we're super excited that you want to contribute to ThunderTable! ⛈ 4 | 5 | ThunderTable is a project used within [3 SIDED CUBE](3sidedcube.com), built over many years, to power our award-winning iOS apps. It is to be used alongside [ThunderCloud](https://github.com/3sidedcube/ThunderCloud), [ThunderRequest](https://github.com/3sidedcube/ThunderRequest), [ThunderCollection](https://github.com/3sidedcube/ThunderCollection), and [ThunderBasics](https://github.com/3sidedcube/ThunderBasics) to provide a foundation for apps to be built against. 6 | 7 | ## _"How can I contribute?"_ 8 | 9 | Before you get started, please take a look at [code of conduct](CODE_OF_CONDUCT.md), and make sure to adhere to it ☮️. 10 | 11 | Then: 12 | - Clone the project down via your preferred means. 13 | - Run the project! ✨ 14 | 15 | ### _"How can I report bugs?"_ 16 | 17 | - Ensure the bug hasn't already been reported in GitHub under [Issues](https://github.com/3sidedcube/ThunderTable/issues) 18 | - If you can't find a bug which seems to match yours, or you're unsure please create a [new one](https://github.com/3sidedcube/ThunderTable/issues) 19 | 20 | ### _"How can I fix bugs?"_ 21 | 22 | - If you find a bug, please create a PR _and also_ [create an issue](https://github.com/3sidedcube/ThunderTable/issues) so it can be tagged against a specific release. 23 | 24 | ### _"What about fixing whitespacing/formatting or making a cosmetic patch?"_ 25 | 26 | - Please go ahead and fix whitespacing (we indent using spaces, please also do this... even if you don't agree!) 27 | 28 | ### _"What about adding functionality / features?"_ 29 | 30 | Please go ahead and add new functionality and features, however it may be wise to [create an issue](https://github.com/3sidedcube/ThunderTable/issues) (or check the open [issues](https://github.com/3sidedcube/ThunderTable/issues)) first to make sure it isn't already being worked on, and it remains in scope of the ThunderCloud framework. 31 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behaviour 4 | 5 | 6 | 7 | ## Current Behaviour 8 | 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | 15 | ## Steps to Reproduce (for bugs) 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * ThunderRequest version: 30 | * iOS / macOS version: 31 | * Link to your project, if public: 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014 Three Sided Cube Design LTD 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ## Checklist: 29 | 30 | 31 | - [ ] My code follows the code style of this project. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] I have read the **CONTRIBUTING** document. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] All new and existing tests passed. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thunder Table 2 | 3 | [![Build Status](https://travis-ci.org/3sidedcube/ThunderTable.svg)](https://travis-ci.org/3sidedcube/ThunderTable) [![Swift 5.5](http://img.shields.io/badge/swift-5.5-brightgreen.svg)](https://swift.org/blog/swift-5-5-released/) [![Apache 2](https://img.shields.io/badge/license-Apache%202-brightgreen.svg)](LICENSE.md) 4 | 5 | Thunder Table is a useful framework which enables quick and easy creation of table views in iOS, making the process of creating complex tables as simple as a few lines of code; and removing the necessity for having long chains of index paths and if statements. 6 | 7 | ## How It Works 8 | 9 | Thunder table comprises of two main types of objects: 10 | 11 | ### Rows 12 | 13 | Table rows are objects that conform to the `Row` protocol, this protocol has properties such as: title, subtitle and image which are responsible for providing the content to a table view cell. As this is a protocol any object can conform to it, which allows you to simply send an array of model objects to the table view to display your content. 14 | 15 | ### Sections 16 | 17 | Table sections are objects that conform to the `Section` protocol, most of the time you won't need to implement this protocol yourself as Thunder Table has a convenience class `TableSection` which can be used in most circumstances. However you can implement more complex layouts using this protocol on your own classes. 18 | 19 | # Installation 20 | 21 | Setting up your app to use ThunderTable is a simple and quick process. You can choose between a manual installation, or use Carthage. 22 | 23 | ## Carthage 24 | 25 | - Add `github "3sidedcube/ThunderTable" == 2.0.0` to your Cartfile. 26 | - Run `carthage update --platform ios --use-xcframeworks` to fetch the framework. 27 | - Drag `ThunderTable` into your project's _Frameworks and Libraries_ section from the `Carthage/Build` folder (Embed). 28 | - Add the Build Phases script step as defined [here](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos). 29 | 30 | ## Manual 31 | 32 | - Clone as a submodule, or download this repo 33 | - Import ThunderTable.xcproject into your project 34 | - Add ThunderTable.framework to your Embedded Binaries. 35 | - Wherever you want to use ThunderTable use `import ThunderTable`. 36 | 37 | # Code Example 38 | ## A Simple Table View Controller 39 | 40 | Setting up a table view is massively simplified using thunder table, in fact, we can get a simple table view running with just a few lines of code. To create a custom table view we subclass from `TableViewController`. We then set up our table in the `viewDidLoad:` method. Below is the full code for a table view that displays one row, with text, a subtitle, image and handles table selection by pushing another view. 41 | 42 | ``` 43 | import ThunderTable 44 | 45 | class MyTableViewController: TableViewController { 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | 50 | let imageRow = TableRow(title: "Settings", subtitle: "Configure your settings", image: UIImage(named: "settings-cog")) { (row, selected, indexPath, tableView) -> (Void) in 51 | 52 | let settings = SettingsViewController() 53 | self.showDetailViewController(settings, sender: self) 54 | } 55 | 56 | let section = TableSection(rows: [imageRow]) 57 | data = [section] 58 | } 59 | } 60 | ``` 61 | 62 | # Code level documentation 63 | Documentation is available for the entire library in AppleDoc format. This is available in the framework itself or in the [Hosted Version](http://3sidedcube.github.io/iOS-ThunderTable/) 64 | 65 | # License 66 | See [LICENSE.md](LICENSE.md) 67 | 68 | -------------------------------------------------------------------------------- /ThunderTable.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ThunderTable.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ThunderTable.xcodeproj/xcshareddata/xcschemes/ThunderTable.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /ThunderTable.xcodeproj/xcshareddata/xcschemes/ThunderTableTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /ThunderTable/ApplicationLoadingIndicatorManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationLoadingIndicatorManager.swift 3 | // ThunderRequest 4 | // 5 | // Created by Simon Mitchell on 13/04/2016. 6 | // Copyright © 2016 threesidedcube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class ApplicationLoadingIndicatorManager: NSObject { 12 | 13 | public static let sharedManager = ApplicationLoadingIndicatorManager() 14 | private var activityCount = 0 15 | 16 | public func showActivityIndicator() { 17 | 18 | objc_sync_enter(self) 19 | if activityCount == 0 { 20 | OperationQueue.main.addOperation({ 21 | UIApplication.shared.isNetworkActivityIndicatorVisible = true 22 | }) 23 | } 24 | activityCount += 1 25 | objc_sync_exit(self) 26 | } 27 | 28 | public func hideActivityIndicator() { 29 | 30 | objc_sync_enter(self) 31 | activityCount -= 1 32 | if activityCount <= 0 { 33 | OperationQueue.main.addOperation({ 34 | UIApplication.shared.isNetworkActivityIndicatorVisible = false 35 | }) 36 | } 37 | objc_sync_exit(self) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ThunderTable/DeclarativeSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclarativeSection.swift 3 | // ThunderTable 4 | // 5 | // Created by Ben Shutt on 01/02/2021. 6 | // Copyright © 2021 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// A `TableSection` with a header and footer specified 13 | open class DeclarativeSection: TableSection { 14 | 15 | /// `CGFloat` height of the header for `Section` 16 | open var headerHeight: CGFloat 17 | 18 | /// `UIView` view of the header for `Section` 19 | open var headerView: UIView 20 | 21 | /// `CGFloat` height of the footer for `Section` 22 | open var footerHeight: CGFloat 23 | 24 | /// `UIView` view of the footer for `Section` 25 | open var footerView: UIView 26 | 27 | /// Default memberwise initializer 28 | /// 29 | /// - Parameters: 30 | /// - rows: `[Row]` 31 | /// - headerHeight: `CGFloat` 32 | /// - headerView: `UIView` 33 | /// - footerHeight: `CGFloat` 34 | /// - footerView: `UIView` 35 | public init( 36 | rows: [Row], 37 | headerHeight: CGFloat = 0, 38 | headerView: UIView = UIView(), 39 | footerHeight: CGFloat = 0, 40 | footerView: UIView = UIView() 41 | ) { 42 | self.headerHeight = headerHeight 43 | self.headerView = headerView 44 | self.footerHeight = footerHeight 45 | self.footerView = footerView 46 | 47 | super.init(rows: rows) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ThunderTable/DeclarativeTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclarativeTableViewController.swift 3 | // ThunderTable 4 | // 5 | // Created by Ben Shutt on 10/06/2021. 6 | // Copyright © 2021 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// A `TableViewController` which uses `DeclarativeSection`. 13 | /// 14 | /// - Note: If we implement both 15 | /// - `tableView(_:titleForHeaderInSection:)` and 16 | /// - `tableView(_:viewForHeaderInSection:)` 17 | /// then `tableView(_:viewForHeaderInSection:)` takes priority (similarly for footers). 18 | /// 19 | /// As `TableViewController` is already implementing `tableView(_:titleForHeaderInSection:)` 20 | /// it would be a breaking change to add this `DeclarativeSection` logic on there. 21 | /// Hence we create a new subclass. 22 | open class DeclarativeTableViewController: TableViewController { 23 | 24 | // MARK: - Header 25 | 26 | override open func tableView( 27 | _ tableView: UITableView, 28 | heightForHeaderInSection section: Int 29 | ) -> CGFloat { 30 | guard let tableSection = data[section] as? DeclarativeSection else { 31 | return super.tableView(tableView, heightForHeaderInSection: section) 32 | } 33 | return tableSection.headerHeight 34 | } 35 | 36 | override open func tableView( 37 | _ tableView: UITableView, 38 | viewForHeaderInSection section: Int 39 | ) -> UIView? { 40 | guard let tableSection = data[section] as? DeclarativeSection else { 41 | return super.tableView(tableView, viewForHeaderInSection: section) 42 | } 43 | return tableSection.headerView 44 | } 45 | 46 | override open func tableView( 47 | _ tableView: UITableView, 48 | titleForHeaderInSection section: Int 49 | ) -> String? { 50 | return nil 51 | } 52 | 53 | // MARK: - Footer 54 | 55 | override open func tableView( 56 | _ tableView: UITableView, 57 | heightForFooterInSection section: Int 58 | ) -> CGFloat { 59 | guard let tableSection = data[section] as? DeclarativeSection else { 60 | return super.tableView(tableView, heightForFooterInSection: section) 61 | } 62 | return tableSection.footerHeight 63 | } 64 | 65 | override open func tableView( 66 | _ tableView: UITableView, 67 | viewForFooterInSection section: Int 68 | ) -> UIView? { 69 | guard let tableSection = data[section] as? DeclarativeSection else { 70 | return super.tableView(tableView, viewForFooterInSection: section) 71 | } 72 | return tableSection.footerView 73 | } 74 | 75 | override open func tableView( 76 | _ tableView: UITableView, 77 | titleForFooterInSection section: Int 78 | ) -> String? { 79 | return nil 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /ThunderTable/DefaultTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultTableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 11/12/2017. 6 | // Copyright © 2017 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal class DefaultTableViewCell: TableViewCell { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ThunderTable/DefaultTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ThunderTable/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 02/08/2017. 6 | // Copyright © 2017 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element == Section { 12 | 13 | /// Returns the full set of index paths that cover the array of sections 14 | var indexPaths: [IndexPath] { 15 | return enumerated().map { (offset, element) -> [IndexPath] in 16 | let rows = element.rows 17 | return (0.. IndexPath in 18 | return IndexPath(row: row, section: offset) 19 | } 20 | }.flatMap({ $0 }) 21 | } 22 | } 23 | 24 | extension Array : Section { 25 | 26 | public var rows: [Row] { 27 | return filter({ (item) -> Bool in 28 | return item is Row 29 | }) as? [Row] ?? [] 30 | } 31 | 32 | public var editHandler: EditHandler? { 33 | return nil 34 | } 35 | 36 | public var selectionHandler: SelectionHandler? { 37 | return nil 38 | } 39 | } 40 | 41 | extension String: PickerRowDisplayable { 42 | 43 | public var rowTitle: String { 44 | return self 45 | } 46 | 47 | public var value: AnyHashable { 48 | return self 49 | } 50 | } 51 | 52 | extension Int: PickerRowDisplayable { 53 | 54 | public var rowTitle: String { 55 | return "\(self)" 56 | } 57 | 58 | public var value: AnyHashable { 59 | return self 60 | } 61 | } 62 | 63 | extension Double: PickerRowDisplayable { 64 | 65 | public var rowTitle: String { 66 | return "\(self)" 67 | } 68 | 69 | public var value: AnyHashable { 70 | return self 71 | } 72 | } 73 | 74 | extension String : Row { 75 | 76 | public var title: String? { 77 | return self 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ThunderTable/GCPlaceholderTextView.m: -------------------------------------------------------------------------------- 1 | // 2 | // GCPlaceholderTextView.m 3 | // GCLibrary 4 | // 5 | // Created by Guillaume Campagna on 10-11-16. 6 | // Copyright 2010 LittleKiwi. All rights reserved. 7 | // 8 | 9 | #import "GCPlaceholderTextView.h" 10 | 11 | @interface GCPlaceholderTextView () 12 | 13 | @property (nonatomic, strong) UIColor *realTextColor; 14 | @property (unsafe_unretained, nonatomic, readonly) NSString *realText; 15 | 16 | - (void)beginEditing:(NSNotification *)notification; 17 | - (void)endEditing:(NSNotification *)notification; 18 | 19 | @end 20 | 21 | @implementation GCPlaceholderTextView 22 | 23 | @synthesize realTextColor; 24 | @synthesize placeholder; 25 | @synthesize placeholderColor; 26 | 27 | #pragma mark - 28 | #pragma mark Initialisation 29 | 30 | - (id)initWithFrame:(CGRect)frame 31 | { 32 | if ((self = [super initWithFrame:frame])) { 33 | [self awakeFromNib]; 34 | } 35 | 36 | return self; 37 | } 38 | 39 | - (void)awakeFromNib 40 | { 41 | [super awakeFromNib]; 42 | 43 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginEditing:) name:UITextViewTextDidBeginEditingNotification object:self]; 44 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(endEditing:) name:UITextViewTextDidEndEditingNotification object:self]; 45 | 46 | self.realTextColor = self.textColor; 47 | self.placeholderColor = [UIColor lightGrayColor]; 48 | } 49 | 50 | #pragma mark - 51 | #pragma mark Setter/Getters 52 | 53 | - (void)setPlaceholder:(NSString *)aPlaceholder 54 | { 55 | if ([self.realText isEqualToString:placeholder] && ![self isFirstResponder]) { 56 | self.text = aPlaceholder; 57 | } 58 | 59 | if (aPlaceholder != placeholder) { 60 | placeholder = aPlaceholder; 61 | } 62 | 63 | [self endEditing:nil]; 64 | } 65 | 66 | - (void)setPlaceholderColor:(UIColor *)aPlaceholderColor 67 | { 68 | placeholderColor = aPlaceholderColor; 69 | 70 | if ([super.text isEqualToString:self.placeholder]) { 71 | self.textColor = self.placeholderColor; 72 | } 73 | } 74 | 75 | - (NSString *)text 76 | { 77 | NSString *text = [super text]; 78 | 79 | if ([text isEqualToString:self.placeholder]) { 80 | return @""; 81 | } 82 | 83 | return text; 84 | } 85 | 86 | - (void)setText:(NSString *)text 87 | { 88 | if (([text isEqualToString:@""] || text == nil) && ![self isFirstResponder]) { 89 | super.text = self.placeholder; 90 | } else { 91 | super.text = text; 92 | } 93 | 94 | if ([text isEqualToString:self.placeholder] || text == nil) { 95 | self.textColor = self.placeholderColor; 96 | } else { 97 | self.textColor = self.realTextColor; 98 | } 99 | } 100 | 101 | - (NSString *)realText 102 | { 103 | return [super text]; 104 | } 105 | 106 | - (void)beginEditing:(NSNotification *)notification 107 | { 108 | if ([self.realText isEqualToString:self.placeholder]) { 109 | super.text = nil; 110 | self.textColor = self.realTextColor; 111 | } 112 | } 113 | 114 | - (void)endEditing:(NSNotification *)notification 115 | { 116 | if ([self.realText isEqualToString:@""] || self.realText == nil) { 117 | super.text = self.placeholder; 118 | self.textColor = self.placeholderColor; 119 | } 120 | } 121 | 122 | - (void)setTextColor:(UIColor *)textColor 123 | { 124 | if ([self.realText isEqualToString:self.placeholder]) { 125 | if ([textColor isEqual:self.placeholderColor]) { 126 | [super setTextColor:textColor]; 127 | } else { 128 | self.realTextColor = textColor; 129 | } 130 | } else { 131 | self.realTextColor = textColor; 132 | [super setTextColor:textColor]; 133 | } 134 | } 135 | 136 | #pragma mark - 137 | #pragma mark Dealloc 138 | 139 | - (void)dealloc 140 | { 141 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 142 | 143 | } 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /ThunderTable/ImageController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageController.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 06/10/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ImageRequestCompletion = (_ image: UIImage?,_ error: Error?,_ request: ImageRequest?) -> Void 12 | 13 | /// A controller which helps with the loading and caching of images from sources other than the app's bundle 14 | open class ImageController { 15 | 16 | /// The shared singleton of the image controller 17 | public static let shared: ImageController = ImageController() 18 | 19 | /// The operation queue that contains all requests added to a default session 20 | private let defaultRequestQueue: OperationQueue 21 | 22 | /// The url session for loading images on 23 | private let defaultSession: URLSession 24 | 25 | private init() { 26 | 27 | defaultRequestQueue = OperationQueue() 28 | 29 | let defaultConfig = URLSessionConfiguration.default 30 | defaultSession = URLSession(configuration: defaultConfig, delegate: nil, delegateQueue: defaultRequestQueue) 31 | 32 | let urlCache = URLCache(memoryCapacity: 50 * 1024 * 1024, diskCapacity: 500 * 1024 * 1024, diskPath: nil) 33 | defaultSession.configuration.urlCache = urlCache 34 | defaultSession.configuration.requestCachePolicy = .returnCacheDataElseLoad 35 | } 36 | 37 | /// Loads a UIImage for an image at a certain url 38 | /// 39 | /// Once the request has completed the image data will be stored in an NSCache for quicker loading the next time the image from this URL is needed 40 | /// - parameter fromURL: The url to get the image from 41 | /// - parameter completion: A closure called once the image has been pulled from the given URL 42 | /// 43 | /// - returns: Returns the image request that will be performed 44 | open func loadImage(fromURL: URL, completion: ImageRequestCompletion?) -> ImageRequest { 45 | 46 | let request = ImageRequest(url: fromURL) 47 | request.urlRequest.cachePolicy = .returnCacheDataElseLoad 48 | return schedule(request: request, withCompletion: completion) 49 | } 50 | 51 | private func schedule(request: ImageRequest, withCompletion: ImageRequestCompletion?) -> ImageRequest { 52 | 53 | ApplicationLoadingIndicatorManager.sharedManager.showActivityIndicator() 54 | 55 | request.dataTask = defaultSession.dataTask(with: request.urlRequest, completionHandler: { (data, response, error) in 56 | 57 | ApplicationLoadingIndicatorManager.sharedManager.hideActivityIndicator() 58 | 59 | guard let data = data else { 60 | 61 | OperationQueue.main.addOperation { 62 | 63 | if let error = error { 64 | withCompletion?(nil, error, request) 65 | } else { 66 | withCompletion?(nil, ImageControllerError.loadFailed, request) 67 | } 68 | } 69 | return 70 | } 71 | 72 | guard let image = UIImage(data: data) else { 73 | 74 | OperationQueue.main.addOperation { 75 | withCompletion?(nil, ImageControllerError.invalidData, request) 76 | } 77 | return 78 | } 79 | 80 | OperationQueue.main.addOperation { 81 | withCompletion?(image, nil, request) 82 | } 83 | }) 84 | 85 | request.dataTask?.resume() 86 | return request 87 | } 88 | 89 | /// Cancels a currently running request operation 90 | /// 91 | /// - parameter imageRequest: The request to cancel 92 | open func cancel(imageRequest: ImageRequest) { 93 | imageRequest.dataTask?.cancel() 94 | } 95 | } 96 | 97 | public enum ImageControllerError: Error { 98 | case loadFailed 99 | case invalidData 100 | } 101 | 102 | /// A containing representation of a `URLRequest` and the dataTask that was created from it 103 | public class ImageRequest { 104 | 105 | /// The `URLRequest` of the request 106 | var urlRequest: URLRequest 107 | 108 | /// The dataTask that was initialised for the request 109 | var dataTask: URLSessionDataTask? 110 | 111 | init(url: URL) { 112 | urlRequest = URLRequest(url: url) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ThunderTable/IndexPath+SafeSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPath+SafeSection.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 21/02/2022. 6 | // Copyright © 2022 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension IndexPath { 12 | 13 | /// This provides safe access to `self.section` as with VoiceOver on sometimes an `IndexPath` 14 | /// is passed to methods which has an empty array of `indexes` and causes a "trap" in the internals of `IndexPath` 15 | /// - Note: See discussion here: https://developer.apple.com/forums/thread/663787 16 | var safeSection: Int? { 17 | guard count == 2 else { return nil } 18 | return section 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ThunderTable/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ThunderTable/InputDatePickerRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputDatePickerRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 03/01/2018. 6 | // Copyright © 2018 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A row which displays a date picker in the keyboard for the user 12 | /// to select a date and formats the date nicely 13 | open class InputDatePickerRow: InputTableRow { 14 | 15 | /// An direct map enum to `UIDatePickerStyle` so we can provide the `preferredDatePickerStyle` on iOS 16 | /// versions before iOS 13.4 without getting "Stored properties cannot be marked potentially unavailable with '@available'" 17 | /// compiler warning! 18 | public enum PickerStyle: Int { 19 | /// Automatically pick the best style available for the current platform & mode. 20 | case automatic = 0 21 | 22 | /// Use the wheels (UIPickerView) style. Editing occurs inline. 23 | case wheels = 1 24 | 25 | /// Use a compact style for the date picker. Editing occurs in an overlay. 26 | case compact = 2 27 | 28 | /// Use a style for the date picker that allows editing in place. 29 | case inline = 3 30 | 31 | @available (iOS 13.4, *) 32 | var datePickerStyle: UIDatePickerStyle { 33 | switch self { 34 | case .automatic: 35 | return .automatic 36 | case .compact: 37 | return .compact 38 | case .inline: 39 | if #available(iOS 14.0, *) { 40 | return .inline 41 | } else { 42 | return .automatic 43 | } 44 | case .wheels: 45 | return .wheels 46 | } 47 | } 48 | 49 | /// Returns the correct cell class for the style given the mode the date picker is set to 50 | /// - Parameter mode: The date picker mode the date picker is in 51 | /// - Returns: A table view cell class used to render the cell 52 | func cellClass(for mode: UIDatePicker.Mode) -> UITableViewCell.Type { 53 | 54 | // Even old versions of iOS can support `inline` no matter how much of a hot mess it is visually! 55 | if self == .inline { 56 | // Return new class 57 | return InputInlineDatePickerViewCell.self 58 | } 59 | 60 | // New `inline` and `compact` date pickers are only available on iOS 14 61 | guard #available(iOS 14, *) else { 62 | return InputDatePickerViewCell.self 63 | } 64 | 65 | switch mode { 66 | case .countDownTimer: 67 | // countdown timer doesn't have an `inline` or `compact` representation, so we 68 | // keep the traditional `wheels` cell for this case 69 | return InputDatePickerViewCell.self 70 | default: 71 | switch self { 72 | // Automatic, Compact and Inline should all use the inline style cell 73 | case .automatic, .compact, .inline: 74 | return InputInlineDatePickerViewCell.self 75 | // All others (.wheels) should use the original date picker cell 76 | default: 77 | return InputDatePickerViewCell.self 78 | } 79 | } 80 | } 81 | } 82 | 83 | /// The date picker mode for the row 84 | open var mode: UIDatePicker.Mode = .dateAndTime 85 | 86 | /// The preferred date picker style for the row, this will only have an effect on iOS > 13.4 87 | open var preferredDatePickerStyle: PickerStyle = .automatic 88 | 89 | /// The minimum date allowed by the row 90 | open var minimumDate: Date? 91 | 92 | /// The maximum date allowed by the row 93 | open var maximumDate: Date? 94 | 95 | /// The formatter to style the date string 96 | open var dateFormatter: DateFormatter = DateFormatter() 97 | 98 | /// Creates a new row from provided properties 99 | /// 100 | /// - Parameters: 101 | /// - title: The title of the row 102 | /// - mode: The mode of the date picker 103 | /// - id: The unique id for this row 104 | /// - required: Whether the value is required 105 | public init(title: String?, mode: UIDatePicker.Mode = .dateAndTime, id: String, required: Bool) { 106 | 107 | self.mode = mode 108 | 109 | super.init(id: id, required: required) 110 | 111 | self.title = title 112 | 113 | switch mode { 114 | case .date: 115 | dateFormatter.dateStyle = .long 116 | dateFormatter.timeStyle = .none 117 | break 118 | case .time: 119 | dateFormatter.timeStyle = .short 120 | break 121 | case .dateAndTime: 122 | dateFormatter.timeStyle = .short 123 | dateFormatter.dateStyle = .medium 124 | break 125 | case .countDownTimer: 126 | dateFormatter.dateFormat = "'Every' HH 'hours' mm 'minutes'" 127 | break 128 | @unknown default: 129 | fatalError("Unknown `UIDatePicker.Mode` encountered in `InputDatePickerRow` please add support for this new enum value") 130 | } 131 | } 132 | 133 | open override var cellClass: UITableViewCell.Type? { 134 | return preferredDatePickerStyle.cellClass(for: mode) 135 | } 136 | 137 | open override func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 138 | 139 | guard let datePickerCell = cell as? InputDatePickerViewCell else { return } 140 | 141 | super.configure(cell: cell, at: indexPath, in: tableViewController) 142 | 143 | // Targets and selectors 144 | // If we have a text field we use that for targets and selectors 145 | if let textField = datePickerCell.inputTextField { 146 | updateTargetsAndSelectors(for: textField) 147 | // Otherwise we add them to the date picker to make sure we still get callbacks! 148 | } else if let datePicker = datePickerCell.datePicker { 149 | updateTargetsAndSelectors(for: datePicker) 150 | } 151 | 152 | datePickerCell.dateFormatter = dateFormatter 153 | datePickerCell.inputTextField?.delegate = self 154 | datePickerCell.datePicker?.addTarget(self, action: #selector(handleChange(sender:)), for: .valueChanged) 155 | 156 | datePickerCell.datePicker?.addTarget(datePickerCell, action: #selector(datePickerCell.updateInputTextFieldText(sender:)), for: .valueChanged) 157 | 158 | // Setting up date picker 159 | datePickerCell.datePicker?.minimumDate = minimumDate 160 | datePickerCell.datePicker?.maximumDate = maximumDate 161 | datePickerCell.datePicker?.datePickerMode = mode 162 | 163 | if #available(iOS 13.4, *) { 164 | datePickerCell.datePicker?.preferredDatePickerStyle = preferredDatePickerStyle.datePickerStyle 165 | } 166 | 167 | if let textField = datePickerCell.inputTextField { 168 | 169 | if let dateValue = value as? Date { 170 | textField.text = dateFormatter.string(from: dateValue) 171 | } else { 172 | textField.text = nil 173 | } 174 | 175 | } else { 176 | 177 | datePickerCell.datePicker?.date = value as? Date ?? Date() 178 | } 179 | } 180 | 181 | @objc private func handleChange(sender: UIDatePicker) { 182 | set(value: sender.date, sender: sender) 183 | } 184 | } 185 | 186 | extension InputDatePickerRow: UITextFieldDelegate { 187 | 188 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 189 | 190 | textField.resignFirstResponder() 191 | nextHandler?(textField) 192 | return false 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /ThunderTable/InputDatePickerViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextFieldViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A `TableViewCell` subclass with an image, title label, and a text field aligned horizontally 12 | /// 13 | /// This cell subclass allows the user to pick a date using a `UIDatePicker`set as the text field's 14 | /// `inputView`, meaning it shows in-place of the default iOS keyboard 15 | open class InputDatePickerViewCell: TableViewCell { 16 | 17 | public var inputTextField: UITextField? { 18 | return textField 19 | } 20 | 21 | /// The text field allowing the user to enter a date 22 | @IBOutlet weak public var textField: UITextField? 23 | 24 | /// The date picker the user uses to pick the date 25 | @IBOutlet public var datePicker: UIDatePicker? = UIDatePicker() 26 | 27 | /// The date formatter used to format the date displayed in `textField` 28 | public var dateFormatter: DateFormatter? = DateFormatter() 29 | 30 | override open func becomeFirstResponder() -> Bool { 31 | return textField?.becomeFirstResponder() ?? false 32 | } 33 | 34 | override open func resignFirstResponder() -> Bool { 35 | return textField?.resignFirstResponder() ?? false 36 | } 37 | 38 | open override func awakeFromNib() { 39 | 40 | super.awakeFromNib() 41 | 42 | let doneToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: bounds.width, height: 44)) 43 | doneToolbar.isTranslucent = true 44 | doneToolbar.barTintColor = .white 45 | doneToolbar.tintColor = ThemeManager.shared.theme.mainColor 46 | 47 | doneToolbar.items = [ 48 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), 49 | UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDone(sender:))) 50 | ] 51 | 52 | textField?.inputView = datePicker 53 | textField?.inputAccessoryView = doneToolbar 54 | } 55 | 56 | @objc private func handleDone(sender: UIBarButtonItem) { 57 | textField?.resignFirstResponder() 58 | } 59 | 60 | @objc public func updateInputTextFieldText(sender: UIDatePicker) { 61 | textField?.text = dateFormatter?.string(from: sender.date) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ThunderTable/InputDatePickerViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ThunderTable/InputInlineDatePickerViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputInlineDatePickerViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2020. 6 | // Copyright © 2020 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// An `InputDatePickerViewCell` subclass with an image, title label, and a date picker aligned horizontally 12 | /// 13 | /// This cell subclass allows the user to pick a date using a `UIDatePicker` embedded "inline" 14 | /// as one of the cell's subviews. This allows for use of 15 | open class InputInlineDatePickerViewCell: InputDatePickerViewCell { 16 | 17 | override open func becomeFirstResponder() -> Bool { 18 | return datePicker?.becomeFirstResponder() ?? false 19 | } 20 | 21 | override open func resignFirstResponder() -> Bool { 22 | return datePicker?.resignFirstResponder() ?? false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ThunderTable/InputInlineDatePickerViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ThunderTable/InputPickerRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputPickerRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/02/2018. 6 | // Copyright © 2018 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A protocol which can be used to represent a component in a `UIPickerView` 12 | public protocol PickerComponentDisplayable { 13 | 14 | /// The items to be displayed in the component 15 | var items: [PickerRowDisplayable] { get } 16 | } 17 | 18 | /// A helper class for ease of use of `PickerComponentDisplayable` 19 | open class PickerComponent: PickerComponentDisplayable { 20 | 21 | /// The items to be displayed in the component 22 | open var items: [PickerRowDisplayable] 23 | 24 | public init(items: [PickerRowDisplayable]) { 25 | self.items = items 26 | } 27 | } 28 | 29 | /// A protocol which can be used to provide rows to a `UIPickerView` component 30 | public protocol PickerRowDisplayable { 31 | 32 | /// The value that this row represents, doesn't have to be the same as title 33 | var value: AnyHashable { get } 34 | 35 | /// The title to display on this row 36 | var rowTitle: String { get } 37 | 38 | /// The attributed title to display on this row (Optional) 39 | var attributedTitle: NSAttributedString? { get } 40 | } 41 | 42 | public extension PickerRowDisplayable { 43 | 44 | var attributedTitle: NSAttributedString? { get { return nil } } 45 | } 46 | 47 | /// A helper class for ease of use of `PickerRowDisplayable` 48 | open class PickerRow: PickerRowDisplayable { 49 | 50 | /// The value that this row represents, doesn't have to be the same as title 51 | open var value: AnyHashable 52 | 53 | /// The title to display on this row 54 | open var rowTitle: String 55 | 56 | /// Creates a new instance with a title and value 57 | /// 58 | /// - Parameters: 59 | /// - title: The title to display in the `UIPickerView` 60 | /// - value: The value that the row represents 61 | public init(title: String, value: AnyHashable) { 62 | self.rowTitle = title 63 | self.value = value 64 | } 65 | } 66 | 67 | /// A row which displays a picker in the keyboard for the user 68 | /// to select from a set of defined options 69 | open class InputPickerRow: InputTableRow { 70 | 71 | /// The components to display in the picker 72 | open var components: [PickerComponentDisplayable] 73 | 74 | /// A closure to format the value of the row 75 | open var formatter: ((_ value: [AnyHashable]) -> String) 76 | 77 | /// Creates a new row from provided properties 78 | /// 79 | /// - Parameters: 80 | /// - title: The title of the row 81 | /// - mode: The mode of the date picker 82 | /// - id: The unique id for this row 83 | /// - required: Whether the value is required 84 | public init(title: String?, components: [PickerComponentDisplayable], formatter: @escaping ((_ value: [AnyHashable]) -> String), id: String, required: Bool) { 85 | 86 | self.formatter = formatter 87 | self.components = components 88 | super.init(id: id, required: required) 89 | self.title = title 90 | } 91 | 92 | open override var cellClass: UITableViewCell.Type? { 93 | return InputPickerViewCell.self 94 | } 95 | 96 | private var cellTextField: UITextField? 97 | 98 | open override func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 99 | 100 | super.configure(cell: cell, at: indexPath, in: tableViewController) 101 | 102 | guard let pickerCell = cell as? InputPickerViewCell else { return } 103 | 104 | // Targets and selectors 105 | updateTargetsAndSelectors(for: pickerCell.textField) 106 | pickerCell.textField.delegate = self 107 | pickerCell.picker.delegate = self 108 | pickerCell.picker.dataSource = self 109 | 110 | cellTextField = pickerCell.textField 111 | 112 | guard let values = value as? [AnyHashable] else { return } 113 | 114 | for (i, component) in components.enumerated() { 115 | 116 | guard i < values.count else { return } 117 | let rowValue = values[i] 118 | 119 | guard let selectedIndex = component.items.firstIndex(where: { 120 | $0.value == rowValue 121 | }) else { return } 122 | 123 | pickerCell.picker.selectRow(selectedIndex, inComponent: i, animated: false) 124 | } 125 | 126 | // Setting up the picker 127 | 128 | if let arrayValue = value as? [AnyHashable] { 129 | pickerCell.textField.text = formatter(arrayValue) 130 | } else { 131 | let arrayValue = components.compactMap({ $0.items.first?.value }) 132 | pickerCell.textField.text = formatter(arrayValue) 133 | } 134 | } 135 | } 136 | 137 | extension InputPickerRow: UITextFieldDelegate { 138 | 139 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 140 | 141 | textField.resignFirstResponder() 142 | nextHandler?(textField) 143 | return false 144 | } 145 | } 146 | 147 | extension InputPickerRow: UIPickerViewDelegate { 148 | 149 | public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 150 | return components[component].items[row].rowTitle 151 | } 152 | 153 | public func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { 154 | return components[component].items[row].attributedTitle 155 | } 156 | 157 | public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 158 | 159 | var values: [AnyHashable] = [] 160 | 161 | for (i, component) in components.enumerated() { 162 | let selectedIndex = pickerView.selectedRow(inComponent: i) 163 | guard selectedIndex < component.items.count else { continue } 164 | let value = component.items[selectedIndex].value 165 | values.append(value) 166 | } 167 | 168 | self.set(value: values, sender: nil) 169 | cellTextField?.text = formatter(values) 170 | } 171 | } 172 | 173 | extension InputPickerRow: UIPickerViewDataSource { 174 | 175 | public func numberOfComponents(in pickerView: UIPickerView) -> Int { 176 | return components.count 177 | } 178 | 179 | public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 180 | return components[component].items.count 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /ThunderTable/InputPickerViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextFieldViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A subclass of TableViewCell which displays a title, and field editable 12 | /// by selecting values from a `UIPickerView`. 13 | open class InputPickerViewCell: TableViewCell { 14 | 15 | @IBOutlet weak public var textField: UITextField! 16 | 17 | public var picker = UIPickerView() 18 | 19 | var formatter: ((_ picker: UIPickerView) -> String)? 20 | 21 | override open func becomeFirstResponder() -> Bool { 22 | return textField.becomeFirstResponder() 23 | } 24 | 25 | override open func resignFirstResponder() -> Bool { 26 | return textField.resignFirstResponder() 27 | } 28 | 29 | open override func awakeFromNib() { 30 | 31 | super.awakeFromNib() 32 | 33 | let doneToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: bounds.width, height: 44)) 34 | doneToolbar.isTranslucent = true 35 | doneToolbar.barTintColor = .white 36 | doneToolbar.tintColor = ThemeManager.shared.theme.mainColor 37 | 38 | doneToolbar.items = [ 39 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), 40 | UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDone(sender:))) 41 | ] 42 | 43 | textField.inputView = picker 44 | textField.inputAccessoryView = doneToolbar 45 | } 46 | 47 | @objc private func handleDone(sender: UIBarButtonItem) { 48 | textField.resignFirstResponder() 49 | } 50 | 51 | @objc func updateLabel(sender: UIPickerView) { 52 | textField.text = formatter?(sender) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ThunderTable/InputPickerViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ThunderTable/InputSliderRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputSliderRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A row which displays a `UISlider` and label displaying it's value 12 | /// 13 | /// - Important: If you use the valueChangeHandler on this row, and want to use the 14 | /// sender part of that closure, make sure to use `correctedValue` rather than `value` 15 | /// due to the custom implementation of UISlider allowing for non-integer steps. 16 | /// This means `sender.value !== value` in that closure! 17 | open class InputSliderRow: InputTableRow { 18 | 19 | override open var cellClass: UITableViewCell.Type? { 20 | return InputSliderViewCell.self 21 | } 22 | 23 | // Defines whether to group the label and slider as a single accessibility element 24 | /// - Note: Defaults to true! 25 | public var accessibilityGroupLabelsAndSlider = true 26 | 27 | /// The minimum value that can be chosen by the slider 28 | open var minValue: Float 29 | 30 | /// The maximum value that can be chosen by the slider 31 | open var maxValue: Float 32 | 33 | /// The interval at which the slider increments or decrements 34 | open var interval: Float 35 | 36 | /// A closure that will be called in order to format the accessibility value for the slider. 37 | /// This can be used to for example make the accessibility value read "2 miles" rather than simply "2" 38 | open var accessibilityValueFormatter: ((Float) -> String)? 39 | 40 | public init(title: String?, minValue: Float, maxValue: Float, id: String, required: Bool) { 41 | 42 | self.minValue = minValue 43 | self.maxValue = maxValue 44 | self.interval = 1.0 45 | 46 | super.init(id: id, required: required) 47 | 48 | self.title = title 49 | } 50 | 51 | override open func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 52 | 53 | guard let sliderCell = cell as? InputSliderViewCell else { return } 54 | 55 | super.configure(cell: cell, at: indexPath, in: tableViewController) 56 | 57 | updateTargetsAndSelectors(for: sliderCell.slider) 58 | sliderCell.accessibilityValueFormatter = accessibilityValueFormatter 59 | sliderCell.slider.addTarget(self, action: #selector(handleChange(sender:)), for: .valueChanged) 60 | sliderCell.slider.addTarget(sliderCell, action: #selector(InputSliderViewCell.updateLabel(sender:)), for: .valueChanged) 61 | 62 | sliderCell.cellTextLabel?.isHidden = title == nil 63 | 64 | sliderCell.accessibilityGroupLabelsAndSlider = accessibilityGroupLabelsAndSlider 65 | 66 | sliderCell.slider.interval = interval 67 | sliderCell.slider.minimumValue = minValue 68 | sliderCell.slider.maximumValue = maxValue 69 | 70 | sliderCell.valueLabel.backgroundColor = ThemeManager.shared.theme.mainColor 71 | sliderCell.valueLabel.textColor = .white 72 | sliderCell.valueLabel.layer.cornerRadius = 5 73 | sliderCell.valueLabel.layer.masksToBounds = true 74 | 75 | if let doubleValue = value as? Float { 76 | sliderCell.slider.value = doubleValue 77 | } else { 78 | sliderCell.slider.value = minValue 79 | } 80 | 81 | if let value = value { 82 | sliderCell.valueLabel.text = "\(value)" 83 | } else { 84 | sliderCell.valueLabel.text = "\(minValue)" 85 | } 86 | } 87 | 88 | @objc func handleChange(sender: IntervalSlider) { 89 | set(value: sender.correctedValue, sender: sender) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ThunderTable/InputSliderViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputSliderViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable internal class PaddedLabel: UILabel { 12 | 13 | /** 14 | The left edge insets of the label 15 | */ 16 | @IBInspectable internal var leftInset: CGFloat = 0 17 | 18 | /** 19 | The right edge insets of the label 20 | */ 21 | @IBInspectable internal var rightInset: CGFloat = 0 22 | 23 | /** 24 | The top edge insets of the label 25 | */ 26 | @IBInspectable internal var topInset: CGFloat = 0 27 | 28 | /** 29 | The bottom edge insets of the label 30 | */ 31 | @IBInspectable internal var bottomInset: CGFloat = 0 32 | 33 | private var edgeInsets: UIEdgeInsets { 34 | get { 35 | return UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) 36 | } 37 | } 38 | 39 | override func drawText(in rect: CGRect) { 40 | super.drawText(in: rect.inset(by: edgeInsets)) 41 | } 42 | 43 | override func sizeThatFits(_ size: CGSize) -> CGSize { 44 | 45 | var adjSize = super.sizeThatFits(size) 46 | adjSize.width += leftInset + rightInset 47 | adjSize.height += topInset + bottomInset 48 | 49 | return adjSize 50 | } 51 | 52 | override var intrinsicContentSize: CGSize { 53 | var superSize = super.intrinsicContentSize 54 | superSize.width += leftInset + rightInset 55 | superSize.height += topInset + bottomInset 56 | return superSize 57 | } 58 | } 59 | 60 | /// A subclass of `UISlider` which allows custom intervals between maximumValue and minimumValue. 61 | /// 62 | /// This is achieved by providing the conversion variable, `correctedValue` which should be used in place of `value` 63 | /// - Important: For intervals other than 1, `value != correctedValue` and `correctedValue` should be used to get 64 | /// the actual value the user has selected! 65 | public class IntervalSlider: UISlider { 66 | 67 | /// A custom interval that the slider should jump at 68 | /// 69 | /// - Warning: Setting this after the user updates the slider is currently not supported, and will break 70 | /// the values the slider returns 71 | public var interval: Float = 1 72 | 73 | /// Should be used instead of `value`, returns the usable value taking into account the custom interval 74 | public var correctedValue: Float { 75 | 76 | var finalValue = value 77 | let tempValue = fabsf(fmodf(value, interval)); 78 | 79 | //if the remainder is greater than or equal to the half of the interval then return the higher interval, otherwise, return the lower interval 80 | if tempValue >= (interval / 2.0) { 81 | finalValue = value - tempValue + interval 82 | } else { 83 | finalValue = value - tempValue 84 | } 85 | 86 | return finalValue 87 | } 88 | 89 | public override func accessibilityIncrement() { 90 | let nextValue = correctedValue + interval 91 | let correctedNextValue = min(nextValue, maximumValue) 92 | self.setValue(correctedNextValue, animated: true) 93 | sendActions(for: .valueChanged) 94 | } 95 | 96 | public override func accessibilityDecrement() { 97 | let nextValue = correctedValue - interval 98 | let correctedNextValue = max(nextValue, minimumValue) 99 | self.setValue(correctedNextValue, animated: true) 100 | sendActions(for: .valueChanged) 101 | } 102 | 103 | public override var accessibilityValue: String? { 104 | get { 105 | return "\(correctedValue)" 106 | } 107 | set { } 108 | } 109 | } 110 | 111 | open class InputSliderViewCell: TableViewCell { 112 | 113 | @IBOutlet weak public var valueLabel: UILabel! 114 | 115 | @IBOutlet weak public var slider: IntervalSlider! 116 | 117 | /// A closure that will be called in order to format the accessibility value for the slider. 118 | /// This can be used to for example make the accessibility value read "2 miles" rather than simply "2" 119 | public var accessibilityValueFormatter: ((Float) -> String)? 120 | 121 | // Defines whether to group the label and slider as a single accessibility element 122 | /// - Note: Defaults to true! 123 | public var accessibilityGroupLabelsAndSlider = true { 124 | didSet { 125 | isAccessibilityElement = accessibilityGroupLabelsAndSlider 126 | } 127 | } 128 | 129 | @objc open func updateLabel(sender: IntervalSlider) { 130 | valueLabel.text = "\(sender.correctedValue)" 131 | } 132 | 133 | open override var accessibilityTraits: UIAccessibilityTraits { 134 | get { 135 | return slider.accessibilityTraits 136 | } 137 | set { 138 | 139 | } 140 | } 141 | 142 | open override var accessibilityValue: String? { 143 | get { 144 | return accessibilityValueFormatter?(slider.correctedValue) ?? slider.accessibilityValue 145 | } 146 | set { } 147 | } 148 | 149 | open override var accessibilityLabel: String? { 150 | get { 151 | // We don't return the value label's text here as that is covered by `accessibilityLabel` 152 | // and so would be read twice if we put it in this array 153 | return [ 154 | cellTextLabel?.accessibilityLabel ?? cellTextLabel?.text, 155 | cellDetailLabel?.accessibilityLabel ?? cellDetailLabel?.text 156 | ].compactMap({ 157 | guard let text = $0, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } 158 | return text 159 | }).joined(separator: ",") 160 | } 161 | set { } 162 | } 163 | 164 | open override func accessibilityDecrement() { 165 | slider.accessibilityDecrement() 166 | } 167 | 168 | open override func accessibilityIncrement() { 169 | slider.accessibilityIncrement() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ThunderTable/InputSliderViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 42 | 43 | 44 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /ThunderTable/InputSwitchRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputSwitchRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// An input row which provides a title, subtitle, and `UISwitch` in it's UI 12 | open class InputSwitchRow: InputTableRow { 13 | 14 | public var isEnabled = true 15 | 16 | public var isUserInteractionEnabled = true 17 | 18 | /// Defines whether to group the label and switch as a single accessibility element 19 | /// - Note: Defaults to true! 20 | public var accessibilityGroupLabelsAndSwitch: Bool = true 21 | 22 | override open var cellClass: UITableViewCell.Type? { 23 | return InputSwitchViewCell.self 24 | } 25 | 26 | public init(title: String?, subtitle: String?, id: String, image: UIImage? = nil) { 27 | 28 | super.init(id: id, required: false) 29 | 30 | isAccessibilityElement = true 31 | self.title = title 32 | self.subtitle = subtitle 33 | self.image = image 34 | } 35 | 36 | override open func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 37 | 38 | guard let switchCell = cell as? InputSwitchViewCell else { return } 39 | 40 | super.configure(cell: cell, at: indexPath, in: tableViewController) 41 | 42 | updateTargetsAndSelectors(for: switchCell.switch) 43 | switchCell.switch.addTarget(self, action: #selector(handleChange(sender:)), for: .valueChanged) 44 | 45 | if let boolValue = value as? Bool { 46 | switchCell.switch.isOn = boolValue 47 | } else { 48 | switchCell.switch.isOn = false 49 | } 50 | 51 | switchCell.accessibilityGroupLabelsAndSwitch = accessibilityGroupLabelsAndSwitch 52 | 53 | switchCell.switch.isEnabled = isEnabled 54 | switchCell.switch.isUserInteractionEnabled = isUserInteractionEnabled 55 | 56 | switchCell.cellTextLabel?.isHidden = title == nil 57 | switchCell.cellDetailLabel?.isHidden = subtitle == nil 58 | } 59 | 60 | @objc func handleChange(sender: UISwitch) { 61 | set(value: sender.isOn, sender: sender) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ThunderTable/InputSwitchViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputSwitchViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class InputSwitchViewCell: TableViewCell { 12 | 13 | @IBOutlet weak public var `switch`: UISwitch! 14 | 15 | /// Defines whether to group the label and switch as a single accessibility element 16 | /// - Note: Defaults to true! 17 | public var accessibilityGroupLabelsAndSwitch = true { 18 | didSet { 19 | isAccessibilityElement = accessibilityGroupLabelsAndSwitch 20 | } 21 | } 22 | 23 | //MARK: - Accessibility 24 | 25 | open override var accessibilityTraits: UIAccessibilityTraits { 26 | get { 27 | return `switch`.accessibilityTraits 28 | } 29 | set { } 30 | } 31 | 32 | open override var accessibilityValue: String? { 33 | get { 34 | return `switch`.accessibilityValue 35 | } 36 | set { } 37 | } 38 | 39 | open override func accessibilityActivate() -> Bool { 40 | self.switch.setOn(!self.switch.isOn, animated: true) 41 | self.switch.sendActions(for: .valueChanged) 42 | UIAccessibility.post(notification: .announcement, argument: self.switch.accessibilityValue) 43 | return true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ThunderTable/InputSwitchViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ThunderTable/InputTextFieldRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextFieldRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ValidCharacterHandler = (_ string: String) -> Bool 12 | 13 | open class InputTextFieldRow: InputTableRow { 14 | 15 | open var placeholder: String? 16 | 17 | open var keyboardType: UIKeyboardType 18 | 19 | open var returnKeyType: UIReturnKeyType 20 | 21 | open var allowCharacterHandler: ValidCharacterHandler? 22 | 23 | open var isSecure: Bool = false 24 | 25 | open var autocorrectionType: UITextAutocorrectionType = .default 26 | 27 | open var autocapitalizationType: UITextAutocapitalizationType = .none 28 | 29 | override open var cellClass: UITableViewCell.Type? { 30 | return InputTextFieldViewCell.self 31 | } 32 | 33 | public init(title: String?, placeholder: String?, id: String, required: Bool, keyboardType: UIKeyboardType = .default, returnKeyType: UIReturnKeyType = .default) { 34 | 35 | self.keyboardType = keyboardType 36 | self.returnKeyType = returnKeyType 37 | 38 | super.init(id: id, required: required) 39 | 40 | self.title = title 41 | self.placeholder = placeholder 42 | self.id = id 43 | } 44 | 45 | override open func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 46 | 47 | guard let textCell = cell as? InputTextFieldViewCell else { return } 48 | 49 | super.configure(cell: cell, at: indexPath, in: tableViewController) 50 | 51 | textCell.textField.placeholder = placeholder 52 | textCell.textField.keyboardType = keyboardType 53 | textCell.textField.returnKeyType = returnKeyType 54 | textCell.textField.isSecureTextEntry = isSecure 55 | textCell.textField.autocorrectionType = autocorrectionType 56 | textCell.textField.autocapitalizationType = autocapitalizationType 57 | 58 | textCell.textField.text = value as? String 59 | textCell.textField.delegate = self 60 | 61 | updateTargetsAndSelectors(for: textCell.textField) 62 | 63 | if let stringValue = value as? String { 64 | textCell.textField.text = stringValue 65 | } else if let value = value, value as? NSNull == nil { 66 | textCell.textField.text = String(describing: value) 67 | } else { 68 | textCell.textField.text = nil 69 | } 70 | 71 | textCell.cellTextLabel?.isHidden = title == nil 72 | } 73 | } 74 | 75 | extension InputTextFieldRow: UITextFieldDelegate { 76 | 77 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 78 | 79 | // If our allow handler says no, then ignore the change that was made 80 | if let allowHandler = allowCharacterHandler, !allowHandler(string) { 81 | return false 82 | } 83 | 84 | var editedString = textField.text as NSString? 85 | editedString = editedString?.replacingCharacters(in: range, with: string) as NSString? 86 | 87 | set(value: editedString, sender: textField) 88 | 89 | return true 90 | } 91 | 92 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 93 | 94 | textField.resignFirstResponder() 95 | _nextHandler?(textField) 96 | return false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ThunderTable/InputTextFieldViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextFieldViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class InputTextFieldViewCell: TableViewCell { 12 | 13 | @IBOutlet weak public var textField: UITextField! 14 | 15 | override open func becomeFirstResponder() -> Bool { 16 | return textField.becomeFirstResponder() 17 | } 18 | 19 | override open func resignFirstResponder() -> Bool { 20 | return textField.resignFirstResponder() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ThunderTable/InputTextFieldViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /ThunderTable/InputTextViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 16/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class InputTextViewCell: TableViewCell { 12 | 13 | @IBOutlet weak public var textView: UITextView! 14 | 15 | @IBOutlet weak public var textViewHeightConstraint: NSLayoutConstraint! 16 | 17 | override open func becomeFirstResponder() -> Bool { 18 | return textView.becomeFirstResponder() 19 | } 20 | 21 | override open func resignFirstResponder() -> Bool { 22 | return textView.resignFirstResponder() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ThunderTable/InputTextViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ThunderTable/InputTextViewRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputTextFieldRow.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class InputTextViewRow: InputTableRow { 12 | 13 | open var placeholder: String? 14 | 15 | open var keyboardType: UIKeyboardType 16 | 17 | open var returnKeyType: UIReturnKeyType 18 | 19 | open var autocorrectionType: UITextAutocorrectionType = .default 20 | 21 | open var autocapitalizationType: UITextAutocapitalizationType = .none 22 | 23 | open var textViewHeight: CGFloat = 60 24 | 25 | open var isSecure: Bool = false 26 | 27 | override open var cellClass: UITableViewCell.Type? { 28 | return InputTextViewCell.self 29 | } 30 | 31 | public init(title: String?, placeholder: String?, id: String, required: Bool, keyboardType: UIKeyboardType = .default, returnKeyType: UIReturnKeyType = .default) { 32 | 33 | self.keyboardType = keyboardType 34 | self.returnKeyType = returnKeyType 35 | 36 | super.init(id: id, required: required) 37 | 38 | self.title = title 39 | self.placeholder = placeholder 40 | self.id = id 41 | } 42 | 43 | override open func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 44 | 45 | guard let textCell = cell as? InputTextViewCell else { return } 46 | 47 | super.configure(cell: cell, at: indexPath, in: tableViewController) 48 | 49 | textCell.cellTextLabel?.isHidden = title == nil 50 | 51 | textCell.textViewHeightConstraint.constant = textViewHeight 52 | textCell.textView.keyboardType = keyboardType 53 | textCell.textView.returnKeyType = returnKeyType 54 | textCell.textView.isSecureTextEntry = isSecure 55 | textCell.textView.autocorrectionType = autocorrectionType 56 | textCell.textView.autocapitalizationType = autocapitalizationType 57 | 58 | textCell.textView.delegate = self 59 | 60 | if let stringValue = value as? String { 61 | 62 | textCell.textView.text = stringValue 63 | textCell.textView.textColor = ThemeManager.shared.theme.cellTitleColor 64 | 65 | } else if let value = value, value as? NSNull == nil { 66 | 67 | textCell.textView.textColor = ThemeManager.shared.theme.cellTitleColor 68 | textCell.textView.text = String(describing: value) 69 | 70 | } else { 71 | 72 | textCell.textView.text = placeholder 73 | if #available(iOS 13.0, *) { 74 | textCell.textView.textColor = .placeholderText 75 | } else { 76 | textCell.textView.textColor = .lightGray 77 | } 78 | } 79 | } 80 | } 81 | 82 | extension InputTextViewRow: UITextViewDelegate { 83 | 84 | public func textViewDidBeginEditing(_ textView: UITextView) { 85 | 86 | if let stringValue = value as? String { 87 | 88 | textView.text = stringValue 89 | textView.textColor = ThemeManager.shared.theme.cellTitleColor 90 | 91 | } else if let value = value, value as? NSNull == nil { 92 | 93 | textView.text = String(describing: value) 94 | textView.textColor = ThemeManager.shared.theme.cellTitleColor 95 | 96 | } else { 97 | 98 | textView.text = nil 99 | textView.textColor = ThemeManager.shared.theme.cellTitleColor 100 | } 101 | } 102 | 103 | public func textViewDidEndEditing(_ textView: UITextView) { 104 | 105 | if textView.text.isEmpty { 106 | textView.text = placeholder 107 | if #available(iOS 13.0, *) { 108 | textView.textColor = .placeholderText 109 | } else { 110 | textView.textColor = .lightGray 111 | } 112 | } 113 | } 114 | 115 | public func textViewDidChange(_ textView: UITextView) { 116 | set(value: textView.text, sender: nil) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /ThunderTable/ScrollOffsetManagable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollDelegatable.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 02/09/2020. 6 | // Copyright © 2020 threesidedcube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A simpler version of `UIScrollViewDelegate` so we don't intefere 12 | /// with users `UIScrollViewDelegate` implementations 13 | public protocol ScrollOffsetDelegate: AnyObject { 14 | 15 | /// Called when the content offset of the scroll view changes due to `scrollViewDidScroll` 16 | /// - Parameters: 17 | /// - scrollable: The scrollable that the change was for 18 | func scrollViewDidChangeContentOffset(_ scrollable: ScrollOffsetManagable) 19 | } 20 | 21 | /// A protocol implemented to allow control and listening to scroll view offset changes 22 | /// by `ScrollOfffsetManager` 23 | public protocol ScrollOffsetManagable: AnyObject { 24 | 25 | /// The scroll view that is controllable on the object 26 | var scrollView: UIScrollView? { get } 27 | 28 | /// The delegate to have scroll view delegate methods passed to, this should be a `weak` 29 | /// property to avoid retain cycles. 30 | /// - Warning: It is your job to call the method on this delegate from your scrollViewDidScroll method. 31 | var scrollDelegate: ScrollOffsetDelegate? { get set } 32 | 33 | /// An identifier used by `ScrollOffsetManager` 34 | var identifier: AnyHashable? { get set } 35 | } 36 | -------------------------------------------------------------------------------- /ThunderTable/ScrollOffsetManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollOffsetManager.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 02/09/2020. 6 | // Copyright © 2020 threesidedcube. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import Foundation 11 | 12 | /// Scroll offset manager manages caching scroll offsets for UI in any scenario where 13 | /// scroll views may be re-used and so we need to store their current offset in memory 14 | /// to avoid re-use issues. 15 | class ScrollOffsetManager { 16 | 17 | /// Internal cache for scroll view offsets 18 | private var offsetMap: [AnyHashable : CGPoint] = [:] 19 | 20 | /// Registers the scrollable to the manager for listening for scroll offset changes 21 | /// - Parameters: 22 | /// - scrollable: The scrollable to register 23 | func register( 24 | scrollable: ScrollOffsetManagable 25 | ) { 26 | scrollable.scrollDelegate = self 27 | } 28 | 29 | /// Caches the offset for the given scrollable 30 | /// - Parameters: 31 | /// - scrollable: The scrollable to update the content offset for 32 | func updateCachedOffset(scrollable: ScrollOffsetManagable) { 33 | guard let identifier = scrollable.identifier else { return } 34 | offsetMap[identifier] = scrollable.scrollView?.contentOffset 35 | } 36 | 37 | /// Sets the content offset on the given scrollable from the internal cache 38 | /// - Parameters: 39 | /// - scrollable: The scrollable to adjust the content offset on 40 | /// - animated: Whether the transition should animate 41 | /// - fallback: A fallback to set the content offset to if there is no cached value 42 | func setScrollOffset( 43 | _ scrollable: ScrollOffsetManagable, 44 | animated: Bool = false, 45 | fallback: CGPoint? = nil 46 | ) { 47 | guard let identifier = scrollable.identifier else { return } 48 | guard let newOffset = offsetMap[identifier] ?? fallback else { return } 49 | 50 | guard let scrollView = scrollable.scrollView else { return } 51 | 52 | // Disable then re-enable scroll indicators otherwise calling setContentOffset flashes the scroll indicators 53 | let preShowVerticalScrollIndicator = scrollView.showsVerticalScrollIndicator 54 | let preShowHorizontalScrollIndicator = scrollView.showsHorizontalScrollIndicator 55 | 56 | scrollView.showsVerticalScrollIndicator = false 57 | scrollView.showsHorizontalScrollIndicator = false 58 | 59 | scrollView.setContentOffset(newOffset, animated: animated) 60 | 61 | scrollView.showsVerticalScrollIndicator = preShowVerticalScrollIndicator 62 | scrollView.showsHorizontalScrollIndicator = preShowHorizontalScrollIndicator 63 | } 64 | 65 | /// Resets all content offsets by removing them from the offset map 66 | func resetAllOffsets() { 67 | offsetMap = [:] 68 | } 69 | } 70 | 71 | extension ScrollOffsetManager: ScrollOffsetDelegate { 72 | 73 | func scrollViewDidChangeContentOffset(_ scrollable: ScrollOffsetManagable) { 74 | updateCachedOffset(scrollable: scrollable) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ThunderTable/SubtitleTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubtitleTableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 11/12/2017. 6 | // Copyright © 2017 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal class SubtitleTableViewCell: TableViewCell { 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /ThunderTable/SubtitleTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThunderTable/TableImageViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 14/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class TableImageViewCell: TableViewCell { 12 | 13 | @IBOutlet weak public var imageHeightConstraint: NSLayoutConstraint! 14 | 15 | open override func prepareForReuse() { 16 | // Has to be done here because re-use resets these to defaults! 17 | super.prepareForReuse() 18 | isAccessibilityElement = false 19 | cellImageView?.isAccessibilityElement = true 20 | } 21 | 22 | open override var accessibilityElements: [Any]? { 23 | get { 24 | return [cellImageView].compactMap({ $0 }) 25 | } 26 | set { } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ThunderTable/TableImageViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ThunderTable/TableRow+Actions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRow+Actions.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 15/02/2018. 6 | // Copyright © 2018 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The action style for a contextual action. 12 | /// 13 | /// - `default`: Equivalent to normal on iOS >= 11. 14 | /// - destructive: A destructive action, i.e. removes the row. 15 | /// - normal: Any other action. 16 | public enum RowActionableStyle { 17 | case `default` 18 | case destructive 19 | case normal 20 | 21 | @available(iOS 11.0, *) 22 | var _UIContextualActionStyle: UIContextualAction.Style { 23 | switch self { 24 | case .destructive: 25 | return .destructive 26 | default: 27 | return .normal 28 | } 29 | } 30 | 31 | var _UITableViewRowActionStyle: UITableViewRowAction.Style { 32 | switch self { 33 | case .destructive: 34 | return .destructive 35 | case .default: 36 | return .default 37 | case .normal: 38 | return .normal 39 | } 40 | } 41 | } 42 | 43 | /// A closure called when a `RowActionable` is triggered by the user. 44 | /// Call the completionHandler (iOS 11 and above) to reset the context to its normal state (e.g. when swiping, resets to unswiped state). 45 | /// Pass YES to the completionHandler if the action was actually performed, to show a visual indication of the successful completion (iOS 11 and above only). 46 | /// `view` will only be non-nil on iOS 11 and above. 47 | public typealias RowActionableHandler = (_ action: RowActionable, _ view: UIView?, _ callback: ((Bool) -> Void)?, _ row: Row, _ indexPath: IndexPath, _ tableView: UITableView) -> Void 48 | 49 | /// A protocol which can be conformed to to provide an action upon swiping a `UITableViewCell`. 50 | /// In an editable table, performing a horizontal swipe in a row reveals a button to delete the row by default. This protocol lets you define one or more custom actions to display for a given row in your table. Each instance of this protocol represents a single action to perform and includes the text, formatting information, and behavior for the corresponding button. 51 | public protocol RowActionable { 52 | 53 | /// The style of the action. 54 | var style: RowActionableStyle { get } 55 | 56 | /// The title to be displayed on the action. 57 | var title: String? { get } 58 | 59 | /// A handler called when the action is actioned. 60 | var handler: RowActionableHandler { get } 61 | 62 | /// Background color of the button. 63 | /// Default background color is dependent on style. 64 | var backgroundColor: UIColor? { get } 65 | 66 | /// The visual effect to apply to the action's button (iOS 10). 67 | var backgroundEffect: UIVisualEffect? { get } 68 | 69 | /// The image to be displayed on the action (iOS 11 >). 70 | var image: UIImage? { get } 71 | } 72 | 73 | /// A base class which can be subclassed providing a template for the `RowActionable` protocol. 74 | open class RowAction: RowActionable { 75 | 76 | /// The background color of the action's button. 77 | open var backgroundColor: UIColor? 78 | 79 | /// The visual effect to apply to the action's button. 80 | open var backgroundEffect: UIVisualEffect? 81 | 82 | /// The style of the action. 83 | open var style: RowActionableStyle 84 | 85 | /// The title to be displayed on the action. 86 | open var title: String? 87 | 88 | /// The image to be displayed on the action (iOS 11 >). 89 | open var image: UIImage? 90 | 91 | /// A closure called when the action is triggered. 92 | open var handler: RowActionableHandler 93 | 94 | /// Creates a new action with the provided parameters. 95 | /// 96 | /// - Parameters: 97 | /// - style: The style of the action. 98 | /// - title: The title to be displayed on the action. 99 | /// - handler: A closure called when the action is triggered. 100 | public init(style: RowActionableStyle, title: String?, handler: @escaping RowActionableHandler) { 101 | 102 | self.style = style 103 | self.title = title 104 | self.handler = handler 105 | } 106 | } 107 | 108 | extension RowActionable { 109 | 110 | @available(iOS 11.0, *) 111 | /// Creates a `UIContextualAction` to be used in a `UISwipeActionsConfiguration` with the provided handler. 112 | /// 113 | /// - Parameter handler: The handler to be called. 114 | /// - Returns: A `UIContextualAction`. 115 | func contextualAction(with handler: @escaping UIContextualAction.Handler) -> UIContextualAction { 116 | 117 | let action = UIContextualAction(style: style._UIContextualActionStyle, title: title, handler: handler) 118 | // Only set this if non-nil otherwise we end up with no default colouring and a transparent background to the button 119 | if backgroundColor != nil { 120 | action.backgroundColor = backgroundColor 121 | } 122 | action.image = image 123 | return action 124 | } 125 | 126 | /// Creates a `UITableViewRowAction` to be used in the delegate method of `UITableViewController` with the provided handler. 127 | /// 128 | /// - Parameter handler: The handler to be called. 129 | /// - Returns: A `UIContextualAction`. 130 | func rowAction(with handler: @escaping (UITableViewRowAction, IndexPath) -> Void) -> UITableViewRowAction { 131 | 132 | let rowAction = UITableViewRowAction(style: style._UITableViewRowActionStyle, title: title, handler: handler) 133 | // Only set this if non-nil otherwise we end up with no default colouring and a transparent background to the button 134 | if backgroundColor != nil { 135 | rowAction.backgroundColor = backgroundColor 136 | } 137 | rowAction.backgroundEffect = backgroundEffect 138 | return rowAction 139 | } 140 | 141 | var backgroundColor: UIColor? { get { return nil } } 142 | 143 | var backgroundEffect: UIVisualEffect? { get { return nil } } 144 | 145 | var image: UIImage? { get { return nil } } 146 | } 147 | 148 | /// A protocol which can be conformed to to provide swipe action information for a row. 149 | public protocol SwipeActionsConfigurable { 150 | 151 | /// The actions to be shown on the row. 152 | var actions: [RowActionable] { get } 153 | 154 | /// Whether a full swipe should perform the first action (iOS 11 and above only). 155 | var performsFirstActionWithFullSwipe: Bool { get set } // default YES, set to NO to prevent a full swipe from performing the first action 156 | } 157 | 158 | 159 | extension SwipeActionsConfigurable { 160 | 161 | /// Creates a `UISwipeActionsConfiguration` from the `SwipeActionsConfigurable` to be used in the delegate methods of `UITableView`. 162 | /// 163 | /// - Parameters: 164 | /// - row: The row that this configurable is for. 165 | /// - indexPath: The indexPath that this configurable was triggered at. 166 | /// - tableView: The table view the configuration will be used in. 167 | /// - Returns: A `UISwipeActionsConfiguration` to be used in the table view. 168 | @available(iOS 11.0, *) 169 | func configurationFor(row: Row, at indexPath: IndexPath, in tableView: UITableView) -> UISwipeActionsConfiguration { 170 | 171 | let contextualActions = actions.map { (actionable) -> UIContextualAction in 172 | let contextualAction = actionable.contextualAction(with: { (action, view, handler) in 173 | actionable.handler(actionable, view, handler, row, indexPath, tableView) 174 | }) 175 | return contextualAction 176 | } 177 | 178 | let configuration = UISwipeActionsConfiguration(actions: contextualActions) 179 | configuration.performsFirstActionWithFullSwipe = performsFirstActionWithFullSwipe 180 | return configuration 181 | } 182 | 183 | func rowActionsFor(row: Row, in tableView: UITableView) -> [UITableViewRowAction] { 184 | 185 | let rowActions = actions.map { (actionable) -> UITableViewRowAction in 186 | 187 | let rowAction = actionable.rowAction(with: { (action, indexPath) in 188 | actionable.handler(actionable, nil, nil, row, indexPath, tableView) 189 | }) 190 | return rowAction 191 | } 192 | 193 | return rowActions 194 | } 195 | } 196 | 197 | /// A base class which can be subclassed providing a template for the `SwipeActionsConfigurable` protocol. 198 | open class SwipeActionsConfiguration: SwipeActionsConfigurable { 199 | 200 | open var performsFirstActionWithFullSwipe: Bool = true 201 | 202 | open var actions: [RowActionable] 203 | 204 | public init(actions: [RowActionable]) { 205 | self.actions = actions 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /ThunderTable/TableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSection.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 14/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias SelectionHandler = (_ row: Row, _ selected: Bool, _ indexPath: IndexPath, _ tableView: UITableView) -> (Void) 12 | 13 | public typealias EditHandler = (_ row: Row, _ editingStyle: UITableViewCell.EditingStyle, _ indexPath: IndexPath, _ tableView: UITableView) -> (Void) 14 | 15 | public protocol Section { 16 | 17 | var rows: [Row] { get } 18 | 19 | var header: String? { get } 20 | 21 | var footer: String? { get } 22 | 23 | var editHandler: EditHandler? { get } 24 | 25 | var selectionHandler: SelectionHandler? { get } 26 | 27 | var rowLeadingSwipeActionsConfiguration: SwipeActionsConfigurable? { get } 28 | 29 | var rowTrailingSwipeActionsConfiguration: SwipeActionsConfigurable? { get } 30 | } 31 | 32 | public extension Section { 33 | 34 | var rows: [Row] { 35 | return [] 36 | } 37 | 38 | var header: String? { 39 | return nil 40 | } 41 | 42 | var footer: String? { 43 | return nil 44 | } 45 | 46 | var rowLeadingSwipeActionsConfiguration: SwipeActionsConfigurable? { return nil } 47 | 48 | var rowTrailingSwipeActionsConfiguration: SwipeActionsConfigurable? { return nil } 49 | } 50 | 51 | open class TableSection: Section { 52 | 53 | open var header: String? 54 | 55 | open var footer: String? 56 | 57 | open var rows: [Row] 58 | 59 | open var selectionHandler: SelectionHandler? 60 | 61 | open var editHandler: EditHandler? 62 | 63 | open var rowLeadingSwipeActionsConfiguration: SwipeActionsConfigurable? 64 | 65 | open var rowTrailingSwipeActionsConfiguration: SwipeActionsConfigurable? 66 | 67 | public init(rows: [Row], header: String? = nil, footer: String? = nil, selectionHandler: SelectionHandler? = nil) { 68 | 69 | self.rows = rows 70 | self.header = header 71 | self.footer = footer 72 | self.selectionHandler = selectionHandler 73 | } 74 | 75 | /// Returns an array of `TableSection` objects sorted by first letter of the row's title 76 | /// 77 | /// - Parameters: 78 | /// - rows: The rows to sort into alphabetised sections 79 | /// - selectionHandler: A selection handler to add to the sections 80 | /// - Returns: An array of `TableSection` objects 81 | public class func sortedSections(with rows: [Row], selectionHandler: SelectionHandler? = nil) -> [TableSection] { 82 | 83 | let sortedAlphabetically = self.alphabeticallySort(rows: rows) 84 | let sortedKeys = sortedAlphabetically.keys.sorted { (stringA, stringB) -> Bool in 85 | return stringB > stringA 86 | } 87 | 88 | return sortedKeys.compactMap({key -> TableSection? in 89 | guard let rows = sortedAlphabetically[key] else { return nil } 90 | return TableSection(rows: rows, header: key, footer: nil, selectionHandler: selectionHandler) 91 | }) 92 | } 93 | 94 | private class func alphabeticallySort(rows: [Row]) -> [String : [Row]] { 95 | 96 | var sortedDict = [String : [Row]]() 97 | 98 | rows.forEach { (row) in 99 | 100 | var firstLetter = "?" 101 | if let rowTitle = row.title, !rowTitle.isEmpty { 102 | firstLetter = String(rowTitle.prefix(1)).uppercased() 103 | } 104 | var subItems = sortedDict[firstLetter] ?? [] 105 | subItems.append(row) 106 | sortedDict[firstLetter] = subItems 107 | } 108 | 109 | return sortedDict 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ThunderTable/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 14/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class TableViewCell: UITableViewCell { 12 | 13 | @IBOutlet open var cellImageView: UIImageView? 14 | 15 | @IBOutlet open var cellTextLabel: UILabel? 16 | 17 | @IBOutlet open var cellDetailLabel: UILabel? 18 | 19 | private var nibBased = false 20 | 21 | public var cellStyle: UITableViewCell.CellStyle = .default 22 | 23 | public var shouldDisplaySeparators = true { 24 | didSet { 25 | 26 | if !shouldDisplaySeparators { 27 | 28 | subviews.forEach { (view) in 29 | if round(view.frame.height * UIScreen.main.scale) == 1 || round(view.frame.height) == 1 { 30 | view.removeFromSuperview() 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 38 | 39 | super.init(style: style, reuseIdentifier: reuseIdentifier) 40 | 41 | cellStyle = style 42 | 43 | if !nibBased { 44 | 45 | cellImageView = UIImageView(); 46 | contentView.addSubview(cellImageView!) 47 | 48 | cellTextLabel = UILabel() 49 | cellTextLabel?.numberOfLines = 0 50 | cellTextLabel?.backgroundColor = .clear 51 | cellTextLabel?.numberOfLines = 0; 52 | cellTextLabel?.font = ThemeManager.shared.theme.cellTitleFont 53 | cellTextLabel?.textColor = ThemeManager.shared.theme.cellTitleColor 54 | 55 | contentView.addSubview(cellTextLabel!) 56 | 57 | cellDetailLabel = UILabel() 58 | cellDetailLabel?.numberOfLines = 0; 59 | cellDetailLabel?.font = ThemeManager.shared.theme.cellDetailFont 60 | cellDetailLabel?.textColor = ThemeManager.shared.theme.cellDetailColor 61 | contentView.addSubview(cellDetailLabel!) 62 | } 63 | } 64 | 65 | required public init?(coder aDecoder: NSCoder) { 66 | super.init(coder: aDecoder) 67 | } 68 | 69 | override open func awakeFromNib() { 70 | 71 | super.awakeFromNib() 72 | nibBased = true 73 | sharedSetup() 74 | } 75 | 76 | private func sharedSetup() { 77 | 78 | backgroundColor = ThemeManager.shared.theme.cellBackgroundColor 79 | // Don't do this on iOS 13 because it causes a rendering issue with highlighted/selected state 80 | if #available(iOS 13, *) { } else { 81 | contentView.backgroundColor = ThemeManager.shared.theme.cellBackgroundColor 82 | } 83 | } 84 | 85 | override open func layoutSubviews() { 86 | 87 | super.layoutSubviews() 88 | 89 | // Don't layout manually if is marked as nib based, or if none of the root labels/image 90 | // views are created or have constraints 91 | guard !nibBased, 92 | let cellTextLabel = cellTextLabel, cellTextLabel.constraints.isEmpty, 93 | let cellDetailLabel = cellDetailLabel, cellDetailLabel.constraints.isEmpty, 94 | let cellImageView = cellImageView, cellImageView.constraints.isEmpty else { 95 | return 96 | } 97 | 98 | if cellImageView.image != nil { 99 | 100 | cellImageView.sizeToFit() 101 | var imageFrame = cellImageView.frame; 102 | imageFrame.origin.x = 12; 103 | cellImageView.frame = imageFrame; 104 | 105 | } else { 106 | 107 | cellImageView.frame = .zero; 108 | } 109 | 110 | switch cellStyle { 111 | case .subtitle: 112 | subtitleLayout() 113 | break 114 | case .value1, .value2: 115 | valueLayout() 116 | break 117 | default: 118 | break 119 | } 120 | 121 | // Only set the center if we have superview to avoid breaking automatic cell height calculations 122 | if superview != nil { 123 | cellImageView.center = CGPoint(x: cellImageView.center.x, y: max(imageView != nil ? imageView!.frame.height/2 : 0, contentView.center.y)) 124 | } 125 | } 126 | 127 | private var edgeInsets: UIEdgeInsets { 128 | 129 | var leftIndentation: CGFloat = max(indentationWidth * CGFloat(indentationLevel), 12); 130 | if cellImageView?.image != nil { 131 | leftIndentation = cellImageView!.frame.maxX + leftIndentation; 132 | } 133 | let insets = UIEdgeInsets(top: 8, left: leftIndentation, bottom: 0, right: 12) 134 | return insets; 135 | } 136 | 137 | private func valueLayout() { 138 | 139 | guard let cellTextLabel = cellTextLabel else { return } 140 | guard let cellDetailLabel = cellDetailLabel else { return } 141 | 142 | let textLabelSize = cellTextLabel.sizeThatFits(CGSize(width: contentView.frame.width - edgeInsets.left - edgeInsets.right, height: CGFloat.greatestFiniteMagnitude)) 143 | var textLabelFrame = CGRect(x: edgeInsets.left, y: edgeInsets.top, width: textLabelSize.width, height: textLabelSize.height) 144 | let textNumberOfLines = max(Int(textLabelSize.height/cellTextLabel.font.lineHeight), 0) 145 | 146 | var (_, remainder) = contentView.frame.divided(atDistance: textLabelSize.width + edgeInsets.right, from: .minXEdge) 147 | remainder.size.width = remainder.size.width - edgeInsets.right; 148 | 149 | let detailLabelSize = cellDetailLabel.sizeThatFits(CGSize(width: remainder.width, height: CGFloat.greatestFiniteMagnitude)) 150 | remainder.size.height = detailLabelSize.height 151 | let detailNumberOfLines = max(Int(remainder.height/cellDetailLabel.font.lineHeight), 0) 152 | 153 | if textNumberOfLines == detailNumberOfLines || cellDetailLabel.text == nil { 154 | 155 | textLabelFrame.origin.y = self.contentView.frame.size.height / 2 - textLabelFrame.size.height / 2; 156 | remainder.origin.y = self.contentView.frame.size.height / 2 - remainder.size.height / 2; 157 | } 158 | 159 | self.cellTextLabel?.frame = textLabelFrame.integral; 160 | self.cellDetailLabel?.frame = remainder.integral; 161 | } 162 | 163 | private func subtitleLayout() { 164 | 165 | guard let cellTextLabel = cellTextLabel else { return } 166 | guard let cellDetailLabel = cellDetailLabel else { return } 167 | 168 | let textLabelSize = cellTextLabel.sizeThatFits(CGSize(width: contentView.frame.width - edgeInsets.left - edgeInsets.right, height: CGFloat.greatestFiniteMagnitude)) 169 | var textLabelFrame = CGRect(x: edgeInsets.left, y: edgeInsets.top, width: contentView.frame.width - edgeInsets.left - edgeInsets.right, height: textLabelSize.height) 170 | 171 | let detailLabelSize = cellDetailLabel.sizeThatFits(CGSize(width: contentView.frame.width - edgeInsets.left - edgeInsets.right, height: CGFloat.greatestFiniteMagnitude)) 172 | var detailLabelFrame = CGRect(x: edgeInsets.left, y: textLabelFrame.maxY, width: contentView.frame.width - edgeInsets.left - edgeInsets.right, height: detailLabelSize.height) 173 | 174 | // If no detail text then center text label 175 | if cellDetailLabel.text == nil || cellDetailLabel.text!.replacingOccurrences(of: " ", with: "").isEmpty { 176 | 177 | textLabelFrame.origin.y = contentView.frame.height / 2 - textLabelFrame.height / 2 178 | 179 | // If image view is larger than both labels, put together then centre them 180 | } else if let cellImageView = cellImageView, cellImageView.frame.height >= detailLabelFrame.maxY - textLabelFrame.minY { 181 | 182 | var compoundRect = CGRect(x: textLabelFrame.minX, y: 0, width: textLabelFrame.width, height: detailLabelFrame.maxY - textLabelFrame.minY) 183 | compoundRect.origin.y = contentView.frame.height / 2 - compoundRect.height / 2 184 | 185 | textLabelFrame.origin.y = compoundRect.origin.y; 186 | detailLabelFrame.origin.y = textLabelFrame.maxY 187 | } 188 | 189 | self.cellTextLabel?.frame = (cellTextLabel.text == nil || cellTextLabel.text!.replacingOccurrences(of: " ", with: "").isEmpty) ? .zero : textLabelFrame.integral 190 | self.cellDetailLabel?.frame = (cellDetailLabel.text == nil || cellDetailLabel.text!.replacingOccurrences(of: " ", with: "").isEmpty) ? .zero : detailLabelFrame.integral 191 | } 192 | 193 | //This is really quite awful but it's the only way to get tableview to remove the 1px line at the top of sections on a group tableview when disabling cell seperators 194 | override open func addSubview(_ view: UIView) { 195 | 196 | if !shouldDisplaySeparators && (round(view.frame.height * UIScreen.main.scale) == 1 || round(view.frame.height) == 1) { 197 | return 198 | } 199 | 200 | super.addSubview(view) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /ThunderTable/TableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThunderTable/TableViewController+Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController+Collection.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 04/12/2019. 6 | // Copyright © 2019 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension TableViewController { 12 | 13 | //MARK: - Subscript Functions 14 | 15 | public subscript (index: IndexPath) -> (section: Section, row: Row)? { 16 | 17 | guard let safeSectionIndex = index.safeSection, safeSectionIndex < data.count else { 18 | return nil 19 | } 20 | 21 | let section = data[safeSectionIndex] 22 | guard index.row < section.rows.count else { 23 | return nil 24 | } 25 | 26 | return (section, section.rows[index.row]) 27 | } 28 | 29 | public subscript (row index: IndexPath) -> Row? { 30 | return self[index]?.row 31 | } 32 | 33 | public subscript (section index: IndexPath) -> Section? { 34 | 35 | guard let safeSectionIndex = index.safeSection, safeSectionIndex < data.count else { 36 | return nil 37 | } 38 | 39 | return data[safeSectionIndex] 40 | } 41 | 42 | /// forEach Array equivalant for looping across all sections and rows of current `data` 43 | /// - Parameter body: A closure that takes each row, indexPath and section as the parameters. 44 | public func forEachRow(_ body: (Row, IndexPath, Section) -> Void) { 45 | 46 | data.enumerated().forEach { (sectionIndex, section) in 47 | section.rows.enumerated().forEach { (rowIndex, row) in 48 | body(row, IndexPath(row: rowIndex, section: sectionIndex), section) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ThunderTable/Value1TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Value1TableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 11/12/2017. 6 | // Copyright © 2017 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Value1TableViewCell: TableViewCell { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | // Initialization code 16 | } 17 | 18 | override func setSelected(_ selected: Bool, animated: Bool) { 19 | super.setSelected(selected, animated: animated) 20 | 21 | // Configure the view for the selected state 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ThunderTable/Value1TableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThunderTable/Value2TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Value1TableViewCell.swift 3 | // ThunderTable 4 | // 5 | // Created by Simon Mitchell on 11/12/2017. 6 | // Copyright © 2017 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Value2TableViewCell: TableViewCell { 12 | 13 | override func awakeFromNib() { 14 | super.awakeFromNib() 15 | // Initialization code 16 | } 17 | 18 | override func setSelected(_ selected: Bool, animated: Bool) { 19 | super.setSelected(selected, animated: animated) 20 | 21 | // Configure the view for the selected state 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ThunderTable/Value2TableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ThunderTableDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 26/02/2018. 6 | // Copyright © 2018 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Logo@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Logo@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/logo.imageset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3sidedcube/ThunderTable/78588358ced89f93713497d7529be661617f681b/ThunderTableDemo/Assets.xcassets/logo.imageset/Logo.png -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/logo.imageset/Logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3sidedcube/ThunderTable/78588358ced89f93713497d7529be661617f681b/ThunderTableDemo/Assets.xcassets/logo.imageset/Logo@2x.png -------------------------------------------------------------------------------- /ThunderTableDemo/Assets.xcassets/logo.imageset/Logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3sidedcube/ThunderTable/78588358ced89f93713497d7529be661617f681b/ThunderTableDemo/Assets.xcassets/logo.imageset/Logo@3x.png -------------------------------------------------------------------------------- /ThunderTableDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ThunderTableDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /ThunderTableDemo/Cells/CollectionTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionTableViewCell.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 02/09/2020. 6 | // Copyright © 2020 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ThunderTable 11 | 12 | class CollectionTableViewCell: UITableViewCell, ScrollOffsetManagable, UICollectionViewDelegate { 13 | 14 | var scrollView: UIScrollView? { 15 | return collectionView 16 | } 17 | 18 | weak var scrollDelegate: ScrollOffsetDelegate? 19 | 20 | var identifier: AnyHashable? 21 | 22 | @IBOutlet weak var collectionView: UICollectionView! 23 | 24 | override func awakeFromNib() { 25 | super.awakeFromNib() 26 | collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell") 27 | collectionView.delegate = self 28 | } 29 | 30 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 31 | scrollDelegate?.scrollViewDidChangeContentOffset(self) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ThunderTableDemo/Cells/CollectionTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /ThunderTableDemo/Cells/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 02/09/2020. 6 | // Copyright © 2020 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewCell: UICollectionViewCell { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ThunderTableDemo/Cells/ContactTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactTableViewCell.swift 3 | // Concact 4 | // 5 | // Created by Simon Mitchell on 27/05/2016. 6 | // Copyright © 2016 Yellow Brick Bear. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ThunderTable 11 | 12 | class ContactTableViewCell: TableViewCell { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /ThunderTableDemo/Cells/ContactTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThunderTableDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSExceptionDomains 26 | 27 | placeholder.com 28 | 29 | NSIncludesSubdomains 30 | 31 | NSTemporaryExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | UILaunchStoryboardName 37 | LaunchScreen 38 | UIMainStoryboardFile 39 | Main 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationPortraitUpsideDown 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | UIUserInterfaceStyle 58 | Light 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThunderTableDemo/Models/CNContact+Row.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CNContact+Row.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 02/03/2018. 6 | // Copyright © 2018 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Contacts 11 | import ThunderTable 12 | 13 | extension CNContact: Row { 14 | 15 | public var title: String? { 16 | return givenName + " " + familyName 17 | } 18 | 19 | public var subtitle: String? { 20 | return phoneNumbers.first?.value.stringValue 21 | } 22 | 23 | public var image: UIImage? { 24 | get { 25 | guard let thumbnailImageData = thumbnailImageData else { return nil } 26 | return UIImage(data: thumbnailImageData) 27 | } 28 | set { } 29 | } 30 | 31 | public var cellClass: UITableViewCell.Type? { 32 | return ContactTableViewCell.self 33 | } 34 | 35 | public func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 36 | guard let contactCell = cell as? ContactTableViewCell else { 37 | return 38 | } 39 | contactCell.cellImageView?.isHidden = image == nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ThunderTableDemo/Models/CNContact+Section.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CNContact+Section.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 25/02/2019. 6 | // Copyright © 2019 3SidedCube. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Contacts 11 | import ThunderTable 12 | 13 | extension CNContact: Section { 14 | 15 | public var editHandler: EditHandler? { 16 | return nil 17 | } 18 | 19 | public var selectionHandler: SelectionHandler? { 20 | return nil 21 | } 22 | 23 | public var rows: [Row] { 24 | 25 | let _rows: [Row] = [ 26 | TableRow(title: CNContact.localizedString(forKey: CNContactGivenNameKey), subtitle: givenName, image: nil, selectionHandler: nil), 27 | TableRow(title: CNContact.localizedString(forKey: CNContactFamilyNameKey), subtitle: familyName, image: nil, selectionHandler: nil) 28 | ] 29 | 30 | return _rows 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ThunderTableDemo/Models/CollectionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionRow.swift 3 | // ThunderTableDemo 4 | // 5 | // Created by Simon Mitchell on 02/09/2020. 6 | // Copyright © 2020 3SidedCube. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ThunderTable 11 | 12 | class CollectionRow: NSObject, Row, UICollectionViewDataSource { 13 | 14 | let colours: [UIColor] 15 | 16 | init(colours: [UIColor]) { 17 | self.colours = colours 18 | super.init() 19 | } 20 | 21 | func configure(cell: UITableViewCell, at indexPath: IndexPath, in tableViewController: TableViewController) { 22 | 23 | guard let collectionCell = cell as? CollectionTableViewCell else { return } 24 | 25 | collectionCell.collectionView.dataSource = self 26 | } 27 | 28 | func numberOfSections(in collectionView: UICollectionView) -> Int { 29 | return 1 30 | } 31 | 32 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 33 | return colours.count 34 | } 35 | 36 | var cellClass: UITableViewCell.Type? { 37 | return CollectionTableViewCell.self 38 | } 39 | 40 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 41 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) 42 | cell.backgroundColor = colours[indexPath.item] 43 | return cell 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ThunderTableTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ThunderTableTests/ThunderTableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThunderTableTests.swift 3 | // ThunderTableTests 4 | // 5 | // Created by Simon Mitchell on 14/09/2016. 6 | // Copyright © 2016 3SidedCube. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ThunderTable 11 | 12 | class ThunderTableTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | func testSectionIndexPathsCalculation() { 37 | 38 | let data = [ 39 | [TableRow(title: ""), TableRow(title: "")], 40 | [], 41 | [TableRow(title: ""), TableRow(title: ""), TableRow(title: ""), TableRow(title: "")], 42 | [TableRow(title: "")] 43 | ] as [Section] 44 | 45 | let indexPaths = data.indexPaths 46 | 47 | XCTAssertEqual( 48 | indexPaths, 49 | [ 50 | IndexPath(row: 0, section: 0), 51 | IndexPath(row: 1, section: 0), 52 | IndexPath(row: 0, section: 2), 53 | IndexPath(row: 1, section: 2), 54 | IndexPath(row: 2, section: 2), 55 | IndexPath(row: 3, section: 2), 56 | IndexPath(row: 0, section: 3) 57 | ] 58 | ) 59 | } 60 | } 61 | --------------------------------------------------------------------------------