├── logo.png ├── logo_source.sketch ├── Documentation ├── Untitled.graffle ├── FlowKit_Banner.sketch ├── Structure_Graph.graffle ├── Structure_CollectionKit.png ├── UIScrollViewDelegate_Events.md ├── Collection_Events.md └── Table_Events.md ├── ExampleApp ├── Assets.xcassets │ ├── Contents.json │ ├── first.imageset │ │ ├── first.pdf │ │ └── Contents.json │ ├── second.imageset │ │ ├── second.pdf │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Collection Example │ ├── CollectionHeader.swift │ ├── CollectionHeader.xib │ └── CollectionExampleController.swift ├── Table Example │ ├── Custom Footer │ │ ├── TableFooterExample.swift │ │ └── TableFooterExample.xib │ ├── Custom Header │ │ ├── TableExampleHeaderView.swift │ │ └── TableExampleHeaderView.xib │ └── TableViewController.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── AppDelegate.swift └── Localizable.strings ├── Tests ├── LinuxMain.swift └── FlowKitTests │ └── FlowKitTests.swift ├── FlowKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── FlowKit-iOS.xcscheme ├── Configs ├── FlowKitTests.plist └── FlowKit.plist ├── FlowKitManager.podspec ├── Package.swift ├── LICENSE ├── .gitignore ├── Sources └── FlowKit │ ├── Shared │ ├── AssociatedValues.swift │ ├── Commons.swift │ ├── DeepDiff+Helpers.swift │ └── DeepDiff.swift │ ├── Collection │ ├── CollectionSectionView.swift │ ├── CollectionAdapter+Events.swift │ ├── Collection+Support.swift │ ├── FlowCollectionDirector.swift │ ├── CollectionSection.swift │ ├── CollectionAdapter.swift │ └── CollectionDragDropManager.swift │ └── Table │ ├── TableSectionView.swift │ ├── TableDirector+Support.swift │ ├── TableDirector+Events.swift │ ├── TableSection.swift │ └── TableAdapter.swift └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/logo.png -------------------------------------------------------------------------------- /logo_source.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/logo_source.sketch -------------------------------------------------------------------------------- /Documentation/Untitled.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/Documentation/Untitled.graffle -------------------------------------------------------------------------------- /Documentation/FlowKit_Banner.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/Documentation/FlowKit_Banner.sketch -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Documentation/Structure_Graph.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/Documentation/Structure_Graph.graffle -------------------------------------------------------------------------------- /Documentation/Structure_CollectionKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/Documentation/Structure_CollectionKit.png -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FlowKitTests 3 | 4 | XCTMain([ 5 | testCase(FlowKitTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/ExampleApp/Assets.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malcommac/FlowKit/HEAD/ExampleApp/Assets.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /FlowKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /FlowKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleApp/Collection Example/CollectionHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionHeader.swift 3 | // ExampleApp 4 | // 5 | // Created by Daniele Margutti on 22/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public class CollectionHeader: UICollectionReusableView { 13 | @IBOutlet public var label: UILabel? 14 | } 15 | -------------------------------------------------------------------------------- /ExampleApp/Table Example/Custom Footer/TableFooterExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableFooterExample.swift 3 | // FlowKit 4 | // 5 | // Created by Daniele Margutti on 22/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public class TableFooterExample: UITableViewHeaderFooterView { 13 | @IBOutlet public var titleLabel: UILabel? 14 | } 15 | -------------------------------------------------------------------------------- /Tests/FlowKitTests/FlowKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowKitTests.swift 3 | // FlowKit 4 | // 5 | // Created by Daniele Margutti on 21/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import FlowKit 12 | 13 | class FlowKitTests: XCTestCase { 14 | func testExample() { 15 | // This is an example of a functional test case. 16 | // Use XCTAssert and related functions to verify your tests produce the correct results. 17 | //// XCTAssertEqual(FlowKit().text, "Hello, World!") 18 | } 19 | 20 | static var allTests = [ 21 | ("testExample", testExample), 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /ExampleApp/Table Example/Custom Header/TableExampleHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableHeaderView.swift 3 | // ExampleApp 4 | // 5 | // Created by Daniele Margutti on 21/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public class TableExampleHeaderView: UITableViewHeaderFooterView { 13 | 14 | @IBOutlet public var titleLabel: UILabel? 15 | 16 | public var whenTapped: (() -> Void)? 17 | 18 | public override func awakeFromNib() { 19 | super.awakeFromNib() 20 | 21 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped)) 22 | self.addGestureRecognizer(tapGesture) 23 | } 24 | 25 | @objc private func tapped() { 26 | whenTapped?() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Configs/FlowKitTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /FlowKitManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "FlowKitManager" 3 | s.version = "0.6.1" 4 | s.summary = "Declarative and type-safe UITableView & UICollectionView; a new way to work with tables and collections" 5 | s.description = <<-DESC 6 | Efficient, declarative and type-safe approach to create and manage UITableView and UICollectionView with built-in animation support. 7 | DESC 8 | s.homepage = "https://github.com/malcommac/FlowKit" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "Daniele Margutti" => "me@danielemargutti.com" } 11 | s.social_media_url = "https://twitter.com/danielemargutti" 12 | s.ios.deployment_target = "8.0" 13 | s.source = { :git => "https://github.com/malcommac/FlowKit.git", :tag => s.version.to_s } 14 | s.source_files = "Sources/**/*" 15 | s.frameworks = "Foundation" 16 | s.swift_version = "4.2" 17 | end 18 | -------------------------------------------------------------------------------- /Configs/FlowKit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2018 Daniele Margutti. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.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: "FlowKit", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "FlowKit", 12 | targets: ["FlowKit"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "FlowKit", 23 | dependencies: []), 24 | .testTarget( 25 | name: "FlowKitTests", 26 | dependencies: ["FlowKit"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Daniele Margutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Documentation/UIScrollViewDelegate_Events.md: -------------------------------------------------------------------------------- 1 | # UIScrollViewDelegate 2 | 3 | The following document describe the events available for FlowKit when you are using it to manage `UIScrollViewDelegate` events both available for `UITableView` and `UICollectionView`. 4 | 5 | The following events are available from director's `.onScroll` property both for `TableDirector` and `CollectionDirector`. 6 | 7 | ### Events 8 | 9 | - `didScroll: ((UIScrollView) -> Void)` 10 | - `willBeginDragging: ((UIScrollView) -> Void)` 11 | - `willEndDragging: ((_ scrollView: UIScrollView, _ velocity: CGPoint, _ targetOffset: UnsafeMutablePointer) -> Void)` 12 | - `endDragging: ((_ scrollView: UIScrollView, _ willDecelerate: Bool) -> Void)` 13 | - `shouldScrollToTop: ((UIScrollView) -> Bool)` 14 | - `didScrollToTop: ((UIScrollView) -> Void)` 15 | - `willBeginDecelerating: ((UIScrollView) -> Void)` 16 | - `endDecelerating: ((UIScrollView) -> Void)` 17 | - `viewForZooming: ((UIScrollView) -> UIView?)`l 18 | - `willBeginZooming: ((_ scrollView: UIScrollView, _ view: UIView?) -> Void)` 19 | - `endZooming: ((_ scrollView: UIScrollView, _ view: UIView?, _ scale: CGFloat) -> Void)` 20 | - `didZoom: ((UIScrollView) -> Void)` 21 | - `endScrollingAnimation: ((UIScrollView) -> Void)` 22 | - `didChangeAdjustedContentInset: ((UIScrollView) -> Void)` 23 | 24 | ### Example 25 | 26 | ```swift 27 | tableView.director.didDidScroll = { scrollView in 28 | print("Scrolling at x:\(scrollView.contentOffset.x), y:\(scrollView.contentOffset.y)") 29 | } 30 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | .DS_Store 69 | -------------------------------------------------------------------------------- /ExampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarTintParameters 32 | 33 | UINavigationBar 34 | 35 | Style 36 | UIBarStyleDefault 37 | Translucent 38 | 39 | 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /ExampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ExampleApp 4 | // 5 | // Created by Daniele Margutti on 21/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | public extension String { 48 | 49 | public var loc: String { 50 | return NSLocalizedString(self, comment: self) 51 | } 52 | 53 | public func loc(default str: String?) -> String? { 54 | let localized = NSLocalizedString(self, comment: self) 55 | if localized == self { return str } 56 | return localized 57 | } 58 | 59 | public var locUp: String { 60 | return NSLocalizedString(self, comment: self).uppercased() 61 | } 62 | 63 | public func loc(_ args: CVarArg...) -> String { 64 | return String(format: self.loc, locale: nil, arguments: args) 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /ExampleApp/Collection Example/CollectionHeader.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /ExampleApp/Table Example/Custom Footer/TableFooterExample.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ExampleApp/Table Example/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.swift 3 | // ExampleApp 4 | // 5 | // Created by Daniele Margutti on 21/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewController: UIViewController { 12 | 13 | @IBOutlet public var tableView: UITableView? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | self.tableView?.director.register(adapter: ArticleAdapter()) 19 | self.tableView?.director.add(section: self.getWinnerSection()) 20 | 21 | self.tableView?.director.rowHeight = .autoLayout(estimated: 100) 22 | self.tableView?.director.reloadData(after: { _ in 23 | 24 | return TableReloadAnimations.default() 25 | }, onEnd: nil) 26 | 27 | } 28 | 29 | func getWinnerSection() -> TableSection { 30 | let articles = (0..<7).map { 31 | return Article(title: "Article_Title_\($0)".loc, text: "Article_Text_\($0)".loc) 32 | } 33 | 34 | let header = TableSectionView() 35 | header.on.willDisplay = { ctx in 36 | guard let section = ctx.table?.director.sections[ctx.section] else { return } 37 | 38 | ctx.view?.titleLabel?.text = section.collapsed ? "Tap to expand" : "Tap to collapse" 39 | ctx.view?.whenTapped = { 40 | ctx.table?.director.reloadData(after: { (director) -> (TableReloadAnimations?) in 41 | section.collapsed = !section.collapsed 42 | return TableReloadAnimations.default() 43 | }, onEnd: { 44 | ctx.view?.titleLabel?.text = section.collapsed ? "Tap to expand" : "Tap to collapse" 45 | }) 46 | } 47 | } 48 | header.on.height = { _ in 49 | return 150 50 | } 51 | 52 | let footer = TableSectionView() 53 | footer.on.height = { _ in 54 | return 30 55 | } 56 | footer.on.dequeue = { ctx in 57 | ctx.view?.titleLabel?.text = "\(articles.count) Articles" 58 | } 59 | 60 | 61 | let section = TableSection(headerView: header, footerView: footer, models: articles) 62 | return section 63 | } 64 | 65 | override func didReceiveMemoryWarning() { 66 | super.didReceiveMemoryWarning() 67 | // Dispose of any resources that can be recreated. 68 | } 69 | 70 | 71 | } 72 | 73 | public class ArticleAdapter: TableAdapter { 74 | 75 | init() { 76 | super.init() 77 | self.on.dequeue = { ctx in 78 | ctx.cell?.titleLabel?.text = ctx.model.title 79 | ctx.cell?.subtitleLabel?.text = ctx.model.text 80 | } 81 | self.on.tap = { ctx in 82 | ctx.cell?.accessoryType = UITableViewCell.AccessoryType.checkmark 83 | print("Tapped on article \(ctx.model.modelID)") 84 | return .deselectAnimated 85 | } 86 | } 87 | 88 | } 89 | 90 | public class Article: ModelProtocol { 91 | 92 | public static func == (lhs: Article, rhs: Article) -> Bool { 93 | return (lhs.modelID == rhs.modelID) 94 | } 95 | 96 | public let title: String 97 | public let text: String 98 | public let articleId: String = NSUUID().uuidString 99 | 100 | public init(title: String, text: String) { 101 | self.title = title 102 | self.text = text 103 | } 104 | 105 | } 106 | 107 | public class TableArticleCell: UITableViewCell { 108 | @IBOutlet public var titleLabel: UILabel? 109 | @IBOutlet public var subtitleLabel: UILabel? 110 | } 111 | -------------------------------------------------------------------------------- /Sources/FlowKit/Shared/AssociatedValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import ObjectiveC.runtime 32 | 33 | internal func getAssociatedValue(key: String, object: AnyObject) -> T? { 34 | return (objc_getAssociatedObject(object, key.address) as? AssociatedValue)?.value as? T 35 | } 36 | 37 | internal func getAssociatedValue(key: String, object: AnyObject, initialValue: @autoclosure () -> T) -> T { 38 | return getAssociatedValue(key: key, object: object) ?? setAndReturn(initialValue: initialValue(), key: key, object: object) 39 | } 40 | 41 | internal func getAssociatedValue(key: String, object: AnyObject, initialValue: () -> T) -> T { 42 | return getAssociatedValue(key: key, object: object) ?? setAndReturn(initialValue: initialValue(), key: key, object: object) 43 | } 44 | 45 | fileprivate func setAndReturn(initialValue: T, key: String, object: AnyObject) -> T { 46 | set(associatedValue: initialValue, key: key, object: object) 47 | return initialValue 48 | } 49 | 50 | internal func set(associatedValue: T?, key: String, object: AnyObject) { 51 | set(associatedValue: AssociatedValue(associatedValue), key: key, object: object) 52 | } 53 | 54 | internal func set(weakAssociatedValue: T?, key: String, object: AnyObject) { 55 | set(associatedValue: AssociatedValue(weak: weakAssociatedValue), key: key, object: object) 56 | } 57 | 58 | extension String { 59 | 60 | fileprivate var address: UnsafeRawPointer { 61 | return UnsafeRawPointer(bitPattern: abs(hashValue))! 62 | } 63 | 64 | } 65 | 66 | private func set(associatedValue: AssociatedValue, key: String, object: AnyObject) { 67 | objc_setAssociatedObject(object, key.address, associatedValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 68 | } 69 | 70 | private class AssociatedValue { 71 | 72 | weak var _weakValue: AnyObject? 73 | var _value: Any? 74 | 75 | var value: Any? { 76 | return _weakValue ?? _value 77 | } 78 | 79 | init(_ value: Any?) { 80 | _value = value 81 | } 82 | 83 | init(weak: AnyObject?) { 84 | _weakValue = weak 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /ExampleApp/Collection Example/CollectionExampleController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionExampleController.swift 3 | // ExampleApp 4 | // 5 | // Created by Daniele Margutti on 21/04/2018. 6 | // Copyright © 2018 FlowKit. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct Number: ModelProtocol { 12 | public var modelID: Int { 13 | return value 14 | } 15 | 16 | let value: Int 17 | 18 | init(_ value: Int) { 19 | self.value = value 20 | } 21 | } 22 | 23 | public struct Letter: ModelProtocol { 24 | public var modelID: Int { 25 | return self.value.hashValue 26 | } 27 | 28 | let value: String 29 | 30 | init(_ value: String) { 31 | self.value = value 32 | } 33 | } 34 | 35 | class CollectionExampleController: UIViewController { 36 | 37 | @IBOutlet public var collectionView: UICollectionView? 38 | 39 | private var director: FlowCollectionDirector? 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | self.director = FlowCollectionDirector(self.collectionView!) 45 | 46 | let letterAdapter = CollectionAdapter() 47 | self.director?.register(adapter: letterAdapter) 48 | letterAdapter.on.dequeue = { ctx in 49 | ctx.cell?.label?.text = "\(ctx.model)" 50 | } 51 | letterAdapter.on.didSelect = { ctx in 52 | print("Tapped letter \(ctx.model)") 53 | } 54 | letterAdapter.on.itemSize = { ctx in 55 | return CGSize.init(width: ctx.collectionSize!.width / 3.0, height: 100) 56 | } 57 | 58 | 59 | 60 | let numberAdapter = CollectionAdapter() 61 | self.director?.register(adapter: numberAdapter) 62 | numberAdapter.on.dequeue = { ctx in 63 | ctx.cell?.label?.text = "#\(ctx.model)" 64 | ctx.cell?.back?.layer.borderWidth = 2 65 | ctx.cell?.back?.layer.borderColor = UIColor.darkGray.cgColor 66 | ctx.cell?.back?.backgroundColor = UIColor.white 67 | } 68 | numberAdapter.on.didSelect = { ctx in 69 | print("Tapped number \(ctx.model)") 70 | } 71 | numberAdapter.on.itemSize = { ctx in 72 | return CGSize.init(width: ctx.collectionSize!.width / 3.0, height: 100) 73 | } 74 | 75 | var list: [ModelProtocol] = (0..<70).map { return Number($0) } 76 | list.append(contentsOf: [Letter("A"),Letter("B"),Letter("C"),Letter("D"),Letter("E"),Letter("F")]) 77 | list.shuffle() 78 | 79 | let header = CollectionSectionView() 80 | header.on.referenceSize = { _ in 81 | return CGSize(width: self.collectionView!.frame.width, height: 40) 82 | } 83 | let section = CollectionSection.init(list, headerView: header) 84 | 85 | self.director?.add(section) 86 | self.director?.reloadData() 87 | } 88 | 89 | override func didReceiveMemoryWarning() { 90 | super.didReceiveMemoryWarning() 91 | // Dispose of any resources that can be recreated. 92 | } 93 | 94 | 95 | } 96 | 97 | public class NumberCell: UICollectionViewCell { 98 | @IBOutlet public var label: UILabel? 99 | @IBOutlet public var back: UIView? 100 | } 101 | 102 | 103 | public class LetterCell: UICollectionViewCell { 104 | @IBOutlet public var label: UILabel? 105 | } 106 | 107 | extension MutableCollection { 108 | /// Shuffles the contents of this collection. 109 | mutating func shuffle() { 110 | let c = count 111 | guard c > 1 else { return } 112 | 113 | for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) { 114 | // Change `Int` in the next line to `IndexDistance` in < Swift 4.1 115 | let d: Int = numericCast(arc4random_uniform(numericCast(unshuffledCount))) 116 | let i = index(firstUnshuffled, offsetBy: d) 117 | swapAt(firstUnshuffled, i) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/FlowKit/Shared/Commons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | /// No data shortcut when adapter does not need of a representable model 34 | //public struct NoData: AnyHashable { 35 | // public var hashValue: Int = 0 36 | //} 37 | 38 | internal struct InternalContext { 39 | var model: ModelProtocol? 40 | var models: [ModelProtocol]? 41 | var path: IndexPath? 42 | var paths: [IndexPath]? 43 | var cell: CellProtocol? 44 | var container: Any 45 | var param1: Any? 46 | var param2: Any? 47 | var param3: Any? 48 | 49 | init(_ model: ModelProtocol?, _ path: IndexPath, _ cell: CellProtocol?, _ scrollview: UIScrollView, 50 | param1: Any? = nil, param2: Any? = nil, param3: Any? = nil) { 51 | self.model = model 52 | self.path = path 53 | self.cell = cell 54 | self.container = scrollview 55 | self.param1 = param1 56 | self.param2 = param2 57 | } 58 | 59 | init(_ models: [ModelProtocol], _ paths: [IndexPath], _ scrollview: UIScrollView) { 60 | self.models = models 61 | self.paths = paths 62 | self.container = scrollview 63 | } 64 | } 65 | 66 | public enum SectionType { 67 | case header 68 | case footer 69 | } 70 | 71 | ///MARK: UIScrollViewDelegate Events 72 | 73 | public struct ScrollViewEvents { 74 | public var didScroll: ((UIScrollView) -> Void)? = nil 75 | public var willBeginDragging: ((UIScrollView) -> Void)? = nil 76 | public var willEndDragging: ((_ scrollView: UIScrollView, _ velocity: CGPoint, _ targetOffset: UnsafeMutablePointer) -> Void)? = nil 77 | public var endDragging: ((_ scrollView: UIScrollView, _ willDecelerate: Bool) -> Void)? = nil 78 | public var shouldScrollToTop: ((UIScrollView) -> Bool)? = nil 79 | public var didScrollToTop: ((UIScrollView) -> Void)? = nil 80 | public var willBeginDecelerating: ((UIScrollView) -> Void)? = nil 81 | public var endDecelerating: ((UIScrollView) -> Void)? = nil 82 | public var viewForZooming: ((UIScrollView) -> UIView?)? = nil 83 | public var willBeginZooming: ((_ scrollView: UIScrollView, _ view: UIView?) -> Void)? = nil 84 | public var endZooming: ((_ scrollView: UIScrollView, _ view: UIView?, _ scale: CGFloat) -> Void)? = nil 85 | public var didZoom: ((UIScrollView) -> Void)? = nil 86 | public var endScrollingAnimation: ((UIScrollView) -> Void)? = nil 87 | public var didChangeAdjustedContentInset: ((UIScrollView) -> Void)? = nil 88 | } 89 | -------------------------------------------------------------------------------- /ExampleApp/Table Example/Custom Header/TableExampleHeaderView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /FlowKit.xcodeproj/xcshareddata/xcschemes/FlowKit-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/CollectionSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | open class CollectionSectionView: CollectionSectionProtocol,AbstractCollectionHeaderFooterItem, CustomStringConvertible { 34 | 35 | // Protocol default implementation 36 | public var viewClass: AnyClass { return T.self } 37 | public var reuseIdentifier: String { return T.reuseIdentifier } 38 | public var registerAsClass: Bool { return T.registerAsClass } 39 | 40 | public weak var section: CollectionSection? = nil 41 | 42 | public var description: String { 43 | return "CollectionSectionView<\(String(describing: type(of: T.self)))>" 44 | } 45 | 46 | /// Context of the event sent to section's view. 47 | public struct Context { 48 | 49 | /// Type of item (footer or header) 50 | public private(set) var type: SectionType 51 | 52 | /// Parent collection 53 | public private(set) weak var collection: UICollectionView? 54 | 55 | /// Instance of the view dequeued for this section. 56 | public private(set) var view: T? 57 | 58 | /// Index of the section. 59 | public private(set) var section: Int 60 | 61 | /// Parent collection's size. 62 | public var collectionSize: CGSize? { 63 | return self.collection?.bounds.size 64 | } 65 | 66 | /// Initialize a new context (private). 67 | public init(type: SectionType, view: UIView?, at section: Int, of collection: UICollectionView) { 68 | self.type = type 69 | self.collection = collection 70 | self.view = view as? T 71 | self.section = section 72 | } 73 | } 74 | 75 | /// Events for section 76 | public var on = Event() 77 | 78 | //MARK: INIT 79 | 80 | /// Initialize a new section view. 81 | /// 82 | /// - Parameter configuration: configuration callback 83 | public init(_ configuration: ((CollectionSectionView) -> (Void))? = nil) { 84 | configuration?(self) 85 | } 86 | 87 | //MARK: INTERNAL METHODS 88 | @discardableResult 89 | func dispatch(_ event: CollectionSectionViewEventsKey, type: SectionType, view: UICollectionReusableView?, section: Int, collection: UICollectionView) -> Any? { 90 | switch event { 91 | case .dequeue: 92 | guard let callback = self.on.dequeue else { return nil } 93 | callback(Context(type: type, view: view, at: section, of: collection)) 94 | case .didDisplay: 95 | guard let callback = self.on.didDisplay else { return nil } 96 | callback(Context(type: type, view: view, at: section, of: collection)) 97 | case .endDisplay: 98 | guard let callback = self.on.endDisplay else { return nil } 99 | callback(Context(type: type, view: view, at: section, of: collection)) 100 | case .willDisplay: 101 | guard let callback = self.on.willDisplay else { return nil } 102 | callback(Context(type: type, view: view, at: section, of: collection)) 103 | case .referenceSize: 104 | guard let callback = self.on.referenceSize else { return nil } 105 | return callback(Context(type: type, view: view, at: section, of: collection)) 106 | } 107 | return nil 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /Sources/FlowKit/Table/TableSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | open class TableSectionView: TableHeaderFooterProtocol, AbstractTableHeaderFooterItem, CustomStringConvertible { 34 | 35 | public var viewClass: AnyClass { return T.self } 36 | public var reuseIdentifier: String { return T.reuseIdentifier } 37 | public var registerAsClass: Bool { return T.registerAsClass } 38 | 39 | public var description: String { 40 | return "CollectionSectionView<\(String(describing: type(of: T.self)))>" 41 | } 42 | 43 | /// Context of the event sent to section's view. 44 | public struct Context { 45 | 46 | public private(set) var type: SectionType 47 | 48 | /// Parent collection 49 | public private(set) weak var table: UITableView? 50 | 51 | /// Instance of the view dequeued for this section. 52 | public private(set) var view: T? 53 | 54 | /// Index of the section. 55 | public private(set) var section: Int 56 | 57 | /// Parent collection's size. 58 | public var tableSize: CGSize? { 59 | return self.table?.bounds.size 60 | } 61 | 62 | /// Initialize a new context (private). 63 | public init(type: SectionType, view: UIView?, at section: Int, of table: UITableView) { 64 | self.type = type 65 | self.table = table 66 | self.view = view as? T 67 | self.section = section 68 | } 69 | } 70 | 71 | /// Events 72 | public var on = TableSectionView.Events() 73 | 74 | /// Parent section 75 | public weak var section: TableSection? = nil 76 | 77 | //MARK: INIT 78 | 79 | /// Initialize a new section view. 80 | /// 81 | /// - Parameter configuration: configuration callback 82 | public init(_ configuration: ((TableSectionView) -> (Void))? = nil) { 83 | configuration?(self) 84 | } 85 | 86 | 87 | //MARK: INTERNAL METHODS 88 | @discardableResult 89 | func dispatch(_ event: TableSectionViewEventsKey, type: SectionType, view: UIView?, section: Int, table: UITableView) -> Any? { 90 | switch event { 91 | case .dequeue: 92 | guard let callback = self.on.dequeue else { return nil } 93 | callback(Context(type: type, view: view, at: section, of: table)) 94 | case .height: 95 | guard let callback = self.on.height else { return nil } 96 | return callback(Context(type: type, view: view, at: section, of: table)) 97 | case .willDisplay: 98 | guard let callback = self.on.willDisplay else { return nil } 99 | return callback(Context(type: type, view: view, at: section, of: table)) 100 | case .didDisplay: 101 | guard let callback = self.on.didDisplay else { return nil } 102 | return callback(Context(type: type, view: view, at: section, of: table)) 103 | case .endDisplay: 104 | guard let callback = self.on.endDisplay else { return nil } 105 | return callback(Context(type: type, view: view, at: section, of: table)) 106 | case .estimatedHeight: 107 | guard let callback = self.on.estimatedHeight else { return nil } 108 | return callback(Context(type: type, view: view, at: section, of: table)) 109 | } 110 | return nil 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /ExampleApp/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | FlowKit 4 | 5 | Created by Daniele Margutti on 21/04/2018. 6 | Copyright © 2018 FlowKit. All rights reserved. 7 | */ 8 | 9 | "Article_Title_0" = "Unformatted, Plain Lorem Ipsum Dummy Text"; 10 | "Article_Text_0" = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; 11 | 12 | "Article_Title_1" = "orem Ipsum Dolor Sit Amet Copyfitting Text"; 13 | "Article_Text_1" = "Below is a sample of “Lorem ipsum dolor sit” dummy copy text often used to show font face samples, for page layout and design as sample layout text by printers, graphic designers, Web designers, people creating Microsoft Word templates, and many other uses. It mimics the look of real text quite well as you design and set up your page layouts."; 14 | 15 | "Article_Title_2" = "Where can I get some?"; 16 | "Article_Text_2" = "There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc."; 17 | 18 | 19 | "Article_Title_3" = "Where does it come from?"; 20 | "Article_Text_3" = "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of de Finibus Bonorum et Malorum (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, Lorem ipsum dolor sit amet.., comes from a line in section 1.10.32."; 21 | 22 | 23 | "Article_Title_4" = "orem Ipsum Dolor Sit Amet Copyfitting Text"; 24 | "Article_Text_4" = "Below is a sample of “Lorem ipsum dolor sit” dummy copy text often used to show font face samples, for page layout and design as sample layout text by printers, graphic designers, Web designers, people creating Microsoft Word templates, and many other uses. It mimics the look of real text quite well as you design and set up your page layouts."; 25 | 26 | 27 | "Article_Title_5" = "Sample Text"; 28 | "Article_Text_5" = "Sample Text is the placeholder text automatically generated when superimposing a caption onto a video clip in the Sony Vegas Pro video-editing software. In montage parodies on YouTube, the filler phrase is often left unchanged for comedic effect."; 29 | 30 | 31 | "Article_Title_6" = "Sample Text Part of a series on Montage Parodies. [View Related Entries]"; 32 | "Article_Text_6" = "On January 25th, 2003, Sony Creative Software released version 1.0 of Sony Vegas Pro,[9] which displays the phrase “Sample Text” when adding a text overlay in a video clip, indicating that the user can customize the type. On November 21st, 2013, YouTuber iTheRainbowDash uploaded a montage parody which included the sample text overlay in a clip of a quickscoping player during a Call of Duty online match (shown below)."; 33 | 34 | 35 | 36 | "Article_Title_7" = "If your Latin is good enough (unlike mine), you can read Cicero’s complete text (or just get a whole bunch of great sample text) here:"; 37 | "Article_Text_7" = "If you’re curious about this, it’s a garbled quotation from Cicero’s De Finibus Bonorum et Malorum (On the Ends of Good and Bad), book 1, paragraph 32, which reads, “Neque porro quisquam est, qui dolorem ipsum, quia dolor sit, amet, consectetur, adipisci velit,” meaning, “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain.” The book was popular during the Renaissance, when the passage was used in a book of type samples for that wonderful new technology, printing."; 38 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/CollectionAdapter+Events.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | // MARK: - CollectionSection Events 34 | 35 | public extension CollectionSectionView { 36 | 37 | public struct Event { 38 | public typealias EventContext = Context 39 | 40 | public var dequeue: ((EventContext) -> Void)? = nil 41 | public var referenceSize: ((EventContext) -> CGSize)? = nil 42 | public var didDisplay: ((EventContext) -> Void)? = nil 43 | public var endDisplay: ((EventContext) -> Void)? = nil 44 | public var willDisplay: ((EventContext) -> Void)? = nil 45 | 46 | } 47 | 48 | } 49 | 50 | // MARK: - CollectionAdapter Events 51 | public extension CollectionAdapter { 52 | 53 | public struct Events { 54 | public typealias EventContext = Context 55 | 56 | public var dequeue: ((EventContext) -> Void)? = nil 57 | public var shouldSelect: ((EventContext) -> Bool)? = nil 58 | public var shouldDeselect: ((EventContext) -> Bool)? = nil 59 | public var didSelect: ((EventContext) -> Void)? = nil 60 | public var didDeselect: ((EventContext) -> Void)? = nil 61 | public var didHighlight: ((EventContext) -> Void)? = nil 62 | public var didUnhighlight: ((EventContext) -> Void)? = nil 63 | public var shouldHighlight: ((EventContext) -> Bool)? = nil 64 | public var willDisplay: ((_ cell: C, _ path: IndexPath) -> Void)? = nil 65 | public var endDisplay: ((_ cell: C, _ path: IndexPath) -> Void)? = nil 66 | public var shouldShowEditMenu: ((EventContext) -> Bool)? = nil 67 | public var canPerformEditAction: ((EventContext) -> Bool)? = nil 68 | public var performEditAction: ((_ ctx: EventContext, _ selector: Selector, _ sender: Any?) -> Void)? = nil 69 | public var canFocus: ((EventContext) -> Bool)? = nil 70 | public var itemSize: ((EventContext) -> CGSize)? = nil 71 | //var generateDragPreview: ((EventContext) -> UIDragPreviewParameters?)? = nil 72 | //var generateDropPreview: ((EventContext) -> UIDragPreviewParameters?)? = nil 73 | public var prefetch: ((_ items: [M], _ paths: [IndexPath], _ collection: UICollectionView) -> Void)? = nil 74 | public var cancelPrefetch: ((_ items: [M], _ paths: [IndexPath], _ collection: UICollectionView) -> Void)? = nil 75 | public var shouldSpringLoad: ((EventContext) -> Bool)? = nil 76 | } 77 | 78 | } 79 | 80 | // MARK: - CollectionDirector Events 81 | public extension CollectionDirector { 82 | 83 | public struct Events { 84 | typealias HeaderFooterEvent = (view: UICollectionReusableView, path: IndexPath, table: UICollectionView) 85 | 86 | var layoutDidChange: ((_ old: UICollectionViewLayout, _ new: UICollectionViewLayout) -> UICollectionViewTransitionLayout?)? = nil 87 | var targetOffset: ((_ proposedContentOffset: CGPoint) -> CGPoint)? = nil 88 | var moveItemPath: ((_ originalIndexPath: IndexPath, _ proposedIndexPath: IndexPath) -> IndexPath)? = nil 89 | 90 | private var _shouldUpdateFocus: ((_ context: AnyObject) -> Bool)? = nil 91 | @available(iOS 9.0, *) 92 | var shouldUpdateFocus: ((_ context: UICollectionViewFocusUpdateContext) -> Bool)? { 93 | get { return _shouldUpdateFocus } 94 | set { _shouldUpdateFocus = newValue as? ((AnyObject) -> Bool) } 95 | } 96 | 97 | private var _didUpdateFocus: ((_ context: AnyObject, _ coordinator: AnyObject) -> Void)? = nil 98 | @available(iOS 9.0, *) 99 | var didUpdateFocus: ((_ context: UICollectionViewFocusUpdateContext, _ coordinator: UIFocusAnimationCoordinator) -> Void)? { 100 | get { return _didUpdateFocus } 101 | set { _didUpdateFocus = newValue as? ((AnyObject, AnyObject) -> Void) } 102 | } 103 | 104 | var willDisplayHeader : ((HeaderFooterEvent) -> Void)? = nil 105 | var willDisplayFooter : ((HeaderFooterEvent) -> Void)? = nil 106 | 107 | var endDisplayHeader : ((HeaderFooterEvent) -> Void)? = nil 108 | var endDisplayFooter : ((HeaderFooterEvent) -> Void)? = nil 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Sources/FlowKit/Table/TableDirector+Support.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | public extension UITableView { 34 | 35 | private static let DIRECTOR_KEY = "flowkit.director" 36 | 37 | /// Return director associated with collection. 38 | /// If not exist it will be created and assigned automatically. 39 | public var director: TableDirector { 40 | get { 41 | return getAssociatedValue(key: UITableView.DIRECTOR_KEY, 42 | object: self, 43 | initialValue: TableDirector(self)) 44 | } 45 | set { 46 | set(associatedValue: newValue, key: UITableView.DIRECTOR_KEY, object: self) 47 | } 48 | } 49 | 50 | } 51 | 52 | /// State of the section 53 | /// 54 | /// - none: don't change the state 55 | /// - deselect: deselect without animation 56 | /// - deselectAnimated: deselect with animation 57 | public enum TableSelectionState { 58 | case none 59 | case deselect 60 | case deselectAnimated 61 | } 62 | 63 | public enum TableAnimationAction { 64 | case delete 65 | case insert 66 | case reload 67 | } 68 | 69 | public protocol TableReloadAnimationProtocol { 70 | 71 | func animationForRow(action: TableAnimationAction) -> UITableView.RowAnimation 72 | func animationForSection(action: TableAnimationAction) -> UITableView.RowAnimation 73 | 74 | } 75 | 76 | public extension TableReloadAnimationProtocol { 77 | 78 | func animationForRow(action: TableAnimationAction) -> UITableView.RowAnimation { 79 | return .automatic 80 | } 81 | 82 | func animationForSection(action: TableAnimationAction) -> UITableView.RowAnimation { 83 | return .automatic 84 | } 85 | 86 | } 87 | 88 | /// Animations used with reload 89 | public struct TableReloadAnimations: TableReloadAnimationProtocol { 90 | public static func `default`() -> TableReloadAnimations { 91 | return TableReloadAnimations() 92 | } 93 | } 94 | 95 | // Protocols 96 | 97 | extension UITableViewCell: CellProtocol { } 98 | 99 | public protocol TableAdapterProtocol : AbstractAdapterProtocol, Equatable { } 100 | 101 | internal protocol AbstractTableHeaderFooterItem { 102 | @discardableResult 103 | func dispatch(_ event: TableSectionViewEventsKey, type: SectionType, view: UIView?, section: Int, table: UITableView) -> Any? 104 | } 105 | 106 | public protocol TableHeaderFooterProtocol : AbstractCollectionReusableView { 107 | var section: TableSection? { get set } 108 | } 109 | 110 | internal protocol TableAdaterProtocolFunctions { 111 | 112 | @discardableResult 113 | func dispatch(_ event: TableAdapterEventsKey, context: InternalContext) -> Any? 114 | 115 | // Dequeue (UITableViewDatasource) 116 | func _instanceCell(in table: UITableView, at indexPath: IndexPath?) -> UITableViewCell 117 | } 118 | 119 | internal protocol TableDirectorEventable { 120 | var name: TableDirectorEventKey { get } 121 | } 122 | 123 | internal enum TableDirectorEventKey: String { 124 | case sectionForSectionIndex 125 | } 126 | 127 | internal enum TableSectionViewEventsKey: Int { 128 | case dequeue 129 | case height 130 | case estimatedHeight 131 | case didDisplay 132 | case endDisplay 133 | case willDisplay 134 | } 135 | 136 | internal enum TableAdapterEventsKey: Int { 137 | case dequeue = 0 138 | case canEdit 139 | case commitEdit 140 | case canMoveRow 141 | case moveRow 142 | case prefetch 143 | case cancelPrefetch 144 | case rowHeight 145 | case rowHeightEstimated 146 | case indentLevel 147 | case willDisplay 148 | case shouldSpringLoad 149 | case editActions 150 | case tapOnAccessory 151 | case willSelect 152 | case tap 153 | case willDeselect 154 | case didDeselect 155 | case willBeginEdit 156 | case didEndEdit 157 | case editStyle 158 | case deleteConfirmTitle 159 | case editShouldIndent 160 | case moveAdjustDestination 161 | case endDisplay 162 | case shouldShowMenu 163 | case canPerformMenuAction 164 | case performMenuAction 165 | case shouldHighlight 166 | case didHighlight 167 | case didUnhighlight 168 | case canFocus 169 | case leadingSwipeActions 170 | case trailingSwipeActions 171 | } 172 | -------------------------------------------------------------------------------- /Sources/FlowKit/Table/TableDirector+Events.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | // MARK: - TableDirector Events 34 | public extension TableDirector { 35 | 36 | public struct Events { 37 | typealias HeaderFooterEvent = (view: UIView, section: Int, table: UITableView) 38 | 39 | var sectionForSectionIndex: ((_ title: String, _ index: Int) -> Int)? = nil 40 | 41 | var willDisplayHeader: ((HeaderFooterEvent) -> Void)? = nil 42 | var willDisplayFooter: ((HeaderFooterEvent) -> Void)? = nil 43 | 44 | var endDisplayHeader: ((HeaderFooterEvent) -> Void)? = nil 45 | var endDisplayFooter: ((HeaderFooterEvent) -> Void)? = nil 46 | } 47 | 48 | } 49 | 50 | // MARK: - TableSection Events 51 | public extension TableSectionView { 52 | 53 | public struct Events { 54 | public var dequeue: ((Context) -> Void)? = nil 55 | public var height: ((Context) -> CGFloat)? = nil 56 | public var estimatedHeight: ((Context) -> CGFloat)? = nil 57 | public var willDisplay: ((Context) -> Void)? = nil 58 | public var endDisplay: ((Context) -> Void)? = nil 59 | public var didDisplay: ((Context) -> Void)? = nil 60 | 61 | public init() {} 62 | } 63 | 64 | } 65 | 66 | // MARK: - TableAdapter Events 67 | public extension TableAdapter { 68 | 69 | public struct Events { 70 | public typealias EventContext = Context 71 | 72 | public var dequeue : ((EventContext) -> (Void))? = nil 73 | 74 | public var canEdit: ((EventContext) -> Bool)? = nil 75 | public var commitEdit: ((_ ctx: EventContext, _ commit: UITableViewCell.EditingStyle) -> Void)? = nil 76 | 77 | public var canMoveRow: ((EventContext) -> Bool)? = nil 78 | public var moveRow: ((_ ctx: EventContext, _ dest: IndexPath) -> Void)? = nil 79 | 80 | public var prefetch: ((_ models: [M], _ paths: [IndexPath]) -> Void)? = nil 81 | public var cancelPrefetch: ((_ models: [M], _ paths: [IndexPath]) -> Void)? = nil 82 | 83 | public var rowHeight: ((EventContext) -> CGFloat)? = nil 84 | public var rowHeightEstimated: ((EventContext) -> CGFloat)? = nil 85 | 86 | public var indentLevel: ((EventContext) -> Int)? = nil 87 | public var willDisplay: ((EventContext) -> Void)? = nil 88 | public var shouldSpringLoad: ((EventContext) -> Bool)? = nil 89 | 90 | public var editActions: ((EventContext) -> [UITableViewRowAction]?)? = nil 91 | public var tapOnAccessory: ((EventContext) -> Void)? = nil 92 | 93 | public var willSelect: ((EventContext) -> IndexPath?)? = nil 94 | public var tap: ((EventContext) -> TableSelectionState)? = nil 95 | public var willDeselect: ((EventContext) -> IndexPath?)? = nil 96 | public var didDeselect: ((EventContext) -> IndexPath?)? = nil 97 | 98 | public var willBeginEdit: ((EventContext) -> Void)? = nil 99 | public var didEndEdit: ((EventContext) -> Void)? = nil 100 | public var editStyle: ((EventContext) -> UITableViewCell.EditingStyle)? = nil 101 | public var deleteConfirmTitle: ((EventContext) -> String?)? = nil 102 | public var editShouldIndent: ((EventContext) -> Bool)? = nil 103 | 104 | public var moveAdjustDestination: ((_ ctx: EventContext, _ proposed: IndexPath) -> IndexPath?)? = nil 105 | 106 | public var endDisplay: ((_ cell: C, _ path: IndexPath) -> Void)? = nil 107 | 108 | public var shouldShowMenu: ((EventContext) -> Bool)? = nil 109 | public var canPerformMenuAction: ((_ ctx: EventContext, _ sel: Selector, _ sender: Any?) -> Bool)? = nil 110 | public var performMenuAction: ((_ ctx: EventContext, _ sel: Selector, _ sender: Any?) -> Void)? = nil 111 | 112 | public var shouldHighlight: ((EventContext) -> Bool)? = nil 113 | public var didHighlight: ((EventContext) -> Void)? = nil 114 | public var didUnhighlight: ((EventContext) -> Void)? = nil 115 | 116 | public var canFocus: ((EventContext) -> Bool)? = nil 117 | 118 | @available(iOS 11, *) 119 | public lazy var leadingSwipeActions: ((EventContext) -> UISwipeActionsConfiguration?)? = nil 120 | 121 | @available(iOS 11, *) 122 | public lazy var trailingSwipeActions: ((EventContext) -> UISwipeActionsConfiguration?)? = nil 123 | 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /Sources/FlowKit/Shared/DeepDiff+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | internal struct SectionChanges { 34 | private var inserts = IndexSet() 35 | private var deletes = IndexSet() 36 | private var replaces = IndexSet() 37 | private var moves: [(from: Int, to: Int)] = [] 38 | 39 | var hasChanges: Bool { 40 | return (self.inserts.count > 0 || self.deletes.count > 0 || self.replaces.count > 0 || self.moves.count > 0) 41 | } 42 | 43 | static func fromTableSections(old: [TableSection], new: [TableSection]) -> SectionChanges { 44 | let changes = diff(old: old, new: new) 45 | var data = SectionChanges() 46 | changes.forEach { 47 | switch $0 { 48 | case .delete(let v): data.deletes.insert(v.index) 49 | case .insert(let v): data.inserts.insert(v.index) 50 | case .move(let v): data.moves.append( (from: v.fromIndex, to: v.toIndex) ) 51 | case .replace(let v): data.replaces.insert(v.index) 52 | } 53 | } 54 | return data 55 | } 56 | 57 | static func fromCollectionSections(old: [CollectionSection], new: [CollectionSection]) -> SectionChanges { 58 | let changes = diff(old: old, new: new) 59 | var data = SectionChanges() 60 | changes.forEach { 61 | switch $0 { 62 | case .delete(let v): data.deletes.insert(v.index) 63 | case .insert(let v): data.inserts.insert(v.index) 64 | case .move(let v): data.moves.append( (from: v.fromIndex, to: v.toIndex) ) 65 | case .replace(let v): data.replaces.insert(v.index) 66 | } 67 | } 68 | return data 69 | } 70 | 71 | func applyChanges(toTable: UITableView?, withAnimations animations: TableReloadAnimationProtocol) { 72 | guard let t = toTable, self.hasChanges else { return } 73 | t.deleteSections(self.deletes, with: animations.animationForSection(action: .delete)) 74 | t.insertSections(self.inserts, with: animations.animationForSection(action: .insert)) 75 | self.moves.forEach { 76 | t.moveSection($0.from, toSection: $0.to) 77 | } 78 | t.reloadSections(self.replaces, with: animations.animationForSection(action: .reload)) 79 | } 80 | 81 | func applyChanges(toCollection: UICollectionView?) { 82 | guard let c = toCollection, self.hasChanges else { return } 83 | c.deleteSections(self.deletes) 84 | c.insertSections(self.inserts) 85 | self.moves.forEach { 86 | c.moveSection($0.from, toSection: $0.to) 87 | } 88 | c.reloadSections(self.replaces) 89 | } 90 | } 91 | 92 | 93 | internal struct SectionItemsChanges { 94 | 95 | let inserts: [IndexPath] 96 | let deletes: [IndexPath] 97 | let replaces: [IndexPath] 98 | let moves: [(from: IndexPath, to: IndexPath)] 99 | 100 | init( 101 | inserts: [IndexPath], 102 | deletes: [IndexPath], 103 | replaces:[IndexPath], 104 | moves: [(from: IndexPath, to: IndexPath)]) { 105 | 106 | self.inserts = inserts 107 | self.deletes = deletes 108 | self.replaces = replaces 109 | self.moves = moves 110 | } 111 | 112 | 113 | static func create(fromChanges changes: [Change], section: Int) -> SectionItemsChanges { 114 | let inserts = changes.compactMap({ $0.insert }).map({ $0.index.toIndexPath(section: section) }) 115 | let deletes = changes.compactMap({ $0.delete }).map({ $0.index.toIndexPath(section: section) }) 116 | let replaces = changes.compactMap({ $0.replace }).map({ $0.index.toIndexPath(section: section) }) 117 | let moves = changes.compactMap({ $0.move }).map({ 118 | ( 119 | from: $0.fromIndex.toIndexPath(section: section), 120 | to: $0.toIndex.toIndexPath(section: section) 121 | ) 122 | }) 123 | 124 | return SectionItemsChanges( 125 | inserts: inserts, 126 | deletes: deletes, 127 | replaces: replaces, 128 | moves: moves 129 | ) 130 | } 131 | 132 | func applyChangesToSectionItems(of collection: UICollectionView?) { 133 | guard let c = collection else { return } 134 | 135 | c.deleteItems(at: self.deletes) 136 | c.insertItems(at: self.inserts) 137 | self.moves.forEach { 138 | c.moveItem(at: $0.from, to: $0.to) 139 | } 140 | c.reloadItems(at: self.replaces) 141 | } 142 | 143 | func applyChangesToSectionItems(ofTable table: UITableView?, withAnimations animations: TableReloadAnimationProtocol) { 144 | guard let t = table else { return } 145 | 146 | t.deleteRows(at: self.deletes, with: animations.animationForRow(action: .delete)) 147 | t.insertRows(at: self.inserts, with: animations.animationForRow(action: .insert)) 148 | self.moves.forEach { 149 | t.moveRow(at: $0.from, to: $0.to) 150 | } 151 | t.reloadRows(at: self.replaces, with: animations.animationForRow(action: .reload)) 152 | } 153 | } 154 | 155 | internal extension Int { 156 | 157 | fileprivate func toIndexPath(section: Int) -> IndexPath { 158 | return IndexPath(item: self, section: section) 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /Documentation/Collection_Events.md: -------------------------------------------------------------------------------- 1 | # Collection Events 2 | 3 | ![](Structure_CollectionKit.png) 4 | 5 | The following document describe the events available for FlowKit when you are using it to manage `UICollectionView`. 6 | 7 | Events are available at 3 different levels: 8 | 9 | - **Director Events**: all general purpose events which are not strictly related to the single model's management. It includes general header/footer and scrollview delegate events. 10 | - **Adapter Events**: each adapter manage a single pair of Model (`M: ModelProtocol`) and View (`C: CellProtocol`). It will receive all events related to single model instance configuration (dequeue, item size etc.). 11 | - **Section Events**: `CollectionSectionView` instances manage custom view header/footer of a specific `CollectionSection`. It will receive events about dequeue, view's visibility. 12 | 13 | ## Contents 14 | 15 | - `CollectionDirector` 16 | - Events 17 | - Received Context 18 | - Example 19 | 20 | - `TableAdapter` 21 | - Events 22 | - Received Context 23 | - Example 24 | 25 | - `TableSectionView` 26 | - Events 27 | - Received Context 28 | - Example 29 | 30 | 31 | -- 32 | 33 | ## `CollectionDirector ` Events 34 | 35 | The following events are available from `CollectionDirector`'s `.on` property. 36 | 37 | ### Events 38 | 39 | - `layoutDidChange: ((_ old: UICollectionViewLayout, _ new: UICollectionViewLayout) -> UICollectionViewTransitionLayout?)` 40 | - `targetOffset: ((_ proposedContentOffset: CGPoint) -> CGPoint)` 41 | - `moveItemPath: ((_ originalIndexPath: IndexPath, _ proposedIndexPath: IndexPath) -> IndexPath)` 42 | - `shouldUpdateFocus: ((_ context: UICollectionViewFocusUpdateContext) -> Bool)` 43 | - `didUpdateFocus: ((_ context: UICollectionViewFocusUpdateContext, _ coordinator: UIFocusAnimationCoordinator) -> Void)` 44 | - `willDisplayHeader : ((HeaderFooterEvent) -> Void)` 45 | - `willDisplayFooter : ((HeaderFooterEvent) -> Void)` 46 | - `endDisplayHeader : ((HeaderFooterEvent) -> Void)` 47 | - `endDisplayFooter : ((HeaderFooterEvent) -> Void)` 48 | 49 | ### Received Context 50 | 51 | Some events will receive 52 | `HeaderFooterEvent `is just a typealias which contains the relevant information of the header/footer. 53 | 54 | `HeaderFooterEvent = (view: UIView, section: Int, table: UITableView)` 55 | 56 | ### Example 57 | 58 | ```swift 59 | collectionView.director.on.willDisplayHeader = { ctx in 60 | print("Will display header for section \(ctx.section)") 61 | } 62 | ``` 63 | 64 | ## `CollectionAdapter ` Events 65 | 66 | The following properties are used to manage the appearance and the behaviour for a pair of registered into the collection. 67 | Each event will receive a `Context` which contains a type safe instance of the involved model and (if available) cell. 68 | 69 | ### Events 70 | 71 | The following events are reachable from `TableAdapter `'s `.on` property. 72 | 73 | - `dequeue: ((EventContext) -> Void)` 74 | - `shouldSelect: ((EventContext) -> Bool)` 75 | - `shouldDeselect: ((EventContext) -> Bool)` 76 | - `didSelect: ((EventContext) -> Void)` 77 | - `didDeselect: ((EventContext) -> Void)` 78 | - `didHighlight: ((EventContext) -> Void)` 79 | - `didUnhighlight: ((EventContext) -> Void)` 80 | - `shouldHighlight: ((EventContext) -> Bool)` 81 | - `willDisplay: ((_ cell: C, _ path: IndexPath) -> Void)` 82 | - `endDisplay: ((_ cell: C, _ path: IndexPath) -> Void)` 83 | - `shouldShowEditMenu: ((EventContext) -> Bool)` 84 | - `canPerformEditAction: ((EventContext) -> Bool)` 85 | - `performEditAction: ((_ ctx: EventContext, _ selector: Selector, _ sender: Any?) -> Void)` 86 | - `canFocus: ((EventContext) -> Bool)` 87 | - `itemSize: ((EventContext) -> CGSize)` 88 | - `prefetch: ((_ items: [M], _ paths: [IndexPath], _ collection: UICollectionView) -> Void)` 89 | - `cancelPrefetch: ((_ items: [M], _ paths: [IndexPath], _ collection: UICollectionView) -> Void)` 90 | - `shouldSpringLoad: ((EventContext) -> Bool)` 91 | 92 | ### Received Context 93 | 94 | `EventContext` is a typealias for Adapter's `Context` with `M` is the `ModelProtocol` instance managed by the table and `C` is `CellProtocol` instance used to represent data. 95 | 96 | The following properties are available: 97 | 98 | - `indexPath: IndexPath`: target model's index path 99 | - `model: M`: type-safe instance of the model 100 | - `cell: C`: type-safe instance of the cell 101 | - `collection: UICollectionView`: parent collection view 102 | - `collectionSize: CGSize`: parent collection view's size 103 | 104 | ### Example 105 | 106 | ```swift 107 | let contactAdapter = CollectionAdapter() 108 | 109 | // intercept tap 110 | contactAdapter.on.tap = { ctx in 111 | print("User tapped on \(ctx.model.fullName)") 112 | return .deselectAnimated 113 | } 114 | 115 | // dequeue event, used to configure the UI of the cell 116 | contactAdapter.on.dequeue = { ctx in 117 | ctx.cell?.labelName?.text = ctx.model.firstName 118 | ctx.cell?.labelLastName?.text = ctx.model.lastName 119 | } 120 | ``` 121 | 122 | ## `CollectionSectionView ` events 123 | 124 | The following events are received from single instances of `headerView`/`footerView` of a `TableSection` instance and are related to header/footer events. 125 | 126 | ### Events 127 | 128 | The following events are available from `TableSectionView `'s `.on` property. 129 | 130 | **`referenceSize` event is required.** 131 | 132 | - `dequeue: ((EventContext) -> Void)` 133 | - `referenceSize: ((EventContext) -> CGSize)` 134 | - `didDisplay: ((EventContext) -> Void)` 135 | - `endDisplay: ((EventContext) -> Void)` 136 | - `willDisplay: ((EventContext) -> Void)` 137 | 138 | ### Example 139 | 140 | ```swift 141 | let header = CollectionSectionView() 142 | header.on.referenceSize = { ctx in 143 | return CGSize(width: ctx.collectionViewSize.width, height: 50) 144 | } 145 | header.on.dequeue = { ctx in 146 | ctx.view?.titleLabel?.text = "\(articles.count) Articles" 147 | } 148 | 149 | let section = CollectionSection(headerView: header, models: articles) 150 | ``` 151 | 152 | ### Received Context 153 | 154 | Where `Context` is a structure which contains the following properties of the section view instance: 155 | 156 | - `type: SectionType`: type of view (`footer` or `header`) 157 | - `section: Int`: section of the item 158 | - `view: T`: type safe instance of th header/footer 159 | - `collection: UICollectionView`: parent collection view 160 | - `collectionSize: CGSize`: parent collection view's size -------------------------------------------------------------------------------- /Documentation/Table_Events.md: -------------------------------------------------------------------------------- 1 | # Table Events 2 | 3 | ![](Structure_TableKit.png) 4 | 5 | The following document describe the events available for FlowKit when you are using it to manage `UITableView`. 6 | 7 | Events are available at 3 different levels: 8 | 9 | - **Director Events**: all general purpose events which are not strictly related to the single model's management. It includes general header/footer and scrollview delegate events. 10 | - **Adapter Events**: each adapter manage a single pair of Model (`M: ModelProtocol`) and View (`C: CellProtocol`). It will receive all events related to single model instance configuration (dequeue, item size etc.). 11 | - **Section Events**: `TableSectionView` instances manage custom view header/footer of a specific `TableSection`. It will receive events about dequeue, view's visibility. 12 | 13 | ## Contents 14 | 15 | - `TableDirector` 16 | - Events 17 | - Received Context 18 | - Example 19 | 20 | - `TableAdapter` 21 | - Events 22 | - Received Context 23 | - Example 24 | 25 | - `TableSectionView` 26 | - Events 27 | - Received Context 28 | - Example 29 | 30 | 31 | -- 32 | 33 | ## `TableDirector` Events 34 | 35 | The following events are available from `TableDirector `'s `.on` property. 36 | 37 | ### Events 38 | 39 | - `sectionForSectionIndex: ((_ title: String, _ index: Int) -> Int)` (name of the section is read automatically from `indexTitle` property of the section) 40 | - `willDisplayHeader: ((HeaderFooterEvent) -> Void)` 41 | - `willDisplayFooter: ((HeaderFooterEvent) -> Void)` 42 | - `endDisplayHeader: ((HeaderFooterEvent) -> Void)` 43 | - `endDisplayFooter: ((HeaderFooterEvent) -> Void)` 44 | 45 | ### Received Context 46 | 47 | where `HeaderFooterEvent` is just a typealias which contains the relevant information of the header/footer. 48 | 49 | `HeaderFooterEvent = (view: UIView, section: Int, table: UITableView)` 50 | 51 | ### Example 52 | 53 | ```swift 54 | tableView.director.on.willDisplayHeader = { ctx in 55 | print("Will display header for section \(ctx.section)") 56 | } 57 | ``` 58 | 59 | ## `TableAdapter` Events 60 | 61 | The following properties are used to manage the appearance and the behaviour for a pair of registered into the table. 62 | Each event will receive a `Context` which contains a type safe instance of the involved model and (if available) cell. 63 | 64 | ### Events 65 | 66 | The following events are reachable from `TableAdapter `'s `.on` property. 67 | 68 | - `dequeue : ((EventContext) -> (Void))` 69 | - `canEdit: ((EventContext) -> Bool)` 70 | - `commitEdit: ((_ ctx: EventContext, _ commit: UITableViewCellEditingStyle) -> Void)` 71 | - `canMoveRow: ((EventContext) -> Bool)` 72 | - `moveRow: ((_ ctx: EventContext, _ dest: IndexPath) -> Void)` 73 | - `prefetch: ((_ models: [M], _ paths: [IndexPath]) -> Void)` 74 | - `cancelPrefetch: ((_ models: [M], _ paths: [IndexPath]) -> Void)` 75 | - `rowHeight: ((EventContext) -> CGFloat)` 76 | - `rowHeightEstimated: ((EventContext) -> CGFloat)` 77 | - `indentLevel: ((EventContext) -> Int)` 78 | - `willDisplay: ((EventContext) -> Void)` 79 | - `shouldSpringLoad: ((EventContext) -> Bool)` 80 | - `editActions: ((EventContext) -> [UITableViewRowAction]?)` 81 | - `tapOnAccessory: ((EventContext) -> Void)` 82 | - `willSelect: ((EventContext) -> IndexPath?)` 83 | - `tap: ((EventContext) -> TableSelectionState)` 84 | - `willDeselect: ((EventContext) -> IndexPath?)` 85 | - `didDeselect: ((EventContext) -> IndexPath?)` 86 | - `willBeginEdit: ((EventContext) -> Void)` 87 | - `didEndEdit: ((EventContext) -> Void)` 88 | - `editStyle: ((EventContext) -> UITableViewCellEditingStyle)` 89 | - `deleteConfirmTitle: ((EventContext) -> String?)` 90 | - `editShouldIndent: ((EventContext) -> Bool)` 91 | - `moveAdjustDestination: ((_ ctx: EventContext, _ proposed: IndexPath) -> IndexPath?)` 92 | - `endDisplay: ((_ cell: C, _ path: IndexPath) -> Void)` 93 | - `shouldShowMenu: ((EventContext) -> Bool)` 94 | - `canPerformMenuAction: ((_ ctx: EventContext, _ sel: Selector, _ sender: Any?) -> Bool)` 95 | - `performMenuAction: ((_ ctx: EventContext, _ sel: Selector, _ sender: Any?) -> Void)` 96 | - `shouldHighlight: ((EventContext) -> Bool)` 97 | - `didHighlight: ((EventContext) -> Void)` 98 | - `didUnhighlight: ((EventContext) -> Void)` 99 | - `canFocus: ((EventContext) -> Bool)` 100 | 101 | ### Received Context 102 | 103 | `EventContext` is a typealias for Adapter's `Context` with `M` is the `ModelProtocol` instance managed by the table and `C` is `CellProtocol` instance used to represent data. 104 | 105 | The following properties are available: 106 | 107 | - `table: UITableView`: target table instance 108 | - `indexPath: IndexPath`: target model index path 109 | - `model: M`: target model instance (type-safe) 110 | - `cell: C?`: target cell instance (type-safe) / if available 111 | 112 | ### Example 113 | 114 | ```swift 115 | let contactAdapter = TableAdapter() 116 | 117 | // intercept tap 118 | contactAdapter.on.tap = { ctx in 119 | print("User tapped on \(ctx.model.fullName)") 120 | return .deselectAnimated 121 | } 122 | 123 | // dequeue event, used to configure the UI of the cell 124 | contactAdapter.on.dequeue = { ctx in 125 | ctx.cell?.labelName?.text = ctx.model.firstName 126 | ctx.cell?.labelLastName?.text = ctx.model.lastName 127 | } 128 | ``` 129 | 130 | ## `TableSectionView` events 131 | 132 | The following events are received from single instances of `headerView`/`footerView` of a `TableSection` instance and are related to header/footer events. 133 | 134 | ### Events 135 | 136 | The following events are available from `TableSectionView `'s `.on` property. 137 | 138 | **`height` event is required.** 139 | 140 | - `dequeue: ((Context) -> Void)` 141 | - `height: ((Context) -> CGFloat)` 142 | - `estimatedHeight: ((Context) -> CGFloat)` 143 | - `willDisplay: ((Context) -> Void)` 144 | - `endDisplay: ((Context) -> Void)` 145 | - `didDisplay: ((Context) -> Void)` 146 | 147 | ### Example 148 | 149 | ```swift 150 | let header = TableSectionView() 151 | header.on.height = { _ in 152 | return 150 153 | } 154 | header.on.dequeue = { ctx in 155 | ctx.view?.titleLabel?.text = "\(articles.count) Articles" 156 | } 157 | 158 | let section = TableSection(headerView: header, models: articles) 159 | ``` 160 | 161 | ### Received Context 162 | 163 | Where `Context` is a structure which contains the following properties of the section view instance: 164 | 165 | - `type: SectionType`: `header` if target view is header, `footer` for footer view 166 | - `table: UITableView`: target table instance 167 | - `view: T`: type-safe target instance of the header/footer view set 168 | - `section: Int`: target section 169 | - `tableSize: CGSize`: size of the target table -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/Collection+Support.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | //MARK: - ModelProtocol 34 | 35 | public protocol ModelProtocol { 36 | 37 | /// Implementation of the protocol require the presence of id property which is used 38 | /// to uniquely identify an model. This is used by the DeepDiff library to evaluate 39 | /// what cells are removed/moved or deleted from table/collection and provide the right 40 | /// animation without an explicitly animation set. 41 | /// 42 | /// NOTE: 43 | /// Default implementation of this property is provided by any class via the extension 44 | /// for `AnyObject` using the `ObjectIdentifier`. 45 | /// An explicit implementation must be done by the developer for struct. 46 | var modelID: Int { get } 47 | 48 | } 49 | 50 | extension ModelProtocol where Self : AnyObject { 51 | 52 | /// Default implementation of the ModelProtocol protocol is provided for any class using 53 | /// the `ObjectIdentifier`. You can still implement your own item. 54 | public var modelID: Int { 55 | return ObjectIdentifier(self).hashValue 56 | } 57 | 58 | } 59 | 60 | //MARK: - CellProtocol Implementation 61 | 62 | extension UICollectionViewCell: CellProtocol { } 63 | 64 | public protocol CellProtocol: class { 65 | static var reuseIdentifier: String { get } 66 | static var registerAsClass: Bool { get } 67 | } 68 | 69 | public extension CellProtocol { 70 | 71 | /// By default the identifier of the cell is the same name of the cell. 72 | static var reuseIdentifier: String { 73 | return String(describing: self) 74 | } 75 | 76 | /// Return true if you want to allocate the cell via class name using classic 77 | /// `initWithFrame`/`initWithCoder`. If your cell UI is defined inside a nib file 78 | /// or inside a storyboard you must return `false`. 79 | static var registerAsClass : Bool { 80 | return false 81 | } 82 | 83 | } 84 | 85 | //MARK: - HeaderFooterProtocol 86 | 87 | public protocol HeaderFooterProtocol: class { 88 | static var reuseIdentifier: String { get } 89 | static var registerAsClass: Bool { get } 90 | } 91 | 92 | public extension HeaderFooterProtocol { 93 | 94 | /// By default the identifier of the cell is the same name of the cell. 95 | static var reuseIdentifier: String { 96 | return String(describing: self) 97 | } 98 | 99 | /// Return true if you want to allocate the cell via class name using classic 100 | /// `initWithFrame`/`initWithCoder`. If your cell UI is defined inside a nib file 101 | /// or inside a storyboard you must return `false`. 102 | static var registerAsClass : Bool { 103 | return false 104 | } 105 | } 106 | 107 | extension UITableViewHeaderFooterView: HeaderFooterProtocol { 108 | 109 | /// By default it uses the same name of the class. 110 | public static var reuseIdentifier: String { 111 | return String(describing: self) 112 | } 113 | 114 | /// Return true if you want to allocate the cell via class name using classic 115 | /// `initWithFrame`/`initWithCoder`. If your header/footer UI is defined inside a nib file 116 | /// or inside a storyboard you must return `false`. 117 | public static var registerAsClass: Bool { 118 | return false 119 | } 120 | 121 | } 122 | 123 | extension UICollectionReusableView : HeaderFooterProtocol { 124 | 125 | /// By default it uses the same name of the class. 126 | public static var reuseIdentifier: String { 127 | return String(describing: self) 128 | } 129 | 130 | /// Return true if you want to allocate the cell via class name using classic 131 | /// `initWithFrame`/`initWithCoder`. If your header/footer UI is defined inside a nib file 132 | /// or inside a storyboard you must return `false`. 133 | public static var registerAsClass: Bool { 134 | return false 135 | } 136 | 137 | } 138 | 139 | //MARK: - Abstract Protocols 140 | 141 | public protocol AbstractAdapterProtocol { 142 | var modelType: Any.Type { get } 143 | var cellType: Any.Type { get } 144 | var cellReuseIdentifier: String { get } 145 | var cellClass: AnyClass { get } 146 | var registerAsClass: Bool { get } 147 | 148 | } 149 | 150 | public protocol AbstractCollectionReusableView { 151 | var viewClass: AnyClass { get } 152 | var reuseIdentifier: String { get } 153 | var registerAsClass: Bool { get } 154 | } 155 | 156 | //MARK: - Internal Protocols 157 | 158 | internal protocol AbstractCollectionHeaderFooterItem { 159 | 160 | @discardableResult 161 | func dispatch(_ event: CollectionSectionViewEventsKey, type: SectionType, view: UICollectionReusableView?, section: Int, collection: UICollectionView) -> Any? 162 | 163 | } 164 | 165 | public protocol CollectionSectionProtocol : AbstractCollectionReusableView { 166 | var section: CollectionSection? { get set } 167 | } 168 | 169 | 170 | internal protocol AbstractAdapterProtocolFunctions { 171 | 172 | @discardableResult 173 | func dispatch(_ event: CollectionAdapterEventKey, context: InternalContext) -> Any? 174 | 175 | func _instanceCell(in collection: UICollectionView, at indexPath: IndexPath?) -> UICollectionViewCell 176 | } 177 | 178 | public protocol CollectionAdapterProtocol : AbstractAdapterProtocol, Equatable { 179 | 180 | } 181 | 182 | internal enum CollectionAdapterEventKey: Int { 183 | case dequeue 184 | case shouldSelect 185 | case shouldDeselect 186 | case didSelect 187 | case didDeselect 188 | case didHighlight 189 | case didUnhighlight 190 | case shouldHighlight 191 | case willDisplay 192 | case endDisplay 193 | case shouldShowEditMenu 194 | case canPerformEditAction 195 | case performEditAction 196 | case canFocus 197 | case itemSize 198 | //case generateDragPreview 199 | //case generateDropPreview 200 | case prefetch 201 | case cancelPrefetch 202 | case shouldSpringLoad 203 | } 204 | 205 | internal enum CollectionSectionViewEventsKey: Int { 206 | case dequeue 207 | case referenceSize 208 | case didDisplay 209 | case endDisplay 210 | case willDisplay 211 | } 212 | 213 | public extension UICollectionView { 214 | 215 | private static let DIRECTOR_KEY = "flowkit.director" 216 | 217 | /// Return director associated with collection. 218 | /// If not exist it will be created and assigned automatically. 219 | public var director: FlowCollectionDirector { 220 | get { 221 | return getAssociatedValue(key: UICollectionView.DIRECTOR_KEY, 222 | object: self, 223 | initialValue: FlowCollectionDirector(self)) 224 | } 225 | set { 226 | set(associatedValue: newValue, key: UICollectionView.DIRECTOR_KEY, object: self) 227 | } 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/FlowCollectionDirector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | open class FlowCollectionDirector: CollectionDirector, UICollectionViewDelegateFlowLayout { 34 | 35 | /// Margins to apply to content. 36 | /// This is a global value, you can customize a per-section behaviour by implementing `sectionInsets` property into a section. 37 | /// Initially is set to `.zero`. 38 | public var sectionsInsets: UIEdgeInsets { 39 | set { self.layout?.sectionInset = newValue } 40 | get { return self.layout!.sectionInset } 41 | } 42 | 43 | /// Minimum spacing (in points) to use between items in the same row or column. 44 | /// This is a global value, you can customize a per-section behaviour by implementing `minimumInteritemSpacing` property into a section. 45 | /// Initially is set to `CGFloat.leastNormalMagnitude`. 46 | public var minimumInteritemSpacing: CGFloat { 47 | set { self.layout?.minimumInteritemSpacing = newValue } 48 | get { return self.layout!.minimumInteritemSpacing } 49 | } 50 | 51 | /// The minimum spacing (in points) to use between rows or columns. 52 | /// This is a global value, you can customize a per-section behaviour by implementing `minimumInteritemSpacing` property into a section. 53 | /// Initially is set to `0`. 54 | public var minimumLineSpacing: CGFloat { 55 | set { self.layout?.minimumLineSpacing = newValue } 56 | get { return self.layout!.minimumLineSpacing } 57 | } 58 | 59 | /// When this property is true, section header views scroll with content until they reach the top of the screen, 60 | /// at which point they are pinned to the upper bounds of the collection view. 61 | /// Each new header view that scrolls to the top of the screen pushes the previously pinned header view offscreen. 62 | /// 63 | /// The default value of this property is `false`. 64 | @available(iOS 9.0, *) 65 | public var stickyHeaders: Bool { 66 | set { self.layout?.sectionHeadersPinToVisibleBounds = newValue } 67 | get { return (self.layout?.sectionHeadersPinToVisibleBounds ?? false) } 68 | } 69 | 70 | /// When this property is true, section footer views scroll with content until they reach the bottom of the screen, 71 | /// at which point they are pinned to the lower bounds of the collection view. 72 | /// Each new footer view that scrolls to the bottom of the screen pushes the previously pinned footer view offscreen. 73 | /// 74 | /// The default value of this property is `false`. 75 | @available(iOS 9.0, *) 76 | public var stickyFooters: Bool { 77 | set { self.layout?.sectionFootersPinToVisibleBounds = newValue } 78 | get { return (self.layout?.sectionFootersPinToVisibleBounds ?? false) } 79 | } 80 | 81 | /// Return/set the `UICollectionViewFlowLayout` associated with the collection. 82 | public var layout: UICollectionViewFlowLayout? { 83 | get { return (self.collection?.collectionViewLayout as? UICollectionViewFlowLayout) } 84 | set { 85 | guard let c = newValue else { return } 86 | self.collection?.collectionViewLayout = c 87 | } 88 | } 89 | 90 | /// Set the section reference starting point. 91 | @available(iOS 11.0, *) 92 | public var sectionInsetReference: UICollectionViewFlowLayout.SectionInsetReference { 93 | set { self.layout?.sectionInsetReference = newValue } 94 | get { return self.layout!.sectionInsetReference } 95 | } 96 | 97 | /// Initialize a new flow collection manager. 98 | /// Note: Layout of the collection must be a UICollectionViewFlowLayout or subclass. 99 | /// 100 | /// - Parameters: 101 | /// - collection: collection instance to manage. 102 | /// - flowLayout: if not `nil` it will be set a `collectionViewLayout` of given collection. 103 | public init(_ collection: UICollectionView, flowLayout: UICollectionViewLayout? = nil) { 104 | let usedLayout = (flowLayout ?? collection.collectionViewLayout) 105 | guard usedLayout is UICollectionViewFlowLayout else { 106 | fatalError("FlowCollectionManager require a UICollectionViewLayout layout.") 107 | } 108 | if let newLayout = flowLayout { 109 | collection.collectionViewLayout = newLayout 110 | } 111 | super.init(collection) 112 | 113 | self.layout?.sectionInset = .zero 114 | self.layout?.minimumInteritemSpacing = CGFloat.leastNormalMagnitude 115 | self.layout?.minimumLineSpacing = 0 116 | } 117 | 118 | //MARK: UICollectionViewDelegateFlowLayout Events 119 | 120 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 121 | let (model,adapter) = self.context(forItemAt: indexPath) 122 | switch self.itemSize { 123 | case .default: 124 | guard let size = adapter.dispatch(.itemSize, context: InternalContext(model, indexPath, nil, collectionView)) as? CGSize else { 125 | return self.layout!.itemSize 126 | } 127 | return size 128 | case .autoLayout(let est): 129 | return est 130 | case .fixed(let size): 131 | return size 132 | } 133 | } 134 | 135 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 136 | guard let value = self.sections[section].sectionInsets?() else { 137 | return self.sectionsInsets 138 | } 139 | return value 140 | } 141 | 142 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 143 | guard let value = self.sections[section].minimumInterItemSpacing?() else { 144 | return self.minimumInteritemSpacing 145 | } 146 | return value 147 | } 148 | 149 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 150 | guard let value = self.sections[section].minimumLineSpacing?() else { 151 | return self.minimumLineSpacing 152 | } 153 | return value 154 | } 155 | 156 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 157 | let header = (sections[section].header as? AbstractCollectionHeaderFooterItem) 158 | guard let size = header?.dispatch(.referenceSize, type: .header, view: nil, section: section, collection: collectionView) as? CGSize else { 159 | return .zero 160 | } 161 | return size 162 | } 163 | 164 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 165 | let footer = (sections[section].footer as? AbstractCollectionHeaderFooterItem) 166 | guard let size = footer?.dispatch(.referenceSize, type: .footer, view: nil, section: section, collection: collectionView) as? CGSize else { 167 | return .zero 168 | } 169 | return size 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /Sources/FlowKit/Table/TableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | /// Represent a single section of the table 34 | open class TableSection: ModelProtocol { 35 | 36 | /// State of collapse for the section. 37 | public var collapsed: Bool = false 38 | 39 | /// Items inside the section. 40 | private var _models: [ModelProtocol] = [] 41 | 42 | /// Items inside the section. 43 | public private(set) var models: [ModelProtocol] { 44 | get { 45 | return collapsed ? [] : _models 46 | } 47 | set { 48 | _models = newValue 49 | } 50 | } 51 | 52 | /// Title of the header; if `headerView` is set this value is ignored. 53 | public var headerTitle: String? 54 | 55 | /// Title of the footer; if `footerView` is set this value is ignored. 56 | public var footerTitle: String? 57 | 58 | /// View of the header 59 | public var headerView: TableHeaderFooterProtocol? { 60 | willSet { 61 | self.headerView?.section = nil 62 | } 63 | didSet { 64 | self.headerView?.section = self 65 | } 66 | } 67 | 68 | /// View of the footer 69 | public var footerView: TableHeaderFooterProtocol? { 70 | willSet { 71 | self.footerView?.section = nil 72 | } 73 | didSet { 74 | self.footerView?.section = self 75 | } 76 | } 77 | 78 | /// Optional index title for this section (used for `sectionIndexTitles(for: UITableView)`) 79 | public var indexTitle: String? 80 | 81 | /// Unique identifier of the section 82 | public let UUID: String = NSUUID().uuidString 83 | 84 | /// Initialize a new section with given initial models. 85 | /// 86 | /// - Parameter models: items to add (`nil` means empty array) 87 | public init(_ models: [ModelProtocol]?) { 88 | self.models = (models ?? []) 89 | } 90 | 91 | /// Initialize a new section with given header/footer's titles and initial items. 92 | /// 93 | /// - Parameters: 94 | /// - headerTitle: header title as string 95 | /// - footerTitle: footer title as string 96 | /// - models: models to add (`nil` means empty array) 97 | public convenience init(headerTitle: String?, footerTitle: String?, 98 | models: [ModelProtocol]? = nil) { 99 | self.init(models) 100 | self.headerTitle = headerTitle 101 | self.footerTitle = footerTitle 102 | } 103 | 104 | /// Initialize a new section with given view for header/footer and initial items. 105 | /// 106 | /// - Parameters: 107 | /// - headerView: header view 108 | /// - footerView: footer view 109 | /// - models: models to add (`nil` means empty array) 110 | public convenience init(headerView: TableHeaderFooterProtocol?, 111 | footerView: TableHeaderFooterProtocol?, 112 | models: [ModelProtocol]? = nil) { 113 | self.init(models) 114 | self.headerView = headerView 115 | self.footerView = footerView 116 | } 117 | 118 | /// Hash identifier of the section. 119 | public var modelID: Int { 120 | return self.UUID.hashValue 121 | } 122 | 123 | /// Equatable support. 124 | public static func == (lhs: TableSection, rhs: TableSection) -> Bool { 125 | return (lhs.UUID == rhs.UUID) 126 | } 127 | 128 | /// Change the content of the section. 129 | /// 130 | /// - Parameter models: array of models to set. 131 | public func set(models: [ModelProtocol]) { 132 | self.models = models 133 | } 134 | 135 | /// Replace a model instance at specified index. 136 | /// 137 | /// - Parameters: 138 | /// - model: new instance to use. 139 | /// - index: index of the instance to replace. 140 | /// - Returns: old instance, `nil` if provided `index` is invalid. 141 | @discardableResult 142 | public func set(model: ModelProtocol, at index: Int) -> ModelProtocol? { 143 | guard index >= 0, index < self.models.count else { return nil } 144 | let oldModel = self.models[index] 145 | self.models[index] = model 146 | return oldModel 147 | } 148 | 149 | /// Add item at given index. 150 | /// 151 | /// - Parameters: 152 | /// - model: model to append 153 | /// - index: destination index; if invalid or `nil` model is append at the end of the list. 154 | public func add(model: ModelProtocol?, at index: Int?) { 155 | guard let model = model else { return } 156 | guard let index = index, index < self.models.count else { 157 | self.models.append(model) 158 | return 159 | } 160 | self.models.insert(model, at: index) 161 | } 162 | 163 | /// Add models starting at given index of the array. 164 | /// 165 | /// - Parameters: 166 | /// - models: models to insert. 167 | /// - index: destination starting index; if invalid or `nil` models are append at the end of the list. 168 | public func add(models: [ModelProtocol]?, at index: Int?) { 169 | guard let models = models else { return } 170 | guard let index = index, index < self.models.count else { 171 | self.models.append(contentsOf: models) 172 | return 173 | } 174 | self.models.insert(contentsOf: models, at: index) 175 | } 176 | 177 | /// Remove model at given index. 178 | /// 179 | /// - Parameter index: index to remove. 180 | /// - Returns: removed model, `nil` if index is invalid. 181 | @discardableResult 182 | public func remove(at index: Int) -> ModelProtocol? { 183 | guard index < self.models.count else { return nil } 184 | return self.models.remove(at: index) 185 | } 186 | 187 | /// Remove model at given indexes set. 188 | /// 189 | /// - Parameter indexes: indexes to remove. 190 | /// - Returns: an array of removed indexes starting from the lower index to the last one. Invalid indexes are ignored. 191 | @discardableResult 192 | public func remove(atIndexes indexes: IndexSet) -> [ModelProtocol] { 193 | var removed: [ModelProtocol] = [] 194 | indexes.reversed().forEach { 195 | if $0 < self.models.count { 196 | removed.append(self.models.remove(at: $0)) 197 | } 198 | } 199 | return removed 200 | } 201 | 202 | /// Remove all models into the section. 203 | /// 204 | /// - Parameter kp: `true` to keep the capacity and optimize operations. 205 | /// - Returns: count removed items. 206 | @discardableResult 207 | public func removeAll(keepingCapacity kp: Bool = false) -> Int { 208 | let count = self.models.count 209 | self.models.removeAll(keepingCapacity: kp) 210 | return count 211 | } 212 | 213 | /// Swap model at given index to another destination index. 214 | /// 215 | /// - Parameters: 216 | /// - sourceIndex: source index 217 | /// - destIndex: destination index 218 | public func move(swappingAt sourceIndex: Int, with destIndex: Int) { 219 | guard sourceIndex < self.models.count, destIndex < self.models.count else { return } 220 | swap(&self.models[sourceIndex], &self._models[destIndex]) 221 | } 222 | 223 | /// Remove model at given index and insert at destination index. 224 | /// 225 | /// - Parameters: 226 | /// - sourceIndex: source index 227 | /// - destIndex: destination index 228 | public func move(from sourceIndex: Int, to destIndex: Int) { 229 | guard sourceIndex < self.models.count, destIndex < self.models.count else { return } 230 | let removed = self.models.remove(at: sourceIndex) 231 | self.models.insert(removed, at: destIndex) 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/CollectionSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | /// Represent a single section of the collection. 34 | open class CollectionSection: Equatable, ModelProtocol { 35 | 36 | /// Identifier of the section 37 | public var UUID: String = NSUUID().uuidString 38 | 39 | /// Items inside the collection 40 | public private(set) var models: [ModelProtocol] 41 | 42 | /// Implement this method when you want to provide margins for sections in the flow layout. 43 | /// If you do not implement this method, the margins are obtained from the properties of the flow layout object. 44 | /// NOTE: It's valid only for flow layout. 45 | open var sectionInsets: (() -> (UIEdgeInsets))? = nil 46 | 47 | /// The minimum spacing (in points) to use between items in the same row or column. 48 | /// If you do not implement this method, value is obtained from the properties of the flow layout object. 49 | /// NOTE: It's valid only for flow layout. 50 | open var minimumInterItemSpacing: (() -> (CGFloat))? = nil 51 | 52 | /// The minimum spacing (in points) to use between rows or columns. 53 | /// If you do not implement this method, value is obtained from the properties of the flow layout object. 54 | /// NOTE: It's valid only for flow layout. 55 | open var minimumLineSpacing: (() -> (CGFloat))? = nil 56 | 57 | /// Header of the sections; instantiate a new object of `CollectionSectionView`. 58 | /// NOTE: It's valid only for flow layout. 59 | open var header: CollectionSectionProtocol? = nil { 60 | willSet { 61 | self.header?.section = nil 62 | } 63 | didSet { 64 | self.header?.section = self 65 | } 66 | } 67 | 68 | /// Footer of the sections; instantiate a new object of `CollectionSectionView`. 69 | /// NOTE: It's valid only for flow layout. 70 | open var footer: CollectionSectionProtocol? = nil { 71 | willSet { 72 | self.footer?.section = nil 73 | } 74 | didSet { 75 | self.footer?.section = self 76 | } 77 | } 78 | 79 | /// Temporary removed models, it's used to pass the correct model 80 | /// to didEndDisplay event; after sent it will be removed automatically. 81 | private var temporaryRemovedModels: [IndexPath : ModelProtocol] = [:] 82 | 83 | /// Managed manager 84 | private weak var manager: CollectionDirector? 85 | 86 | /// Index of the section in manager. 87 | /// If section is not part of a manager it returns `nil`. 88 | private var index: Int? { 89 | guard let man = manager, let idx = man.sections.index(of: self) else { return nil } 90 | return idx 91 | } 92 | 93 | /// Initialize a new section with given objects as models. 94 | /// 95 | /// - Parameters: 96 | /// - models: models, `nil` create an empty set 97 | /// - headerView: optional custom header 98 | /// - footerView: optional custom footer 99 | public init(_ models: [ModelProtocol]?, headerView: CollectionSectionProtocol? = nil, footerView: CollectionSectionProtocol? = nil) { 100 | self.models = (models ?? []) 101 | self.header = headerView 102 | self.footer = footerView 103 | } 104 | 105 | 106 | /// Hash identifier of the section 107 | public var modelID: Int { 108 | return self.UUID.hashValue 109 | } 110 | 111 | /// Equatable support. 112 | public static func == (lhs: CollectionSection, rhs: CollectionSection) -> Bool { 113 | return (lhs.UUID == rhs.UUID) 114 | } 115 | 116 | /// Change the content of the section. 117 | /// 118 | /// - Parameter models: array of models to set. 119 | public func set(models: [ModelProtocol]) { 120 | self.models = models 121 | } 122 | 123 | /// Replace a model instance at specified index. 124 | /// 125 | /// - Parameters: 126 | /// - model: new instance to use. 127 | /// - index: index of the instance to replace. 128 | /// - Returns: old instance, `nil` if provided `index` is invalid. 129 | @discardableResult 130 | public func set(model: ModelProtocol, at index: Int) -> ModelProtocol? { 131 | guard index >= 0, index < self.models.count else { return nil } 132 | let oldModel = self.models[index] 133 | self.models[index] = model 134 | return oldModel 135 | } 136 | 137 | /// Add item at given index. 138 | /// 139 | /// - Parameters: 140 | /// - model: model to append 141 | /// - index: destination index; if invalid or `nil` model is append at the end of the list. 142 | public func add(model: ModelProtocol?, at index: Int?) { 143 | guard let model = model else { return } 144 | guard let index = index, index < self.models.count else { 145 | self.models.append(model) 146 | return 147 | } 148 | self.models.insert(model, at: index) 149 | } 150 | 151 | /// Add models starting at given index of the array. 152 | /// 153 | /// - Parameters: 154 | /// - models: models to insert. 155 | /// - index: destination starting index; if invalid or `nil` models are append at the end of the list. 156 | public func add(models: [ModelProtocol]?, at index: Int?) { 157 | guard let models = models else { return } 158 | guard let index = index, index < self.models.count else { 159 | self.models.append(contentsOf: models) 160 | return 161 | } 162 | self.models.insert(contentsOf: models, at: index) 163 | } 164 | 165 | /// Remove model at given index. 166 | /// 167 | /// - Parameter index: index to remove. 168 | /// - Returns: removed model, `nil` if index is invalid. 169 | @discardableResult 170 | public func remove(at index: Int) -> ModelProtocol? { 171 | guard index < self.models.count else { return nil } 172 | return self.models.remove(at: index) 173 | } 174 | 175 | /// Remove model at given indexes set. 176 | /// 177 | /// - Parameter indexes: indexes to remove. 178 | /// - Returns: an array of removed indexes starting from the lower index to the last one. Invalid indexes are ignored. 179 | @discardableResult 180 | public func remove(atIndexes indexes: IndexSet) -> [ModelProtocol] { 181 | var removed: [ModelProtocol] = [] 182 | indexes.reversed().forEach { 183 | if $0 < self.models.count { 184 | removed.append(self.models.remove(at: $0)) 185 | } 186 | } 187 | return removed 188 | } 189 | 190 | /// Remove all models into the section. 191 | /// 192 | /// - Parameter kp: `true` to keep the capacity and optimize operations. 193 | /// - Returns: count removed items. 194 | @discardableResult 195 | public func removeAll(keepingCapacity kp: Bool = false) -> Int { 196 | let count = self.models.count 197 | self.models.removeAll(keepingCapacity: kp) 198 | return count 199 | } 200 | 201 | /// Swap model at given index to another destination index. 202 | /// 203 | /// - Parameters: 204 | /// - sourceIndex: source index 205 | /// - destIndex: destination index 206 | public func move(swappingAt sourceIndex: Int, with destIndex: Int) { 207 | guard sourceIndex < self.models.count, destIndex < self.models.count else { return } 208 | swap(&self.models[sourceIndex], &self.models[destIndex]) 209 | } 210 | 211 | /// Remove model at given index and insert at destination index. 212 | /// 213 | /// - Parameters: 214 | /// - sourceIndex: source index 215 | /// - destIndex: destination index 216 | public func move(from sourceIndex: Int, to destIndex: Int) { 217 | guard sourceIndex < self.models.count, destIndex < self.models.count else { return } 218 | let removed = self.models.remove(at: sourceIndex) 219 | self.models.insert(removed, at: destIndex) 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/CollectionAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | /// The adapter identify a pair of model and cell used to represent the data. 34 | open class CollectionAdapter: CollectionAdapterProtocol, CustomStringConvertible, AbstractAdapterProtocolFunctions { 35 | 36 | public var modelType: Any.Type = M.self 37 | public var cellType: Any.Type = C.self 38 | 39 | public var registerAsClass: Bool { 40 | return C.registerAsClass 41 | } 42 | 43 | public var cellReuseIdentifier: String { 44 | return C.reuseIdentifier 45 | } 46 | 47 | public var cellClass: AnyClass { 48 | return C.self 49 | } 50 | 51 | /// Events for adapter 52 | public var on = CollectionAdapter.Events() 53 | 54 | /// Initialize a new adapter and allows its configuration via builder callback. 55 | /// 56 | /// - Parameter configuration: configuration callback 57 | public init(_ configuration: ((CollectionAdapter) -> (Void))? = nil) { 58 | configuration?(self) 59 | } 60 | 61 | //MARK: Standard Protocol Implementations 62 | 63 | /// Description of the adapter 64 | public var description: String { 65 | return "Adapter<\(String(describing: self.cellType)),\(String(describing: self.modelType))>" 66 | } 67 | 68 | /// Equatable support. 69 | /// Two adapters are equal if it manages the same pair of data. 70 | public static func == (lhs: CollectionAdapter, rhs: CollectionAdapter) -> Bool { 71 | return (String(describing: lhs.modelType) == String(describing: rhs.modelType)) && 72 | (String(describing: lhs.cellType) == String(describing: rhs.cellType)) 73 | } 74 | 75 | func dispatch(_ event: CollectionAdapterEventKey, context: InternalContext) -> Any? { 76 | switch event { 77 | 78 | case .dequeue: 79 | guard let callback = self.on.dequeue else { return nil } 80 | callback(Context(generic: context)) 81 | 82 | case .shouldSelect: 83 | guard let callback = self.on.shouldSelect else { return nil } 84 | return callback(Context(generic: context)) 85 | 86 | case .didSelect: 87 | guard let callback = self.on.didSelect else { return nil } 88 | callback(Context(generic: context)) 89 | 90 | case .didDeselect: 91 | guard let callback = self.on.didDeselect else { return nil } 92 | callback(Context(generic: context)) 93 | 94 | case .didHighlight: 95 | guard let callback = self.on.didHighlight else { return nil } 96 | callback(Context(generic: context)) 97 | 98 | case .didUnhighlight: 99 | guard let callback = self.on.didUnhighlight else { return nil } 100 | callback(Context(generic: context)) 101 | 102 | case .shouldHighlight: 103 | guard let callback = self.on.shouldHighlight else { return nil } 104 | return callback(Context(generic: context)) 105 | 106 | case .willDisplay: 107 | guard let callback = self.on.willDisplay else { return nil } 108 | callback((context.cell as! C), context.path!) 109 | 110 | case .endDisplay: 111 | guard let callback = self.on.endDisplay else { return nil } 112 | callback((context.cell as! C), context.path!) 113 | 114 | case .shouldShowEditMenu: 115 | guard let callback = self.on.shouldShowEditMenu else { return nil } 116 | return callback(Context(generic: context)) 117 | 118 | case .canPerformEditAction: 119 | guard let callback = self.on.canPerformEditAction else { return nil } 120 | return callback(Context(generic: context)) 121 | 122 | case .performEditAction: 123 | guard let callback = self.on.performEditAction else { return nil } 124 | return callback(Context(generic: context), (context.param1 as! Selector), context.param2) 125 | 126 | case .canFocus: 127 | guard let callback = self.on.canFocus else { return nil } 128 | return callback(Context(generic: context)) 129 | 130 | case .itemSize: 131 | guard let callback = self.on.itemSize else { return nil } 132 | return callback(Context(generic: context)) 133 | 134 | /* case .generateDragPreview: 135 | guard let callback = self.on.generateDragPreview else { return nil } 136 | return callback(Context(generic: context)) 137 | 138 | case .generateDropPreview: 139 | guard let callback = self.on.generateDropPreview else { return nil } 140 | return callback(Context(generic: context)) 141 | */ 142 | case .prefetch: 143 | guard let callback = self.on.prefetch else { return nil } 144 | callback((context.models as! [M]), context.paths!, (context.container as! UICollectionView)) 145 | 146 | case .cancelPrefetch: 147 | guard let callback = self.on.cancelPrefetch else { return nil } 148 | callback((context.models as! [M]), context.paths!, (context.container as! UICollectionView)) 149 | 150 | case .shouldSpringLoad: 151 | guard let callback = self.on.shouldSpringLoad else { return nil } 152 | return callback(Context(generic: context)) 153 | 154 | case .shouldDeselect: 155 | guard let callback = self.on.shouldDeselect else { return nil } 156 | return callback(Context(generic: context)) 157 | 158 | } 159 | return nil 160 | } 161 | 162 | // MARK: Internal Protocol Methods 163 | 164 | func _instanceCell(in collection: UICollectionView, at indexPath: IndexPath?) -> UICollectionViewCell { 165 | guard let indexPath = indexPath else { 166 | let castedCell = self.cellClass as! UICollectionViewCell.Type 167 | let cellInstance = castedCell.init() 168 | return cellInstance 169 | } 170 | return collection.dequeueReusableCell(withReuseIdentifier: C.reuseIdentifier, for: indexPath) 171 | } 172 | 173 | } 174 | 175 | public extension CollectionAdapter { 176 | 177 | /// Context of the adapter. 178 | public struct Context { 179 | 180 | /// Index path of represented context's cell instance 181 | public let indexPath: IndexPath 182 | 183 | /// Represented model instance 184 | public let model: M 185 | 186 | /// Managed source collection 187 | public private(set) weak var collection: UICollectionView? 188 | 189 | /// Managed source collection's bounds size 190 | public var collectionSize: CGSize? { 191 | guard let c = collection else { return nil } 192 | return c.bounds.size 193 | } 194 | 195 | /// Internal cell representation. For some events it may be nil. 196 | /// You can use public's `cell` property to attempt to get a valid instance of the cell 197 | /// (if source events allows it). 198 | private let _cell: C? 199 | 200 | /// Represented cell instance. 201 | /// Depending from the source event where the context is generated it maybe nil. 202 | /// When not `nil` it's stricly typed to its parent adapter cell's definition. 203 | public var cell: C? { 204 | guard let c = _cell else { 205 | return collection?.cellForItem(at: self.indexPath) as? C 206 | } 207 | return c 208 | } 209 | 210 | /// Initialize a new context from a source event. 211 | /// Instances of the Context are generated automatically and received from events; you don't need to allocate on your own. 212 | /// 213 | /// - Parameters: 214 | /// - model: source generic model 215 | /// - cell: source generic cell 216 | /// - path: cell's path 217 | /// - collection: parent cell's collection instance 218 | internal init(model: ModelProtocol, cell: CellProtocol?, path: IndexPath, collection: UICollectionView) { 219 | self.model = model as! M 220 | self._cell = cell as? C 221 | self.indexPath = path 222 | self.collection = collection 223 | } 224 | 225 | internal init(generic: InternalContext) { 226 | self.model = (generic.model as! M) 227 | self._cell = (generic.cell as? C) 228 | self.indexPath = generic.path! 229 | self.collection = (generic.container as! UICollectionView) 230 | } 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /Sources/FlowKit/Table/TableAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | /// Adapter manages a model type with its associated view representation (a particular cell type). 34 | open class TableAdapter: TableAdapterProtocol,TableAdaterProtocolFunctions { 35 | 36 | /// TableAdapterProtocol conformances 37 | public var modelType: Any.Type = M.self 38 | public var cellType: Any.Type = C.self 39 | public var cellReuseIdentifier: String { return C.reuseIdentifier } 40 | public var cellClass: AnyClass { return C.self } 41 | public var registerAsClass: Bool { return C.registerAsClass } 42 | 43 | public static func == (lhs: TableAdapter, rhs: TableAdapter) -> Bool { 44 | return (String(describing: lhs.modelType) == String(describing: rhs.modelType)) && 45 | (String(describing: lhs.cellType) == String(describing: rhs.cellType)) 46 | } 47 | 48 | 49 | /// Events of the adapter 50 | public var on: Events = Events() 51 | 52 | /// Initialize a new adapter with optional configuration callback. 53 | /// 54 | /// - Parameter configuration: configuration callback 55 | public init(_ configuration: ((TableAdapter) -> (Void))? = nil) { 56 | configuration?(self) 57 | } 58 | 59 | //MARK: TableAdaterProtocolFunctions Protocol 60 | 61 | func _instanceCell(in table: UITableView, at indexPath: IndexPath?) -> UITableViewCell { 62 | guard let indexPath = indexPath else { 63 | let castedCell = self.cellClass as! UITableViewCell.Type 64 | let cellInstance = castedCell.init() 65 | return cellInstance 66 | } 67 | return table.dequeueReusableCell(withIdentifier: C.reuseIdentifier, for: indexPath) 68 | } 69 | 70 | func dispatch(_ event: TableAdapterEventsKey, context: InternalContext) -> Any? { 71 | switch event { 72 | 73 | case .dequeue: 74 | guard let callback = self.on.dequeue else { return nil } 75 | callback(Context(generic: context)) 76 | 77 | case .canEdit: 78 | guard let callback = self.on.canEdit else { return nil } 79 | return callback(Context(generic: context)) 80 | 81 | case .commitEdit: 82 | guard let callback = self.on.commitEdit else { return nil } 83 | return callback(Context(generic: context), (context.param1 as! UITableViewCell.EditingStyle)) 84 | 85 | case .canMoveRow: 86 | guard let callback = self.on.canMoveRow else { return nil } 87 | return callback(Context(generic: context)) 88 | 89 | case .moveRow: 90 | guard let callback = self.on.moveRow else { return nil } 91 | callback(Context(generic: context), (context.param1 as! IndexPath)) 92 | 93 | case .prefetch: 94 | guard let callback = self.on.prefetch else { return nil } 95 | callback( (context.models as! [M]), context.paths!) 96 | 97 | case .cancelPrefetch: 98 | guard let callback = self.on.cancelPrefetch else { return nil } 99 | callback( (context.models as! [M]), context.paths!) 100 | 101 | case .rowHeight: 102 | guard let callback = self.on.rowHeight else { return nil } 103 | return callback(Context(generic: context)) 104 | 105 | case .rowHeightEstimated: 106 | guard let callback = self.on.rowHeightEstimated else { return nil } 107 | return callback(Context(generic: context)) 108 | 109 | case .indentLevel: 110 | guard let callback = self.on.indentLevel else { return nil } 111 | return callback(Context(generic: context)) 112 | 113 | case .willDisplay: 114 | guard let callback = self.on.willDisplay else { return nil } 115 | return callback(Context(generic: context)) 116 | 117 | case .shouldSpringLoad: 118 | guard let callback = self.on.shouldSpringLoad else { return nil } 119 | return callback(Context(generic: context)) 120 | 121 | case .editActions: 122 | guard let callback = self.on.editActions else { return nil } 123 | return callback(Context(generic: context)) 124 | 125 | case .tapOnAccessory: 126 | guard let callback = self.on.tapOnAccessory else { return nil } 127 | callback(Context(generic: context)) 128 | 129 | case .willSelect: 130 | guard let callback = self.on.willSelect else { return nil } 131 | return callback(Context(generic: context)) 132 | 133 | case .tap: 134 | guard let callback = self.on.tap else { return nil } 135 | return callback(Context(generic: context)) 136 | 137 | case .willDeselect: 138 | guard let callback = self.on.willDeselect else { return nil } 139 | return callback(Context(generic: context)) 140 | 141 | case .didDeselect: 142 | guard let callback = self.on.didDeselect else { return nil } 143 | return callback(Context(generic: context)) 144 | 145 | case .willBeginEdit: 146 | guard let callback = self.on.willBeginEdit else { return nil } 147 | callback(Context(generic: context)) 148 | 149 | case .didEndEdit: 150 | guard let callback = self.on.didEndEdit else { return nil } 151 | callback(Context(generic: context)) 152 | 153 | case .editStyle: 154 | guard let callback = self.on.editStyle else { return nil } 155 | return callback(Context(generic: context)) 156 | 157 | case .deleteConfirmTitle: 158 | guard let callback = self.on.deleteConfirmTitle else { return nil } 159 | return callback(Context(generic: context)) 160 | 161 | case .editShouldIndent: 162 | guard let callback = self.on.editShouldIndent else { return nil } 163 | return callback(Context(generic: context)) 164 | 165 | case .moveAdjustDestination: 166 | guard let callback = self.on.moveAdjustDestination else { return nil } 167 | return callback(Context(generic: context), (context.param1 as! IndexPath)) 168 | 169 | case .endDisplay: 170 | guard let callback = self.on.endDisplay else { return nil } 171 | callback((context.cell as! C), context.path!) 172 | 173 | case .shouldShowMenu: 174 | guard let callback = self.on.shouldShowMenu else { return nil } 175 | return callback(Context(generic: context)) 176 | 177 | case .canPerformMenuAction: 178 | guard let callback = self.on.canPerformMenuAction else { return nil } 179 | return callback(Context(generic: context), (context.param1 as! Selector), context.param2) 180 | 181 | case .performMenuAction: 182 | guard let callback = self.on.performMenuAction else { return nil } 183 | return callback(Context(generic: context), (context.param1 as! Selector), context.param2) 184 | 185 | case .shouldHighlight: 186 | guard let callback = self.on.shouldHighlight else { return nil } 187 | return callback(Context(generic: context)) 188 | 189 | case .didHighlight: 190 | guard let callback = self.on.didHighlight else { return nil } 191 | callback(Context(generic: context)) 192 | 193 | case .didUnhighlight: 194 | guard let callback = self.on.didUnhighlight else { return nil } 195 | callback(Context(generic: context)) 196 | 197 | case .canFocus: 198 | guard let callback = self.on.canFocus else { return nil } 199 | return callback(Context(generic: context)) 200 | 201 | case .leadingSwipeActions: 202 | if #available(iOS 11, *) { 203 | guard let callback = self.on.leadingSwipeActions else { return nil } 204 | return callback(Context(generic: context)) 205 | } else { 206 | debugPrint("Supported only for iOS 11 or higher") 207 | } 208 | 209 | case .trailingSwipeActions: 210 | if #available(iOS 11, *) { 211 | guard let callback = self.on.trailingSwipeActions else { return nil } 212 | return callback(Context(generic: context)) 213 | } else { 214 | debugPrint("Supported only for iOS 11 or higher") 215 | } 216 | } 217 | return nil 218 | } 219 | 220 | } 221 | 222 | public extension TableAdapter { 223 | 224 | /// Context of the adapter. 225 | /// A context is sent when an event is fired and includes type-safe informations (context) 226 | /// related to triggered event. 227 | public struct Context { 228 | 229 | /// Parent table 230 | public private(set) weak var table: UITableView? 231 | 232 | /// Index path 233 | public let indexPath: IndexPath 234 | 235 | /// Model instance 236 | public let model: M 237 | 238 | 239 | /// Cell instance 240 | /// NOTE: For some events cell instance is not reachable and may return `nil`. 241 | public var cell: C? { 242 | guard let callback = _cell else { 243 | return table?.cellForRow(at: self.indexPath) as? C 244 | } 245 | return callback 246 | } 247 | private let _cell: C? 248 | 249 | internal init(generic: InternalContext) { 250 | self.model = (generic.model as! M) 251 | self._cell = (generic.cell as? C) 252 | self.indexPath = generic.path! 253 | self.table = (generic.container as! UITableView) 254 | } 255 | 256 | /// Instance a new context with given data. 257 | /// Init of these objects are reserved. 258 | internal init(model: ModelProtocol, cell: CellProtocol?, path: IndexPath, table: UITableView) { 259 | self.model = model as! M 260 | self._cell = cell as? C 261 | self.indexPath = path 262 | self.table = table 263 | } 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /Sources/FlowKit/Shared/DeepDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // DeepDiff is a project by Khoa - https://github.com/onmyway133/DeepDiff 10 | // 11 | // Twitter: @danielemargutti 12 | // 13 | // 14 | // Permission is hereby granted, free of charge, to any person obtaining a copy 15 | // of this software and associated documentation files (the "Software"), to deal 16 | // in the Software without restriction, including without limitation the rights 17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | // copies of the Software, and to permit persons to whom the Software is 19 | // furnished to do so, subject to the following conditions: 20 | // 21 | // The above copyright notice and this permission notice shall be included in 22 | // all copies or substantial portions of the Software. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | // THE SOFTWARE. 31 | 32 | import Foundation 33 | 34 | // https://gist.github.com/ndarville/3166060 35 | 36 | /// Perform diff between old and new collections 37 | /// 38 | /// - Parameters: 39 | /// - old: Old collection 40 | /// - new: New collection 41 | /// - Returns: A set of changes 42 | internal func diff( 43 | old: [ModelProtocol], 44 | new: [ModelProtocol], 45 | algorithm: DiffAware = Heckel()) -> [Change] { 46 | 47 | if let changes = algorithm.preprocess(old: old, new: new) { 48 | return changes 49 | } 50 | 51 | return algorithm.diff(old: old, new: new) 52 | } 53 | 54 | internal struct Insert { 55 | let item: T 56 | let index: Int 57 | } 58 | 59 | internal struct Delete { 60 | let item: T 61 | let index: Int 62 | } 63 | 64 | internal struct Replace { 65 | let oldItem: T 66 | let newItem: T 67 | let index: Int 68 | } 69 | 70 | internal struct Move { 71 | let item: T 72 | let fromIndex: Int 73 | let toIndex: Int 74 | } 75 | 76 | /// The computed changes from diff 77 | /// 78 | /// - insert: Insert an item at index 79 | /// - delete: Delete an item from index 80 | /// - replace: Replace an item at index with another item 81 | /// - move: Move the same item from this index to another index 82 | internal enum Change { 83 | case insert(Insert) 84 | case delete(Delete) 85 | case replace(Replace) 86 | case move(Move) 87 | 88 | var insert: Insert? { 89 | if case .insert(let insert) = self { 90 | return insert 91 | } 92 | 93 | return nil 94 | } 95 | 96 | var delete: Delete? { 97 | if case .delete(let delete) = self { 98 | return delete 99 | } 100 | 101 | return nil 102 | } 103 | 104 | var replace: Replace? { 105 | if case .replace(let replace) = self { 106 | return replace 107 | } 108 | 109 | return nil 110 | } 111 | 112 | var move: Move? { 113 | if case .move(let move) = self { 114 | return move 115 | } 116 | 117 | return nil 118 | } 119 | } 120 | 121 | internal protocol DiffAware { 122 | func diff(old: [ModelProtocol], new: [ModelProtocol]) -> [Change] 123 | } 124 | 125 | extension DiffAware { 126 | func preprocess(old: [ModelProtocol], new: [ModelProtocol]) -> [Change]? { 127 | switch (old.isEmpty, new.isEmpty) { 128 | case (true, true): 129 | // empty 130 | return [] 131 | case (true, false): 132 | // all .insert 133 | return new.enumerated().map { index, item in 134 | return .insert(Insert(item: item, index: index)) 135 | } 136 | case (false, true): 137 | // all .delete 138 | return old.enumerated().map { index, item in 139 | return .delete(Delete(item: item, index: index)) 140 | } 141 | default: 142 | return nil 143 | } 144 | } 145 | } 146 | 147 | internal final class Heckel: DiffAware { 148 | 149 | // OC and NC can assume three values: 1, 2, and many. 150 | enum Counter { 151 | case zero, one, many 152 | 153 | func increment() -> Counter { 154 | switch self { 155 | case .zero: 156 | return .one 157 | case .one: 158 | return .many 159 | case .many: 160 | return self 161 | } 162 | } 163 | } 164 | 165 | // The symbol table stores three entries for each line 166 | class TableEntry: Equatable { 167 | // The value entry for each line in table has two counters. 168 | // They specify the line's number of occurrences in O and N: OC and NC. 169 | var oldCounter: Counter = .zero 170 | var newCounter: Counter = .zero 171 | 172 | // Aside from the two counters, the line's entry 173 | // also includes a reference to the line's line number in O: OLNO. 174 | // OLNO is only interesting, if OC == 1. 175 | // Alternatively, OLNO would have to assume multiple values or none at all. 176 | var indexesInOld: [Int] = [] 177 | 178 | static func ==(lhs: TableEntry, rhs: TableEntry) -> Bool { 179 | return lhs.oldCounter == rhs.oldCounter && lhs.newCounter == rhs.newCounter && lhs.indexesInOld == rhs.indexesInOld 180 | } 181 | } 182 | 183 | // The arrays OA and NA have one entry for each line in their respective files, O and N. 184 | // The arrays contain either: 185 | enum ArrayEntry: Equatable { 186 | // a pointer to the line's symbol table entry, table[line] 187 | case tableEntry(TableEntry) 188 | 189 | // the line's number in the other file (N for OA, O for NA) 190 | case indexInOther(Int) 191 | 192 | static func == (lhs: ArrayEntry, rhs: ArrayEntry) -> Bool { 193 | switch (lhs, rhs) { 194 | case (.tableEntry(let l), .tableEntry(let r)): 195 | return l == r 196 | case (.indexInOther(let l), .indexInOther(let r)): 197 | return l == r 198 | default: 199 | return false 200 | } 201 | } 202 | } 203 | 204 | init() {} 205 | 206 | func diff(old: [ModelProtocol], new: [ModelProtocol]) -> [Change] { 207 | // The Symbol Table 208 | // Each line works as the key in the table look-up, i.e. as table[line]. 209 | var table: [Int: TableEntry] = [:] 210 | 211 | // The arrays OA and NA have one entry for each line in their respective files, O and N 212 | var oldArray = [ArrayEntry]() 213 | var newArray = [ArrayEntry]() 214 | 215 | perform1stPass(new: new, table: &table, newArray: &newArray) 216 | perform2ndPass(old: old, table: &table, oldArray: &oldArray) 217 | perform345Pass(newArray: &newArray, oldArray: &oldArray) 218 | let changes = perform6thPass(new: new, old: old, newArray: newArray, oldArray: oldArray) 219 | return changes 220 | } 221 | 222 | private func perform1stPass( 223 | new: [ModelProtocol], 224 | table: inout [Int: TableEntry], 225 | newArray: inout [ArrayEntry]) { 226 | 227 | // 1st pass 228 | // a. Each line i of file N is read in sequence 229 | new.forEach { item in 230 | // b. An entry for each line i is created in the table, if it doesn't already exist 231 | let entry = table[item.modelID] ?? TableEntry() 232 | 233 | // c. NC for the line's table entry is incremented 234 | entry.newCounter = entry.newCounter.increment() 235 | 236 | // d. NA[i] is set to point to the table entry of line i 237 | newArray.append(.tableEntry(entry)) 238 | 239 | // 240 | table[item.modelID] = entry 241 | } 242 | } 243 | 244 | private func perform2ndPass( 245 | old: [ModelProtocol], 246 | table: inout [Int: TableEntry], 247 | oldArray: inout [ArrayEntry]) { 248 | 249 | // 2nd pass 250 | // Similar to first pass, except it acts on files 251 | 252 | old.enumerated().forEach { tuple in 253 | // old 254 | let entry = table[tuple.element.modelID] ?? TableEntry() 255 | 256 | // oldCounter 257 | entry.oldCounter = entry.oldCounter.increment() 258 | 259 | // lineNumberInOld which is set to the line's number 260 | entry.indexesInOld.append(tuple.offset) 261 | 262 | // oldArray 263 | oldArray.append(.tableEntry(entry)) 264 | 265 | // 266 | table[tuple.element.modelID] = entry 267 | } 268 | } 269 | 270 | private func perform345Pass(newArray: inout [ArrayEntry], oldArray: inout [ArrayEntry]) { 271 | // 3rd pass 272 | // a. We use Observation 1: 273 | // If a line occurs only once in each file, then it must be the same line, 274 | // although it may have been moved. 275 | // We use this observation to locate unaltered lines that we 276 | // subsequently exclude from further treatment. 277 | // b. Using this, we only process the lines where OC == NC == 1 278 | // c. As the lines between O and N "must be the same line, 279 | // although it may have been moved", we alter the table pointers 280 | // in OA and NA to the number of the line in the other file. 281 | // d. We also locate unique virtual lines 282 | // immediately before the first and 283 | // immediately after the last lines of the files ??? 284 | // 285 | // 4th pass 286 | // a. We use Observation 2: 287 | // If a line has been found to be unaltered, 288 | // and the lines immediately adjacent to it in both files are identical, 289 | // then these lines must be the same line. 290 | // This information can be used to find blocks of unchanged lines. 291 | // b. Using this, we process each entry in ascending order. 292 | // c. If 293 | // NA[i] points to OA[j], and 294 | // NA[i+1] and OA[j+1] contain identical table entry pointers 295 | // then 296 | // OA[j+1] is set to line i+1, and 297 | // NA[i+1] is set to line j+1 298 | // 299 | // 5th pass 300 | // Similar to fourth pass, except: 301 | // It processes each entry in descending order 302 | // It uses j-1 and i-1 instead of j+1 and i+1 303 | 304 | newArray.enumerated().forEach { (indexOfNew, item) in 305 | switch item { 306 | case .tableEntry(let entry): 307 | guard !entry.indexesInOld.isEmpty else { 308 | return 309 | } 310 | let indexOfOld = entry.indexesInOld.removeFirst() 311 | let isObservation1 = entry.newCounter == .one && entry.oldCounter == .one 312 | let isObservation2 = entry.newCounter != .zero && entry.oldCounter != .zero && newArray[indexOfNew] == oldArray[indexOfOld] 313 | guard isObservation1 || isObservation2 else { 314 | return 315 | } 316 | newArray[indexOfNew] = .indexInOther(indexOfOld) 317 | oldArray[indexOfOld] = .indexInOther(indexOfNew) 318 | case .indexInOther(_): 319 | break 320 | } 321 | } 322 | } 323 | 324 | private func perform6thPass( 325 | new: [ModelProtocol], 326 | old: [ModelProtocol], 327 | newArray: [ArrayEntry], 328 | oldArray: [ArrayEntry]) -> [Change] { 329 | 330 | // 6th pass 331 | // At this point following our five passes, 332 | // we have the necessary information contained in NA to tell the differences between O and N. 333 | // This pass uses NA and OA to tell when a line has changed between O and N, 334 | // and how far the change extends. 335 | 336 | // a. Determining a New Line 337 | // Recall our initial description of NA in which we said that the array has either: 338 | // one entry for each line of file N containing either 339 | // a pointer to table[line] 340 | // the line's number in file O 341 | 342 | // Using these two cases, we know that if NA[i] refers 343 | // to an entry in table (case 1), then line i must be new 344 | // We know this, because otherwise, NA[i] would have contained 345 | // the line's number in O (case 2), if it existed in O and N 346 | 347 | // b. Determining the Boundaries of the New Line 348 | // We now know that we are dealing with a new line, but we have yet to figure where the change ends. 349 | // Recall Observation 2: 350 | 351 | // If NA[i] points to OA[j], but NA[i+1] does not 352 | // point to OA[j+1], then line i is the boundary for the alteration. 353 | 354 | // You can look at it this way: 355 | // i : The quick brown fox | j : The quick brown fox 356 | // i+1: jumps over the lazy dog | j+1: jumps over the loafing cat 357 | 358 | // Here, NA[i] == OA[j], but NA[i+1] != OA[j+1]. 359 | // This means our boundary is between the two lines. 360 | 361 | var changes = [Change]() 362 | var deleteOffsets = Array(repeating: 0, count: old.count) 363 | 364 | // deletions 365 | do { 366 | var runningOffset = 0 367 | 368 | oldArray.enumerated().forEach { oldTuple in 369 | deleteOffsets[oldTuple.offset] = runningOffset 370 | 371 | guard case .tableEntry = oldTuple.element else { 372 | return 373 | } 374 | 375 | changes.append(.delete(Delete( 376 | item: old[oldTuple.offset], 377 | index: oldTuple.offset 378 | ))) 379 | 380 | runningOffset += 1 381 | } 382 | } 383 | 384 | // insertions, replaces, moves 385 | do { 386 | var runningOffset = 0 387 | 388 | newArray.enumerated().forEach { newTuple in 389 | switch newTuple.element { 390 | case .tableEntry: 391 | runningOffset += 1 392 | changes.append(.insert(Insert( 393 | item: new[newTuple.offset], 394 | index: newTuple.offset 395 | ))) 396 | case .indexInOther(let oldIndex): 397 | if old[oldIndex].modelID != new[newTuple.offset].modelID { 398 | changes.append(.replace(Replace( 399 | oldItem: old[oldIndex], 400 | newItem: new[newTuple.offset], 401 | index: newTuple.offset 402 | ))) 403 | } 404 | 405 | let deleteOffset = deleteOffsets[oldIndex] 406 | // The object is not at the expected position, so move it. 407 | if (oldIndex - deleteOffset + runningOffset) != newTuple.offset { 408 | changes.append(.move(Move( 409 | item: new[newTuple.offset], 410 | fromIndex: oldIndex, 411 | toIndex: newTuple.offset 412 | ))) 413 | } 414 | } 415 | } 416 | } 417 | 418 | return changes 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /Sources/FlowKit/Collection/CollectionDragDropManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flow 3 | // A declarative approach to UICollectionView & UITableView management 4 | // -------------------------------------------------------------------- 5 | // Created by: Daniele Margutti 6 | // hello@danielemargutti.com 7 | // http://www.danielemargutti.com 8 | // 9 | // Twitter: @danielemargutti 10 | // 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in 20 | // all copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | // THE SOFTWARE. 29 | 30 | import Foundation 31 | import UIKit 32 | 33 | public extension CollectionDirector { 34 | 35 | @available(iOS 11.0, *) 36 | public final class DragAndDropManager: NSObject, UICollectionViewDragDelegate, UICollectionViewDropDelegate { 37 | 38 | /// Context of Drag/Drop operation 39 | public struct Context { 40 | 41 | /// Involved index path 42 | public let indexPath: IndexPath 43 | 44 | /// Parent collection 45 | public private(set) weak var collection: UICollectionView? 46 | 47 | /// Involved item instance 48 | public private(set) var item: ModelProtocol 49 | 50 | /// Initialize a new context (private) 51 | internal init(item: ModelProtocol, at path: IndexPath, of collection: UICollectionView) { 52 | self.indexPath = path 53 | self.collection = collection 54 | self.item = item 55 | } 56 | } 57 | 58 | //MARK: DRAG EVENTS: PROVIDING ITEMS TO DRAG 59 | 60 | /// Managed collection manager 61 | public internal(set) weak var manager: CollectionDirector? 62 | 63 | /// Provides the initial set of items (if any) to drag. 64 | /// 65 | /// You must implement this method to allow the dragging of items from your collection view. 66 | /// In your implementation, create one or more UIDragItem objects for the item at the specified indexPath. 67 | /// Normally, you return only one drag item, but if the specified item has children or cannot be dragged 68 | /// without one or more associated items, include those items as well. 69 | /// 70 | /// The collection view calls this method one or more times when a new drag begins within its bounds. 71 | /// Specifically, if the user begins the drag from a selected item, the collection view calls this method 72 | /// once for each item that is part of the selection. If the user begins the drag from an unselected item, 73 | /// the collection view calls the method only once for that item. 74 | /// 75 | /// An array of UIDragItem objects containing the details of the items to drag. 76 | /// Return an empty array to prevent the item from being dragged. 77 | /// 78 | /// NOTE: If not implemented, the default behaviour return an empty set. 79 | public var onPrepareItemForDrag: ((DragAndDropManager.Context) -> [UIDragItem])? = nil 80 | 81 | /// Adds the specified items to an existing drag session. 82 | /// 83 | /// Implement this method when you want to allow the user to add items to an active drag session. 84 | /// If you do not implement this method, taps in the collection view trigger the selection of items or other behaviors. 85 | /// However, when a drag session is active and a tap occurs, the collection view calls this method to give you 86 | /// an opportunity to add the underlying item to the drag session. 87 | /// 88 | /// In your implementation, create one or more UIDragItem objects for the item at the specified indexPath. 89 | /// Normally, you return only one drag item, but if the specified item has children or cannot be dragged 90 | /// without one or more associated items, include those items as well. 91 | /// 92 | /// It must return an array of UIDragItem objects containing the items to add to the current drag session. 93 | /// Return an empty array to prevent the items from being added to the drag session. 94 | /// 95 | /// NOTE: If not implemented, the default behaviour return an empty set. 96 | public var onAddItemToDragSession: ((DragAndDropManager.Context) -> [UIDragItem])? = nil 97 | 98 | //MARK: DRAG EVENTS: TRACKING THE DRAG SESSION 99 | 100 | /// Called to let you know that a drag session is about to begin for the collection view. 101 | public var onWillBeginDragSession: ((UIDragSession) -> Void)? = nil 102 | 103 | /// This method is called after the drag session ended, usually because the content was dropped 104 | /// but possibly because the drag was aborted. 105 | /// Use this method to close out any tasks related to the management of the drag session in your app. 106 | /// 107 | /// Each call to this method is always balanced by a call to the `onWillBeginDragSession` event. 108 | public var onDidEndDragSession: ((UIDragSession) -> Void)? = nil 109 | 110 | /// Restrict drag session to the app only 111 | /// If not implemented it return `true`. 112 | public var onDragSessionRestrictedToApp: ((UIDragSession) -> Bool)? = nil 113 | 114 | /// Allows move operation for given drag session 115 | /// If not implemented it return `true`. 116 | public var onDragSessionAllowsMoveOperation: ((UIDragSession) -> Bool)? = nil 117 | 118 | //MARK: DROP EVENTS: DECLARING SUPPORT TO DROP 119 | 120 | /// Asks whether the collection view can accept a drop with the specified type of data. 121 | /// 122 | /// Implement this method when you want to dynamically determine whether to accept dropped data in your collection view. 123 | /// In your implementation, check the type of the dragged data and return a Boolean value indicating whether you can 124 | /// accept the drop. 125 | /// 126 | /// For example, you might call the hasItemsConforming(toTypeIdentifier:) method of the session object 127 | /// to determine whether it contains data that your app can accept. 128 | /// 129 | /// If you do not implement this method, the collection view assumes a return value of true. 130 | /// If you return false from this method, the collection view does not call any more methods of 131 | /// your drop delegate for the given session. 132 | public var onAcceptDropDession: ((UIDropSession) -> Bool)? = nil 133 | 134 | //MARK: DROP EVENTS: INCORPORATING DROPPED DATA 135 | 136 | /// Tells to incorporate the drop data into the collection view. 137 | /// Use this method to accept the dropped content and integrate it into your collection view. 138 | /// In your implementation, iterate over the items property of the coordinator object and fetch 139 | /// the data from each UIDragItem. 140 | /// 141 | /// Incorporate the data into your collection view's data source and update the collection view itself 142 | /// by inserting any needed items. 143 | /// When incorporating items, use the methods of the coordinator object to animate the transition 144 | /// from the drag item's preview to the corresponding item in your collection view. For items that you incorporate 145 | /// immediately, you can use the drop(_:to:) or drop(_:toItemAt:) method to perform the animation. 146 | /// When loading content asynchronously from an NSItemProvider, you can animate the drop to a placeholder 147 | /// cell using the drop(_:toPlaceholderInsertedAt:withReuseIdentifier:cellUpdateHandler:) method. 148 | public var onPerformDrop: ((UICollectionViewDropCoordinator) -> Void)? = nil 149 | 150 | //MARK: DROP EVENTS: TRACKING DROP MOVEMENTS 151 | 152 | /// Tells that the position of the dragged data over the collection view changed. 153 | /// 154 | /// While the user is dragging content, the collection view calls this method repeatedly to 155 | /// determine how you would handle the drop if it occurred at the specified location. 156 | /// The collection view provides visual feedback to the user based on your proposal. 157 | /// 158 | /// You must implement this method to support drop; nil implementation raise a fatalError. 159 | public var onDropSessionDidUpdate: ((_ session: UIDropSession, _ path: IndexPath?) -> UICollectionViewDropProposal)? = nil 160 | 161 | /// Called when dragged content enters the collection view's bounds rectangle. 162 | /// The collection view calls this method when dragged content enters its bounds rectangle. 163 | /// The method is not called again until the dragged content exits the collection view's bounds 164 | /// (triggering a call to the `onDropSessionDidExit` method) and enters again. 165 | /// 166 | /// Use this method to perform any one-time setup associated with tracking dragged content over the collection view. 167 | public var onDropSessionDidEnter: ((_ session: UIDropSession) -> Void)? = nil 168 | 169 | /// Called when dragged content exits the table view's bounds rectangle. 170 | /// UIKit calls this method when dragged content exits the bounds rectangle of the specified collection view. 171 | /// The method is not called again until the dragged content enters the collection view's bounds 172 | /// (triggering a call to the `onDropSessionDidEnter` method) and exits again. 173 | /// 174 | /// Use this method to clean up any state information that you configured in your `onDropSessionDidEnter`. 175 | public var onDropSessionDidExit: ((_ session: UIDropSession) -> Void)? = nil 176 | 177 | /// Called to notify you when the drag operation ends. 178 | /// The collection view calls this method at the conclusion of a drag that was over the collection view at one point. 179 | /// Use it to clean up any state information that you used to handle the drag. 180 | /// This method is called regardless of whether the data was actually dropped onto the collection view. 181 | public var onDropSessionDidEnd: ((_ session: UIDropSession) -> Void)? = nil 182 | 183 | //MARK: INIT 184 | 185 | /// Internal init 186 | internal init(manager: CollectionDirector) { 187 | super.init() 188 | self.manager = manager 189 | } 190 | 191 | //MARK: UICollectionViewDragDelegate EVENTS 192 | 193 | public func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { 194 | guard let event = self.onPrepareItemForDrag else { 195 | return [] 196 | } 197 | let ctx = Context(item: self.manager!.item(at: indexPath, safe: false)!, at: indexPath, of: collectionView) 198 | return event(ctx) 199 | } 200 | 201 | public func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { 202 | guard let event = self.onAddItemToDragSession else { 203 | return [] 204 | } 205 | let ctx = Context(item: self.manager!.item(at: indexPath, safe: false)!, at: indexPath, of: collectionView) 206 | return event(ctx) 207 | } 208 | 209 | public func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) { 210 | guard let event = self.onWillBeginDragSession else { return } 211 | event(session) 212 | } 213 | 214 | public func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) { 215 | guard let event = self.onDidEndDragSession else { return } 216 | event(session) 217 | } 218 | 219 | /*public func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? { 220 | let (model,adapter) = self.manager!.context(forItemAt: indexPath) 221 | return (adapter.dispatch(.generateDragPreview, context: InternalContext(model, indexPath, nil, collectionView)) as? UIDragPreviewParameters) 222 | }*/ 223 | 224 | public func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool { 225 | guard let event = self.onDragSessionRestrictedToApp else { return true } 226 | return event(session) 227 | } 228 | 229 | public func collectionView(_ collectionView: UICollectionView, dragSessionAllowsMoveOperation session: UIDragSession) -> Bool { 230 | guard let event = self.onDragSessionAllowsMoveOperation else { return true } 231 | return event(session) 232 | } 233 | 234 | //MARK: Drop Events Manager 235 | 236 | public func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { 237 | self.onPerformDrop?(coordinator) 238 | } 239 | 240 | public func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool { 241 | guard let event = self.onAcceptDropDession else { return true } 242 | return event(session) 243 | } 244 | 245 | public func collectionView(_ collectionView: UICollectionView, dropSessionDidEnter session: UIDropSession) { 246 | self.onDropSessionDidEnter?(session) 247 | } 248 | 249 | public func collectionView(_ collectionView: UICollectionView, dropSessionDidExit session: UIDropSession) { 250 | self.onDropSessionDidExit?(session) 251 | } 252 | 253 | public func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession) { 254 | self.onDropSessionDidEnd?(session) 255 | } 256 | 257 | public func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { 258 | guard let event = self.onDropSessionDidUpdate else { 259 | fatalError("Missing CollectionManager Drag&Drop Implementation of onDropSessionDidUpdate event") 260 | } 261 | return event(session,destinationIndexPath) 262 | } 263 | 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /ExampleApp/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | FlowKit 3 |

4 | 5 | ## THE ENTIRE PROJECT WAS MOVED TO THE NEW HOME AND IT'S NOW CALLED OWL. 6 | ## [https://github.com/malcommac/Owl](https://github.com/malcommac/Owl) 7 | ### This repository will be removed in few months. 8 | 9 | -- 10 | 11 | 12 | [![Version](https://img.shields.io/cocoapods/v/FlowKitManager.svg?style=flat)](http://cocoadocs.org/docsets/FlowKitManager) [![License](https://img.shields.io/cocoapods/l/FlowKitManager.svg?style=flat)](http://cocoadocs.org/docsets/FlowKitManager) [![Platform](https://img.shields.io/cocoapods/p/FlowKitManager.svg?style=flat)](http://cocoadocs.org/docsets/FlowKitManager) 13 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/FlowKitManager.svg)](https://img.shields.io/cocoapods/v/FlowKitManager.svg) 14 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 15 | [![Twitter](https://img.shields.io/badge/twitter-@danielemargutti-blue.svg?style=flat)](http://twitter.com/danielemargutti) 16 | 17 |

★★ Star me to follow the project! ★★
18 | Created by Daniele Margutti - danielemargutti.com 19 |

20 | 21 | ## What's FlowKit 22 | FlowKit is a new approach to create, populate and manage `UITableView` and `UICollectionView`. 23 | 24 | With a declarative and type-safe approach you **don't need to implement datasource/delegate** anymore: your code is easy to read, maintain and [SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)). 25 | 26 | Want to know more about FlowKit? 27 | 28 | **I've made an introductory article: [click here to read it now!](http://danielemargutti.com/2018/04/23/tables-collections-with-declarative-approach/)** 29 | 30 | ## Features Highlights 31 | 32 | - **No more datasource/delegate**: you don't need to implements tons of methods just to render your data. Just plain understandable methods to manage what kind of data you want to display, remove or move. 33 | - **Type-safe**: register pair of model/cell types you want to render, then access to instances of them in pure Swift type-safe style. 34 | - **Auto-Layout**: self-sized cells are easy to be configured, both for tables and collection views. 35 | - **Built-In Auto Animations**: changes & sync between your datasource and table/collection is evaluated automatically and you can get animations for free. 36 | - **Compact Code**: your code for table and collection is easy to read & maintain; changes in datasource are done declaratively via `add`/`move` and `remove` functions (both for sections, header/footers and single rows). 37 | 38 | 39 | ## What you will get 40 | The following code is just a small example which shows how to make a simple Contacts list UITableView using FlowKit (it works similar with collection too): 41 | 42 | ```swift 43 | // Create a director which manage the table/collection 44 | let director = TableDirector(self.tableView) 45 | 46 | // Declare an adapter which renders Contact Model using ContactCell 47 | let cAdapter = TableAdapter() 48 | 49 | // Hook events you need 50 | // ...dequeue 51 | cAdapter.on.dequeue = { ctx in 52 | self.fullNameLabel.text = "\(ctx.model.firstName) \(ctx.model.lastName)" 53 | self.imageView.image = loadFromURL(ctx.model.avatarURL) 54 | } 55 | // ...tap (or all the other events you typically have for tables/collections) 56 | cAdapter.on.tap = { ctx in 57 | openDetail(forContact: ctx.model) 58 | } 59 | 60 | // Register adapter; now the director know how to render your data 61 | director.register(adapter: cAdapter) 62 | 63 | // Manage your source by adding/removing/moving sections & rows 64 | director.add(models: arrayOfContacts) 65 | // Finally reload your table 66 | director.reloadData() 67 | ``` 68 | 69 | Pretty simple uh? 70 | 71 | No datasource, no delegate, just a declarative syntax to create & manage your data easily and in type-safe manner (both for models and cells). 72 | Learn more about sections, header/footer & events by reading the rest of guide. 73 | 74 | 75 | ## Table Of Contents 76 | 77 | - [Installation](#installation) 78 | - [Documentation](#documentation) 79 | - [Requirements](#requirements) 80 | 81 | 82 | 83 | ## Documentation 84 | 85 | The following guide explain how to use features available in FlowKit with a real example. 86 | If you want to see a live example open `FlowKit.xcodeproj` and run the `Example` app. 87 | 88 | - [Overview](#overview) 89 | - [Create the Director](#createdirector) 90 | - [Register Adapters](#registeradapters) 91 | - [Create Data Models](#createdatamodels) 92 | - [Create Cells](#createcells) 93 | - [Add Sections](#addsections) 94 | - [Manage Models/Items in Section](#managemodels) 95 | - [Setup Headers & Footers](#setupheadersfooters) 96 | - [Reload Data with/out Animations](#reloaddata) 97 | - [Listen for Events](#events) 98 | - [Sizing Cells](#sizingcells) 99 | 100 | **Note**: *The following concepts are valid even if work with tables or collections using FlowKit (each class used starts with `Table[...]` or `Collection[...]` prefixes and where there are similaties between functions the name of functions/properties are consistence).* 101 | 102 | ### Overview 103 | 104 | The following graph describe the infrastructure of FlowKit for Collection (the same graph is [also available for Tables](Documentation/Structure_TableKit.png))). 105 | 106 | ![](Documentation/Structure_CollectionKit.png) 107 | 108 | The most important class of FlowKit is the Director; this class (`TableDirector` for tables, `CollectionDirector`/`FlowCollectionDirector` for collections) manage the sync between the data and the UI: you can add/remove/move sections and configure the appearance and the behaviour of the list directly from this instance. 109 | The first step to use FlowKit is to assign a director to your list: you can do it by calling `list.director` or just creating your own director with the list instance to manage: 110 | 111 | ```swift 112 | let director = FlowCollectionDirector(self.collectionView) 113 | ``` 114 | 115 | In order to render some data FlowKit must know what kind of data you want to show into the list; data is organized as pair of `` (where `Model` is the object you want to add into the table and view is the cell used to represent the data). 116 | **A `Model` must be an object (both class or struct) conform to `ModelProtocol`: this is a simple protocol which require the presence of a property `modelID`. 117 | This property is used to uniquely identify the model and evaluate the difference between items during automatic reload with animations.** 118 | 119 | Adapter also allows to receive events used to configure the view and the behaviour: you can intercept tap for an instance of your model and do something, or just fillup received type-safe cell instance with model instance. 120 | 121 | So, as second step, you need to register some adapters: 122 | 123 | ```swift 124 | let adapter = CollectionAdapter() 125 | adapter.on.tap = { ctx in 126 | print("User tapped on \(ctx.model.firstName)") 127 | } 128 | adapter.on.dequeue = { ctx in 129 | ctx.cell?.titleLabel?.text = ctx.model.firstName 130 | } 131 | ``` 132 | 133 | Now you are ready to create sections with your models inside: 134 | 135 | ```swift 136 | let section = TableSection(headerTitle: "Contacts", models: contactsList) 137 | self.tableView.director.add(section: section) 138 | ``` 139 | 140 | Models array can be etherogenous, just remeber to make your objects conform to `ModelProtocol` and `Hashable` protocol and register the associated adapter. FlowKit will take care to call your adapter events as it needs. 141 | 142 | Finally you can reload the data: 143 | 144 | ```swift 145 | self.tableView.reloadData() 146 | ``` 147 | 148 | Et voilà! In just few lines of code you have created e managed even complex lists. 149 | 150 | The following guide describe all the other features of the library. 151 | 152 | 153 | 154 | 155 | ### Create the Director (`TableDirector`/`CollectionDirector`) 156 | 157 | You can think about the Director has the owner/manager of the table/collection: using it you can declare what kind of data your scroller is able to show (both models and views/cells), add/remove/move both sections and items in sections. 158 | Since you will start using FlowKit you will use the director instance to manage the content of the UI. 159 | 160 | *Keep in mind: a single director instance is able to manage only a single instance of a scroller.* 161 | 162 | You have two ways to set a director; explicitly: 163 | 164 | ```swift 165 | public class ViewController: UIViewController { 166 | @IBOutlet public var tableView: UITableView? 167 | private var director: TableDirector? 168 | 169 | override func viewDidLoad() { 170 | super.viewDidLoad() 171 | self.director = TableDirector(self.tableView!) 172 | } 173 | } 174 | ``` 175 | 176 | or using implicitly, by accessing to the `director` property: the first time you call it a new director instance is created and assigned with strong reference to the table/collection instance. 177 | 178 | **Note:** For UICollectionView `FlowCollectionDirector` is created automatically; if you use another layout you must create a new one manually. 179 | 180 | ```swift 181 | let director = self.tableView.director // create a director automatically and assign as strong reference to the table/collection 182 | // do something with it... 183 | ``` 184 | 185 | 186 | 187 | ### Register Adapters (`TableAdapter`/`CollectionAdapter`) 188 | 189 | Once you have a director you need to tell to it what kind of data you are about to render: you can have an etherogeneus collection of Models and View (cells) in your scroller but a single Model can be rendered to a single type of View. 190 | 191 | Suppose you want to render two types of models: 192 | 193 | - `Contact` instances using `ContactCell` view 194 | - `ContactGroup` instances using `ContactGroupCell` view 195 | 196 | You will need two adapters: 197 | 198 | ```swift 199 | let contactAdpt = TableAdapter() 200 | let groupAdpt = TableAdapter() 201 | tableView.director.register(adapters: [contactAdpt, groupAdpt]) 202 | ``` 203 | 204 | Now you are ready to present your data. 205 | 206 | 207 | 208 | ### Create Data Models (`ModelProtocol`) 209 | 210 | In order to render your data each object of the scroller must conforms to `ModelProtocol`, a simple protocol which require the implementation of `modelID` property (an `Int`). This property is used to uniquely identify the model and evaluate the difference between items during automatic reload with animations. 211 | A default implementation of this property is available for class based object (`AnyObject`) which uses the `ObjectIdentifier()`. 212 | Instead an explicit implementation must be provided for value based objects (ie. Structs). 213 | 214 | This is an example implementation of `Contact` model: 215 | 216 | ```swift 217 | public class Contact: ModelProtocol { 218 | public var name: String 219 | public var GUID: String = NSUUID().uuidString 220 | 221 | public var id: Int { 222 | return GUID.hashValue 223 | } 224 | 225 | public static func == (lhs: Contact, rhs: Contact) -> Bool { 226 | return lhs.GUID == rhs.GUID 227 | } 228 | 229 | public init(_ name: String) { 230 | self.name = name 231 | } 232 | } 233 | ``` 234 | 235 | 236 | 237 | ### Create Cells (`UITableViewCell`/`UICollectionViewCell`) 238 | 239 | Both `UITableViewCell` and `UICollectionViewCell` and its subclasses are automatically conforms to `CellProtocol`. 240 | 241 | The only constraint is about `reuseIdentifier`: **cells must have`reuseIdentifier` (`Identifier` in Interface Builder) the name of the class itself.**. 242 | 243 | If you need you can override this behaviour by overrinding the `reuseIdentifier: String` property of your cell and returing your own identifier. 244 | 245 | Cell can be loaded in three ways: 246 | 247 | - **Cells from Storyboard**: This is the default behaviour; you don't need to do anything, cells are registered automatically. 248 | - **Cells from XIB files**: Be sure your xib files have the same name of the class (ie. `ContactCell.xib`) and the cell as root item. 249 | - **Cells from `initWithFrame`**: Override `CellProtocol`'s `registerAsClass` to return `true`. 250 | 251 | This is a small example of the `ContactCell`: 252 | 253 | ```swift 254 | public class ContactCell: UITableViewCell { 255 | @IBOutlet public var labelName: UILabel? 256 | @IBOutlet public var labelSurname: UILabel? 257 | @IBOutlet public var icon: UIImageView? 258 | } 259 | ``` 260 | 261 | 262 | 263 | ### Add Sections (`TableSection`/`CollectionSection`) 264 | 265 | Each Table/Collection must have at least one section to show something. 266 | `TableSection`/`CollectionSection` instances hold the items to show (into `.models` property) and optionally any header/Footer you can apply. 267 | 268 | In order to manage sections of your table you need to use the following methods of the parent Director: 269 | 270 | - `set(models:)` change the section of the table/collection. 271 | - `section(at:)` return section at index. 272 | - `firstSection()` return first section, if any. 273 | - `lastSection()` return last section, if any. 274 | - `add(section:at:)` add or insert a new section. 275 | - `add(sections:at:)` add an array of sections. 276 | - `add(models:)` create a new section with given models inside and add it. 277 | - `removeAll(keepingCapacity:)` remove all sections. 278 | - `remove(section:)` remove section at given index. 279 | - `remove(sectionsAt:)` remove sections at given indexes set. 280 | - `move(swappingAt:with:)` swap section at given index with destination index. 281 | - `move(from:to)` combo remove/insert of section at given index to destination index. 282 | 283 | The following example create a new `TableSection` with some items inside, a `String` based header, then append it a the end of the table. 284 | 285 | ```swift 286 | let section = TableSection(headerTitle: "The Strangers", items: [mrBrown,mrGreen,mrWhite]) 287 | table.director.add(section: section) 288 | ``` 289 | 290 | 291 | 292 | ### Manage Models/Items in Section 293 | 294 | As for section the same `add`/`remove`/`move` function are also available for `models` array which describe the content (rows/items) inside each section (`TableSection`/`CollectionSection` instances). 295 | 296 | This is the complete list: 297 | 298 | - `set(models:)` change models array. 299 | - `add(model:at:)` add model at given index, if nil is append at the bottom. 300 | - `add(models:at:)` add models starting at given index; if nil models are append at the bottom. 301 | - `remove(at:)` remove model at given index. 302 | - `remove(atIndexes:)` remove models at given index set. 303 | - `removeAll(keepingCapacity:)` remove all models of the section. 304 | - `move(swappingAt:with:)` Swap model at given index to another destination index. 305 | - `move(from:to:)` Remove model at given index and insert at destination index. 306 | 307 | After any change you must call the `director.reloadData()` function from main thread to update the UI. 308 | 309 | This is an example of items management: 310 | 311 | ```swift 312 | let section = self.tableView.director.firstSection() 313 | 314 | let newItems = [Contact("Daniele","Margutti"),Contact("Fabio","Rossi")] 315 | section.add(models: newItems) // add two new contacts 316 | section.remove(at: 0) // remove first item 317 | 318 | // ... 319 | self.tableView.director.reloadData() // reload data 320 | ``` 321 | 322 | 323 | 324 | ### Setup Headers & Footers (`TableSectionView`/`CollectionSectionView`) 325 | 326 | **Simple Header/Footer** 327 | 328 | Section may have or not headers/footers; these can be simple `String` (as you seen above) or custom views. 329 | 330 | Setting simple headers is pretty straightforward, just set the `headerTitle`/`footerTitle`: 331 | 332 | ```swift 333 | section.headerTitle = "New Strangers" 334 | section.footerTitle = "\(contacts.count) contacts") 335 | ``` 336 | **Custom View Header/Footer** 337 | 338 | To use custom view as header/footer you need to create a custom `xib` file with a `UITableViewHeaderFooterView` (for table) or `UICollectionReusableView` (for collection) view subclass as root item. 339 | 340 | The following example shows a custom header and how to set it: 341 | 342 | ```swift 343 | // we also need of TableExampleHeaderView.xib file 344 | // with TableExampleHeaderView view as root item 345 | public class TableExampleHeaderView: UITableViewHeaderFooterView { 346 | @IBOutlet public var titleLabel: UILabel? 347 | } 348 | 349 | // create the header container (will receive events) 350 | let header = TableSectionView() 351 | // hooks any event. You need at least the `height` as below 352 | header.on.height = { _ in 353 | return 150 354 | } 355 | 356 | // Use it 357 | let section = TableSection(headerView: header, items: [mrBrown,mrGreen,mrWhite]) 358 | table.director.add(section: section) 359 | ``` 360 | 361 | 362 | 363 | ### Reload Data with/out Animations 364 | 365 | Each change to the data model must be done by calling the `add`/`remove`/`move` function available both at sections and items levels. 366 | After changes you need to call the director's `reloadData()` function to update the UI. 367 | 368 | The following example update a table after some changes: 369 | 370 | ```swift 371 | // do some changes 372 | tableView.director.remove(section: 0) 373 | tableView.director.add(section: newSection, at: 2) 374 | ... 375 | tableView.director.firstSection().remove(at: 0) 376 | 377 | // then reload 378 | tableView.director.reloadData() 379 | ``` 380 | 381 | If you need to perform an animated reload just make your changes to the model inside the callback available into the method. 382 | Animations are evaluated and applied for you! 383 | 384 | ```swift 385 | tableView.director.reloadData(after: { _ in 386 | tableView.director.remove(section: 0) 387 | tableView.director.add(section: newSection, at: 2) 388 | 389 | return TableReloadAnimations.default() 390 | }) 391 | ``` 392 | 393 | For `TableDirector` you must provide a `TableReloadAnimations` configuration which defines what kind of `UITableViewRowAnimation` must be applied for each type of change (insert/delete/reload/move). `TableReloadAnimations.default()` just uses `.automatic` for each type (you can also implement your own object which need to be conform to `TableReloadAnimationProtocol` protocol). 394 | 395 | For `CollectionDirector` you don't need to return anything; evaluation is made for you based upon the layout used. 396 | 397 | 398 | 399 | 400 | ## Listen for Events 401 | 402 | All events are hookable from their respective objects starting from `.on` property. All standard table & collections events are available from FlowKit; name of the events is similar to the their standard corrispettive in UIKit (see the official documentation for more info abou any specific event). 403 | 404 | - [Table Events](/Documentation/Table_Events.md) 405 | - [Collection Events](/Documentation/Collection_Events.md) 406 | - [UIScrollViewDelegate Events](/Documentation/UIScrollViewDelegate_Events.md) 407 | 408 | 409 | 410 | ## Sizing Cells 411 | 412 | FlowKit support easy cell sizing using autolayout. 413 | You can set the size of the cell by adapter or collection based. For autolayout driven cell sizing set the `rowHeight` (for `TableDirector`) or `itemSize` (for `CollectionDirector`/`FlowCollectionDirector`) to the `autoLayout` value, then provide an estimated value. 414 | 415 | Accepted values are: 416 | - `default`: you must provide the height (table) or size (collection) of the cell 417 | - `autoLayout`: uses autolayout to evaluate the height of the cell; for Collection Views you can also provide your own calculation by overriding `preferredLayoutAttributesFitting()` function in cell instance. 418 | - `fixed`: provide a fixed height for all cell types (faster if you plan to have all cell sized same) 419 | 420 | 421 | 422 | 423 | ## Installation 424 | 425 | ### Install via CocoaPods 426 | 427 | [CocoaPods](http://cocoapods.org) is a dependency manager which automates and simplifies the process of using 3rd-party libraries like FlowKit in your projects. You can install it with the following command: 428 | 429 | ```bash 430 | $ sudo gem install cocoapods 431 | ``` 432 | 433 | > CocoaPods 1.0.1+ is required to build FlowKit. 434 | 435 | #### Install via Podfile 436 | 437 | To integrate FlowKit into your Xcode project using CocoaPods, specify it in your `Podfile`: 438 | 439 | ```ruby 440 | source 'https://github.com/CocoaPods/Specs.git' 441 | platform :ios, '8.0' 442 | 443 | target 'TargetName' do 444 | use_frameworks! 445 | pod 'FlowKitManager' 446 | end 447 | ``` 448 | 449 | Then, run the following command: 450 | 451 | ```bash 452 | $ pod install 453 | ``` 454 | 455 | 456 | 457 | ### Carthage 458 | 459 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. 460 | 461 | You can install Carthage with [Homebrew](http://brew.sh/) using the following command: 462 | 463 | ```bash 464 | $ brew update 465 | $ brew install carthage 466 | ``` 467 | 468 | To integrate FlowKit into your Xcode project using Carthage, specify it in your `Cartfile`: 469 | 470 | ```ogdl 471 | github "malcommac/FlowKitManager" 472 | ``` 473 | 474 | Run `carthage` to build the framework and drag the built `FlowKit.framework` into your Xcode project. 475 | 476 | 477 | 478 | ## Requirements 479 | 480 | FlowKit is compatible with Swift 4.x. 481 | 482 | * iOS 8.0+ 483 | --------------------------------------------------------------------------------