├── .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 | [](https://travis-ci.org/3sidedcube/ThunderTable) [](https://swift.org/blog/swift-5-5-released/) [](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 |
--------------------------------------------------------------------------------