";
206 | };
207 | /* End PBXGroup section */
208 |
209 | /* Begin PBXNativeTarget section */
210 | DAB7EB261BEF787300D2AD5E /* TableKitDemo */ = {
211 | isa = PBXNativeTarget;
212 | buildConfigurationList = DAB7EB391BEF787300D2AD5E /* Build configuration list for PBXNativeTarget "TableKitDemo" */;
213 | buildPhases = (
214 | DAB7EB231BEF787300D2AD5E /* Sources */,
215 | DAB7EB241BEF787300D2AD5E /* Frameworks */,
216 | DAB7EB251BEF787300D2AD5E /* Resources */,
217 | DA9EA7DD1D0EC65B0021F650 /* Embed Frameworks */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | DA9EA7DC1D0EC65B0021F650 /* PBXTargetDependency */,
223 | );
224 | name = TableKitDemo;
225 | productName = TabletDemo;
226 | productReference = DAB7EB271BEF787300D2AD5E /* TableKitDemo.app */;
227 | productType = "com.apple.product-type.application";
228 | };
229 | /* End PBXNativeTarget section */
230 |
231 | /* Begin PBXProject section */
232 | DAB7EB1F1BEF787300D2AD5E /* Project object */ = {
233 | isa = PBXProject;
234 | attributes = {
235 | LastSwiftUpdateCheck = 0720;
236 | LastUpgradeCheck = 1000;
237 | ORGANIZATIONNAME = Tablet;
238 | TargetAttributes = {
239 | DAB7EB261BEF787300D2AD5E = {
240 | CreatedOnToolsVersion = 7.0.1;
241 | DevelopmentTeam = Z48R734SJX;
242 | LastSwiftMigration = 1000;
243 | };
244 | };
245 | };
246 | buildConfigurationList = DAB7EB221BEF787300D2AD5E /* Build configuration list for PBXProject "TableKitDemo" */;
247 | compatibilityVersion = "Xcode 3.2";
248 | developmentRegion = English;
249 | hasScannedForEncodings = 0;
250 | knownRegions = (
251 | English,
252 | en,
253 | Base,
254 | );
255 | mainGroup = DAB7EB1E1BEF787300D2AD5E;
256 | productRefGroup = DAB7EB281BEF787300D2AD5E /* Products */;
257 | projectDirPath = "";
258 | projectReferences = (
259 | {
260 | ProductGroup = DA9EA7D11D0EC5C50021F650 /* Products */;
261 | ProjectRef = DA9EA7D01D0EC5C50021F650 /* TableKit.xcodeproj */;
262 | },
263 | );
264 | projectRoot = "";
265 | targets = (
266 | DAB7EB261BEF787300D2AD5E /* TableKitDemo */,
267 | );
268 | };
269 | /* End PBXProject section */
270 |
271 | /* Begin PBXReferenceProxy section */
272 | DA9EA7D61D0EC5C60021F650 /* TableKit.framework */ = {
273 | isa = PBXReferenceProxy;
274 | fileType = wrapper.framework;
275 | path = TableKit.framework;
276 | remoteRef = DA9EA7D51D0EC5C60021F650 /* PBXContainerItemProxy */;
277 | sourceTree = BUILT_PRODUCTS_DIR;
278 | };
279 | DA9EA7D81D0EC5C60021F650 /* TableKitTests.xctest */ = {
280 | isa = PBXReferenceProxy;
281 | fileType = wrapper.cfbundle;
282 | path = TableKitTests.xctest;
283 | remoteRef = DA9EA7D71D0EC5C60021F650 /* PBXContainerItemProxy */;
284 | sourceTree = BUILT_PRODUCTS_DIR;
285 | };
286 | /* End PBXReferenceProxy section */
287 |
288 | /* Begin PBXResourcesBuildPhase section */
289 | DAB7EB251BEF787300D2AD5E /* Resources */ = {
290 | isa = PBXResourcesBuildPhase;
291 | buildActionMask = 2147483647;
292 | files = (
293 | DAC2D5CF1C9D30A7009E9C19 /* Main.storyboard in Resources */,
294 | 5079ADE61FE1BF65000CC345 /* AutolayoutSectionHeaderView.xib in Resources */,
295 | DA5546661D15765900AA83EE /* NibTableViewCell.xib in Resources */,
296 | DAC2D5D01C9D30A7009E9C19 /* LaunchScreen.storyboard in Resources */,
297 | DAC2D69C1C9E75E3009E9C19 /* Assets.xcassets in Resources */,
298 | );
299 | runOnlyForDeploymentPostprocessing = 0;
300 | };
301 | /* End PBXResourcesBuildPhase section */
302 |
303 | /* Begin PBXSourcesBuildPhase section */
304 | DAB7EB231BEF787300D2AD5E /* Sources */ = {
305 | isa = PBXSourcesBuildPhase;
306 | buildActionMask = 2147483647;
307 | files = (
308 | DACB71761CC2D63D00432BD3 /* MainController.swift in Sources */,
309 | DA55465D1D1569CC00AA83EE /* AutolayoutCellsController.swift in Sources */,
310 | DA5546681D15771D00AA83EE /* NibCellsController.swift in Sources */,
311 | DAC2D5CA1C9D303E009E9C19 /* AppDelegate.swift in Sources */,
312 | 5079ADE31FE1BF1B000CC345 /* AutolayoutSectionHeaderView.swift in Sources */,
313 | DA5546641D15762000AA83EE /* NibTableViewCell.swift in Sources */,
314 | DA5546601D156A4F00AA83EE /* ConfigurableTableViewCell.swift in Sources */,
315 | DA08A0531CF4E9B500BBF1F8 /* AutolayoutTableViewCell.swift in Sources */,
316 | );
317 | runOnlyForDeploymentPostprocessing = 0;
318 | };
319 | /* End PBXSourcesBuildPhase section */
320 |
321 | /* Begin PBXTargetDependency section */
322 | DA9EA7DC1D0EC65B0021F650 /* PBXTargetDependency */ = {
323 | isa = PBXTargetDependency;
324 | name = TableKit;
325 | targetProxy = DA9EA7DB1D0EC65B0021F650 /* PBXContainerItemProxy */;
326 | };
327 | /* End PBXTargetDependency section */
328 |
329 | /* Begin XCBuildConfiguration section */
330 | DAB7EB371BEF787300D2AD5E /* Debug */ = {
331 | isa = XCBuildConfiguration;
332 | buildSettings = {
333 | ALWAYS_SEARCH_USER_PATHS = NO;
334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
335 | CLANG_CXX_LIBRARY = "libc++";
336 | CLANG_ENABLE_MODULES = YES;
337 | CLANG_ENABLE_OBJC_ARC = YES;
338 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
339 | CLANG_WARN_BOOL_CONVERSION = YES;
340 | CLANG_WARN_COMMA = YES;
341 | CLANG_WARN_CONSTANT_CONVERSION = YES;
342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
344 | CLANG_WARN_EMPTY_BODY = YES;
345 | CLANG_WARN_ENUM_CONVERSION = YES;
346 | CLANG_WARN_INFINITE_RECURSION = YES;
347 | CLANG_WARN_INT_CONVERSION = YES;
348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
349 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
352 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
353 | CLANG_WARN_STRICT_PROTOTYPES = YES;
354 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
355 | CLANG_WARN_UNREACHABLE_CODE = YES;
356 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
357 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
358 | COPY_PHASE_STRIP = NO;
359 | DEBUG_INFORMATION_FORMAT = dwarf;
360 | ENABLE_STRICT_OBJC_MSGSEND = YES;
361 | ENABLE_TESTABILITY = YES;
362 | GCC_C_LANGUAGE_STANDARD = gnu99;
363 | GCC_DYNAMIC_NO_PIC = NO;
364 | GCC_NO_COMMON_BLOCKS = YES;
365 | GCC_OPTIMIZATION_LEVEL = 0;
366 | GCC_PREPROCESSOR_DEFINITIONS = (
367 | "DEBUG=1",
368 | "$(inherited)",
369 | );
370 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
371 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
372 | GCC_WARN_UNDECLARED_SELECTOR = YES;
373 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
374 | GCC_WARN_UNUSED_FUNCTION = YES;
375 | GCC_WARN_UNUSED_VARIABLE = YES;
376 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
377 | MTL_ENABLE_DEBUG_INFO = YES;
378 | ONLY_ACTIVE_ARCH = YES;
379 | SDKROOT = iphoneos;
380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
381 | SWIFT_VERSION = 5.0;
382 | };
383 | name = Debug;
384 | };
385 | DAB7EB381BEF787300D2AD5E /* Release */ = {
386 | isa = XCBuildConfiguration;
387 | buildSettings = {
388 | ALWAYS_SEARCH_USER_PATHS = NO;
389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
390 | CLANG_CXX_LIBRARY = "libc++";
391 | CLANG_ENABLE_MODULES = YES;
392 | CLANG_ENABLE_OBJC_ARC = YES;
393 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
394 | CLANG_WARN_BOOL_CONVERSION = YES;
395 | CLANG_WARN_COMMA = YES;
396 | CLANG_WARN_CONSTANT_CONVERSION = YES;
397 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
399 | CLANG_WARN_EMPTY_BODY = YES;
400 | CLANG_WARN_ENUM_CONVERSION = YES;
401 | CLANG_WARN_INFINITE_RECURSION = YES;
402 | CLANG_WARN_INT_CONVERSION = YES;
403 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
404 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
405 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
406 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
407 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
408 | CLANG_WARN_STRICT_PROTOTYPES = YES;
409 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
410 | CLANG_WARN_UNREACHABLE_CODE = YES;
411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
412 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
413 | COPY_PHASE_STRIP = NO;
414 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
415 | ENABLE_NS_ASSERTIONS = NO;
416 | ENABLE_STRICT_OBJC_MSGSEND = YES;
417 | GCC_C_LANGUAGE_STANDARD = gnu99;
418 | GCC_NO_COMMON_BLOCKS = YES;
419 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
420 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
421 | GCC_WARN_UNDECLARED_SELECTOR = YES;
422 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
423 | GCC_WARN_UNUSED_FUNCTION = YES;
424 | GCC_WARN_UNUSED_VARIABLE = YES;
425 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
426 | MTL_ENABLE_DEBUG_INFO = NO;
427 | SDKROOT = iphoneos;
428 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
429 | SWIFT_VERSION = 5.0;
430 | VALIDATE_PRODUCT = YES;
431 | };
432 | name = Release;
433 | };
434 | DAB7EB3A1BEF787300D2AD5E /* Debug */ = {
435 | isa = XCBuildConfiguration;
436 | buildSettings = {
437 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
439 | CODE_SIGN_IDENTITY = "iPhone Developer";
440 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
441 | INFOPLIST_FILE = Resources/Info.plist;
442 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
443 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
444 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.demo;
445 | PRODUCT_NAME = TableKitDemo;
446 | PROVISIONING_PROFILE = "";
447 | SWIFT_VERSION = 5.0;
448 | };
449 | name = Debug;
450 | };
451 | DAB7EB3B1BEF787300D2AD5E /* Release */ = {
452 | isa = XCBuildConfiguration;
453 | buildSettings = {
454 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
455 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
456 | CODE_SIGN_IDENTITY = "iPhone Developer";
457 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
458 | INFOPLIST_FILE = Resources/Info.plist;
459 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
460 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
461 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.demo;
462 | PRODUCT_NAME = TableKitDemo;
463 | PROVISIONING_PROFILE = "";
464 | SWIFT_VERSION = 5.0;
465 | };
466 | name = Release;
467 | };
468 | /* End XCBuildConfiguration section */
469 |
470 | /* Begin XCConfigurationList section */
471 | DAB7EB221BEF787300D2AD5E /* Build configuration list for PBXProject "TableKitDemo" */ = {
472 | isa = XCConfigurationList;
473 | buildConfigurations = (
474 | DAB7EB371BEF787300D2AD5E /* Debug */,
475 | DAB7EB381BEF787300D2AD5E /* Release */,
476 | );
477 | defaultConfigurationIsVisible = 0;
478 | defaultConfigurationName = Release;
479 | };
480 | DAB7EB391BEF787300D2AD5E /* Build configuration list for PBXNativeTarget "TableKitDemo" */ = {
481 | isa = XCConfigurationList;
482 | buildConfigurations = (
483 | DAB7EB3A1BEF787300D2AD5E /* Debug */,
484 | DAB7EB3B1BEF787300D2AD5E /* Release */,
485 | );
486 | defaultConfigurationIsVisible = 0;
487 | defaultConfigurationName = Release;
488 | };
489 | /* End XCConfigurationList section */
490 | };
491 | rootObject = DAB7EB1F1BEF787300D2AD5E /* Project object */;
492 | }
493 |
--------------------------------------------------------------------------------
/Demo/TableKitDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/TableKitDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "TableKit",
8 |
9 | products: [
10 | .library(
11 | name: "TableKit",
12 | targets: ["TableKit"]),
13 | ],
14 |
15 | targets: [
16 | .target(
17 | name: "TableKit",
18 | path: "Sources")
19 | ]
20 | )
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TableKit
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | TableKit is a super lightweight yet powerful generic library that allows you to build complex table views in a declarative type-safe manner.
13 | It hides a complexity of `UITableViewDataSource` and `UITableViewDelegate` methods behind the scene, so your code will be look clean, easy to read and nice to maintain.
14 |
15 | # Features
16 |
17 | - [x] Type-safe generic cells
18 | - [x] Functional programming style friendly
19 | - [x] The easiest way to map your models or view models to cells
20 | - [x] Automatic cell registration*
21 | - [x] Correctly handles autolayout cells with multiline labels
22 | - [x] Chainable cell actions (select/deselect etc.)
23 | - [x] Support cells created from code, xib, or storyboard
24 | - [x] Support different cells height calculation strategies
25 | - [x] Support portrait and landscape orientations
26 | - [x] No need to subclass
27 | - [x] Extensibility
28 |
29 | # Getting Started
30 |
31 | An [example app](Demo) is included demonstrating TableKit's functionality.
32 |
33 | ## Basic usage
34 |
35 | Create your rows:
36 | ```swift
37 | import TableKit
38 |
39 | let row1 = TableRow(item: "1")
40 | let row2 = TableRow(item: 2)
41 | let row3 = TableRow(item: User(name: "John Doe", rating: 5))
42 | ```
43 | Put rows into section:
44 | ```swift
45 | let section = TableSection(rows: [row1, row2, row3])
46 | ```
47 | And setup your table:
48 | ```swift
49 | let tableDirector = TableDirector(tableView: tableView)
50 | tableDirector += section
51 | ```
52 | Done. Your table is ready. Your cells have to conform to `ConfigurableCell` protocol:
53 | ```swift
54 | class StringTableViewCell: UITableViewCell, ConfigurableCell {
55 |
56 | func configure(with string: String) {
57 |
58 | textLabel?.text = string
59 | }
60 | }
61 |
62 | class UserTableViewCell: UITableViewCell, ConfigurableCell {
63 |
64 | static var estimatedHeight: CGFloat? {
65 | return 100
66 | }
67 |
68 | // is not required to be implemented
69 | // by default reuse id is equal to cell's class name
70 | static var reuseIdentifier: String {
71 | return "my id"
72 | }
73 |
74 | func configure(with user: User) {
75 |
76 | textLabel?.text = user.name
77 | detailTextLabel?.text = "Rating: \(user.rating)"
78 | }
79 | }
80 | ```
81 | You could have as many rows and sections as you need.
82 |
83 | ## Row actions
84 |
85 | It nice to have some actions that related to your cells:
86 | ```swift
87 | let action = TableRowAction(.click) { (options) in
88 |
89 | // you could access any useful information that relates to the action
90 |
91 | // options.cell - StringTableViewCell?
92 | // options.item - String
93 | // options.indexPath - IndexPath
94 | // options.userInfo - [AnyHashable: Any]?
95 | }
96 |
97 | let row = TableRow(item: "some", actions: [action])
98 | ```
99 | Or, using nice chaining approach:
100 | ```swift
101 | let row = TableRow(item: "some")
102 | .on(.click) { (options) in
103 |
104 | }
105 | .on(.shouldHighlight) { (options) -> Bool in
106 | return false
107 | }
108 | ```
109 | You could find all available actions [here](Sources/TableRowAction.swift).
110 |
111 | ## Custom row actions
112 |
113 | You are able to define your own actions:
114 | ```swift
115 | struct MyActions {
116 |
117 | static let ButtonClicked = "ButtonClicked"
118 | }
119 |
120 | class MyTableViewCell: UITableViewCell, ConfigurableCell {
121 |
122 | @IBAction func myButtonClicked(sender: UIButton) {
123 |
124 | TableCellAction(key: MyActions.ButtonClicked, sender: self).invoke()
125 | }
126 | }
127 | ```
128 | And handle them accordingly:
129 | ```swift
130 | let myAction = TableRowAction(.custom(MyActions.ButtonClicked)) { (options) in
131 |
132 | }
133 | ```
134 | ## Multiple actions with same type
135 |
136 | It's also possible to use multiple actions with same type:
137 | ```swift
138 | let click1 = TableRowAction(.click) { (options) in }
139 | click1.id = "click1" // optional
140 |
141 | let click2 = TableRowAction(.click) { (options) in }
142 | click2.id = "click2" // optional
143 |
144 | let row = TableRow(item: "some", actions: [click1, click2])
145 | ```
146 | Could be useful in case if you want to separate your logic somehow. Actions will be invoked in order which they were attached.
147 | > If you define multiple actions with same type which also return a value, only last return value will be used for table view.
148 |
149 | You could also remove any action by id:
150 | ```swift
151 | row.removeAction(forActionId: "action_id")
152 | ```
153 |
154 | # Advanced
155 |
156 | ## Cell height calculating strategy
157 | By default TableKit relies on self-sizing cells. In that case you have to provide an estimated height for your cells:
158 | ```swift
159 | class StringTableViewCell: UITableViewCell, ConfigurableCell {
160 |
161 | // ...
162 |
163 | static var estimatedHeight: CGFloat? {
164 | return 255
165 | }
166 | }
167 | ```
168 | It's enough for most cases. But you may be not happy with this. So you could use a prototype cell to calculate cells heights. To enable this feature simply use this property:
169 | ```swift
170 | let tableDirector = TableDirector(tableView: tableView, shouldUsePrototypeCellHeightCalculation: true)
171 | ```
172 | It does all dirty work with prototypes for you [behind the scene](Sources/TablePrototypeCellHeightCalculator.swift), so you don't have to worry about anything except of your cell configuration:
173 | ```swift
174 | class ImageTableViewCell: UITableViewCell, ConfigurableCell {
175 |
176 | func configure(with url: NSURL) {
177 |
178 | loadImageAsync(url: url, imageView: imageView)
179 | }
180 |
181 | override func layoutSubviews() {
182 | super.layoutSubviews()
183 |
184 | contentView.layoutIfNeeded()
185 | multilineLabel.preferredMaxLayoutWidth = multilineLabel.bounds.size.width
186 | }
187 | }
188 | ```
189 | You have to additionally set `preferredMaxLayoutWidth` for all your multiline labels.
190 |
191 | ## Functional programming
192 | It's never been so easy to deal with table views.
193 | ```swift
194 | let users = /* some users array */
195 |
196 | let click = TableRowAction(.click) {
197 |
198 | }
199 |
200 | let rows = users.filter({ $0.state == .active }).map({ TableRow(item: $0.name, actions: [click]) })
201 |
202 | tableDirector += rows
203 | ```
204 | Done, your table is ready.
205 | ## Automatic cell registration
206 |
207 | TableKit can register your cells in a table view automatically. In case if your reusable cell id matches cell's xib name:
208 |
209 | ```ruby
210 | MyTableViewCell.swift
211 | MyTableViewCell.xib
212 |
213 | ```
214 | You can also turn off this behaviour:
215 | ```swift
216 | let tableDirector = TableDirector(tableView: tableView, shouldUseAutomaticCellRegistration: false)
217 | ```
218 | and register your cell manually.
219 |
220 | # Installation
221 |
222 | ## CocoaPods
223 | To integrate TableKit into your Xcode project using CocoaPods, specify it in your `Podfile`:
224 |
225 | ```ruby
226 | pod 'TableKit'
227 | ```
228 | ## Carthage
229 | Add the line `github "maxsokolov/tablekit"` to your `Cartfile`.
230 | ## Manual
231 | Clone the repo and drag files from `Sources` folder into your Xcode project.
232 |
233 | # Requirements
234 |
235 | - iOS 8.0
236 | - Xcode 9.0
237 |
238 | # Changelog
239 |
240 | Keep an eye on [changes](CHANGELOG.md).
241 |
242 | # License
243 |
244 | TableKit is available under the MIT license. See LICENSE for details.
245 |
--------------------------------------------------------------------------------
/Sources/ConfigurableCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | public protocol ConfigurableCell {
24 |
25 | associatedtype CellData
26 |
27 | static var reuseIdentifier: String { get }
28 | static var estimatedHeight: CGFloat? { get }
29 | static var defaultHeight: CGFloat? { get }
30 |
31 | func configure(with _: CellData)
32 | }
33 |
34 | public extension ConfigurableCell where Self: UITableViewCell {
35 |
36 | static var reuseIdentifier: String {
37 | return String(describing: self)
38 | }
39 |
40 | static var estimatedHeight: CGFloat? {
41 | return nil
42 | }
43 |
44 | static var defaultHeight: CGFloat? {
45 | return nil
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/Operators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | // --
22 | public func +=(left: TableDirector, right: TableSection) {
23 | left.append(section: right)
24 | }
25 |
26 | public func +=(left: TableDirector, right: [TableSection]) {
27 | left.append(sections: right)
28 | }
29 |
30 | // --
31 | public func +=(left: TableDirector, right: Row) {
32 | left.append(sections: [TableSection(rows: [right])])
33 | }
34 |
35 | public func +=(left: TableDirector, right: [Row]) {
36 | left.append(sections: [TableSection(rows: right)])
37 | }
38 |
39 | // --
40 | public func +=(left: TableSection, right: Row) {
41 | left.append(row: right)
42 | }
43 |
44 | public func +=(left: TableSection, right: [Row]) {
45 | left.append(rows: right)
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/TableCellAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | /**
24 | A custom action that you can trigger from your cell.
25 | You can easily catch actions using a chaining manner with your row.
26 | */
27 | open class TableCellAction {
28 |
29 | /// The cell that triggers an action.
30 | public let cell: UITableViewCell
31 |
32 | /// The action unique key.
33 | public let key: String
34 |
35 | /// The custom user info.
36 | public let userInfo: [AnyHashable: Any]?
37 |
38 | public init(key: String, sender: UITableViewCell, userInfo: [AnyHashable: Any]? = nil) {
39 |
40 | self.key = key
41 | self.cell = sender
42 | self.userInfo = userInfo
43 | }
44 |
45 | open func invoke() {
46 | NotificationCenter.default.post(name: Notification.Name(rawValue: TableKitNotifications.CellAction), object: self, userInfo: userInfo)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/TableCellRegisterer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | class TableCellRegisterer {
24 |
25 | private var registeredIds = Set()
26 | private weak var tableView: UITableView?
27 |
28 | init(tableView: UITableView?) {
29 | self.tableView = tableView
30 | }
31 |
32 | func register(cellType: AnyClass, forCellReuseIdentifier reuseIdentifier: String) {
33 |
34 | if registeredIds.contains(reuseIdentifier) {
35 | return
36 | }
37 |
38 | // check if cell is already registered, probably cell has been registered by storyboard
39 | if tableView?.dequeueReusableCell(withIdentifier: reuseIdentifier) != nil {
40 |
41 | registeredIds.insert(reuseIdentifier)
42 | return
43 | }
44 |
45 | let bundle = Bundle(for: cellType)
46 |
47 | // we hope that cell's xib file has name that equals to cell's class name
48 | // in that case we could register nib
49 | if let _ = bundle.path(forResource: reuseIdentifier, ofType: "nib") {
50 | tableView?.register(UINib(nibName: reuseIdentifier, bundle: bundle), forCellReuseIdentifier: reuseIdentifier)
51 | // otherwise, register cell class
52 | } else {
53 | tableView?.register(cellType, forCellReuseIdentifier: reuseIdentifier)
54 | }
55 |
56 | registeredIds.insert(reuseIdentifier)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/TableDirector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | /**
24 | Responsible for table view's datasource and delegate.
25 | */
26 | open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
27 |
28 | open private(set) weak var tableView: UITableView?
29 | open fileprivate(set) var sections = [TableSection]()
30 |
31 | private weak var scrollDelegate: UIScrollViewDelegate?
32 | private var cellRegisterer: TableCellRegisterer?
33 | public private(set) var rowHeightCalculator: RowHeightCalculator?
34 | private var sectionsIndexTitlesIndexes: [Int]?
35 |
36 | @available(*, deprecated, message: "Produced incorrect behaviour")
37 | open var shouldUsePrototypeCellHeightCalculation: Bool = false {
38 | didSet {
39 | if shouldUsePrototypeCellHeightCalculation {
40 | rowHeightCalculator = TablePrototypeCellHeightCalculator(tableView: tableView)
41 | } else {
42 | rowHeightCalculator = nil
43 | }
44 | }
45 | }
46 |
47 | open var isEmpty: Bool {
48 | return sections.isEmpty
49 | }
50 |
51 | public init(
52 | tableView: UITableView,
53 | scrollDelegate: UIScrollViewDelegate? = nil,
54 | shouldUseAutomaticCellRegistration: Bool = true,
55 | cellHeightCalculator: RowHeightCalculator?)
56 | {
57 | super.init()
58 |
59 | if shouldUseAutomaticCellRegistration {
60 | self.cellRegisterer = TableCellRegisterer(tableView: tableView)
61 | }
62 |
63 | self.rowHeightCalculator = cellHeightCalculator
64 | self.scrollDelegate = scrollDelegate
65 | self.tableView = tableView
66 | self.tableView?.delegate = self
67 | self.tableView?.dataSource = self
68 |
69 | NotificationCenter.default.addObserver(self, selector: #selector(didReceiveAction), name: NSNotification.Name(rawValue: TableKitNotifications.CellAction), object: nil)
70 | }
71 |
72 | public convenience init(
73 | tableView: UITableView,
74 | scrollDelegate: UIScrollViewDelegate? = nil,
75 | shouldUseAutomaticCellRegistration: Bool = true,
76 | shouldUsePrototypeCellHeightCalculation: Bool = false)
77 | {
78 | let heightCalculator: TablePrototypeCellHeightCalculator? = shouldUsePrototypeCellHeightCalculation
79 | ? TablePrototypeCellHeightCalculator(tableView: tableView)
80 | : nil
81 |
82 | self.init(
83 | tableView: tableView,
84 | scrollDelegate: scrollDelegate,
85 | shouldUseAutomaticCellRegistration: shouldUseAutomaticCellRegistration,
86 | cellHeightCalculator: heightCalculator
87 | )
88 | }
89 |
90 | deinit {
91 | NotificationCenter.default.removeObserver(self)
92 | }
93 |
94 | open func reload() {
95 | tableView?.reloadData()
96 | }
97 |
98 | // MARK: - Private
99 | private func row(at indexPath: IndexPath) -> Row? {
100 | if indexPath.section < sections.count && indexPath.row < sections[indexPath.section].rows.count {
101 | return sections[indexPath.section].rows[indexPath.row]
102 | }
103 | return nil
104 | }
105 |
106 | // MARK: Public
107 | @discardableResult
108 | open func invoke(
109 | action: TableRowActionType,
110 | cell: UITableViewCell?, indexPath: IndexPath,
111 | userInfo: [AnyHashable: Any]? = nil) -> Any?
112 | {
113 | guard let row = row(at: indexPath) else { return nil }
114 | return row.invoke(
115 | action: action,
116 | cell: cell,
117 | path: indexPath,
118 | userInfo: userInfo
119 | )
120 | }
121 |
122 | open override func responds(to selector: Selector) -> Bool {
123 | return super.responds(to: selector) || scrollDelegate?.responds(to: selector) == true
124 | }
125 |
126 | open override func forwardingTarget(for selector: Selector) -> Any? {
127 | return scrollDelegate?.responds(to: selector) == true
128 | ? scrollDelegate
129 | : super.forwardingTarget(for: selector)
130 | }
131 |
132 | // MARK: - Internal
133 | func hasAction(_ action: TableRowActionType, atIndexPath indexPath: IndexPath) -> Bool {
134 | guard let row = row(at: indexPath) else { return false }
135 | return row.has(action: action)
136 | }
137 |
138 | @objc
139 | func didReceiveAction(_ notification: Notification) {
140 |
141 | guard let action = notification.object as? TableCellAction, let indexPath = tableView?.indexPath(for: action.cell) else { return }
142 | invoke(action: .custom(action.key), cell: action.cell, indexPath: indexPath, userInfo: notification.userInfo)
143 | }
144 |
145 | // MARK: - Height
146 | open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
147 |
148 | let row = sections[indexPath.section].rows[indexPath.row]
149 |
150 | if rowHeightCalculator != nil {
151 | cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
152 | }
153 |
154 | return row.defaultHeight
155 | ?? row.estimatedHeight
156 | ?? rowHeightCalculator?.estimatedHeight(forRow: row, at: indexPath)
157 | ?? UITableView.automaticDimension
158 | }
159 |
160 | open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
161 |
162 | let row = sections[indexPath.section].rows[indexPath.row]
163 |
164 | if rowHeightCalculator != nil {
165 | cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
166 | }
167 |
168 | let rowHeight = invoke(action: .height, cell: nil, indexPath: indexPath) as? CGFloat
169 |
170 | return rowHeight
171 | ?? row.defaultHeight
172 | ?? rowHeightCalculator?.height(forRow: row, at: indexPath)
173 | ?? UITableView.automaticDimension
174 | }
175 |
176 | // MARK: UITableViewDataSource - configuration
177 | open func numberOfSections(in tableView: UITableView) -> Int {
178 | return sections.count
179 | }
180 |
181 | open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
182 | guard section < sections.count else { return 0 }
183 |
184 | return sections[section].numberOfRows
185 | }
186 |
187 | open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
188 |
189 | let row = sections[indexPath.section].rows[indexPath.row]
190 |
191 | cellRegisterer?.register(cellType: row.cellType, forCellReuseIdentifier: row.reuseIdentifier)
192 |
193 | let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath)
194 |
195 | if cell.frame.size.width != tableView.frame.size.width {
196 | cell.frame = CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: cell.frame.size.height)
197 | cell.layoutIfNeeded()
198 | }
199 |
200 | row.configure(cell)
201 | invoke(action: .configure, cell: cell, indexPath: indexPath)
202 |
203 | return cell
204 | }
205 |
206 | // MARK: UITableViewDataSource - section setup
207 | open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
208 | guard section < sections.count else { return nil }
209 |
210 | return sections[section].headerTitle
211 | }
212 |
213 | open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
214 | guard section < sections.count else { return nil }
215 |
216 | return sections[section].footerTitle
217 | }
218 |
219 | // MARK: UITableViewDelegate - section setup
220 | open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
221 | guard section < sections.count else { return nil }
222 |
223 | return sections[section].headerView
224 | }
225 |
226 | open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
227 | guard section < sections.count else { return nil }
228 |
229 | return sections[section].footerView
230 | }
231 |
232 | open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
233 | guard section < sections.count else { return 0 }
234 |
235 | let section = sections[section]
236 | return section.headerHeight ?? section.headerView?.frame.size.height ?? UITableView.automaticDimension
237 | }
238 |
239 | open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
240 | guard section < sections.count else { return 0 }
241 |
242 | let section = sections[section]
243 | return section.footerHeight
244 | ?? section.footerView?.frame.size.height
245 | ?? UITableView.automaticDimension
246 | }
247 |
248 | // MARK: UITableViewDataSource - Index
249 | public func sectionIndexTitles(for tableView: UITableView) -> [String]? {
250 |
251 | var indexTitles = [String]()
252 | var indexTitlesIndexes = [Int]()
253 | sections.enumerated().forEach { index, section in
254 |
255 | if let title = section.indexTitle {
256 | indexTitles.append(title)
257 | indexTitlesIndexes.append(index)
258 | }
259 | }
260 | if !indexTitles.isEmpty {
261 |
262 | sectionsIndexTitlesIndexes = indexTitlesIndexes
263 | return indexTitles
264 | }
265 | sectionsIndexTitlesIndexes = nil
266 | return nil
267 | }
268 |
269 | public func tableView(
270 | _ tableView: UITableView,
271 | sectionForSectionIndexTitle title: String,
272 | at index: Int) -> Int
273 | {
274 | return sectionsIndexTitlesIndexes?[index] ?? 0
275 | }
276 |
277 | // MARK: UITableViewDelegate - actions
278 | open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
279 | let cell = tableView.cellForRow(at: indexPath)
280 |
281 | if invoke(action: .click, cell: cell, indexPath: indexPath) != nil {
282 | tableView.deselectRow(at: indexPath, animated: true)
283 | } else {
284 | invoke(action: .select, cell: cell, indexPath: indexPath)
285 | }
286 | }
287 |
288 | open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
289 | invoke(action: .deselect, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath)
290 | }
291 |
292 | open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
293 | invoke(action: .willDisplay, cell: cell, indexPath: indexPath)
294 | }
295 |
296 | public func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
297 | invoke(action: .didEndDisplaying, cell: cell, indexPath: indexPath)
298 | }
299 |
300 | open func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
301 | return invoke(action: .shouldHighlight, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? Bool ?? true
302 | }
303 |
304 | open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
305 | if hasAction(.willSelect, atIndexPath: indexPath) {
306 | return invoke(action: .willSelect, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? IndexPath
307 | }
308 |
309 | return indexPath
310 | }
311 |
312 | open func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
313 | if hasAction(.willDeselect, atIndexPath: indexPath) {
314 | return invoke(action: .willDeselect, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? IndexPath
315 | }
316 |
317 | return indexPath
318 | }
319 |
320 | @available(iOS 13.0, *)
321 | open func tableView(
322 | _ tableView: UITableView,
323 | shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool
324 | {
325 | invoke(action: .shouldBeginMultipleSelection, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? Bool ?? false
326 | }
327 |
328 | @available(iOS 13.0, *)
329 | open func tableView(
330 | _ tableView: UITableView,
331 | didBeginMultipleSelectionInteractionAt indexPath: IndexPath)
332 | {
333 | invoke(action: .didBeginMultipleSelection, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath)
334 | }
335 |
336 | @available(iOS 13.0, *)
337 | open func tableView(
338 | _ tableView: UITableView,
339 | contextMenuConfigurationForRowAt indexPath: IndexPath,
340 | point: CGPoint) -> UIContextMenuConfiguration?
341 | {
342 | invoke(action: .showContextMenu, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath, userInfo: [TableKitUserInfoKeys.ContextMenuInvokePoint: point]) as? UIContextMenuConfiguration
343 | }
344 |
345 | // MARK: - Row editing
346 | open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
347 | return sections[indexPath.section].rows[indexPath.row].isEditingAllowed(forIndexPath: indexPath)
348 | }
349 |
350 | open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
351 | return sections[indexPath.section].rows[indexPath.row].editingActions
352 | }
353 |
354 | open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
355 | if invoke(action: .canDelete, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? Bool ?? false {
356 | return UITableViewCell.EditingStyle.delete
357 | }
358 |
359 | return UITableViewCell.EditingStyle.none
360 | }
361 |
362 | public func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
363 | return false
364 | }
365 |
366 | public func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
367 | return invoke(action: .canMoveTo, cell: tableView.cellForRow(at: sourceIndexPath), indexPath: sourceIndexPath, userInfo: [TableKitUserInfoKeys.CellCanMoveProposedIndexPath: proposedDestinationIndexPath]) as? IndexPath ?? proposedDestinationIndexPath
368 | }
369 |
370 | open func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
371 | if editingStyle == .delete {
372 | invoke(action: .clickDelete, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath)
373 | }
374 | }
375 |
376 | open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
377 | return invoke(action: .canMove, cell: tableView.cellForRow(at: indexPath), indexPath: indexPath) as? Bool ?? false
378 | }
379 |
380 | open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
381 | invoke(action: .move, cell: tableView.cellForRow(at: sourceIndexPath), indexPath: sourceIndexPath, userInfo: [TableKitUserInfoKeys.CellMoveDestinationIndexPath: destinationIndexPath])
382 | }
383 |
384 | open func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
385 | let cell = tableView.cellForRow(at: indexPath)
386 | invoke(action: .accessoryButtonTap, cell: cell, indexPath: indexPath)
387 | }
388 | }
389 |
390 | // MARK: - Sections manipulation
391 | extension TableDirector {
392 |
393 | @discardableResult
394 | open func append(section: TableSection) -> Self {
395 |
396 | append(sections: [section])
397 | return self
398 | }
399 |
400 | @discardableResult
401 | open func append(sections: [TableSection]) -> Self {
402 |
403 | self.sections.append(contentsOf: sections)
404 | return self
405 | }
406 |
407 | @discardableResult
408 | open func append(rows: [Row]) -> Self {
409 |
410 | append(section: TableSection(rows: rows))
411 | return self
412 | }
413 |
414 | @discardableResult
415 | open func insert(section: TableSection, atIndex index: Int) -> Self {
416 |
417 | sections.insert(section, at: index)
418 | return self
419 | }
420 |
421 | @discardableResult
422 | open func replaceSection(at index: Int, with section: TableSection) -> Self {
423 |
424 | if index < sections.count {
425 | sections[index] = section
426 | }
427 | return self
428 | }
429 |
430 | @discardableResult
431 | open func delete(sectionAt index: Int) -> Self {
432 |
433 | sections.remove(at: index)
434 | return self
435 | }
436 |
437 | @discardableResult
438 | open func remove(sectionAt index: Int) -> Self {
439 | return delete(sectionAt: index)
440 | }
441 |
442 | @discardableResult
443 | open func clear() -> Self {
444 |
445 | rowHeightCalculator?.invalidate()
446 | sections.removeAll()
447 |
448 | return self
449 | }
450 |
451 | // MARK: - deprecated methods
452 | @available(*, deprecated, message: "Use 'delete(sectionAt:)' method instead")
453 | @discardableResult
454 | open func delete(index: Int) -> Self {
455 |
456 | sections.remove(at: index)
457 | return self
458 | }
459 | }
460 |
--------------------------------------------------------------------------------
/Sources/TableKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | struct TableKitNotifications {
24 | static let CellAction = "TableKitNotificationsCellAction"
25 | }
26 |
27 | public struct TableKitUserInfoKeys {
28 | public static let CellMoveDestinationIndexPath = "TableKitCellMoveDestinationIndexPath"
29 | public static let CellCanMoveProposedIndexPath = "CellCanMoveProposedIndexPath"
30 | public static let ContextMenuInvokePoint = "ContextMenuInvokePoint"
31 | }
32 |
33 | public protocol RowConfigurable {
34 |
35 | func configure(_ cell: UITableViewCell)
36 | }
37 |
38 | public protocol RowActionable {
39 |
40 | var editingActions: [UITableViewRowAction]? { get }
41 | func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool
42 |
43 | func invoke(
44 | action: TableRowActionType,
45 | cell: UITableViewCell?,
46 | path: IndexPath,
47 | userInfo: [AnyHashable: Any]?) -> Any?
48 |
49 | func has(action: TableRowActionType) -> Bool
50 | }
51 |
52 | public protocol RowHashable {
53 |
54 | var hashValue: Int { get }
55 | }
56 |
57 | public protocol Row: RowConfigurable, RowActionable, RowHashable {
58 |
59 | var reuseIdentifier: String { get }
60 | var cellType: AnyClass { get }
61 |
62 | var estimatedHeight: CGFloat? { get }
63 | var defaultHeight: CGFloat? { get }
64 | }
65 |
66 | public enum TableRowActionType {
67 |
68 | case click
69 | case clickDelete
70 | case select
71 | case deselect
72 | case willSelect
73 | case willDeselect
74 | case willDisplay
75 | case didEndDisplaying
76 | case shouldHighlight
77 | case shouldBeginMultipleSelection
78 | case didBeginMultipleSelection
79 | case height
80 | case canEdit
81 | case configure
82 | case canDelete
83 | case canMove
84 | case canMoveTo
85 | case move
86 | case showContextMenu
87 | case accessoryButtonTap
88 | case custom(String)
89 |
90 | var key: String {
91 |
92 | switch (self) {
93 | case .custom(let key):
94 | return key
95 | default:
96 | return "_\(self)"
97 | }
98 | }
99 | }
100 |
101 | public protocol RowHeightCalculator {
102 |
103 | func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat
104 | func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat
105 |
106 | func invalidate()
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/TablePrototypeCellHeightCalculator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | open class TablePrototypeCellHeightCalculator: RowHeightCalculator {
24 |
25 | private(set) weak var tableView: UITableView?
26 | private var prototypes = [String: UITableViewCell]()
27 | private var cachedHeights = [Int: CGFloat]()
28 | private var separatorHeight = 1 / UIScreen.main.scale
29 |
30 | public init(tableView: UITableView?) {
31 | self.tableView = tableView
32 | }
33 |
34 | open func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
35 |
36 | guard let tableView = tableView else { return 0 }
37 |
38 | let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue
39 |
40 | if let height = cachedHeights[hash] {
41 | return height
42 | }
43 |
44 | var prototypeCell = prototypes[row.reuseIdentifier]
45 | if prototypeCell == nil {
46 |
47 | prototypeCell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier)
48 | prototypes[row.reuseIdentifier] = prototypeCell
49 | }
50 |
51 | guard let cell = prototypeCell else { return 0 }
52 |
53 | cell.prepareForReuse()
54 | row.configure(cell)
55 |
56 | cell.bounds = CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: cell.bounds.height)
57 | cell.setNeedsLayout()
58 | cell.layoutIfNeeded()
59 |
60 | let height = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + (tableView.separatorStyle != .none ? separatorHeight : 0)
61 |
62 | cachedHeights[hash] = height
63 |
64 | return height
65 | }
66 |
67 | open func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
68 |
69 | guard let tableView = tableView else { return 0 }
70 |
71 | let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue
72 |
73 | if let height = cachedHeights[hash] {
74 | return height
75 | }
76 |
77 | if let estimatedHeight = row.estimatedHeight , estimatedHeight > 0 {
78 | return estimatedHeight
79 | }
80 |
81 | return UITableView.automaticDimension
82 | }
83 |
84 | open func invalidate() {
85 | cachedHeights.removeAll()
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/TableRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | open class TableRow: Row where CellType: UITableViewCell {
24 |
25 | public let item: CellType.CellData
26 | private lazy var actions = [String: [TableRowAction]]()
27 | private(set) open var editingActions: [UITableViewRowAction]?
28 |
29 | open var hashValue: Int {
30 | return ObjectIdentifier(self).hashValue
31 | }
32 |
33 | open var reuseIdentifier: String {
34 | return CellType.reuseIdentifier
35 | }
36 |
37 | open var estimatedHeight: CGFloat? {
38 | return CellType.estimatedHeight
39 | }
40 |
41 | open var defaultHeight: CGFloat? {
42 | return CellType.defaultHeight
43 | }
44 |
45 | open var cellType: AnyClass {
46 | return CellType.self
47 | }
48 |
49 | public init(item: CellType.CellData, actions: [TableRowAction]? = nil, editingActions: [UITableViewRowAction]? = nil) {
50 |
51 | self.item = item
52 | self.editingActions = editingActions
53 | actions?.forEach { on($0) }
54 | }
55 |
56 | // MARK: - RowConfigurable -
57 |
58 | open func configure(_ cell: UITableViewCell) {
59 |
60 | (cell as? CellType)?.configure(with: item)
61 | }
62 |
63 | // MARK: - RowActionable -
64 |
65 | open func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? {
66 |
67 | return actions[action.key]?.compactMap({ $0.invokeActionOn(cell: cell, item: item, path: path, userInfo: userInfo) }).last
68 | }
69 |
70 | open func has(action: TableRowActionType) -> Bool {
71 |
72 | return actions[action.key] != nil
73 | }
74 |
75 | open func isEditingAllowed(forIndexPath indexPath: IndexPath) -> Bool {
76 |
77 | if actions[TableRowActionType.canEdit.key] != nil {
78 | return invoke(action: .canEdit, cell: nil, path: indexPath) as? Bool ?? false
79 | }
80 | return editingActions?.isEmpty == false || actions[TableRowActionType.clickDelete.key] != nil
81 | }
82 |
83 | // MARK: - actions -
84 |
85 | @discardableResult
86 | open func on(_ action: TableRowAction) -> Self {
87 |
88 | if actions[action.type.key] == nil {
89 | actions[action.type.key] = [TableRowAction]()
90 | }
91 | actions[action.type.key]?.append(action)
92 |
93 | return self
94 | }
95 |
96 | @discardableResult
97 | open func on(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self {
98 |
99 | return on(TableRowAction(type, handler: handler))
100 | }
101 |
102 | @discardableResult
103 | open func on(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> ()) -> Self {
104 |
105 | return on(TableRowAction(.custom(key), handler: handler))
106 | }
107 |
108 | open func removeAllActions() {
109 |
110 | actions.removeAll()
111 | }
112 |
113 | open func removeAction(forActionId actionId: String) {
114 |
115 | for (key, value) in actions {
116 | if let actionIndex = value.firstIndex(where: { $0.id == actionId }) {
117 | actions[key]?.remove(at: actionIndex)
118 | }
119 | }
120 | }
121 |
122 | // MARK: - deprecated actions -
123 |
124 | @available(*, deprecated, message: "Use 'on' method instead")
125 | @discardableResult
126 | open func action(_ action: TableRowAction) -> Self {
127 |
128 | return on(action)
129 | }
130 |
131 | @available(*, deprecated, message: "Use 'on' method instead")
132 | @discardableResult
133 | open func action(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) -> Self {
134 |
135 | return on(TableRowAction(type, handler: handler))
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Sources/TableRowAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | open class TableRowActionOptions where CellType: UITableViewCell {
24 |
25 | public let item: CellType.CellData
26 | public let cell: CellType?
27 | public let indexPath: IndexPath
28 | public let userInfo: [AnyHashable: Any]?
29 |
30 | init(item: CellType.CellData, cell: CellType?, path: IndexPath, userInfo: [AnyHashable: Any]?) {
31 |
32 | self.item = item
33 | self.cell = cell
34 | self.indexPath = path
35 | self.userInfo = userInfo
36 | }
37 | }
38 |
39 | private enum TableRowActionHandler where CellType: UITableViewCell {
40 |
41 | case voidAction((TableRowActionOptions) -> Void)
42 | case action((TableRowActionOptions) -> Any?)
43 |
44 | func invoke(withOptions options: TableRowActionOptions) -> Any? {
45 |
46 | switch self {
47 | case .voidAction(let handler):
48 | return handler(options)
49 | case .action(let handler):
50 | return handler(options)
51 | }
52 | }
53 | }
54 |
55 | open class TableRowAction where CellType: UITableViewCell {
56 |
57 | open var id: String?
58 | public let type: TableRowActionType
59 | private let handler: TableRowActionHandler
60 |
61 | public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> Void) {
62 |
63 | self.type = type
64 | self.handler = .voidAction(handler)
65 | }
66 |
67 | public init(_ key: String, handler: @escaping (_ options: TableRowActionOptions) -> Void) {
68 |
69 | self.type = .custom(key)
70 | self.handler = .voidAction(handler)
71 | }
72 |
73 | public init(_ type: TableRowActionType, handler: @escaping (_ options: TableRowActionOptions) -> T) {
74 |
75 | self.type = type
76 | self.handler = .action(handler)
77 | }
78 |
79 | public func invokeActionOn(cell: UITableViewCell?, item: CellType.CellData, path: IndexPath, userInfo: [AnyHashable: Any]?) -> Any? {
80 |
81 | return handler.invoke(withOptions: TableRowActionOptions(item: item, cell: cell as? CellType, path: path, userInfo: userInfo))
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/TableSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import UIKit
22 |
23 | open class TableSection {
24 |
25 | open private(set) var rows = [Row]()
26 |
27 | open var headerTitle: String?
28 | open var footerTitle: String?
29 | open var indexTitle: String?
30 |
31 | open var headerView: UIView?
32 | open var footerView: UIView?
33 |
34 | open var headerHeight: CGFloat? = nil
35 | open var footerHeight: CGFloat? = nil
36 |
37 | open var numberOfRows: Int {
38 | return rows.count
39 | }
40 |
41 | open var isEmpty: Bool {
42 | return rows.isEmpty
43 | }
44 |
45 | public init(rows: [Row]? = nil) {
46 |
47 | if let initialRows = rows {
48 | self.rows.append(contentsOf: initialRows)
49 | }
50 | }
51 |
52 | public convenience init(headerTitle: String?, footerTitle: String?, rows: [Row]? = nil) {
53 | self.init(rows: rows)
54 |
55 | self.headerTitle = headerTitle
56 | self.footerTitle = footerTitle
57 | }
58 |
59 | public convenience init(headerView: UIView?, footerView: UIView?, rows: [Row]? = nil) {
60 | self.init(rows: rows)
61 |
62 | self.headerView = headerView
63 | self.footerView = footerView
64 | }
65 |
66 | // MARK: - Public -
67 |
68 | open func clear() {
69 | rows.removeAll()
70 | }
71 |
72 | open func append(row: Row) {
73 | append(rows: [row])
74 | }
75 |
76 | open func append(rows: [Row]) {
77 | self.rows.append(contentsOf: rows)
78 | }
79 |
80 | open func insert(row: Row, at index: Int) {
81 | rows.insert(row, at: index)
82 | }
83 |
84 | open func insert(rows: [Row], at index: Int) {
85 | self.rows.insert(contentsOf: rows, at: index)
86 | }
87 |
88 | open func replace(rowAt index: Int, with row: Row) {
89 | rows[index] = row
90 | }
91 |
92 | open func swap(from: Int, to: Int) {
93 | rows.swapAt(from, to)
94 | }
95 |
96 | open func delete(rowAt index: Int) {
97 | rows.remove(at: index)
98 | }
99 |
100 | open func remove(rowAt index: Int) {
101 | rows.remove(at: index)
102 | }
103 |
104 | // MARK: - deprecated methods -
105 |
106 | @available(*, deprecated, message: "Use 'delete(rowAt:)' method instead")
107 | open func delete(index: Int) {
108 | rows.remove(at: index)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/TableKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'TableKit'
3 | s.module_name = 'TableKit'
4 |
5 | s.version = '2.11.0'
6 |
7 | s.homepage = 'https://github.com/maxsokolov/TableKit'
8 | s.summary = 'Type-safe declarative table views with Swift.'
9 |
10 | s.author = { 'Max Sokolov' => 'i@maxsokolov.net' }
11 | s.license = { :type => 'MIT', :file => 'LICENSE' }
12 | s.platforms = { :ios => '8.0' }
13 | s.ios.deployment_target = '8.0'
14 |
15 | s.source_files = 'Sources/*.swift'
16 | s.source = { :git => 'https://github.com/maxsokolov/TableKit.git', :tag => s.version }
17 | end
18 |
--------------------------------------------------------------------------------
/TableKit.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */; };
11 | 50E858581DB153F500A9AA55 /* TableKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E858571DB153F500A9AA55 /* TableKit.swift */; };
12 | DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */; };
13 | DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */; };
14 | DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A81D0EC2C90021F650 /* Operators.swift */; };
15 | DA9EA7B21D0EC2C90021F650 /* TableCellAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */; };
16 | DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */; };
17 | DA9EA7B41D0EC2C90021F650 /* TableRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */; };
18 | DA9EA7B51D0EC2C90021F650 /* TableRowAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */; };
19 | DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */; };
20 | DA9EA7C91D0EC45F0021F650 /* TableKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA9EA7561D0B679A0021F650 /* TableKit.framework */; };
21 | DA9EA7CF1D0EC4930021F650 /* TableKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7BE1D0EC41D0021F650 /* TableKitTests.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXContainerItemProxy section */
25 | DA9EA7CA1D0EC45F0021F650 /* PBXContainerItemProxy */ = {
26 | isa = PBXContainerItemProxy;
27 | containerPortal = DA9EA74D1D0B679A0021F650 /* Project object */;
28 | proxyType = 1;
29 | remoteGlobalIDString = DA9EA7551D0B679A0021F650;
30 | remoteInfo = TableKit;
31 | };
32 | /* End PBXContainerItemProxy section */
33 |
34 | /* Begin PBXFileReference section */
35 | 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellRegisterer.swift; sourceTree = ""; };
36 | 50E858571DB153F500A9AA55 /* TableKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKit.swift; sourceTree = ""; };
37 | DA9EA7561D0B679A0021F650 /* TableKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TableKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
38 | DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = ""; };
39 | DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TablePrototypeCellHeightCalculator.swift; sourceTree = ""; };
40 | DA9EA7A81D0EC2C90021F650 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; };
41 | DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellAction.swift; sourceTree = ""; };
42 | DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableDirector.swift; sourceTree = ""; };
43 | DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRow.swift; sourceTree = ""; };
44 | DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRowAction.swift; sourceTree = ""; };
45 | DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableSection.swift; sourceTree = ""; };
46 | DA9EA7B91D0EC34E0021F650 /* TableKit.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TableKit.plist; sourceTree = ""; };
47 | DA9EA7BA1D0EC34E0021F650 /* TableKitTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TableKitTests.plist; sourceTree = ""; };
48 | DA9EA7BE1D0EC41D0021F650 /* TableKitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKitTests.swift; sourceTree = ""; };
49 | DA9EA7C41D0EC45F0021F650 /* TableKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TableKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | DA9EA7521D0B679A0021F650 /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | );
58 | runOnlyForDeploymentPostprocessing = 0;
59 | };
60 | DA9EA7C11D0EC45F0021F650 /* Frameworks */ = {
61 | isa = PBXFrameworksBuildPhase;
62 | buildActionMask = 2147483647;
63 | files = (
64 | DA9EA7C91D0EC45F0021F650 /* TableKit.framework in Frameworks */,
65 | );
66 | runOnlyForDeploymentPostprocessing = 0;
67 | };
68 | /* End PBXFrameworksBuildPhase section */
69 |
70 | /* Begin PBXGroup section */
71 | DA9EA74C1D0B679A0021F650 = {
72 | isa = PBXGroup;
73 | children = (
74 | DA9EA7B81D0EC31B0021F650 /* Configs */,
75 | DA9EA7571D0B679A0021F650 /* Products */,
76 | DA9EA7A51D0EC2B90021F650 /* Sources */,
77 | DA9EA7BD1D0EC3D70021F650 /* Tests */,
78 | );
79 | sourceTree = "";
80 | };
81 | DA9EA7571D0B679A0021F650 /* Products */ = {
82 | isa = PBXGroup;
83 | children = (
84 | DA9EA7561D0B679A0021F650 /* TableKit.framework */,
85 | DA9EA7C41D0EC45F0021F650 /* TableKitTests.xctest */,
86 | );
87 | name = Products;
88 | sourceTree = "";
89 | };
90 | DA9EA7A51D0EC2B90021F650 /* Sources */ = {
91 | isa = PBXGroup;
92 | children = (
93 | 50E858571DB153F500A9AA55 /* TableKit.swift */,
94 | DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */,
95 | 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */,
96 | DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */,
97 | DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */,
98 | DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */,
99 | DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */,
100 | DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */,
101 | DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */,
102 | DA9EA7A81D0EC2C90021F650 /* Operators.swift */,
103 | );
104 | path = Sources;
105 | sourceTree = "";
106 | };
107 | DA9EA7B81D0EC31B0021F650 /* Configs */ = {
108 | isa = PBXGroup;
109 | children = (
110 | DA9EA7B91D0EC34E0021F650 /* TableKit.plist */,
111 | DA9EA7BA1D0EC34E0021F650 /* TableKitTests.plist */,
112 | );
113 | path = Configs;
114 | sourceTree = "";
115 | };
116 | DA9EA7BD1D0EC3D70021F650 /* Tests */ = {
117 | isa = PBXGroup;
118 | children = (
119 | DA9EA7BE1D0EC41D0021F650 /* TableKitTests.swift */,
120 | );
121 | path = Tests;
122 | sourceTree = "";
123 | };
124 | /* End PBXGroup section */
125 |
126 | /* Begin PBXHeadersBuildPhase section */
127 | DA9EA7531D0B679A0021F650 /* Headers */ = {
128 | isa = PBXHeadersBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | );
132 | runOnlyForDeploymentPostprocessing = 0;
133 | };
134 | /* End PBXHeadersBuildPhase section */
135 |
136 | /* Begin PBXNativeTarget section */
137 | DA9EA7551D0B679A0021F650 /* TableKit */ = {
138 | isa = PBXNativeTarget;
139 | buildConfigurationList = DA9EA75E1D0B679A0021F650 /* Build configuration list for PBXNativeTarget "TableKit" */;
140 | buildPhases = (
141 | DA9EA7511D0B679A0021F650 /* Sources */,
142 | DA9EA7521D0B679A0021F650 /* Frameworks */,
143 | DA9EA7531D0B679A0021F650 /* Headers */,
144 | DA9EA7541D0B679A0021F650 /* Resources */,
145 | );
146 | buildRules = (
147 | );
148 | dependencies = (
149 | );
150 | name = TableKit;
151 | productName = TableKit;
152 | productReference = DA9EA7561D0B679A0021F650 /* TableKit.framework */;
153 | productType = "com.apple.product-type.framework";
154 | };
155 | DA9EA7C31D0EC45F0021F650 /* TableKitTests */ = {
156 | isa = PBXNativeTarget;
157 | buildConfigurationList = DA9EA7CC1D0EC45F0021F650 /* Build configuration list for PBXNativeTarget "TableKitTests" */;
158 | buildPhases = (
159 | DA9EA7C01D0EC45F0021F650 /* Sources */,
160 | DA9EA7C11D0EC45F0021F650 /* Frameworks */,
161 | DA9EA7C21D0EC45F0021F650 /* Resources */,
162 | );
163 | buildRules = (
164 | );
165 | dependencies = (
166 | DA9EA7CB1D0EC45F0021F650 /* PBXTargetDependency */,
167 | );
168 | name = TableKitTests;
169 | productName = TableKitTests;
170 | productReference = DA9EA7C41D0EC45F0021F650 /* TableKitTests.xctest */;
171 | productType = "com.apple.product-type.bundle.unit-test";
172 | };
173 | /* End PBXNativeTarget section */
174 |
175 | /* Begin PBXProject section */
176 | DA9EA74D1D0B679A0021F650 /* Project object */ = {
177 | isa = PBXProject;
178 | attributes = {
179 | LastSwiftUpdateCheck = 0730;
180 | LastUpgradeCheck = 1000;
181 | ORGANIZATIONNAME = "Max Sokolov";
182 | TargetAttributes = {
183 | DA9EA7551D0B679A0021F650 = {
184 | CreatedOnToolsVersion = 7.3;
185 | LastSwiftMigration = 1000;
186 | };
187 | DA9EA7C31D0EC45F0021F650 = {
188 | CreatedOnToolsVersion = 7.3;
189 | LastSwiftMigration = 1000;
190 | };
191 | };
192 | };
193 | buildConfigurationList = DA9EA7501D0B679A0021F650 /* Build configuration list for PBXProject "TableKit" */;
194 | compatibilityVersion = "Xcode 3.2";
195 | developmentRegion = English;
196 | hasScannedForEncodings = 0;
197 | knownRegions = (
198 | English,
199 | en,
200 | );
201 | mainGroup = DA9EA74C1D0B679A0021F650;
202 | productRefGroup = DA9EA7571D0B679A0021F650 /* Products */;
203 | projectDirPath = "";
204 | projectRoot = "";
205 | targets = (
206 | DA9EA7551D0B679A0021F650 /* TableKit */,
207 | DA9EA7C31D0EC45F0021F650 /* TableKitTests */,
208 | );
209 | };
210 | /* End PBXProject section */
211 |
212 | /* Begin PBXResourcesBuildPhase section */
213 | DA9EA7541D0B679A0021F650 /* Resources */ = {
214 | isa = PBXResourcesBuildPhase;
215 | buildActionMask = 2147483647;
216 | files = (
217 | );
218 | runOnlyForDeploymentPostprocessing = 0;
219 | };
220 | DA9EA7C21D0EC45F0021F650 /* Resources */ = {
221 | isa = PBXResourcesBuildPhase;
222 | buildActionMask = 2147483647;
223 | files = (
224 | );
225 | runOnlyForDeploymentPostprocessing = 0;
226 | };
227 | /* End PBXResourcesBuildPhase section */
228 |
229 | /* Begin PBXSourcesBuildPhase section */
230 | DA9EA7511D0B679A0021F650 /* Sources */ = {
231 | isa = PBXSourcesBuildPhase;
232 | buildActionMask = 2147483647;
233 | files = (
234 | 50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */,
235 | DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */,
236 | DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */,
237 | DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */,
238 | DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */,
239 | DA9EA7B51D0EC2C90021F650 /* TableRowAction.swift in Sources */,
240 | DA9EA7B21D0EC2C90021F650 /* TableCellAction.swift in Sources */,
241 | DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */,
242 | DA9EA7B41D0EC2C90021F650 /* TableRow.swift in Sources */,
243 | 50E858581DB153F500A9AA55 /* TableKit.swift in Sources */,
244 | );
245 | runOnlyForDeploymentPostprocessing = 0;
246 | };
247 | DA9EA7C01D0EC45F0021F650 /* Sources */ = {
248 | isa = PBXSourcesBuildPhase;
249 | buildActionMask = 2147483647;
250 | files = (
251 | DA9EA7CF1D0EC4930021F650 /* TableKitTests.swift in Sources */,
252 | );
253 | runOnlyForDeploymentPostprocessing = 0;
254 | };
255 | /* End PBXSourcesBuildPhase section */
256 |
257 | /* Begin PBXTargetDependency section */
258 | DA9EA7CB1D0EC45F0021F650 /* PBXTargetDependency */ = {
259 | isa = PBXTargetDependency;
260 | target = DA9EA7551D0B679A0021F650 /* TableKit */;
261 | targetProxy = DA9EA7CA1D0EC45F0021F650 /* PBXContainerItemProxy */;
262 | };
263 | /* End PBXTargetDependency section */
264 |
265 | /* Begin XCBuildConfiguration section */
266 | DA9EA75C1D0B679A0021F650 /* Debug */ = {
267 | isa = XCBuildConfiguration;
268 | buildSettings = {
269 | ALWAYS_SEARCH_USER_PATHS = NO;
270 | CLANG_ANALYZER_NONNULL = YES;
271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
272 | CLANG_CXX_LIBRARY = "libc++";
273 | CLANG_ENABLE_MODULES = YES;
274 | CLANG_ENABLE_OBJC_ARC = YES;
275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
276 | CLANG_WARN_BOOL_CONVERSION = YES;
277 | CLANG_WARN_COMMA = YES;
278 | CLANG_WARN_CONSTANT_CONVERSION = YES;
279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
281 | CLANG_WARN_EMPTY_BODY = YES;
282 | CLANG_WARN_ENUM_CONVERSION = YES;
283 | CLANG_WARN_INFINITE_RECURSION = YES;
284 | CLANG_WARN_INT_CONVERSION = YES;
285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
286 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
287 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
288 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
289 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
290 | CLANG_WARN_STRICT_PROTOTYPES = YES;
291 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
292 | CLANG_WARN_UNREACHABLE_CODE = YES;
293 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
294 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
295 | COPY_PHASE_STRIP = NO;
296 | CURRENT_PROJECT_VERSION = 1;
297 | DEBUG_INFORMATION_FORMAT = dwarf;
298 | ENABLE_STRICT_OBJC_MSGSEND = YES;
299 | ENABLE_TESTABILITY = YES;
300 | GCC_C_LANGUAGE_STANDARD = gnu99;
301 | GCC_DYNAMIC_NO_PIC = NO;
302 | GCC_NO_COMMON_BLOCKS = YES;
303 | GCC_OPTIMIZATION_LEVEL = 0;
304 | GCC_PREPROCESSOR_DEFINITIONS = (
305 | "DEBUG=1",
306 | "$(inherited)",
307 | );
308 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
309 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
310 | GCC_WARN_UNDECLARED_SELECTOR = YES;
311 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
312 | GCC_WARN_UNUSED_FUNCTION = YES;
313 | GCC_WARN_UNUSED_VARIABLE = YES;
314 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
315 | MTL_ENABLE_DEBUG_INFO = YES;
316 | ONLY_ACTIVE_ARCH = YES;
317 | SDKROOT = iphoneos;
318 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
319 | SWIFT_VERSION = 5.0;
320 | TARGETED_DEVICE_FAMILY = "1,2";
321 | VERSIONING_SYSTEM = "apple-generic";
322 | VERSION_INFO_PREFIX = "";
323 | };
324 | name = Debug;
325 | };
326 | DA9EA75D1D0B679A0021F650 /* Release */ = {
327 | isa = XCBuildConfiguration;
328 | buildSettings = {
329 | ALWAYS_SEARCH_USER_PATHS = NO;
330 | CLANG_ANALYZER_NONNULL = YES;
331 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
332 | CLANG_CXX_LIBRARY = "libc++";
333 | CLANG_ENABLE_MODULES = YES;
334 | CLANG_ENABLE_OBJC_ARC = YES;
335 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
336 | CLANG_WARN_BOOL_CONVERSION = YES;
337 | CLANG_WARN_COMMA = YES;
338 | CLANG_WARN_CONSTANT_CONVERSION = YES;
339 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
341 | CLANG_WARN_EMPTY_BODY = YES;
342 | CLANG_WARN_ENUM_CONVERSION = YES;
343 | CLANG_WARN_INFINITE_RECURSION = YES;
344 | CLANG_WARN_INT_CONVERSION = YES;
345 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
346 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
347 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
349 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
350 | CLANG_WARN_STRICT_PROTOTYPES = YES;
351 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
352 | CLANG_WARN_UNREACHABLE_CODE = YES;
353 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
354 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
355 | COPY_PHASE_STRIP = NO;
356 | CURRENT_PROJECT_VERSION = 1;
357 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
358 | ENABLE_NS_ASSERTIONS = NO;
359 | ENABLE_STRICT_OBJC_MSGSEND = YES;
360 | GCC_C_LANGUAGE_STANDARD = gnu99;
361 | GCC_NO_COMMON_BLOCKS = YES;
362 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
363 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
364 | GCC_WARN_UNDECLARED_SELECTOR = YES;
365 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
366 | GCC_WARN_UNUSED_FUNCTION = YES;
367 | GCC_WARN_UNUSED_VARIABLE = YES;
368 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
369 | MTL_ENABLE_DEBUG_INFO = NO;
370 | SDKROOT = iphoneos;
371 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
372 | SWIFT_VERSION = 5.0;
373 | TARGETED_DEVICE_FAMILY = "1,2";
374 | VALIDATE_PRODUCT = YES;
375 | VERSIONING_SYSTEM = "apple-generic";
376 | VERSION_INFO_PREFIX = "";
377 | };
378 | name = Release;
379 | };
380 | DA9EA75F1D0B679A0021F650 /* Debug */ = {
381 | isa = XCBuildConfiguration;
382 | buildSettings = {
383 | CLANG_ENABLE_MODULES = YES;
384 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
385 | DEFINES_MODULE = YES;
386 | DYLIB_COMPATIBILITY_VERSION = 1;
387 | DYLIB_CURRENT_VERSION = 1;
388 | DYLIB_INSTALL_NAME_BASE = "@rpath";
389 | INFOPLIST_FILE = "$(SRCROOT)/Configs/TableKit.plist";
390 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
391 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
392 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
393 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.TableKit;
394 | PRODUCT_NAME = "$(TARGET_NAME)";
395 | SKIP_INSTALL = YES;
396 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
397 | SWIFT_VERSION = 5.0;
398 | };
399 | name = Debug;
400 | };
401 | DA9EA7601D0B679A0021F650 /* Release */ = {
402 | isa = XCBuildConfiguration;
403 | buildSettings = {
404 | CLANG_ENABLE_MODULES = YES;
405 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
406 | DEFINES_MODULE = YES;
407 | DYLIB_COMPATIBILITY_VERSION = 1;
408 | DYLIB_CURRENT_VERSION = 1;
409 | DYLIB_INSTALL_NAME_BASE = "@rpath";
410 | INFOPLIST_FILE = "$(SRCROOT)/Configs/TableKit.plist";
411 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
412 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
413 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
414 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.TableKit;
415 | PRODUCT_NAME = "$(TARGET_NAME)";
416 | SKIP_INSTALL = YES;
417 | SWIFT_VERSION = 5.0;
418 | };
419 | name = Release;
420 | };
421 | DA9EA7CD1D0EC45F0021F650 /* Debug */ = {
422 | isa = XCBuildConfiguration;
423 | buildSettings = {
424 | INFOPLIST_FILE = Configs/TableKitTests.plist;
425 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
426 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
427 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.TableKitTests;
428 | PRODUCT_NAME = "$(TARGET_NAME)";
429 | SWIFT_VERSION = 5.0;
430 | };
431 | name = Debug;
432 | };
433 | DA9EA7CE1D0EC45F0021F650 /* Release */ = {
434 | isa = XCBuildConfiguration;
435 | buildSettings = {
436 | INFOPLIST_FILE = Configs/TableKitTests.plist;
437 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
438 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
439 | PRODUCT_BUNDLE_IDENTIFIER = com.tablekit.TableKitTests;
440 | PRODUCT_NAME = "$(TARGET_NAME)";
441 | SWIFT_VERSION = 5.0;
442 | };
443 | name = Release;
444 | };
445 | /* End XCBuildConfiguration section */
446 |
447 | /* Begin XCConfigurationList section */
448 | DA9EA7501D0B679A0021F650 /* Build configuration list for PBXProject "TableKit" */ = {
449 | isa = XCConfigurationList;
450 | buildConfigurations = (
451 | DA9EA75C1D0B679A0021F650 /* Debug */,
452 | DA9EA75D1D0B679A0021F650 /* Release */,
453 | );
454 | defaultConfigurationIsVisible = 0;
455 | defaultConfigurationName = Release;
456 | };
457 | DA9EA75E1D0B679A0021F650 /* Build configuration list for PBXNativeTarget "TableKit" */ = {
458 | isa = XCConfigurationList;
459 | buildConfigurations = (
460 | DA9EA75F1D0B679A0021F650 /* Debug */,
461 | DA9EA7601D0B679A0021F650 /* Release */,
462 | );
463 | defaultConfigurationIsVisible = 0;
464 | defaultConfigurationName = Release;
465 | };
466 | DA9EA7CC1D0EC45F0021F650 /* Build configuration list for PBXNativeTarget "TableKitTests" */ = {
467 | isa = XCConfigurationList;
468 | buildConfigurations = (
469 | DA9EA7CD1D0EC45F0021F650 /* Debug */,
470 | DA9EA7CE1D0EC45F0021F650 /* Release */,
471 | );
472 | defaultConfigurationIsVisible = 0;
473 | defaultConfigurationName = Release;
474 | };
475 | /* End XCConfigurationList section */
476 | };
477 | rootObject = DA9EA74D1D0B679A0021F650 /* Project object */;
478 | }
479 |
--------------------------------------------------------------------------------
/TableKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TableKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TableKit.xcodeproj/xcshareddata/xcschemes/TableKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Tests/TableKitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015 Max Sokolov https://twitter.com/max_sokolov
3 | //
4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | // this software and associated documentation files (the "Software"), to deal in
6 | // the Software without restriction, including without limitation the rights to
7 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | // the Software, and to permit persons to whom the Software is furnished to do so,
9 | // subject to the following conditions:
10 | //
11 | // The above copyright notice and this permission notice shall be included in all
12 | // copies or substantial portions of the Software.
13 | //
14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 |
21 | import XCTest
22 | import TableKit
23 |
24 | class TestController: UITableViewController {
25 |
26 | var tableDirector: TableDirector!
27 |
28 | override func viewDidLoad() {
29 | super.viewDidLoad()
30 | tableDirector = TableDirector(tableView: tableView)
31 | }
32 | }
33 |
34 | struct TestData {
35 |
36 | let title: String
37 | }
38 |
39 | struct TestTableViewCellOptions {
40 |
41 | static let ReusableIdentifier: String = "ReusableIdentifier"
42 | static let CellAction: String = "CellAction"
43 | static let CellActionUserInfoKey: String = "CellActionUserInfoKey"
44 | static let CellActionUserInfoValue: String = "CellActionUserInfoValue"
45 | static let EstimatedHeight: CGFloat = 255
46 | }
47 |
48 | class TestTableViewCell: UITableViewCell, ConfigurableCell {
49 |
50 | typealias T = TestData
51 |
52 | static var estimatedHeight: CGFloat? {
53 | return TestTableViewCellOptions.EstimatedHeight
54 | }
55 |
56 | static var reuseIdentifier: String {
57 | return TestTableViewCellOptions.ReusableIdentifier
58 | }
59 |
60 | func configure(with item: T) {
61 | textLabel?.text = item.title
62 | }
63 |
64 | func raiseAction() {
65 | TableCellAction(key: TestTableViewCellOptions.CellAction, sender: self, userInfo: nil).invoke()
66 | }
67 | }
68 |
69 | class TableKitTests: XCTestCase {
70 |
71 | var testController: TestController!
72 |
73 | override func setUp() {
74 | super.setUp()
75 |
76 | testController = TestController()
77 | testController.tableView.frame = UIScreen.main.bounds
78 | testController.tableView.isHidden = false
79 | testController.tableView.setNeedsLayout()
80 | testController.tableView.layoutIfNeeded()
81 | }
82 |
83 | override func tearDown() {
84 |
85 | testController = nil
86 | super.tearDown()
87 | }
88 |
89 | func testTableDirectorHasTableView() {
90 |
91 | XCTAssertNotNil(testController.tableView, "TestController should have table view")
92 | XCTAssertNotNil(testController.tableDirector, "TestController should have table director")
93 | XCTAssertNotNil(testController.tableDirector.tableView, "TableDirector should have table view")
94 | }
95 |
96 | func testRowInSection() {
97 |
98 | let data = TestData(title: "title")
99 |
100 | let row = TableRow(item: data)
101 |
102 | testController.tableDirector += row
103 | testController.tableView.reloadData()
104 |
105 | XCTAssertTrue(testController.tableView.dataSource?.numberOfSections?(in: testController.tableView) == 1, "Table view should have a section")
106 | XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == 1, "Table view should have certain number of rows in a section")
107 |
108 | let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
109 | XCTAssertNotNil(cell)
110 | XCTAssertTrue(cell?.textLabel?.text == data.title)
111 | }
112 |
113 | func testManyRowsInSection() {
114 |
115 | let data = [TestData(title: "1"), TestData(title: "2"), TestData(title: "3")]
116 |
117 | let rows: [Row] = data.map({ TableRow(item: $0) })
118 |
119 | testController.tableDirector += rows
120 | testController.tableView.reloadData()
121 |
122 | XCTAssertTrue(testController.tableView.dataSource?.numberOfSections?(in: testController.tableView) == 1, "Table view should have a section")
123 | XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == data.count, "Table view should have certain number of rows in a section")
124 |
125 | for (index, element) in data.enumerated() {
126 |
127 | let cell = testController.tableView.cellForRow(at: IndexPath(row: index, section: 0)) as? TestTableViewCell
128 | XCTAssertNotNil(cell)
129 | XCTAssertTrue(cell?.textLabel?.text == element.title)
130 | }
131 | }
132 |
133 | func testTableSectionCreatesSectionWithHeaderAndFooterTitles() {
134 |
135 | let row = TableRow(item: TestData(title: "title"))
136 |
137 | let sectionHeaderTitle = "Header Title"
138 | let sectionFooterTitle = "Footer Title"
139 |
140 | let section = TableSection(headerTitle: sectionHeaderTitle, footerTitle: sectionFooterTitle, rows: [row])
141 |
142 | testController.tableDirector += section
143 | testController.tableView.reloadData()
144 |
145 | XCTAssertTrue(testController.tableView.dataSource?.numberOfSections?(in: testController.tableView) == 1, "Table view should have a section")
146 | XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == 1, "Table view should have certain number of rows in a section")
147 |
148 | XCTAssertTrue(testController.tableView.dataSource?.tableView?(testController.tableView, titleForHeaderInSection: 0) == sectionHeaderTitle)
149 | XCTAssertTrue(testController.tableView.dataSource?.tableView?(testController.tableView, titleForFooterInSection: 0) == sectionFooterTitle)
150 | }
151 |
152 | func testTableSectionCreatesSectionWithHeaderAndFooterViews() {
153 |
154 | let row = TableRow(item: TestData(title: "title"))
155 |
156 | let sectionHeaderView = UIView()
157 | let sectionFooterView = UIView()
158 |
159 | let section = TableSection(headerView: sectionHeaderView, footerView: sectionFooterView, rows: nil)
160 | section += row
161 |
162 | testController.tableDirector += section
163 | testController.tableView.reloadData()
164 |
165 | XCTAssertTrue(testController.tableView.dataSource?.numberOfSections?(in: testController.tableView) == 1, "Table view should have a section")
166 | XCTAssertTrue(testController.tableView.dataSource?.tableView(testController.tableView, numberOfRowsInSection: 0) == 1, "Table view should have certain number of rows in a section")
167 |
168 | XCTAssertTrue(testController.tableView.delegate?.tableView?(testController.tableView, viewForHeaderInSection: 0) == sectionHeaderView)
169 | XCTAssertTrue(testController.tableView.delegate?.tableView?(testController.tableView, viewForFooterInSection: 0) == sectionFooterView)
170 | }
171 |
172 | func testRowBuilderCustomActionInvokedAndSentUserInfo() {
173 |
174 | let expectation = self.expectation(description: "cell action")
175 |
176 | let row = TableRow(item: TestData(title: "title"))
177 | .on(TableRowAction(.custom(TestTableViewCellOptions.CellAction)) { (data) in
178 |
179 | XCTAssertNotNil(data.cell, "Action data should have a cell")
180 |
181 | expectation.fulfill()
182 | })
183 |
184 | testController.view.isHidden = false
185 | testController.tableDirector += row
186 | testController.tableView.reloadData()
187 |
188 | let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
189 |
190 | XCTAssertNotNil(cell, "Cell should exists and should be TestTableViewCell")
191 |
192 | cell?.raiseAction()
193 |
194 | waitForExpectations(timeout: 1.0, handler: nil)
195 | }
196 |
197 | func testReplaceSectionOnExistingIndex() {
198 |
199 | let row1 = TableRow(item: TestData(title: "title1"))
200 | let row2 = TableRow(item: TestData(title: "title2"))
201 |
202 | let section1 = TableSection(headerView: nil, footerView: nil, rows: nil)
203 | section1 += row1
204 |
205 | let section2 = TableSection(headerView: nil, footerView: nil, rows: nil)
206 | section2 += row2
207 |
208 | testController.tableDirector += section1
209 | testController.tableView.reloadData()
210 |
211 | let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
212 | XCTAssertTrue(cell?.textLabel?.text == "title1")
213 |
214 | testController.tableDirector.replaceSection(at: 0, with: section2)
215 | testController.tableView.reloadData()
216 |
217 | let cell1 = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
218 | XCTAssertTrue(cell1?.textLabel?.text == "title2")
219 | }
220 |
221 | func testReplaceSectionOnWrongIndex() {
222 |
223 | let row1 = TableRow(item: TestData(title: "title1"))
224 | let row2 = TableRow(item: TestData(title: "title2"))
225 |
226 | let section1 = TableSection(headerView: nil, footerView: nil, rows: nil)
227 | section1 += row1
228 |
229 | let section2 = TableSection(headerView: nil, footerView: nil, rows: nil)
230 | section2 += row2
231 |
232 | testController.tableDirector += section1
233 | testController.tableView.reloadData()
234 |
235 | let cell = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
236 | XCTAssertTrue(cell?.textLabel?.text == "title1")
237 |
238 | testController.tableDirector.replaceSection(at: 33, with: section2)
239 | testController.tableView.reloadData()
240 |
241 | let cell1 = testController.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? TestTableViewCell
242 | XCTAssertTrue(cell1?.textLabel?.text == "title1")
243 | }
244 |
245 | }
246 |
--------------------------------------------------------------------------------