├── .gitignore ├── .travis.yml ├── Example ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── SwiftyComments.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── SwiftScanner │ │ ├── LICENSE │ │ ├── README.md │ │ └── Sources │ │ │ └── StringScanner.swift │ ├── SwipeCellKit │ │ ├── LICENSE │ │ ├── README.md │ │ └── Source │ │ │ ├── Extensions.swift │ │ │ ├── SwipeAccessibilityCustomAction.swift │ │ │ ├── SwipeAction.swift │ │ │ ├── SwipeActionButton.swift │ │ │ ├── SwipeActionTransitioning.swift │ │ │ ├── SwipeActionsView.swift │ │ │ ├── SwipeAnimator.swift │ │ │ ├── SwipeCollectionViewCell+Accessibility.swift │ │ │ ├── SwipeCollectionViewCell+Display.swift │ │ │ ├── SwipeCollectionViewCell.swift │ │ │ ├── SwipeCollectionViewCellDelegate.swift │ │ │ ├── SwipeController.swift │ │ │ ├── SwipeExpanding.swift │ │ │ ├── SwipeExpansionStyle.swift │ │ │ ├── SwipeFeedback.swift │ │ │ ├── SwipeOptions.swift │ │ │ ├── SwipeTableViewCell+Accessibility.swift │ │ │ ├── SwipeTableViewCell+Display.swift │ │ │ ├── SwipeTableViewCell.swift │ │ │ ├── SwipeTableViewCellDelegate.swift │ │ │ ├── SwipeTransitionLayout.swift │ │ │ └── Swipeable.swift │ └── Target Support Files │ │ ├── Pods-SwiftyComments_Example │ │ ├── Info.plist │ │ ├── Pods-SwiftyComments_Example-acknowledgements.markdown │ │ ├── Pods-SwiftyComments_Example-acknowledgements.plist │ │ ├── Pods-SwiftyComments_Example-dummy.m │ │ ├── Pods-SwiftyComments_Example-frameworks.sh │ │ ├── Pods-SwiftyComments_Example-resources.sh │ │ ├── Pods-SwiftyComments_Example-umbrella.h │ │ ├── Pods-SwiftyComments_Example.debug.xcconfig │ │ ├── Pods-SwiftyComments_Example.modulemap │ │ └── Pods-SwiftyComments_Example.release.xcconfig │ │ ├── SwiftScanner │ │ ├── Info.plist │ │ ├── SwiftScanner-dummy.m │ │ ├── SwiftScanner-prefix.pch │ │ ├── SwiftScanner-umbrella.h │ │ ├── SwiftScanner.modulemap │ │ └── SwiftScanner.xcconfig │ │ ├── SwiftyComments │ │ ├── Info.plist │ │ ├── SwiftyComments-dummy.m │ │ ├── SwiftyComments-prefix.pch │ │ ├── SwiftyComments-umbrella.h │ │ ├── SwiftyComments.modulemap │ │ └── SwiftyComments.xcconfig │ │ └── SwipeCellKit │ │ ├── Info.plist │ │ ├── SwipeCellKit-dummy.m │ │ ├── SwipeCellKit-prefix.pch │ │ ├── SwipeCellKit-umbrella.h │ │ ├── SwipeCellKit.modulemap │ │ └── SwipeCellKit.xcconfig ├── SwiftyComments.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── SwiftyComments-Example.xcscheme ├── SwiftyComments.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── SwiftyComments │ ├── AppDelegate.swift │ ├── Base.lproj │ └── LaunchScreen.xib │ ├── DemosTableViewController.swift │ ├── HNCommentCell.swift │ ├── HNCommentContentParser.swift │ ├── HNCommentsViewController.swift │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── HNRespond.imageset │ │ ├── Contents.json │ │ └── HNRespond.pdf │ ├── downvte.imageset │ │ ├── Contents.json │ │ └── downvte.pdf │ ├── exprt.imageset │ │ ├── Contents.json │ │ └── exprt.pdf │ ├── hadDownvte.imageset │ │ ├── Contents.json │ │ └── hadDownvte.pdf │ ├── hadUpvte.imageset │ │ ├── Contents.json │ │ └── hadUpvte.pdf │ ├── imgurDownvte.imageset │ │ ├── Contents.json │ │ └── imgurDownvte.pdf │ ├── imgurUpvte.imageset │ │ ├── Contents.json │ │ └── imgurUpvte.pdf │ ├── mre.imageset │ │ ├── Contents.json │ │ └── mre.pdf │ └── upvte.imageset │ │ ├── Contents.json │ │ └── upvte.pdf │ ├── ImgurCommentCell.swift │ ├── ImgurCommentsViewController.swift │ ├── Info.plist │ ├── LoremSwiftum.swift │ ├── MockupData.swift │ ├── RedditCommentCell.swift │ ├── RedditCommentsViewController.swift │ ├── SimpleCommentCell.swift │ ├── SimpleCommentsViewController.swift │ └── ViewController.swift ├── LICENSE ├── README.md ├── Screenshots ├── HN.mov ├── HNExample.gif ├── HNExample.png ├── HNSwipe.gif ├── HNSwipe.mov ├── Imgur.mov ├── ImgurExample.gif ├── ImgurExample.png ├── Reddit.mov ├── RedditExample.gif ├── RedditExample.png ├── RedditSwipe.gif ├── RedditSwipe.mov ├── schema.png └── schema_small.png ├── SwiftyComments.podspec ├── SwiftyComments ├── Assets │ └── Media.xcassets │ │ ├── Contents.json │ │ └── collapse.imageset │ │ ├── Collapse.pdf │ │ └── Contents.json └── Classes │ ├── AbstractComment.swift │ ├── CommentCell.swift │ ├── CommentsViewController.swift │ └── CommentsViewDelegate.swift └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.1 3 | #xcode_workspace: Example/SwiftyComments.xcworkspace 4 | #xcode_scheme: SwiftyComments-Example 5 | #xcode_sdk: iphonesimulator9.3 6 | podfile: Example/Podfile 7 | before_install: 8 | - pod install --project-directory=Example 9 | script: 10 | - xcodebuild -workspace Example/SwiftyComments.xcworkspace -scheme 'SwiftyComments-Example' -sdk iphonesimulator build 11 | #cache: 12 | # directories: 13 | # - Pods 14 | # - $HOME/.cocoapods 15 | # - $HOME/Library/Caches/CocoaPods 16 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'SwiftyComments_Example' do 4 | pod 'SwiftyComments', :path => '../' 5 | pod 'SwiftScanner', '~> 1.0.2' 6 | end 7 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftScanner (1.0.3) 3 | - SwiftyComments (0.2.0): 4 | - SwipeCellKit (= 2.6.0) 5 | - SwipeCellKit (2.6.0) 6 | 7 | DEPENDENCIES: 8 | - SwiftScanner (~> 1.0.2) 9 | - SwiftyComments (from `../`) 10 | 11 | EXTERNAL SOURCES: 12 | SwiftyComments: 13 | :path: ../ 14 | 15 | SPEC CHECKSUMS: 16 | SwiftScanner: 70897e82bece6e6a0803b105efed6118c46fa20e 17 | SwiftyComments: 38006364a5aaa93914b98d5af1b2959e35c999b2 18 | SwipeCellKit: 935ca28c187ec6e1ffb2b578cf8ddca842bfdcbb 19 | 20 | PODFILE CHECKSUM: 7711333d67fdc56a7062f213fae2e44bac4a7fa6 21 | 22 | COCOAPODS: 1.4.0 23 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/SwiftyComments.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SwiftyComments", 3 | "version": "0.2.0", 4 | "summary": "UITableView based component designed to display a hierarchy of expandable/foldable comments.", 5 | "description": "SwiftyComments is a UITableView based component designed to display a hierarchy of expandable/foldable comments.", 6 | "homepage": "https://github.com/tsucres/SwiftyComments", 7 | "screenshots": [ 8 | "https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/ImgurExample.png", 9 | "https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/HNExample.png", 10 | "https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/RedditExample.png" 11 | ], 12 | "license": { 13 | "type": "MIT", 14 | "file": "LICENSE" 15 | }, 16 | "authors": { 17 | "Stéphane Sercu": "stefsercu@gmail.com" 18 | }, 19 | "source": { 20 | "git": "https://github.com/tsucres/SwiftyComments.git", 21 | "tag": "0.2.0" 22 | }, 23 | "platforms": { 24 | "ios": "9.0" 25 | }, 26 | "source_files": "SwiftyComments/Classes/**/*", 27 | "resources": "SwiftyComments/Assets/*.xcassets", 28 | "dependencies": { 29 | "SwipeCellKit": [ 30 | "2.6.0" 31 | ] 32 | }, 33 | "frameworks": "UIKit", 34 | "swift_version": "4.1" 35 | } 36 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftScanner (1.0.3) 3 | - SwiftyComments (0.2.0): 4 | - SwipeCellKit (= 2.6.0) 5 | - SwipeCellKit (2.6.0) 6 | 7 | DEPENDENCIES: 8 | - SwiftScanner (~> 1.0.2) 9 | - SwiftyComments (from `../`) 10 | 11 | EXTERNAL SOURCES: 12 | SwiftyComments: 13 | :path: ../ 14 | 15 | SPEC CHECKSUMS: 16 | SwiftScanner: 70897e82bece6e6a0803b105efed6118c46fa20e 17 | SwiftyComments: 38006364a5aaa93914b98d5af1b2959e35c999b2 18 | SwipeCellKit: 935ca28c187ec6e1ffb2b578cf8ddca842bfdcbb 19 | 20 | PODFILE CHECKSUM: 7711333d67fdc56a7062f213fae2e44bac4a7fa6 21 | 22 | COCOAPODS: 1.4.0 23 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Pods/SwiftScanner/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 daniele margutti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2017 Jeremy Koch 3 | 4 | http://jerkoch.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/README.md: -------------------------------------------------------------------------------- 1 | # SwipeCellKit 2 | 3 | [![Build Status](https://travis-ci.org/jerkoch/SwipeCellKit.svg)](https://travis-ci.org/jerkoch/SwipeCellKit) 4 | [![Version Status](https://img.shields.io/cocoapods/v/SwipeCellKit.svg)][podLink] 5 | [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://developer.apple.com/swift/) 6 | [![license MIT](https://img.shields.io/cocoapods/l/SwipeCellKit.svg)][mitLink] 7 | [![Platform](https://img.shields.io/cocoapods/p/SwipeCellKit.svg)][docsLink] 8 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 9 | [![Twitter](https://img.shields.io/badge/twitter-@mkurabi-blue.svg?style=flat)](https://twitter.com/mkurabi) 10 | 11 | *Swipeable UITableViewCell/UICollectionViewCell based on the stock Mail.app, implemented in Swift.* 12 | 13 |

14 | 15 | ## About 16 | 17 | A swipeable `UITableViewCell` or `UICollectionViewCell` with support for: 18 | 19 | * Left and right swipe actions 20 | * Action buttons with: *text only, text + image, image only* 21 | * Haptic Feedback 22 | * Customizable transitions: *Border, Drag, and Reveal* 23 | * Customizable action button behavior during swipe 24 | * Animated expansion when dragging past threshold 25 | * Customizable expansion animations 26 | * Support for both `UITableView` and `UICollectionView` 27 | * Accessibility 28 | 29 | ## Background 30 | 31 | Check out my [blog post](https://jerkoch.com/2017/02/07/swiper-no-swiping.html) on how *SwipeCellKit* came to be. 32 | 33 | ## Demo 34 | 35 | ### Transition Styles 36 | 37 | The transition style describes how the action buttons are exposed during the swipe. 38 | 39 | #### Border 40 | 41 |

42 | 43 | #### Drag 44 | 45 |

46 | 47 | #### Reveal 48 | 49 |

50 | 51 | #### Customized 52 | 53 |

54 | 55 | ### Expansion Styles 56 | 57 | The expansion style describes the behavior when the cell is swiped past a defined threshold. 58 | 59 | #### None 60 | 61 |

62 | 63 | #### Selection 64 | 65 |

66 | 67 | #### Destructive 68 | 69 |

70 | 71 | #### Customized 72 | 73 |

74 | 75 | ## Requirements 76 | 77 | * Swift 5.0 78 | * Xcode 10.2+ 79 | * iOS 9.0+ 80 | 81 | ## Installation 82 | 83 | #### [CocoaPods](http://cocoapods.org) (recommended) 84 | 85 | ````ruby 86 | use_frameworks! 87 | 88 | # Latest release in CocoaPods 89 | pod 'SwipeCellKit' 90 | 91 | # Get the latest on develop 92 | pod 'SwipeCellKit', :git => 'https://github.com/SwipeCellKit/SwipeCellKit.git', :branch => 'develop' 93 | 94 | # If you have NOT upgraded to Swift 5.0, use the last Swift 4.2/Xcode 10.2 compatible release 95 | pod 'SwipeCellKit', '2.5.4' 96 | 97 | # If you have NOT upgraded to Swift 4.2, use the last non-swift 4.2 compatible release 98 | pod 'SwipeCellKit', '2.4.3' 99 | ```` 100 | 101 | #### [Carthage](https://github.com/Carthage/Carthage) 102 | 103 | ````bash 104 | github "SwipeCellKit/SwipeCellKit" 105 | ```` 106 | 107 | ## Documentation 108 | 109 | Read the [docs][docsLink]. Generated with [jazzy](https://github.com/realm/jazzy). Hosted by [GitHub Pages](https://pages.github.com). 110 | 111 | ## Usage for UITableView 112 | 113 | Set the `delegate` property on `SwipeTableViewCell`: 114 | 115 | ````swift 116 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 117 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! SwipeTableViewCell 118 | cell.delegate = self 119 | return cell 120 | } 121 | ```` 122 | 123 | Adopt the `SwipeTableViewCellDelegate` protocol: 124 | 125 | ````swift 126 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? { 127 | guard orientation == .right else { return nil } 128 | 129 | let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in 130 | // handle action by updating model with deletion 131 | } 132 | 133 | // customize the action appearance 134 | deleteAction.image = UIImage(named: "delete") 135 | 136 | return [deleteAction] 137 | } 138 | ```` 139 | 140 | Optionally, you can implement the `editActionsOptionsForRowAt` method to customize the behavior of the swipe actions: 141 | 142 | ````swift 143 | func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { 144 | var options = SwipeOptions() 145 | options.expansionStyle = .destructive 146 | options.transitionStyle = .border 147 | return options 148 | } 149 | ```` 150 | ## Usage for UICollectionView 151 | 152 | Set the `delegate` property on `SwipeCollectionViewCell`: 153 | 154 | ````swift 155 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 156 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! SwipeCollectionViewCell 157 | cell.delegate = self 158 | return cell 159 | } 160 | ```` 161 | 162 | Adopt the `SwipeCollectionViewCellDelegate` protocol: 163 | 164 | ````swift 165 | func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? { 166 | guard orientation == .right else { return nil } 167 | 168 | let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in 169 | // handle action by updating model with deletion 170 | } 171 | 172 | // customize the action appearance 173 | deleteAction.image = UIImage(named: "delete") 174 | 175 | return [deleteAction] 176 | } 177 | ```` 178 | 179 | Optionally, you can implement the `editActionsOptionsForItemAt` method to customize the behavior of the swipe actions: 180 | 181 | ````swift 182 | func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { 183 | var options = SwipeOptions() 184 | options.expansionStyle = .destructive 185 | options.transitionStyle = .border 186 | return options 187 | } 188 | ```` 189 | ### Transitions 190 | 191 | Three built-in transition styles are provided by `SwipeTransitionStyle`: 192 | 193 | * .border: The visible action area is equally divide between all action buttons. 194 | * .drag: The visible action area is dragged, pinned to the cell, with each action button fully sized as it is exposed. 195 | * .reveal: The visible action area sits behind the cell, pinned to the edge of the table view, and is revealed as the cell is dragged aside. 196 | 197 | See [Customizing Transitions](https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/Guides/Advanced.md) for more details on customizing button appearance as the swipe is performed. 198 | 199 | #### Transition Delegate 200 | 201 | Transition for a `SwipeAction` can be observered by setting a `SwipeActionTransitioning` on the `transitionDelegate` property. This allows you to observe what percentage is visible and access to the underlying `UIButton` for that `SwipeAction`. 202 | 203 | ### Expansion 204 | 205 | Four built-in expansion styles are provided by `SwipeExpansionStyle`: 206 | 207 | * .selection 208 | * .destructive (like Mail.app) 209 | * .destructiveAfterFill (like Mailbox/Tweetbot) 210 | * .fill 211 | 212 | Much effort has gone into making `SwipeExpansionStyle` extremely customizable. If these built-in styles do not meet your needs, see [Customizing Expansion](https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/Guides/Advanced.md) for more details on creating custom styles. 213 | 214 | The built-in `.fill` expansion style requires manual action fulfillment. This means your action handler must call `SwipeAction.fulfill(style:)` at some point during or after invocation to resolve the fill expansion. The supplied `ExpansionFulfillmentStyle` allows you to delete or reset the cell at some later point (possibly after further user interaction). 215 | 216 | The built-in `.destructive`, and `.destructiveAfterFill` expansion styles are configured to automatically perform row deletion when the action handler is invoked (automatic fulfillment). Your deletion behavior may require coordination with other row animations (eg. inside `beginUpdates` and `endUpdates`). In this case, you can easily create a custom `SwipeExpansionStyle` which requires manual fulfillment to trigger deletion: 217 | 218 | ````swift 219 | var options = SwipeTableOptions() 220 | options.expansionStyle = .destructive(automaticallyDelete: false) 221 | ```` 222 | 223 | > **NOTE**: You must call `SwipeAction.fulfill(with style:)` at some point while/after your action handler is invoked to trigger deletion. Do not call `deleteRows` directly. 224 | 225 | ````swift 226 | let delete = SwipeAction(style: .destructive, title: nil) { action, indexPath in 227 | // Update model 228 | self.emails.remove(at: indexPath.row) 229 | action.fulfill(with: .delete) 230 | } 231 | ```` 232 | 233 | ## Advanced 234 | 235 | See the [Advanced Guide](https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/Guides/Advanced.md) for more details on customization. 236 | 237 | ## Credits 238 | 239 | Maintained by [**@mkurabi**](https://twitter.com/mkurabi). 240 | 241 | ## Showcase 242 | 243 | We're interested in knowing [who's using *SwipeCellKit*](https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/SHOWCASE.md) in their app. Please submit a pull request to add your app! 244 | 245 | ## License 246 | 247 | `SwipeCellKit` is released under an [MIT License][mitLink]. See `LICENSE` for details. 248 | 249 | *Please provide attribution, it is greatly appreciated.* 250 | 251 | [podLink]:https://cocoapods.org/pods/SwipeCellKit 252 | [docsLink]:https://swipecellkit.github.io/SwipeCellKit/ 253 | [mitLink]:http://opensource.org/licenses/MIT 254 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITableView { 11 | var swipeCells: [SwipeTableViewCell] { 12 | return visibleCells.compactMap({ $0 as? SwipeTableViewCell }) 13 | } 14 | 15 | func hideSwipeCell() { 16 | swipeCells.forEach { $0.hideSwipe(animated: true) } 17 | } 18 | } 19 | 20 | extension UICollectionView { 21 | var swipeCells: [SwipeCollectionViewCell] { 22 | return visibleCells.compactMap({ $0 as? SwipeCollectionViewCell }) 23 | } 24 | 25 | func hideSwipeCell() { 26 | swipeCells.forEach { $0.hideSwipe(animated: true) } 27 | } 28 | 29 | func setGestureEnabled(_ enabled: Bool) { 30 | gestureRecognizers?.forEach { 31 | guard $0 != panGestureRecognizer else { return } 32 | 33 | $0.isEnabled = enabled 34 | } 35 | } 36 | } 37 | 38 | extension UIScrollView { 39 | var swipeables: [Swipeable] { 40 | switch self { 41 | case let tableView as UITableView: 42 | return tableView.swipeCells 43 | case let collectionView as UICollectionView: 44 | return collectionView.swipeCells 45 | default: 46 | return [] 47 | } 48 | } 49 | 50 | func hideSwipeables() { 51 | switch self { 52 | case let tableView as UITableView: 53 | tableView.hideSwipeCell() 54 | case let collectionView as UICollectionView: 55 | collectionView.hideSwipeCell() 56 | default: 57 | return 58 | } 59 | } 60 | } 61 | 62 | extension UIPanGestureRecognizer { 63 | func elasticTranslation(in view: UIView?, withLimit limit: CGSize, fromOriginalCenter center: CGPoint, applyingRatio ratio: CGFloat = 0.20) -> CGPoint { 64 | let translation = self.translation(in: view) 65 | 66 | guard let sourceView = self.view else { 67 | return translation 68 | } 69 | 70 | let updatedCenter = CGPoint(x: center.x + translation.x, y: center.y + translation.y) 71 | let distanceFromCenter = CGSize(width: abs(updatedCenter.x - sourceView.bounds.midX), 72 | height: abs(updatedCenter.y - sourceView.bounds.midY)) 73 | 74 | let inverseRatio = 1.0 - ratio 75 | let scale: (x: CGFloat, y: CGFloat) = (updatedCenter.x < sourceView.bounds.midX ? -1 : 1, updatedCenter.y < sourceView.bounds.midY ? -1 : 1) 76 | let x = updatedCenter.x - (distanceFromCenter.width > limit.width ? inverseRatio * (distanceFromCenter.width - limit.width) * scale.x : 0) 77 | let y = updatedCenter.y - (distanceFromCenter.height > limit.height ? inverseRatio * (distanceFromCenter.height - limit.height) * scale.y : 0) 78 | 79 | return CGPoint(x: x, y: y) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeAccessibilityCustomAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeAccessibilityCustomAction.swift 3 | // SwipeCellKit 4 | // 5 | // Created by Jeremy Koch 6 | // Copyright © 2017 Jeremy Koch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SwipeAccessibilityCustomAction: UIAccessibilityCustomAction { 12 | let action: SwipeAction 13 | let indexPath: IndexPath 14 | 15 | init?(action: SwipeAction, indexPath: IndexPath, target: Any, selector: Selector) { 16 | 17 | self.action = action 18 | self.indexPath = indexPath 19 | 20 | let name = action.accessibilityLabel ?? action.title ?? action.image?.accessibilityIdentifier ?? nil 21 | 22 | if let name = name { 23 | super.init(name: name, target: target, selector: selector) 24 | } else { 25 | return nil 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeAction.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Constants that help define the appearance of action buttons. 11 | public enum SwipeActionStyle: Int { 12 | /// Apply a style that reflects standard non-destructive actions. 13 | case `default` 14 | 15 | /// Apply a style that reflects destructive actions. 16 | case destructive 17 | } 18 | 19 | /** 20 | The `SwipeAction` object defines a single action to present when the user swipes horizontally in a table/collection item. 21 | 22 | This class lets you define one or more custom actions to display for a given item in your table/collection. Each instance of this class represents a single action to perform and includes the text, formatting information, and behavior for the corresponding button. 23 | */ 24 | public class SwipeAction: NSObject { 25 | /// An optional unique action identifier. 26 | public var identifier: String? 27 | 28 | /// The title of the action button. 29 | /// 30 | /// - note: You must specify a title or an image. 31 | public var title: String? 32 | 33 | /// The style applied to the action button. 34 | public var style: SwipeActionStyle 35 | 36 | /// The object that is notified as transitioning occurs. 37 | public var transitionDelegate: SwipeActionTransitioning? 38 | 39 | /// The font to use for the title of the action button. 40 | /// 41 | /// - note: If you do not specify a font, a 15pt system font is used. 42 | public var font: UIFont? 43 | 44 | /// The text color of the action button. 45 | /// 46 | /// - note: If you do not specify a color, white is used. 47 | public var textColor: UIColor? 48 | 49 | /// The highlighted text color of the action button. 50 | /// 51 | /// - note: If you do not specify a color, `textColor` is used. 52 | public var highlightedTextColor: UIColor? 53 | 54 | /// The image used for the action button. 55 | /// 56 | /// - note: You must specify a title or an image. 57 | public var image: UIImage? 58 | 59 | /// The highlighted image used for the action button. 60 | /// 61 | /// - note: If you do not specify a highlight image, the default `image` is used for the highlighted state. 62 | public var highlightedImage: UIImage? 63 | 64 | /// The closure to execute when the user taps the button associated with this action. 65 | public var handler: ((SwipeAction, IndexPath) -> Void)? 66 | 67 | /// The background color of the action button. 68 | /// 69 | /// - note: Use this property to specify the background color for your button. If you do not specify a value for this property, the framework assigns a default color based on the value in the style property. 70 | public var backgroundColor: UIColor? 71 | 72 | /// The highlighted background color of the action button. 73 | /// 74 | /// - note: Use this property to specify the highlighted background color for your button. 75 | public var highlightedBackgroundColor: UIColor? 76 | 77 | /// The visual effect to apply to the action button. 78 | /// 79 | /// - note: Assigning a visual effect object to this property adds that effect to the background of the action button. 80 | public var backgroundEffect: UIVisualEffect? 81 | 82 | /// A Boolean value that determines whether the actions menu is automatically hidden upon selection. 83 | /// 84 | /// - note: When set to `true`, the actions menu is automatically hidden when the action is selected. The default value is `false`. 85 | public var hidesWhenSelected = false 86 | 87 | /** 88 | Constructs a new `SwipeAction` instance. 89 | 90 | - parameter style: The style of the action button. 91 | - parameter title: The title of the action button. 92 | - parameter handler: The closure to execute when the user taps the button associated with this action. 93 | */ 94 | public init(style: SwipeActionStyle, title: String?, handler: ((SwipeAction, IndexPath) -> Void)?) { 95 | self.title = title 96 | self.style = style 97 | self.handler = handler 98 | } 99 | 100 | /** 101 | Calling this method performs the configured expansion completion animation including deletion, if necessary. Calling this method more than once has no effect. 102 | 103 | You should only call this method from the implementation of your action `handler` method. 104 | 105 | - parameter style: The desired style for completing the expansion action. 106 | */ 107 | public func fulfill(with style: ExpansionFulfillmentStyle) { 108 | completionHandler?(style) 109 | } 110 | 111 | // MARK: - Internal 112 | 113 | internal var completionHandler: ((ExpansionFulfillmentStyle) -> Void)? 114 | } 115 | 116 | /// Describes how expansion should be resolved once the action has been fulfilled. 117 | public enum ExpansionFulfillmentStyle { 118 | /// Implies the item will be deleted upon action fulfillment. 119 | case delete 120 | 121 | /// Implies the item will be reset and the actions view hidden upon action fulfillment. 122 | case reset 123 | } 124 | 125 | // MARK: - Internal 126 | 127 | internal extension SwipeAction { 128 | var hasBackgroundColor: Bool { 129 | return backgroundColor != .clear && backgroundEffect == nil 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeActionButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeActionButton.swift 3 | // 4 | // Created by Jeremy Koch. 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class SwipeActionButton: UIButton { 11 | var spacing: CGFloat = 8 12 | var shouldHighlight = true 13 | var highlightedBackgroundColor: UIColor? 14 | 15 | var maximumImageHeight: CGFloat = 0 16 | var verticalAlignment: SwipeVerticalAlignment = .centerFirstBaseline 17 | 18 | 19 | var currentSpacing: CGFloat { 20 | return (currentTitle?.isEmpty == false && imageHeight > 0) ? spacing : 0 21 | } 22 | 23 | var alignmentRect: CGRect { 24 | let contentRect = self.contentRect(forBounds: bounds) 25 | let titleHeight = titleBoundingRect(with: verticalAlignment == .centerFirstBaseline ? CGRect.infinite.size : contentRect.size).integral.height 26 | let totalHeight = imageHeight + titleHeight + currentSpacing 27 | 28 | return contentRect.center(size: CGSize(width: contentRect.width, height: totalHeight)) 29 | } 30 | 31 | private var imageHeight: CGFloat { 32 | get { 33 | return currentImage == nil ? 0 : maximumImageHeight 34 | } 35 | } 36 | 37 | override var intrinsicContentSize: CGSize { 38 | return CGSize(width: UIView.noIntrinsicMetric, height: contentEdgeInsets.top + alignmentRect.height + contentEdgeInsets.bottom) 39 | } 40 | 41 | convenience init(action: SwipeAction) { 42 | self.init(frame: .zero) 43 | 44 | contentHorizontalAlignment = .center 45 | 46 | tintColor = action.textColor ?? .white 47 | let highlightedTextColor = action.highlightedTextColor ?? tintColor 48 | highlightedBackgroundColor = action.highlightedBackgroundColor ?? UIColor.black.withAlphaComponent(0.1) 49 | 50 | titleLabel?.font = action.font ?? UIFont.systemFont(ofSize: 15, weight: UIFont.Weight.medium) 51 | titleLabel?.textAlignment = .center 52 | titleLabel?.lineBreakMode = .byWordWrapping 53 | titleLabel?.numberOfLines = 0 54 | 55 | accessibilityLabel = action.accessibilityLabel 56 | 57 | setTitle(action.title, for: .normal) 58 | setTitleColor(tintColor, for: .normal) 59 | setTitleColor(highlightedTextColor, for: .highlighted) 60 | setImage(action.image, for: .normal) 61 | setImage(action.highlightedImage ?? action.image, for: .highlighted) 62 | } 63 | 64 | override var isHighlighted: Bool { 65 | didSet { 66 | guard shouldHighlight else { return } 67 | 68 | backgroundColor = isHighlighted ? highlightedBackgroundColor : .clear 69 | } 70 | } 71 | 72 | func preferredWidth(maximum: CGFloat) -> CGFloat { 73 | let width = maximum > 0 ? maximum : CGFloat.greatestFiniteMagnitude 74 | let textWidth = titleBoundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)).width 75 | let imageWidth = currentImage?.size.width ?? 0 76 | 77 | return min(width, max(textWidth, imageWidth) + contentEdgeInsets.left + contentEdgeInsets.right) 78 | } 79 | 80 | func titleBoundingRect(with size: CGSize) -> CGRect { 81 | guard let title = currentTitle, let font = titleLabel?.font else { return .zero } 82 | 83 | return title.boundingRect(with: size, 84 | options: [.usesLineFragmentOrigin], 85 | attributes: [NSAttributedString.Key.font: font], 86 | context: nil).integral 87 | } 88 | 89 | override func titleRect(forContentRect contentRect: CGRect) -> CGRect { 90 | var rect = contentRect.center(size: titleBoundingRect(with: contentRect.size).size) 91 | rect.origin.y = alignmentRect.minY + imageHeight + currentSpacing 92 | return rect.integral 93 | } 94 | 95 | override func imageRect(forContentRect contentRect: CGRect) -> CGRect { 96 | var rect = contentRect.center(size: currentImage?.size ?? .zero) 97 | rect.origin.y = alignmentRect.minY + (imageHeight - rect.height) / 2 98 | return rect 99 | } 100 | } 101 | 102 | extension CGRect { 103 | func center(size: CGSize) -> CGRect { 104 | let dx = width - size.width 105 | let dy = height - size.height 106 | return CGRect(x: origin.x + dx * 0.5, y: origin.y + dy * 0.5, width: size.width, height: size.height) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeActionTransitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeActionTransitioning.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | Adopt the `SwipeActionTransitioning` protocol in objects that implement custom appearance of actions during transition. 12 | */ 13 | public protocol SwipeActionTransitioning { 14 | /** 15 | Tells the delegate that transition change has occured. 16 | */ 17 | func didTransition(with context: SwipeActionTransitioningContext) -> Void 18 | } 19 | 20 | /** 21 | The `SwipeActionTransitioningContext` type provides information relevant to a specific action as transitioning occurs. 22 | */ 23 | public struct SwipeActionTransitioningContext { 24 | /// The unique action identifier. 25 | public let actionIdentifier: String? 26 | 27 | /// The button that is changing. 28 | public let button: UIButton 29 | 30 | /// The old visibility percentage between 0.0 and 1.0. 31 | public let newPercentVisible: CGFloat 32 | 33 | /// The new visibility percentage between 0.0 and 1.0. 34 | public let oldPercentVisible: CGFloat 35 | 36 | internal let wrapperView: UIView 37 | 38 | internal init(actionIdentifier: String?, button: UIButton, newPercentVisible: CGFloat, oldPercentVisible: CGFloat, wrapperView: UIView) { 39 | self.actionIdentifier = actionIdentifier 40 | self.button = button 41 | self.newPercentVisible = newPercentVisible 42 | self.oldPercentVisible = oldPercentVisible 43 | self.wrapperView = wrapperView 44 | } 45 | 46 | /// Sets the background color behind the action button. 47 | /// 48 | /// - parameter color: The background color. 49 | public func setBackgroundColor(_ color: UIColor?) { 50 | wrapperView.backgroundColor = color 51 | } 52 | } 53 | 54 | /** 55 | A scale transition object drives the custom appearance of actions during transition. 56 | 57 | As button's percentage visibility crosses the `threshold`, the `ScaleTransition` object will animate from `initialScale` to `identity`. The default settings provide a "pop-like" effect as the buttons are exposed more than 50%. 58 | */ 59 | public struct ScaleTransition: SwipeActionTransitioning { 60 | 61 | /// Returns a `ScaleTransition` instance with default transition options. 62 | public static var `default`: ScaleTransition { return ScaleTransition() } 63 | 64 | /// The duration of the animation. 65 | public let duration: Double 66 | 67 | /// The initial scale factor used before the action button percent visible is greater than the threshold. 68 | public let initialScale: CGFloat 69 | 70 | /// The percent visible threshold that triggers the scaling animation. 71 | public let threshold: CGFloat 72 | 73 | /** 74 | Contructs a new `ScaleTransition` instance. 75 | 76 | - parameter duration: The duration of the animation. 77 | 78 | - parameter initialScale: The initial scale factor used before the action button percent visible is greater than the threshold. 79 | 80 | - parameter threshold: The percent visible threshold that triggers the scaling animation. 81 | 82 | - returns: The new `ScaleTransition` instance. 83 | */ 84 | public init(duration: Double = 0.15, initialScale: CGFloat = 0.8, threshold: CGFloat = 0.5) { 85 | self.duration = duration 86 | self.initialScale = initialScale 87 | self.threshold = threshold 88 | } 89 | 90 | /// :nodoc: 91 | public func didTransition(with context: SwipeActionTransitioningContext) -> Void { 92 | if context.oldPercentVisible == 0 { 93 | context.button.transform = .init(scaleX: initialScale, y: initialScale) 94 | } 95 | 96 | if context.oldPercentVisible < threshold && context.newPercentVisible >= threshold { 97 | UIView.animate(withDuration: duration) { 98 | context.button.transform = .identity 99 | } 100 | } else if context.oldPercentVisible >= threshold && context.newPercentVisible < threshold { 101 | UIView.animate(withDuration: duration) { 102 | context.button.transform = .init(scaleX: self.initialScale, y: self.initialScale) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeAnimator.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol SwipeAnimator { 11 | /// A Boolean value indicating whether the animation is currently running. 12 | var isRunning: Bool { get } 13 | 14 | /** 15 | The animation to be run by the SwipeAnimator 16 | 17 | - parameter animation: The closure to be executed by the animator 18 | */ 19 | func addAnimations(_ animation: @escaping () -> Void) 20 | 21 | /** 22 | Completion handler for the animation that is going to be started 23 | 24 | - parameter completion: The closure to be execute on completion of the animator 25 | */ 26 | func addCompletion(completion: @escaping (Bool) -> Void) 27 | 28 | /** 29 | Starts the defined animation 30 | */ 31 | func startAnimation() 32 | 33 | /** 34 | Starts the defined animation after the given delay 35 | 36 | - parameter delay: Delay of the animation 37 | */ 38 | func startAnimation(afterDelay delay: TimeInterval) 39 | 40 | /** 41 | Stops the animations at their current positions. 42 | 43 | - parameter withoutFinishing: A Boolean indicating whether any final actions should be performed. 44 | */ 45 | func stopAnimation(_ withoutFinishing: Bool) 46 | } 47 | 48 | @available(iOS 10.0, *) 49 | extension UIViewPropertyAnimator: SwipeAnimator { 50 | func addCompletion(completion: @escaping (Bool) -> Void) { 51 | addCompletion { position in 52 | completion(position == .end) 53 | } 54 | } 55 | } 56 | 57 | class UIViewSpringAnimator: SwipeAnimator { 58 | var isRunning: Bool = false 59 | 60 | let duration:TimeInterval 61 | let damping:CGFloat 62 | let velocity:CGFloat 63 | 64 | var animations:(() -> Void)? 65 | var completion:((Bool) -> Void)? 66 | 67 | required init(duration: TimeInterval, 68 | damping: CGFloat, 69 | initialVelocity velocity: CGFloat = 0) { 70 | self.duration = duration 71 | self.damping = damping 72 | self.velocity = velocity 73 | } 74 | 75 | func addAnimations(_ animations: @escaping () -> Void) { 76 | self.animations = animations 77 | } 78 | 79 | func addCompletion(completion: @escaping (Bool) -> Void) { 80 | self.completion = { [weak self] finished in 81 | guard self?.isRunning == true else { return } 82 | 83 | self?.isRunning = false 84 | self?.animations = nil 85 | self?.completion = nil 86 | 87 | completion(finished) 88 | } 89 | } 90 | 91 | func startAnimation() { 92 | self.startAnimation(afterDelay: 0) 93 | } 94 | 95 | func startAnimation(afterDelay delay:TimeInterval) { 96 | guard let animations = animations else { return } 97 | 98 | isRunning = true 99 | 100 | UIView.animate(withDuration: duration, 101 | delay: delay, 102 | usingSpringWithDamping: damping, 103 | initialSpringVelocity: velocity, 104 | options: [.curveEaseInOut, .allowUserInteraction], 105 | animations: animations, 106 | completion: completion) 107 | } 108 | 109 | func stopAnimation(_ withoutFinishing: Bool) { 110 | isRunning = false 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeCollectionViewCell+Accessibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeCollectionViewCell+Accessibility.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension SwipeCollectionViewCell { 11 | /// :nodoc: 12 | open override func accessibilityElementCount() -> Int { 13 | guard state != .center else { 14 | return super.accessibilityElementCount() 15 | } 16 | 17 | return 1 18 | } 19 | 20 | /// :nodoc: 21 | open override func accessibilityElement(at index: Int) -> Any? { 22 | guard state != .center else { 23 | return super.accessibilityElement(at: index) 24 | } 25 | 26 | return actionsView 27 | } 28 | 29 | /// :nodoc: 30 | open override func index(ofAccessibilityElement element: Any) -> Int { 31 | guard state != .center else { 32 | return super.index(ofAccessibilityElement: element) 33 | } 34 | 35 | return element is SwipeActionsView ? 0 : NSNotFound 36 | } 37 | } 38 | 39 | extension SwipeCollectionViewCell { 40 | /// :nodoc: 41 | open override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { 42 | get { 43 | guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { 44 | return super.accessibilityCustomActions 45 | } 46 | 47 | let leftActions = delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: .left) ?? [] 48 | let rightActions = delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: .right) ?? [] 49 | 50 | let actions = [rightActions.first, leftActions.first].compactMap({ $0 }) + rightActions.dropFirst() + leftActions.dropFirst() 51 | 52 | if actions.count > 0 { 53 | return actions.compactMap({ SwipeAccessibilityCustomAction(action: $0, 54 | indexPath: indexPath, 55 | target: self, 56 | selector: #selector(performAccessibilityCustomAction(accessibilityCustomAction:))) }) 57 | } else { 58 | return super.accessibilityCustomActions 59 | } 60 | } 61 | 62 | set { 63 | super.accessibilityCustomActions = newValue 64 | } 65 | } 66 | 67 | @objc func performAccessibilityCustomAction(accessibilityCustomAction: SwipeAccessibilityCustomAction) -> Bool { 68 | guard let collectionView = collectionView else { return false } 69 | 70 | let swipeAction = accessibilityCustomAction.action 71 | 72 | swipeAction.handler?(swipeAction, accessibilityCustomAction.indexPath) 73 | 74 | if swipeAction.style == .destructive { 75 | collectionView.deleteItems(at: [accessibilityCustomAction.indexPath]) 76 | } 77 | 78 | return true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeCollectionViewCell+Display.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeCollectionViewCell+Display.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension SwipeCollectionViewCell { 11 | /// The point at which the origin of the cell is offset from the non-swiped origin. 12 | public var swipeOffset: CGFloat { 13 | set { setSwipeOffset(newValue, animated: false) } 14 | get { return contentView.frame.midX - bounds.midX } 15 | } 16 | 17 | /** 18 | Hides the swipe actions and returns the cell to center. 19 | 20 | - parameter animated: Specify `true` to animate the hiding of the swipe actions or `false` to hide it immediately. 21 | 22 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 23 | */ 24 | public func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) { 25 | swipeController.hideSwipe(animated: animated, completion: completion) 26 | } 27 | 28 | /** 29 | Shows the swipe actions for the specified orientation. 30 | 31 | - parameter orientation: The side of the cell on which to show the swipe actions. 32 | 33 | - parameter animated: Specify `true` to animate the showing of the swipe actions or `false` to show them immediately. 34 | 35 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 36 | */ 37 | public func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { 38 | setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1, 39 | animated: animated, 40 | completion: completion) 41 | } 42 | 43 | /** 44 | The point at which the origin of the cell is offset from the non-swiped origin. 45 | 46 | - parameter offset: A point (expressed in points) that is offset from the non-swiped origin. 47 | 48 | - parameter animated: Specify `true` to animate the transition to the new offset, `false` to make the transition immediate. 49 | 50 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 51 | */ 52 | public func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { 53 | swipeController.setSwipeOffset(offset, animated: animated, completion: completion) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeCollectionViewCell.swift 3 | // SwipeCellKit 4 | // 5 | // Created by Jeremy Koch 6 | // Copyright © 2017 Jeremy Koch. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | The `SwipeCollectionViewCell` class extends `UICollectionViewCell` and provides more flexible options for cell swiping behavior. 13 | 14 | 15 | The default behavior closely matches the stock Mail.app. If you want to customize the transition style (ie. how the action buttons are exposed), or the expansion style (the behavior when the row is swiped passes a defined threshold), you can return the appropriately configured `SwipeOptions` via the `SwipeCollectionViewCellDelegate` delegate. 16 | */ 17 | open class SwipeCollectionViewCell: UICollectionViewCell { 18 | /// The object that acts as the delegate of the `SwipeCollectionViewCell`. 19 | public weak var delegate: SwipeCollectionViewCellDelegate? 20 | 21 | var state = SwipeState.center 22 | var actionsView: SwipeActionsView? 23 | var scrollView: UIScrollView? { 24 | return collectionView 25 | } 26 | var indexPath: IndexPath? { 27 | return collectionView?.indexPath(for: self) 28 | } 29 | var panGestureRecognizer: UIGestureRecognizer 30 | { 31 | return swipeController.panGestureRecognizer; 32 | } 33 | 34 | var swipeController: SwipeController! 35 | var isPreviouslySelected = false 36 | 37 | weak var collectionView: UICollectionView? 38 | 39 | /// :nodoc: 40 | open override var frame: CGRect { 41 | set { super.frame = state.isActive ? CGRect(origin: CGPoint(x: frame.minX, y: newValue.minY), size: newValue.size) : newValue } 42 | get { return super.frame } 43 | } 44 | 45 | /// :nodoc: 46 | open override var isHighlighted: Bool { 47 | set { 48 | guard state == .center else { return } 49 | super.isHighlighted = newValue 50 | } 51 | get { return super.isHighlighted } 52 | } 53 | 54 | /// :nodoc: 55 | open override var layoutMargins: UIEdgeInsets { 56 | get { 57 | return frame.origin.x != 0 ? swipeController.originalLayoutMargins : super.layoutMargins 58 | } 59 | set { 60 | super.layoutMargins = newValue 61 | } 62 | } 63 | 64 | /// :nodoc: 65 | override public init(frame: CGRect) { 66 | super.init(frame: frame) 67 | 68 | configure() 69 | } 70 | 71 | /// :nodoc: 72 | required public init?(coder aDecoder: NSCoder) { 73 | super.init(coder: aDecoder) 74 | 75 | configure() 76 | } 77 | 78 | deinit { 79 | collectionView?.panGestureRecognizer.removeTarget(self, action: nil) 80 | } 81 | 82 | func configure() { 83 | contentView.clipsToBounds = false 84 | 85 | swipeController = SwipeController(swipeable: self, actionsContainerView: contentView) 86 | swipeController.delegate = self 87 | } 88 | 89 | /// :nodoc: 90 | override open func prepareForReuse() { 91 | super.prepareForReuse() 92 | 93 | reset() 94 | resetSelectedState() 95 | } 96 | 97 | /// :nodoc: 98 | override open func didMoveToSuperview() { 99 | super.didMoveToSuperview() 100 | 101 | var view: UIView = self 102 | while let superview = view.superview { 103 | view = superview 104 | 105 | if let collectionView = view as? UICollectionView { 106 | self.collectionView = collectionView 107 | 108 | swipeController.scrollView = scrollView 109 | 110 | collectionView.panGestureRecognizer.removeTarget(self, action: nil) 111 | collectionView.panGestureRecognizer.addTarget(self, action: #selector(handleCollectionPan(gesture:))) 112 | return 113 | } 114 | } 115 | } 116 | 117 | /// :nodoc: 118 | open override func willMove(toWindow newWindow: UIWindow?) { 119 | super.willMove(toWindow: newWindow) 120 | 121 | if newWindow == nil { 122 | reset() 123 | } 124 | } 125 | 126 | // Override so we can accept touches anywhere within the cell's original frame. 127 | // This is required to detect touches on the `SwipeActionsView` sitting alongside the 128 | // `SwipeCollectionViewCell`. 129 | /// :nodoc: 130 | override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 131 | guard let superview = superview else { return false } 132 | 133 | let point = convert(point, to: superview) 134 | 135 | if !UIAccessibility.isVoiceOverRunning { 136 | for cell in collectionView?.swipeCells ?? [] { 137 | if (cell.state == .left || cell.state == .right) && !cell.contains(point: point) { 138 | collectionView?.hideSwipeCell() 139 | return false 140 | } 141 | } 142 | } 143 | 144 | return contains(point: point) 145 | } 146 | 147 | func contains(point: CGPoint) -> Bool { 148 | return frame.contains(point) 149 | } 150 | 151 | // Override hitTest(_:with:) here so that we can make sure our `actionsView` gets the touch event 152 | // if it's supposed to, since otherwise, our `contentView` will swallow it and pass it up to 153 | // the collection view. 154 | /// :nodoc: 155 | open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 156 | guard let actionsView = actionsView else { return super.hitTest(point, with: event) } 157 | let modifiedPoint = actionsView.convert(point, from: self) 158 | return actionsView.hitTest(modifiedPoint, with: event) ?? super.hitTest(point, with: event) 159 | } 160 | 161 | /// :nodoc: 162 | override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 163 | return swipeController.gestureRecognizerShouldBegin(gestureRecognizer) 164 | } 165 | 166 | /// :nodoc: 167 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 168 | super.traitCollectionDidChange(previousTraitCollection) 169 | 170 | swipeController.traitCollectionDidChange(from: previousTraitCollection, to: self.traitCollection) 171 | } 172 | 173 | @objc func handleCollectionPan(gesture: UIPanGestureRecognizer) { 174 | if gesture.state == .began { 175 | hideSwipe(animated: true) 176 | } 177 | } 178 | 179 | func reset() { 180 | contentView.clipsToBounds = false 181 | swipeController.reset() 182 | collectionView?.setGestureEnabled(true) 183 | } 184 | 185 | func resetSelectedState() { 186 | if isPreviouslySelected { 187 | if let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) { 188 | collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) 189 | } 190 | } 191 | isPreviouslySelected = false 192 | } 193 | } 194 | 195 | extension SwipeCollectionViewCell: SwipeControllerDelegate { 196 | func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool { 197 | return true 198 | } 199 | 200 | func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? { 201 | guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return nil } 202 | 203 | return delegate?.collectionView(collectionView, editActionsForItemAt: indexPath, for: orientation) 204 | } 205 | 206 | func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions { 207 | guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return SwipeOptions() } 208 | 209 | return delegate?.collectionView(collectionView, editActionsOptionsForItemAt: indexPath, for: orientation) ?? SwipeOptions() 210 | } 211 | 212 | func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? { 213 | guard let collectionView = collectionView else { return nil } 214 | 215 | return delegate?.visibleRect(for: collectionView) 216 | } 217 | 218 | func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) { 219 | guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self) else { return } 220 | 221 | // Remove highlight and deselect any selected cells 222 | super.isHighlighted = false 223 | isPreviouslySelected = isSelected 224 | collectionView.deselectItem(at: indexPath, animated: false) 225 | 226 | delegate?.collectionView(collectionView, willBeginEditingItemAt: indexPath, for: orientation) 227 | } 228 | 229 | func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) { 230 | guard let collectionView = collectionView, let indexPath = collectionView.indexPath(for: self), let actionsView = self.actionsView else { return } 231 | 232 | resetSelectedState() 233 | 234 | delegate?.collectionView(collectionView, didEndEditingItemAt: indexPath, for: actionsView.orientation) 235 | } 236 | 237 | func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) { 238 | collectionView?.deleteItems(at: [indexPath]) 239 | } 240 | } 241 | 242 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeCollectionViewCellDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeCollectionViewCellDelegate.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | The `SwipeCollectionViewCellDelegate` protocol is adopted by an object that manages the display of action buttons when the item is swiped. 12 | */ 13 | public protocol SwipeCollectionViewCellDelegate: class { 14 | /** 15 | Asks the delegate for the actions to display in response to a swipe in the specified item. 16 | 17 | - parameter collectionView: The collection view object which owns the item requesting this information. 18 | 19 | - parameter indexPath: The index path of the item. 20 | 21 | - parameter orientation: The side of the item requesting this information. 22 | 23 | - returns: An array of `SwipeAction` objects representing the actions for the item. Each action you provide is used to create a button that the user can tap. Returning `nil` will prevent swiping for the supplied orientation. 24 | */ 25 | func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? 26 | 27 | /** 28 | Asks the delegate for the display options to be used while presenting the action buttons. 29 | 30 | - parameter collectionView: The collection view object which owns the item requesting this information. 31 | 32 | - parameter indexPath: The index path of the item. 33 | 34 | - parameter orientation: The side of the item requesting this information. 35 | 36 | - returns: A `SwipeOptions` instance which configures the behavior of the action buttons. 37 | 38 | - note: If not implemented, a default `SwipeOptions` instance is used. 39 | */ 40 | func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions 41 | 42 | /** 43 | Tells the delegate that the collection view is about to go into editing mode. 44 | 45 | - parameter collectionView: The collection view object providing this information. 46 | 47 | - parameter indexPath: The index path of the item. 48 | 49 | - parameter orientation: The side of the item. 50 | */ 51 | func collectionView(_ collectionView: UICollectionView, willBeginEditingItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) 52 | 53 | /** 54 | Tells the delegate that the collection view has left editing mode. 55 | 56 | - parameter collectionView: The collection view object providing this information. 57 | 58 | - parameter indexPath: The index path of the item. 59 | 60 | - parameter orientation: The side of the item. 61 | */ 62 | func collectionView(_ collectionView: UICollectionView, didEndEditingItemAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) 63 | 64 | /** 65 | Asks the delegate for visibile rectangle of the collection view, which is used to ensure swipe actions are vertically centered within the visible portion of the item. 66 | 67 | - parameter collectionView: The collection view object providing this information. 68 | 69 | - returns: The visible rectangle of the collection view. 70 | 71 | - note: The returned rectange should be in the collection view's own coordinate system. Returning `nil` will result in no vertical offset to be be calculated. 72 | */ 73 | func visibleRect(for collectionView: UICollectionView) -> CGRect? 74 | } 75 | 76 | /** 77 | Default implementation of `SwipeCollectionViewCellDelegate` methods 78 | */ 79 | public extension SwipeCollectionViewCellDelegate { 80 | func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { 81 | return SwipeOptions() 82 | } 83 | 84 | func collectionView(_ collectionView: UICollectionView, willBeginEditingItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) {} 85 | 86 | func collectionView(_ collectionView: UICollectionView, didEndEditingItemAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) {} 87 | 88 | func visibleRect(for collectionView: UICollectionView) -> CGRect? { 89 | return nil 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeExpanding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeExpanding.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | Adopt the `SwipeExpanding` protocol in objects that implement custom appearance of actions during expansion. 12 | */ 13 | public protocol SwipeExpanding { 14 | 15 | /** 16 | Asks your object for the animation timing parameters. 17 | 18 | - parameter buttons: The expansion action button, which includes expanding action plus the remaining actions in the view. 19 | 20 | - parameter expanding: The new expansion state. 21 | 22 | - parameter otherActionButtons: The other action buttons in the view, not including the action button being expanded. 23 | */ 24 | 25 | func animationTimingParameters(buttons: [UIButton], expanding: Bool) -> SwipeExpansionAnimationTimingParameters 26 | 27 | /** 28 | Tells your object when the expansion state is changing. 29 | 30 | - parameter button: The expansion action button. 31 | 32 | - parameter expanding: The new expansion state. 33 | 34 | - parameter otherActionButtons: The other action buttons in the view, not including the action button being expanded. 35 | */ 36 | func actionButton(_ button: UIButton, didChange expanding: Bool, otherActionButtons: [UIButton]) 37 | } 38 | 39 | /** 40 | Specifies timing information for the overall expansion animation. 41 | */ 42 | public struct SwipeExpansionAnimationTimingParameters { 43 | 44 | /// Returns a `SwipeExpansionAnimationTimingParameters` instance with default animation parameters. 45 | public static var `default`: SwipeExpansionAnimationTimingParameters { return SwipeExpansionAnimationTimingParameters() } 46 | 47 | /// The duration of the expansion animation. 48 | public var duration: Double 49 | 50 | /// The delay before starting the expansion animation. 51 | public var delay: Double 52 | 53 | /** 54 | Contructs a new `SwipeExpansionAnimationTimingParameters` instance. 55 | 56 | - parameter duration: The duration of the animation. 57 | 58 | - parameter delay: The delay before starting the expansion animation. 59 | 60 | - returns: The new `SwipeExpansionAnimationTimingParameters` instance. 61 | */ 62 | public init(duration: Double = 0.6, delay: Double = 0) { 63 | self.duration = duration 64 | self.delay = delay 65 | } 66 | } 67 | 68 | /** 69 | A scale and alpha expansion object drives the custom appearance of the effected actions during expansion. 70 | */ 71 | public struct ScaleAndAlphaExpansion: SwipeExpanding { 72 | 73 | /// Returns a `ScaleAndAlphaExpansion` instance with default expansion options. 74 | public static var `default`: ScaleAndAlphaExpansion { return ScaleAndAlphaExpansion() } 75 | 76 | /// The duration of the animation. 77 | public let duration: Double 78 | 79 | /// The scale factor used during animation. 80 | public let scale: CGFloat 81 | 82 | /// The inter-button delay between animations. 83 | public let interButtonDelay: Double 84 | 85 | /** 86 | Contructs a new `ScaleAndAlphaExpansion` instance. 87 | 88 | - parameter duration: The duration of the animation. 89 | 90 | - parameter scale: The scale factor used during animation. 91 | 92 | - parameter interButtonDelay: The inter-button delay between animations. 93 | 94 | - returns: The new `ScaleAndAlphaExpansion` instance. 95 | */ 96 | public init(duration: Double = 0.15, scale: CGFloat = 0.8, interButtonDelay: Double = 0.1) { 97 | self.duration = duration 98 | self.scale = scale 99 | self.interButtonDelay = interButtonDelay 100 | } 101 | 102 | /// :nodoc: 103 | public func animationTimingParameters(buttons: [UIButton], expanding: Bool) -> SwipeExpansionAnimationTimingParameters { 104 | var timingParameters = SwipeExpansionAnimationTimingParameters.default 105 | timingParameters.delay = expanding ? interButtonDelay : 0 106 | return timingParameters 107 | } 108 | 109 | /// :nodoc: 110 | public func actionButton(_ button: UIButton, didChange expanding: Bool, otherActionButtons: [UIButton]) { 111 | let buttons = expanding ? otherActionButtons : otherActionButtons.reversed() 112 | 113 | buttons.enumerated().forEach { index, button in 114 | UIView.animate(withDuration: duration, delay: interButtonDelay * Double(expanding ? index : index + 1), options: [], animations: { 115 | button.transform = expanding ? .init(scaleX: self.scale, y: self.scale) : .identity 116 | button.alpha = expanding ? 0.0 : 1.0 117 | }, completion: nil) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeFeedback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeFeedback.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SwipeFeedback { 11 | enum Style { 12 | case light 13 | case medium 14 | case heavy 15 | } 16 | 17 | @available(iOS 10.0.1, *) 18 | private var feedbackGenerator: UIImpactFeedbackGenerator? { 19 | get { 20 | return _feedbackGenerator as? UIImpactFeedbackGenerator 21 | } 22 | set { 23 | _feedbackGenerator = newValue 24 | } 25 | } 26 | 27 | private var _feedbackGenerator: Any? 28 | 29 | init(style: Style) { 30 | if #available(iOS 10.0.1, *) { 31 | switch style { 32 | case .light: 33 | feedbackGenerator = UIImpactFeedbackGenerator(style: .light) 34 | case .medium: 35 | feedbackGenerator = UIImpactFeedbackGenerator(style: .medium) 36 | case .heavy: 37 | feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) 38 | } 39 | } else { 40 | _feedbackGenerator = nil 41 | } 42 | } 43 | 44 | func prepare() { 45 | if #available(iOS 10.0.1, *) { 46 | feedbackGenerator?.prepare() 47 | } 48 | } 49 | 50 | func impactOccurred() { 51 | if #available(iOS 10.0.1, *) { 52 | feedbackGenerator?.impactOccurred() 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Options.swift 3 | // 4 | // Created by Jeremy Koch. 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// :nodoc: 11 | public typealias SwipeTableOptions = SwipeOptions 12 | 13 | /// The `SwipeOptions` class provides options for transistion and expansion behavior for swiped cell. 14 | public struct SwipeOptions { 15 | /// The transition style. Transition is the style of how the action buttons are exposed during the swipe. 16 | public var transitionStyle: SwipeTransitionStyle = .border 17 | 18 | /// The expansion style. Expansion is the behavior when the cell is swiped past a defined threshold. 19 | public var expansionStyle: SwipeExpansionStyle? 20 | 21 | /// The object that is notified when expansion changes. 22 | /// 23 | /// - note: If an `expansionDelegate` is not provided, and the expanding action is configured with a clear background, the system automatically uses the default `ScaleAndAlphaExpansion` to show/hide underlying actions. 24 | public var expansionDelegate: SwipeExpanding? 25 | 26 | /// The background color behind the action buttons. 27 | public var backgroundColor: UIColor? 28 | 29 | /// The largest allowable button width. 30 | /// 31 | /// - note: By default, the value is set to the table/collection view divided by the number of action buttons minus some additional padding. If the value is set to 0, then word wrapping will not occur and the buttons will grow as large as needed to fit the entire title/image. 32 | public var maximumButtonWidth: CGFloat? 33 | 34 | /// The smallest allowable button width. 35 | /// 36 | /// - note: By default, the system chooses an appropriate size. 37 | public var minimumButtonWidth: CGFloat? 38 | 39 | /// The vertical alignment mode used for when a button image and title are present. 40 | public var buttonVerticalAlignment: SwipeVerticalAlignment = .centerFirstBaseline 41 | 42 | /// The amount of space, in points, between the border and the button image or title. 43 | public var buttonPadding: CGFloat? 44 | 45 | /// The amount of space, in points, between the button image and the button title. 46 | public var buttonSpacing: CGFloat? 47 | 48 | /// Constructs a new `SwipeOptions` instance with default options. 49 | public init() {} 50 | } 51 | 52 | /// Describes the transition style. Transition is the style of how the action buttons are exposed during the swipe. 53 | public enum SwipeTransitionStyle { 54 | /// The visible action area is equally divide between all action buttons. 55 | case border 56 | 57 | /// The visible action area is dragged, pinned to the cell, with each action button fully sized as it is exposed. 58 | case drag 59 | 60 | /// The visible action area sits behind the cell, pinned to the edge of the table/collection view, and is revealed as the cell is dragged aside. 61 | case reveal 62 | } 63 | 64 | /// Describes which side of the cell that the action buttons will be displayed. 65 | public enum SwipeActionsOrientation: CGFloat { 66 | /// The left side of the cell. 67 | case left = -1 68 | 69 | /// The right side of the cell. 70 | case right = 1 71 | 72 | var scale: CGFloat { 73 | return rawValue 74 | } 75 | } 76 | 77 | /// Describes the alignment mode used when action button images and titles are provided. 78 | public enum SwipeVerticalAlignment { 79 | /// All actions will be inspected and the tallest image and first baseline offset of title text will be used to create the alignment rectangle. 80 | /// 81 | /// - note: This mode will ensure the image and first line of each button title and consistently aligned across the swipe view. 82 | case centerFirstBaseline 83 | 84 | /// The action button image height and full title height are used to create the aligment rectange. 85 | /// 86 | /// - note: Buttons with varying number of lines will not be consistently aligned across the swipe view. 87 | case center 88 | } 89 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeTableViewCell+Accessibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeTableViewCell+Accessibility.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension SwipeTableViewCell { 11 | /// :nodoc: 12 | open override func accessibilityElementCount() -> Int { 13 | guard state != .center else { 14 | return super.accessibilityElementCount() 15 | } 16 | 17 | return 1 18 | } 19 | 20 | /// :nodoc: 21 | open override func accessibilityElement(at index: Int) -> Any? { 22 | guard state != .center else { 23 | return super.accessibilityElement(at: index) 24 | } 25 | 26 | return actionsView 27 | } 28 | 29 | /// :nodoc: 30 | open override func index(ofAccessibilityElement element: Any) -> Int { 31 | guard state != .center else { 32 | return super.index(ofAccessibilityElement: element) 33 | } 34 | 35 | return element is SwipeActionsView ? 0 : NSNotFound 36 | } 37 | } 38 | 39 | extension SwipeTableViewCell { 40 | /// :nodoc: 41 | open override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { 42 | get { 43 | guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { 44 | return super.accessibilityCustomActions 45 | } 46 | 47 | let leftActions = delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: .left) ?? [] 48 | let rightActions = delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: .right) ?? [] 49 | 50 | let actions = [rightActions.first, leftActions.first].compactMap({ $0 }) + rightActions.dropFirst() + leftActions.dropFirst() 51 | 52 | if actions.count > 0 { 53 | return actions.compactMap({ SwipeAccessibilityCustomAction(action: $0, 54 | indexPath: indexPath, 55 | target: self, 56 | selector: #selector(performAccessibilityCustomAction(accessibilityCustomAction:))) }) 57 | } else { 58 | return super.accessibilityCustomActions 59 | } 60 | } 61 | 62 | set { 63 | super.accessibilityCustomActions = newValue 64 | } 65 | } 66 | 67 | @objc func performAccessibilityCustomAction(accessibilityCustomAction: SwipeAccessibilityCustomAction) -> Bool { 68 | guard let tableView = tableView else { return false } 69 | 70 | let swipeAction = accessibilityCustomAction.action 71 | 72 | swipeAction.handler?(swipeAction, accessibilityCustomAction.indexPath) 73 | 74 | if swipeAction.style == .destructive { 75 | tableView.deleteRows(at: [accessibilityCustomAction.indexPath], with: .fade) 76 | } 77 | 78 | return true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeTableViewCell+Display.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeTableViewCell+Display.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension SwipeTableViewCell { 11 | /// The point at which the origin of the cell is offset from the non-swiped origin. 12 | public var swipeOffset: CGFloat { 13 | set { setSwipeOffset(newValue, animated: false) } 14 | get { return frame.midX - bounds.midX } 15 | } 16 | 17 | /** 18 | Hides the swipe actions and returns the cell to center. 19 | 20 | - parameter animated: Specify `true` to animate the hiding of the swipe actions or `false` to hide it immediately. 21 | 22 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 23 | */ 24 | public func hideSwipe(animated: Bool, completion: ((Bool) -> Void)? = nil) { 25 | swipeController.hideSwipe(animated: animated, completion: completion) 26 | } 27 | 28 | /** 29 | Shows the swipe actions for the specified orientation. 30 | 31 | - parameter orientation: The side of the cell on which to show the swipe actions. 32 | 33 | - parameter animated: Specify `true` to animate the showing of the swipe actions or `false` to show them immediately. 34 | 35 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 36 | */ 37 | public func showSwipe(orientation: SwipeActionsOrientation, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { 38 | setSwipeOffset(.greatestFiniteMagnitude * orientation.scale * -1, 39 | animated: animated, 40 | completion: completion) 41 | } 42 | 43 | /** 44 | The point at which the origin of the cell is offset from the non-swiped origin. 45 | 46 | - parameter offset: A point (expressed in points) that is offset from the non-swiped origin. 47 | 48 | - parameter animated: Specify `true` to animate the transition to the new offset, `false` to make the transition immediate. 49 | 50 | - parameter completion: The closure to be executed once the animation has finished. A `Boolean` argument indicates whether or not the animations actually finished before the completion handler was called. 51 | */ 52 | public func setSwipeOffset(_ offset: CGFloat, animated: Bool = true, completion: ((Bool) -> Void)? = nil) { 53 | swipeController.setSwipeOffset(offset, animated: animated, completion: completion) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeTableViewCell.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | The `SwipeTableViewCell` class extends `UITableViewCell` and provides more flexible options for cell swiping behavior. 12 | 13 | 14 | The default behavior closely matches the stock Mail.app. If you want to customize the transition style (ie. how the action buttons are exposed), or the expansion style (the behavior when the row is swiped passes a defined threshold), you can return the appropriately configured `SwipeOptions` via the `SwipeTableViewCellDelegate` delegate. 15 | */ 16 | open class SwipeTableViewCell: UITableViewCell { 17 | 18 | /// The object that acts as the delegate of the `SwipeTableViewCell`. 19 | public weak var delegate: SwipeTableViewCellDelegate? 20 | 21 | var state = SwipeState.center 22 | var actionsView: SwipeActionsView? 23 | var scrollView: UIScrollView? { 24 | return tableView 25 | } 26 | var indexPath: IndexPath? { 27 | return tableView?.indexPath(for: self) 28 | } 29 | var panGestureRecognizer: UIGestureRecognizer 30 | { 31 | return swipeController.panGestureRecognizer; 32 | } 33 | 34 | var swipeController: SwipeController! 35 | var isPreviouslySelected = false 36 | 37 | weak var tableView: UITableView? 38 | 39 | /// :nodoc: 40 | open override var frame: CGRect { 41 | set { super.frame = state.isActive ? CGRect(origin: CGPoint(x: frame.minX, y: newValue.minY), size: newValue.size) : newValue } 42 | get { return super.frame } 43 | } 44 | 45 | /// :nodoc: 46 | open override var layoutMargins: UIEdgeInsets { 47 | get { 48 | return frame.origin.x != 0 ? swipeController.originalLayoutMargins : super.layoutMargins 49 | } 50 | set { 51 | super.layoutMargins = newValue 52 | } 53 | } 54 | 55 | /// :nodoc: 56 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 57 | super.init(style: style, reuseIdentifier: reuseIdentifier) 58 | 59 | configure() 60 | } 61 | 62 | /// :nodoc: 63 | required public init?(coder aDecoder: NSCoder) { 64 | super.init(coder: aDecoder) 65 | 66 | configure() 67 | } 68 | 69 | deinit { 70 | tableView?.panGestureRecognizer.removeTarget(self, action: nil) 71 | } 72 | 73 | func configure() { 74 | clipsToBounds = false 75 | 76 | swipeController = SwipeController(swipeable: self, actionsContainerView: self) 77 | swipeController.delegate = self 78 | } 79 | 80 | /// :nodoc: 81 | override open func prepareForReuse() { 82 | super.prepareForReuse() 83 | 84 | reset() 85 | resetSelectedState() 86 | } 87 | 88 | /// :nodoc: 89 | override open func didMoveToSuperview() { 90 | super.didMoveToSuperview() 91 | 92 | var view: UIView = self 93 | while let superview = view.superview { 94 | view = superview 95 | 96 | if let tableView = view as? UITableView { 97 | self.tableView = tableView 98 | 99 | swipeController.scrollView = tableView; 100 | 101 | tableView.panGestureRecognizer.removeTarget(self, action: nil) 102 | tableView.panGestureRecognizer.addTarget(self, action: #selector(handleTablePan(gesture:))) 103 | return 104 | } 105 | } 106 | } 107 | 108 | /// :nodoc: 109 | override open func setEditing(_ editing: Bool, animated: Bool) { 110 | super.setEditing(editing, animated: animated) 111 | 112 | if editing { 113 | hideSwipe(animated: false) 114 | } 115 | } 116 | 117 | // Override so we can accept touches anywhere within the cell's minY/maxY. 118 | // This is required to detect touches on the `SwipeActionsView` sitting alongside the 119 | // `SwipeTableCell`. 120 | /// :nodoc: 121 | override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 122 | guard let superview = superview else { return false } 123 | 124 | let point = convert(point, to: superview) 125 | 126 | if !UIAccessibility.isVoiceOverRunning { 127 | for cell in tableView?.swipeCells ?? [] { 128 | if (cell.state == .left || cell.state == .right) && !cell.contains(point: point) { 129 | tableView?.hideSwipeCell() 130 | return false 131 | } 132 | } 133 | } 134 | 135 | return contains(point: point) 136 | } 137 | 138 | func contains(point: CGPoint) -> Bool { 139 | return point.y > frame.minY && point.y < frame.maxY 140 | } 141 | 142 | /// :nodoc: 143 | override open func setHighlighted(_ highlighted: Bool, animated: Bool) { 144 | if state == .center { 145 | super.setHighlighted(highlighted, animated: animated) 146 | } 147 | } 148 | 149 | /// :nodoc: 150 | override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 151 | return swipeController.gestureRecognizerShouldBegin(gestureRecognizer) 152 | } 153 | 154 | /// :nodoc: 155 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 156 | super.traitCollectionDidChange(previousTraitCollection) 157 | 158 | swipeController.traitCollectionDidChange(from: previousTraitCollection, to: self.traitCollection) 159 | } 160 | 161 | @objc func handleTablePan(gesture: UIPanGestureRecognizer) { 162 | if gesture.state == .began { 163 | hideSwipe(animated: true) 164 | } 165 | } 166 | 167 | func reset() { 168 | swipeController.reset() 169 | clipsToBounds = false 170 | } 171 | 172 | func resetSelectedState() { 173 | if isPreviouslySelected { 174 | if let tableView = tableView, let indexPath = tableView.indexPath(for: self) { 175 | tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) 176 | } 177 | } 178 | isPreviouslySelected = false 179 | } 180 | } 181 | 182 | extension SwipeTableViewCell: SwipeControllerDelegate { 183 | func swipeController(_ controller: SwipeController, canBeginEditingSwipeableFor orientation: SwipeActionsOrientation) -> Bool { 184 | return self.isEditing == false 185 | } 186 | 187 | func swipeController(_ controller: SwipeController, editActionsForSwipeableFor orientation: SwipeActionsOrientation) -> [SwipeAction]? { 188 | guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return nil } 189 | 190 | return delegate?.tableView(tableView, editActionsForRowAt: indexPath, for: orientation) 191 | } 192 | 193 | func swipeController(_ controller: SwipeController, editActionsOptionsForSwipeableFor orientation: SwipeActionsOrientation) -> SwipeOptions { 194 | guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return SwipeOptions() } 195 | 196 | return delegate?.tableView(tableView, editActionsOptionsForRowAt: indexPath, for: orientation) ?? SwipeOptions() 197 | } 198 | 199 | func swipeController(_ controller: SwipeController, visibleRectFor scrollView: UIScrollView) -> CGRect? { 200 | guard let tableView = tableView else { return nil } 201 | 202 | return delegate?.visibleRect(for: tableView) 203 | } 204 | 205 | func swipeController(_ controller: SwipeController, willBeginEditingSwipeableFor orientation: SwipeActionsOrientation) { 206 | guard let tableView = tableView, let indexPath = tableView.indexPath(for: self) else { return } 207 | 208 | // Remove highlight and deselect any selected cells 209 | super.setHighlighted(false, animated: false) 210 | isPreviouslySelected = isSelected 211 | tableView.deselectRow(at: indexPath, animated: false) 212 | 213 | delegate?.tableView(tableView, willBeginEditingRowAt: indexPath, for: orientation) 214 | } 215 | 216 | func swipeController(_ controller: SwipeController, didEndEditingSwipeableFor orientation: SwipeActionsOrientation) { 217 | guard let tableView = tableView, let indexPath = tableView.indexPath(for: self), let actionsView = self.actionsView else { return } 218 | 219 | resetSelectedState() 220 | 221 | delegate?.tableView(tableView, didEndEditingRowAt: indexPath, for: actionsView.orientation) 222 | } 223 | 224 | func swipeController(_ controller: SwipeController, didDeleteSwipeableAt indexPath: IndexPath) { 225 | tableView?.deleteRows(at: [indexPath], with: .none) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeTableViewCellDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeTableViewCellDelegate.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /** 11 | The `SwipeTableViewCellDelegate` protocol is adopted by an object that manages the display of action buttons when the cell is swiped. 12 | */ 13 | public protocol SwipeTableViewCellDelegate: class { 14 | 15 | /** 16 | Asks the delegate for the actions to display in response to a swipe in the specified row. 17 | 18 | - parameter tableView: The table view object which owns the cell requesting this information. 19 | 20 | - parameter indexPath: The index path of the row. 21 | 22 | - parameter orientation: The side of the cell requesting this information. 23 | 24 | - returns: An array of `SwipeAction` objects representing the actions for the row. Each action you provide is used to create a button that the user can tap. Returning `nil` will prevent swiping for the supplied orientation. 25 | */ 26 | func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? 27 | 28 | /** 29 | Asks the delegate for the display options to be used while presenting the action buttons. 30 | 31 | - parameter tableView: The table view object which owns the cell requesting this information. 32 | 33 | - parameter indexPath: The index path of the row. 34 | 35 | - parameter orientation: The side of the cell requesting this information. 36 | 37 | - returns: A `SwipeOptions` instance which configures the behavior of the action buttons. 38 | 39 | - note: If not implemented, a default `SwipeOptions` instance is used. 40 | */ 41 | func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions 42 | 43 | /** 44 | Tells the delegate that the table view is about to go into editing mode. 45 | 46 | - parameter tableView: The table view object providing this information. 47 | 48 | - parameter indexPath: The index path of the row. 49 | 50 | - parameter orientation: The side of the cell. 51 | */ 52 | func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) 53 | 54 | /** 55 | Tells the delegate that the table view has left editing mode. 56 | 57 | - parameter tableView: The table view object providing this information. 58 | 59 | - parameter indexPath: The index path of the row. 60 | 61 | - parameter orientation: The side of the cell. 62 | */ 63 | func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) 64 | 65 | /** 66 | Asks the delegate for visibile rectangle of the table view, which is used to ensure swipe actions are vertically centered within the visible portion of the cell. 67 | 68 | - parameter tableView: The table view object providing this information. 69 | 70 | - returns: The visible rectangle of the table view. 71 | 72 | - note: The returned rectange should be in the table view's own coordinate system. Returning `nil` will result in no vertical offset to be be calculated. 73 | */ 74 | func visibleRect(for tableView: UITableView) -> CGRect? 75 | } 76 | 77 | /** 78 | Default implementation of `SwipeTableViewCellDelegate` methods 79 | */ 80 | public extension SwipeTableViewCellDelegate { 81 | func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { 82 | return SwipeOptions() 83 | } 84 | 85 | func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) {} 86 | 87 | func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?, for orientation: SwipeActionsOrientation) {} 88 | 89 | func visibleRect(for tableView: UITableView) -> CGRect? { 90 | return nil 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/SwipeTransitionLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwipeTransitionLayout.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - Layout Protocol 11 | 12 | protocol SwipeTransitionLayout { 13 | func container(view: UIView, didChangeVisibleWidthWithContext context: ActionsViewLayoutContext) 14 | func layout(view: UIView, atIndex index: Int, with context: ActionsViewLayoutContext) 15 | func visibleWidthsForViews(with context: ActionsViewLayoutContext) -> [CGFloat] 16 | } 17 | 18 | // MARK: - Layout Context 19 | 20 | struct ActionsViewLayoutContext { 21 | let numberOfActions: Int 22 | let orientation: SwipeActionsOrientation 23 | let contentSize: CGSize 24 | let visibleWidth: CGFloat 25 | let minimumButtonWidth: CGFloat 26 | 27 | init(numberOfActions: Int, orientation: SwipeActionsOrientation, contentSize: CGSize = .zero, visibleWidth: CGFloat = 0, minimumButtonWidth: CGFloat = 0) { 28 | self.numberOfActions = numberOfActions 29 | self.orientation = orientation 30 | self.contentSize = contentSize 31 | self.visibleWidth = visibleWidth 32 | self.minimumButtonWidth = minimumButtonWidth 33 | } 34 | 35 | static func newContext(for actionsView: SwipeActionsView) -> ActionsViewLayoutContext { 36 | return ActionsViewLayoutContext(numberOfActions: actionsView.actions.count, 37 | orientation: actionsView.orientation, 38 | contentSize: actionsView.contentSize, 39 | visibleWidth: actionsView.visibleWidth, 40 | minimumButtonWidth: actionsView.minimumButtonWidth) 41 | } 42 | } 43 | 44 | // MARK: - Supported Layout Implementations 45 | 46 | class BorderTransitionLayout: SwipeTransitionLayout { 47 | func container(view: UIView, didChangeVisibleWidthWithContext context: ActionsViewLayoutContext) { 48 | } 49 | 50 | func layout(view: UIView, atIndex index: Int, with context: ActionsViewLayoutContext) { 51 | let diff = context.visibleWidth - context.contentSize.width 52 | view.frame.origin.x = (CGFloat(index) * context.contentSize.width / CGFloat(context.numberOfActions) + diff) * context.orientation.scale 53 | } 54 | 55 | func visibleWidthsForViews(with context: ActionsViewLayoutContext) -> [CGFloat] { 56 | let diff = context.visibleWidth - context.contentSize.width 57 | let visibleWidth = context.contentSize.width / CGFloat(context.numberOfActions) + diff 58 | 59 | // visible widths are all the same regardless of the action view position 60 | return (0.. [CGFloat] { 74 | return (0.. [CGFloat] { 86 | return super.visibleWidthsForViews(with: context).reversed() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Example/Pods/SwipeCellKit/Source/Swipeable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Swipeable.swift 3 | // 4 | // Created by Jeremy Koch 5 | // Copyright © 2017 Jeremy Koch. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - Internal 11 | 12 | protocol Swipeable { 13 | var state: SwipeState { get set } 14 | 15 | var actionsView: SwipeActionsView? { get set } 16 | 17 | var frame: CGRect { get } 18 | 19 | var scrollView: UIScrollView? { get } 20 | 21 | var indexPath: IndexPath? { get } 22 | 23 | var panGestureRecognizer: UIGestureRecognizer { get } 24 | } 25 | 26 | extension SwipeTableViewCell: Swipeable {} 27 | extension SwipeCollectionViewCell: Swipeable {} 28 | 29 | enum SwipeState: Int { 30 | case center = 0 31 | case left 32 | case right 33 | case dragging 34 | case animatingToCenter 35 | 36 | init(orientation: SwipeActionsOrientation) { 37 | self = orientation == .left ? .left : .right 38 | } 39 | 40 | var isActive: Bool { return self != .center } 41 | } 42 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SwiftScanner 5 | 6 | Copyright (c) 2015 daniele margutti 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | ## SwiftyComments 27 | 28 | Copyright (c) 2017 Stéphane Sercu 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | THE SOFTWARE. 47 | 48 | 49 | ## SwipeCellKit 50 | 51 | MIT License 52 | Copyright (c) 2017 Jeremy Koch 53 | 54 | http://jerkoch.com 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy 57 | of this software and associated documentation files (the "Software"), to deal 58 | in the Software without restriction, including without limitation the rights 59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the Software is 61 | furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in all 64 | copies or substantial portions of the Software. 65 | 66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 72 | SOFTWARE. 73 | Generated by CocoaPods - https://cocoapods.org 74 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2015 daniele margutti <me@danielemargutti.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | License 37 | MIT 38 | Title 39 | SwiftScanner 40 | Type 41 | PSGroupSpecifier 42 | 43 | 44 | FooterText 45 | Copyright (c) 2017 Stéphane Sercu <stefsercu@gmail.com> 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy 48 | of this software and associated documentation files (the "Software"), to deal 49 | in the Software without restriction, including without limitation the rights 50 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 51 | copies of the Software, and to permit persons to whom the Software is 52 | furnished to do so, subject to the following conditions: 53 | 54 | The above copyright notice and this permission notice shall be included in 55 | all copies or substantial portions of the Software. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 58 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 59 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 60 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 61 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 62 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 63 | THE SOFTWARE. 64 | 65 | License 66 | MIT 67 | Title 68 | SwiftyComments 69 | Type 70 | PSGroupSpecifier 71 | 72 | 73 | FooterText 74 | MIT License 75 | Copyright (c) 2017 Jeremy Koch 76 | 77 | http://jerkoch.com 78 | 79 | Permission is hereby granted, free of charge, to any person obtaining a copy 80 | of this software and associated documentation files (the "Software"), to deal 81 | in the Software without restriction, including without limitation the rights 82 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 83 | copies of the Software, and to permit persons to whom the Software is 84 | furnished to do so, subject to the following conditions: 85 | 86 | The above copyright notice and this permission notice shall be included in all 87 | copies or substantial portions of the Software. 88 | 89 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 90 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 91 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 92 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 93 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 94 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 95 | SOFTWARE. 96 | License 97 | MIT 98 | Title 99 | SwipeCellKit 100 | Type 101 | PSGroupSpecifier 102 | 103 | 104 | FooterText 105 | Generated by CocoaPods - https://cocoapods.org 106 | Title 107 | 108 | Type 109 | PSGroupSpecifier 110 | 111 | 112 | StringsTable 113 | Acknowledgements 114 | Title 115 | Acknowledgements 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_SwiftyComments_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_SwiftyComments_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 5 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 6 | 7 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 8 | 9 | # Used as a return value for each invocation of `strip_invalid_archs` function. 10 | STRIP_BINARY_RETVAL=0 11 | 12 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 13 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 14 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 15 | 16 | # Copies and strips a vendored framework 17 | install_framework() 18 | { 19 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 20 | local source="${BUILT_PRODUCTS_DIR}/$1" 21 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 22 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 23 | elif [ -r "$1" ]; then 24 | local source="$1" 25 | fi 26 | 27 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 28 | 29 | if [ -L "${source}" ]; then 30 | echo "Symlinked..." 31 | source="$(readlink "${source}")" 32 | fi 33 | 34 | # Use filter instead of exclude so missing patterns don't throw errors. 35 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 36 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 37 | 38 | local basename 39 | basename="$(basename -s .framework "$1")" 40 | binary="${destination}/${basename}.framework/${basename}" 41 | if ! [ -r "$binary" ]; then 42 | binary="${destination}/${basename}" 43 | fi 44 | 45 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 46 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 47 | strip_invalid_archs "$binary" 48 | fi 49 | 50 | # Resign the code if required by the build settings to avoid unstable apps 51 | code_sign_if_enabled "${destination}/$(basename "$1")" 52 | 53 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 54 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 55 | local swift_runtime_libs 56 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 57 | for lib in $swift_runtime_libs; do 58 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 59 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 60 | code_sign_if_enabled "${destination}/${lib}" 61 | done 62 | fi 63 | } 64 | 65 | # Copies and strips a vendored dSYM 66 | install_dsym() { 67 | local source="$1" 68 | if [ -r "$source" ]; then 69 | # Copy the dSYM into a the targets temp dir. 70 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 71 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 72 | 73 | local basename 74 | basename="$(basename -s .framework.dSYM "$source")" 75 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 76 | 77 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 78 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 79 | strip_invalid_archs "$binary" 80 | fi 81 | 82 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 83 | # Move the stripped file into its final destination. 84 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 85 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 86 | else 87 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 88 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 89 | fi 90 | fi 91 | } 92 | 93 | # Signs a framework with the provided identity 94 | code_sign_if_enabled() { 95 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 96 | # Use the current code_sign_identitiy 97 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 98 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" 99 | 100 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 101 | code_sign_cmd="$code_sign_cmd &" 102 | fi 103 | echo "$code_sign_cmd" 104 | eval "$code_sign_cmd" 105 | fi 106 | } 107 | 108 | # Strip invalid architectures 109 | strip_invalid_archs() { 110 | binary="$1" 111 | # Get architectures for current target binary 112 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 113 | # Intersect them with the architectures we are building for 114 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 115 | # If there are no archs supported by this binary then warn the user 116 | if [[ -z "$intersected_archs" ]]; then 117 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 118 | STRIP_BINARY_RETVAL=0 119 | return 120 | fi 121 | stripped="" 122 | for arch in $binary_archs; do 123 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 124 | # Strip non-valid architectures in-place 125 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 126 | stripped="$stripped $arch" 127 | fi 128 | done 129 | if [[ "$stripped" ]]; then 130 | echo "Stripped $binary of architectures:$stripped" 131 | fi 132 | STRIP_BINARY_RETVAL=1 133 | } 134 | 135 | 136 | if [[ "$CONFIGURATION" == "Debug" ]]; then 137 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftScanner/SwiftScanner.framework" 138 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftyComments/SwiftyComments.framework" 139 | install_framework "${BUILT_PRODUCTS_DIR}/SwipeCellKit/SwipeCellKit.framework" 140 | fi 141 | if [[ "$CONFIGURATION" == "Release" ]]; then 142 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftScanner/SwiftScanner.framework" 143 | install_framework "${BUILT_PRODUCTS_DIR}/SwiftyComments/SwiftyComments.framework" 144 | install_framework "${BUILT_PRODUCTS_DIR}/SwipeCellKit/SwipeCellKit.framework" 145 | fi 146 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 147 | wait 148 | fi 149 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 12 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 13 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 14 | 15 | case "${TARGETED_DEVICE_FAMILY}" in 16 | 1,2) 17 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 18 | ;; 19 | 1) 20 | TARGET_DEVICE_ARGS="--target-device iphone" 21 | ;; 22 | 2) 23 | TARGET_DEVICE_ARGS="--target-device ipad" 24 | ;; 25 | 3) 26 | TARGET_DEVICE_ARGS="--target-device tv" 27 | ;; 28 | 4) 29 | TARGET_DEVICE_ARGS="--target-device watch" 30 | ;; 31 | *) 32 | TARGET_DEVICE_ARGS="--target-device mac" 33 | ;; 34 | esac 35 | 36 | install_resource() 37 | { 38 | if [[ "$1" = /* ]] ; then 39 | RESOURCE_PATH="$1" 40 | else 41 | RESOURCE_PATH="${PODS_ROOT}/$1" 42 | fi 43 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 44 | cat << EOM 45 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 46 | EOM 47 | exit 1 48 | fi 49 | case $RESOURCE_PATH in 50 | *.storyboard) 51 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 52 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 53 | ;; 54 | *.xib) 55 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 56 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 57 | ;; 58 | *.framework) 59 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 60 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 61 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 62 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 63 | ;; 64 | *.xcdatamodel) 65 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 66 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 67 | ;; 68 | *.xcdatamodeld) 69 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 70 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 71 | ;; 72 | *.xcmappingmodel) 73 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 74 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 75 | ;; 76 | *.xcassets) 77 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 78 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 79 | ;; 80 | *) 81 | echo "$RESOURCE_PATH" || true 82 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 83 | ;; 84 | esac 85 | } 86 | 87 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 88 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 89 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 90 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 91 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 92 | fi 93 | rm -f "$RESOURCES_TO_COPY" 94 | 95 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 96 | then 97 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 98 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 99 | while read line; do 100 | if [[ $line != "${PODS_ROOT}*" ]]; then 101 | XCASSET_FILES+=("$line") 102 | fi 103 | done <<<"$OTHER_XCASSETS" 104 | 105 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 106 | fi 107 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_SwiftyComments_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_SwiftyComments_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftScanner" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyComments" "${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwiftScanner/SwiftScanner.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyComments/SwiftyComments.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit/SwipeCellKit.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "SwiftScanner" -framework "SwiftyComments" -framework "SwipeCellKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_SwiftyComments_Example { 2 | umbrella header "Pods-SwiftyComments_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SwiftyComments_Example/Pods-SwiftyComments_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwiftScanner" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyComments" "${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwiftScanner/SwiftScanner.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyComments/SwiftyComments.framework/Headers" -iquote "${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit/SwipeCellKit.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "SwiftScanner" -framework "SwiftyComments" -framework "SwipeCellKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/SwiftScanner-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SwiftScanner : NSObject 3 | @end 4 | @implementation PodsDummy_SwiftScanner 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/SwiftScanner-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/SwiftScanner-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SwiftScannerVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SwiftScannerVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/SwiftScanner.modulemap: -------------------------------------------------------------------------------- 1 | framework module SwiftScanner { 2 | umbrella header "SwiftScanner-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftScanner/SwiftScanner.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftScanner 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftScanner 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/SwiftyComments-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SwiftyComments : NSObject 3 | @end 4 | @implementation PodsDummy_SwiftyComments 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/SwiftyComments-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/SwiftyComments-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SwiftyCommentsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SwiftyCommentsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/SwiftyComments.modulemap: -------------------------------------------------------------------------------- 1 | framework module SwiftyComments { 2 | umbrella header "SwiftyComments-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwiftyComments/SwiftyComments.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftyComments 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 5 | OTHER_LDFLAGS = -framework "UIKit" 6 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_ROOT = ${SRCROOT} 10 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.6.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/SwipeCellKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SwipeCellKit : NSObject 3 | @end 4 | @implementation PodsDummy_SwipeCellKit 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/SwipeCellKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/SwipeCellKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SwipeCellKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SwipeCellKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/SwipeCellKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module SwipeCellKit { 2 | umbrella header "SwipeCellKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SwipeCellKit/SwipeCellKit.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwipeCellKit 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwipeCellKit 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Example/SwiftyComments.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/SwiftyComments.xcodeproj/xcshareddata/xcschemes/SwiftyComments-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/SwiftyComments.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/SwiftyComments.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SwiftyComments/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyComments 4 | // 5 | // Created by tsucres on 10/09/2017. 6 | // Copyright (c) 2017 tsucres. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.makeKeyAndVisible() 20 | let navController = UINavigationController(rootViewController: DemosTableViewController(style: .plain)) 21 | window?.rootViewController = navController 22 | 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // 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. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // 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. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | // 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. 42 | } 43 | 44 | func applicationWillTerminate(_ application: UIApplication) { 45 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Example/SwiftyComments/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Example/SwiftyComments/DemosTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemosTableViewController.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 1/10/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DemosTableViewController: UITableViewController { 12 | private let demoNames = ["Basic", "Imgur", "Reddit", "Hackernews", "Hackernews - foldable", "Imgur - Full expanded", "Reddit - foldable"] 13 | 14 | private let controllerClasses = [SimpleCommentsViewController.self, 15 | ImgurCommentsViewController.self, 16 | RedditCommentsViewController.self, 17 | HNCommentsViewController.self, 18 | FoldableHNCommentsViewController.self, 19 | FullyExpandedImgurVC.self, 20 | FoldableRedditCommentsViewController.self] 21 | 22 | private var defaultNavBarTintColor: UIColor? 23 | private var defaultNavTintColor: UIColor? 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | self.defaultNavBarTintColor = self.navigationController?.navigationBar.barTintColor 27 | self.defaultNavTintColor = self.navigationController?.navigationBar.tintColor 28 | 29 | } 30 | 31 | override func viewWillAppear(_ animated: Bool) { 32 | super.viewWillAppear(animated) 33 | if #available(iOS 13.0, *) { 34 | self.navigationController?.navigationBar.barTintColor = .systemBackground 35 | self.navigationController?.navigationBar.tintColor = .label 36 | } else { 37 | self.navigationController?.navigationBar.barTintColor = self.defaultNavBarTintColor ?? .white 38 | self.navigationController?.navigationBar.tintColor = self.defaultNavTintColor ?? .black 39 | } 40 | 41 | } 42 | 43 | override var preferredStatusBarStyle: UIStatusBarStyle { 44 | return .default 45 | } 46 | 47 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 48 | self.navigationController!.pushViewController(controllerClasses[indexPath.row].init(), animated: true) 49 | } 50 | 51 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 52 | return controllerClasses.count 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 56 | let cellId = "cellId" 57 | var cell = tableView.dequeueReusableCell(withIdentifier: cellId) 58 | if cell == nil { 59 | cell = UITableViewCell(style: .default, reuseIdentifier: cellId) 60 | } 61 | cell!.textLabel?.text = demoNames[indexPath.row] 62 | return cell! 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/SwiftyComments/HNCommentContentParser.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | import SwiftScanner 4 | 5 | // ========================== 6 | // Adaptation of https://github.com/malcommac/SwiftRichString/blob/master/Sources/SwiftRichString/MarkupString.swift 7 | // ========================== 8 | 9 | class HNCommentContentParser { 10 | static let htmlEntities = ["quot":"\"","amp":"&","apos":"'","lt":"<","gt":">", "#x2F":"/", "#38":"&", "#62":">", "#x27":"'", "#60":"<"] 11 | 12 | /// Turns the html of a comment from Hackernews to an AttributedString 13 | /// valid html = https://news.ycombinator.com/formatdoc 14 | public static func buildAttributedText(From markup: String, textColor: UIColor = .white, fontSize: CGFloat = 14, linkColor: UIColor = .orange) -> NSAttributedString? { 15 | guard let (text, tags) = try? parse(markup) else { 16 | return nil 17 | } 18 | 19 | 20 | 21 | let attributedText = NSMutableAttributedString(string: text) 22 | let font = UIFont.systemFont(ofSize: fontSize, weight: .regular) 23 | let paragraphStyle = NSMutableParagraphStyle() 24 | paragraphStyle.paragraphSpacing = 0.4*font.lineHeight 25 | paragraphStyle.lineSpacing = 0.1*font.lineHeight 26 | attributedText.addAttributes([.font: font, 27 | .foregroundColor: textColor, 28 | NSAttributedString.Key.paragraphStyle: paragraphStyle 29 | ], range: NSRange.init(location: 0, length: text.count)) 30 | for tag in tags { 31 | if tag.name == "a" { 32 | let initialLen = text.count 33 | var aContent = String(text.dropFirst(tag.range!.lowerBound)) 34 | aContent = String(aContent.dropLast(initialLen - tag.range!.upperBound)) 35 | let attributes: [NSAttributedString.Key: Any] = [.link: aContent, .underlineStyle: NSUnderlineStyle.single.rawValue, .foregroundColor: linkColor] 36 | attributedText.addAttributes(attributes, range: NSRange(tag.range!)) 37 | } else if tag.name == "code" { 38 | let attributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Courier", size: 14)! ] 39 | attributedText.addAttributes(attributes, range: NSRange(tag.range!)) 40 | } else if tag.name == "i" { 41 | let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.italicSystemFont(ofSize: 14)] 42 | attributedText.addAttributes(attributes, range: NSRange(tag.range!)) 43 | } else if tag.name == "h1" { // for AskHN only 44 | let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 18)] 45 | attributedText.addAttributes(attributes, range: NSRange(tag.range!)) 46 | } 47 | 48 | } 49 | return NSAttributedString(attributedString: attributedText) 50 | } 51 | 52 | private static func parse(_ content: String) throws -> (text: String, tags: [Tag]) { 53 | let scanner = StringScanner(content) 54 | var tagStacks: [Tag] = [] // temporary stack 55 | var tagsList: [Tag] = [] // final stack with all found tags 56 | 57 | var plainText = String() 58 | while !scanner.isAtEnd { 59 | // scan text and accumulate it until we found a special entity (starting with &) or an open tag character (<) 60 | if let textString = try scanner.scan(upTo: CharacterSet(charactersIn: "<&")) { 61 | plainText += textString 62 | } else { 63 | // We have encountered a special entity or an open/close tag 64 | if scanner.match("&") == true { 65 | // It's a special entity so get it until the end (; character) and replace it with encoded char 66 | if let entityValue = try scanner.scan(upTo: ";") { 67 | if let spec = htmlEntities[entityValue] { 68 | plainText += spec 69 | } 70 | try scanner.skip() 71 | } 72 | continue 73 | } else if scanner.match("<") == true { 74 | let rawTag = try scanner.scan(upTo: ">") 75 | 76 | if var tag = Tag(raw: rawTag) { 77 | if tag.name == "p" { // it's a return carriage, we want to translate it directly 78 | plainText += "\n" 79 | continue 80 | } 81 | let endIndex = plainText.count 82 | if tag.isOpenTag == true { 83 | // it's an open tag, store the start index 84 | // (the upperbund is temporary the same of the lower bound, we will update it 85 | // at the end, before adding it to the list of the tags) 86 | tag.range = endIndex..? 114 | /// true if tag represent an open tag 115 | fileprivate(set) var isOpenTag: Bool 116 | /// the content of the href attribute (only if the tag name is 'a' 117 | public var href: String? 118 | 119 | public init?(raw content: String?) { 120 | guard let content = content else { 121 | return nil 122 | } 123 | // Read tag name 124 | let tagScanner = StringScanner(content) 125 | do { 126 | self.isOpenTag = (tagScanner.match("/") == false) 127 | guard let name = try tagScanner.scan(untilIn: CharacterSet.alphanumerics) else { 128 | return nil 129 | } 130 | self.name = name 131 | if self.name == "a" { 132 | let linkPattern = "href=\\\"(.*)\\\"" // #bestURLRegexEver 133 | let linkRegex = try! NSRegularExpression(pattern: linkPattern, options: []) 134 | let matches = linkRegex.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) 135 | if matches.count > 0 { 136 | let linkMatch = matches[0] 137 | if linkMatch.numberOfRanges == 2 { 138 | let startIndex = String.Index(encodedOffset: linkMatch.range(at: 1).lowerBound) 139 | let endIndex = String.Index(encodedOffset: linkMatch.range(at: 1).upperBound) 140 | self.href = String(content[startIndex.. CommentCell { 88 | let commentCell = tableView.dequeueReusableCell(withIdentifier: commentCellId) as! HNCommentCell 89 | let comment = commentModel as! AttributedTextComment 90 | commentCell.level = comment.level 91 | commentCell.commentContentAttributed = comment.attributedContent 92 | commentCell.posterName = comment.posterName 93 | commentCell.date = comment.soMuchTimeAgo() 94 | commentCell.nReplies = comment.replies.count 95 | commentCell.upvoted = comment.upvoted 96 | 97 | commentCell.content.upvoteBtn.addTarget(self, action: #selector(upvoted), for: .touchUpInside) 98 | commentCell.content.upvoteBtn.tag = indexPath.row 99 | commentCell.content.replyBtn.addTarget(self, action: #selector(replied), for: .touchUpInside) 100 | commentCell.content.replyBtn.tag = indexPath.row 101 | 102 | commentCell.delegate = nil 103 | commentCell.isFolded = comment.isFolded && !isCellExpanded(indexPath: indexPath) 104 | // =========================================== 105 | // If you want to experiment loading the attributedText with DocumentType.html 106 | // uncoment the following 107 | /* 108 | if let data = comment.body!.data(using: String.Encoding.utf8, allowLossyConversion: true) { 109 | commentCell.commentContentAttributed = try? NSAttributedString(data: data, 110 | options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], 111 | documentAttributes: nil) 112 | } 113 | */ 114 | // =========================================== 115 | 116 | 117 | // Enable the shouldInterractWithURL 118 | //commentCell.content.bodyView.delegate = self 119 | 120 | return commentCell 121 | } 122 | 123 | @objc func upvoted(sender: UIButton) { 124 | print("Upvote comment at index \(sender.tag) of currentlyDisplayed") 125 | } 126 | @objc func replied(sender: UIButton) { 127 | print("Reply to comment at index \(sender.tag) of currentlyDisplayed") 128 | } 129 | 130 | // What to do when the user taps on a link in the comment 131 | /*func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { 132 | let startPos = textView.position(from: textView.beginningOfDocument, in: .right, offset: characterRange.location) 133 | let endPos = textView.position(from: textView.beginningOfDocument, in: .right, offset: characterRange.location + characterRange.length) 134 | 135 | return true 136 | }*/ 137 | 138 | } 139 | -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.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" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/HNRespond.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "HNRespond.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/HNRespond.imageset/HNRespond.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/HNRespond.imageset/HNRespond.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/downvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "downvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/downvte.imageset/downvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/downvte.imageset/downvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/exprt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "exprt.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/exprt.imageset/exprt.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/exprt.imageset/exprt.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/hadDownvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "hadDownvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/hadDownvte.imageset/hadDownvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/hadDownvte.imageset/hadDownvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/hadUpvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "hadUpvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/hadUpvte.imageset/hadUpvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/hadUpvte.imageset/hadUpvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/imgurDownvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "imgurDownvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/imgurDownvte.imageset/imgurDownvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/imgurDownvte.imageset/imgurDownvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/imgurUpvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "imgurUpvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/imgurUpvte.imageset/imgurUpvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/imgurUpvte.imageset/imgurUpvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/mre.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mre.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/mre.imageset/mre.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/mre.imageset/mre.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/upvte.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "upvte.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/SwiftyComments/Images.xcassets/upvte.imageset/upvte.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Example/SwiftyComments/Images.xcassets/upvte.imageset/upvte.pdf -------------------------------------------------------------------------------- /Example/SwiftyComments/ImgurCommentsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImgurCommentsViewController.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 27/06/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyComments 11 | 12 | class FullyExpandedImgurVC: ImgurCommentsViewController { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.fullyExpanded = true 16 | } 17 | } 18 | class ImgurCommentsViewController: CommentsViewController { 19 | private let commentCellId = "imgurComentCellId" 20 | var allComments: [RichComment] = [] // All the comments (nested, not in a linear format) 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | tableView.register(ImgurCommentCell.self, forCellReuseIdentifier: commentCellId) 25 | 26 | tableView.backgroundColor = ImgurConstants.backgroundColor 27 | 28 | allComments = RandomDiscussion.generate().comments 29 | 30 | //linearizeComments(comments: allComments, linearizedComment: ¤tlyDisplayed) 31 | 32 | currentlyDisplayed = allComments 33 | 34 | } 35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> CommentCell { 36 | let commentCell = tableView.dequeueReusableCell(withIdentifier: commentCellId, for: indexPath) as! ImgurCommentCell 37 | let comment = currentlyDisplayed[indexPath.row] as! RichComment 38 | commentCell.level = comment.level 39 | commentCell.commentContent = comment.body 40 | commentCell.posterName = comment.posterName 41 | commentCell.date = comment.soMuchTimeAgo() 42 | commentCell.upvotes = comment.upvotes ?? 0 43 | commentCell.downvotes = comment.downvotes ?? 0 44 | commentCell.nReplies = comment.replies.count 45 | 46 | commentCell.content.upvoteButton.tag = indexPath.row 47 | commentCell.content.upvoteButton.addTarget(self, action: #selector(upvoted(sender:)), for: .touchUpInside) 48 | 49 | commentCell.content.downvoteButton.tag = indexPath.row 50 | commentCell.content.downvoteButton.addTarget(self, action: #selector(downvoted(sender:)), for: .touchUpInside) 51 | 52 | 53 | commentCell.upvoted = comment.upvoted 54 | commentCell.downvoted = comment.downvoted 55 | return commentCell 56 | } 57 | 58 | @objc func upvoted(sender: UIButton) { 59 | let indexPath = IndexPath(row: sender.tag, section: 0) 60 | if let cell = tableView.cellForRow(at: indexPath) as? ImgurCommentCell { 61 | let intendToUpvote = !cell.upvoted 62 | cell.upvoted = intendToUpvote 63 | if cell.upvotes != nil { 64 | cell.upvotes! += Int((intendToUpvote ? 1 : -1)) 65 | } 66 | if cell.downvoted { 67 | cell.downvoted = false 68 | cell.downvotes! -= 1 69 | } 70 | 71 | if let comment = currentlyDisplayed[sender.tag] as? RichComment { 72 | comment.upvoted = intendToUpvote 73 | if comment.upvotes != nil { 74 | comment.upvotes! += Int((intendToUpvote ? 1 : -1)) 75 | } 76 | if comment.downvoted { 77 | comment.downvoted = false 78 | comment.downvotes! -= 1 79 | } 80 | } 81 | } 82 | } 83 | @objc func downvoted(sender: UIButton) { 84 | let indexPath = IndexPath(row: sender.tag, section: 0) 85 | if let cell = tableView.cellForRow(at: indexPath) as? ImgurCommentCell { 86 | let intendToDownvote = !cell.downvoted 87 | cell.downvoted = intendToDownvote 88 | if cell.downvotes != nil { 89 | cell.downvotes! += Int((intendToDownvote ? 1 : -1)) 90 | } 91 | if cell.upvoted { 92 | cell.upvoted = false 93 | cell.upvotes! -= 1 94 | } 95 | if let comment = currentlyDisplayed[sender.tag] as? RichComment { 96 | comment.downvoted = intendToDownvote 97 | if comment.downvotes != nil { 98 | comment.downvotes! += Int((intendToDownvote ? 1 : -1)) 99 | } 100 | if comment.upvoted { 101 | comment.upvoted = false 102 | comment.upvotes! -= 1 103 | } 104 | } 105 | } 106 | } 107 | 108 | 109 | 110 | 111 | override func viewWillAppear(_ animated: Bool) { 112 | super.viewWillAppear(animated) 113 | self.navigationController?.navigationBar.barTintColor = ImgurConstants.backgroundColor 114 | self.navigationController?.navigationBar.tintColor = ImgurConstants.posterColor 115 | } 116 | 117 | override var preferredStatusBarStyle: UIStatusBarStyle { 118 | return .lightContent 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/SwiftyComments/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIViewControllerBasedStatusBarAppearance 6 | 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/SwiftyComments/MockupData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockupData.swift 3 | // SwiftyComments_Example 4 | // 5 | // Created by Stéphane Sercu on 9/10/17. 6 | // Copyright © 2017 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftyComments 11 | 12 | class RandomDiscussion { 13 | var comments: [AttributedTextComment]! = [] 14 | 15 | 16 | /** 17 | Generate a random list of comments 18 | - note: the postedDate of the replies may be anterior to the comment they are a reply to, and you know what? No one cares. 19 | - parameters: 20 | - size: number of root comments 21 | - maximumChildren: maximum number of replies to a comment. This number is randomly chosen. 22 | */ 23 | static func generate(size: Int = 10, maximumChildren: Int = 4) -> RandomDiscussion { 24 | let discussion = RandomDiscussion() 25 | for _ in 0 ..< size { 26 | var rootComment = randomComent() 27 | addReplyRecurs(&rootComment, maximumChildren: maximumChildren) 28 | discussion.comments.append(rootComment) 29 | } 30 | return discussion 31 | } 32 | 33 | /** 34 | Generate a random, lipsum-filled, comment. 35 | */ 36 | static func randomComent() -> AttributedTextComment { 37 | let com = AttributedTextComment() 38 | com.id = Int(arc4random_uniform(100000)) + 1 39 | com.body = Lorem.sentences(Int(arc4random_uniform(4)) + 1) 40 | com.posterName = Lorem.word 41 | com.postedDate = Double(arc4random_uniform(UInt32(1483228800 - 1262304000)-1)) + 1262304000 42 | com.upvotes = Int(arc4random_uniform(500)) + 1 43 | com.downvotes = Int(arc4random_uniform(100)) + 1 44 | com.title = Lorem.sentence 45 | return com 46 | } 47 | 48 | /** 49 | Recursively add a random number of replies to parent. 50 | At each recursion, the maximum number of children is 51 | decreased by 1 until it reaches 0. 52 | */ 53 | private static func addReplyRecurs( _ parent: inout AttributedTextComment, maximumChildren: Int) { 54 | if maximumChildren == 0 { return } 55 | for _ in 0..<(Int(arc4random_uniform(UInt32(maximumChildren-1))) + 1) { 56 | var com = randomComent() 57 | parent.addReply(com) 58 | com.replyTo = parent 59 | com.level = parent.level+1 60 | addReplyRecurs(&com, maximumChildren: maximumChildren-1) 61 | } 62 | 63 | } 64 | 65 | 66 | } 67 | 68 | /// Model of a comment with attributedText content. 69 | class AttributedTextComment: RichComment { 70 | var attributedContent: NSAttributedString? 71 | } 72 | 73 | 74 | /** 75 | This class models a comment with all the most 76 | common attributes in the commenting systems. 77 | It's used as an exemple through the implemented 78 | commenting systems. 79 | **/ 80 | class RichComment: BaseComment { 81 | var id: Int? 82 | var upvotes: Int? 83 | var downvotes: Int? 84 | var body: String? 85 | var title: String? 86 | var posterName: String? 87 | var postedDate: Double? // epochtime (since 1970) 88 | var upvoted: Bool = false 89 | var downvoted: Bool = false 90 | var isFolded: Bool = false 91 | 92 | /** 93 | Express the postedDate in following format: "[x] [time period] ago" 94 | */ 95 | func soMuchTimeAgo() -> String? { 96 | if self.postedDate == nil { 97 | return nil 98 | } 99 | let diff = Date().timeIntervalSince1970 - self.postedDate! 100 | var str: String = "" 101 | if diff < 60 { 102 | str = "now" 103 | } else if diff < 3600 { 104 | let out = Int(round(diff/60)) 105 | str = (out == 1 ? "1 minute ago" : "\(out) minutes ago") 106 | } else if diff < 3600 * 24 { 107 | let out = Int(round(diff/3600)) 108 | str = (out == 1 ? "1 hour ago" : "\(out) hours ago") 109 | } else if diff < 3600 * 24 * 2 { 110 | str = "yesterday" 111 | } else if diff < 3600 * 24 * 7 { 112 | let out = Int(round(diff/(3600*24))) 113 | str = (out == 1 ? "1 day ago" : "\(out) days ago") 114 | } else if diff < 3600 * 24 * 7 * 4{ 115 | let out = Int(round(diff/(3600*24*7))) 116 | str = (out == 1 ? "1 week ago" : "\(out) weeks ago") 117 | } else if diff < 3600 * 24 * 7 * 4 * 12{ 118 | let out = Int(round(diff/(3600*24*7*4))) 119 | str = (out == 1 ? "1 month ago" : "\(out) months ago") 120 | } else {//if diff < 3600 * 24 * 7 * 4 * 12{ 121 | let out = Int(round(diff/(3600*24*7*4*12))) 122 | str = (out == 1 ? "1 year ago" : "\(out) years ago") 123 | } 124 | return str 125 | } 126 | } 127 | 128 | class BaseComment: AbstractComment { 129 | var replies: [AbstractComment]! = [] 130 | var level: Int! 131 | weak var replyTo: AbstractComment? 132 | 133 | convenience init() { 134 | self.init(level: 0, replyTo: nil) 135 | } 136 | init(level: Int, replyTo: BaseComment?) { 137 | self.level = level 138 | self.replyTo = replyTo 139 | } 140 | func addReply(_ reply: BaseComment) { 141 | self.replies.append(reply) 142 | } 143 | 144 | } 145 | 146 | -------------------------------------------------------------------------------- /Example/SwiftyComments/RedditCommentsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedditCommentsViewController.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 27/06/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyComments 11 | 12 | class FoldableRedditCommentsViewController: RedditCommentsViewController, CommentsViewDelegate { 13 | 14 | func commentCellExpanded(atIndex index: Int) { 15 | updateCellFoldState(false, atIndex: index) 16 | } 17 | 18 | func commentCellFolded(atIndex index: Int) { 19 | updateCellFoldState(true, atIndex: index) 20 | } 21 | 22 | private func updateCellFoldState(_ folded: Bool, atIndex index: Int) { 23 | let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) as! RedditCommentCell 24 | cell.animateIsFolded(fold: folded) 25 | (self.currentlyDisplayed[index] as! RichComment).isFolded = folded 26 | self.tableView.beginUpdates() 27 | self.tableView.endUpdates() 28 | } 29 | 30 | override func viewDidLoad() { 31 | self.fullyExpanded = true 32 | super.viewDidLoad() 33 | self.delegate = self 34 | 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 38 | let selectedCom: AbstractComment = currentlyDisplayed[indexPath.row] 39 | let selectedIndex = indexPath.row 40 | 41 | // Enable cell folding for comments without replies 42 | if selectedCom.replies.count == 0 { 43 | if (selectedCom as! RichComment).isFolded { 44 | commentCellExpanded(atIndex: selectedIndex) 45 | } else { 46 | commentCellFolded(atIndex: selectedIndex) 47 | } 48 | } else { 49 | super.tableView(tableView, didSelectRowAt: indexPath) 50 | } 51 | } 52 | 53 | 54 | } 55 | class RedditCommentsViewController: CommentsViewController { 56 | private let commentCellId = "redditComentCellId" 57 | var allComments: [RichComment] = [] // All the comments (nested, not in a linear format) 58 | 59 | override func viewDidLoad() { 60 | super.viewDidLoad() 61 | tableView.register(RedditCommentCell.self, forCellReuseIdentifier: commentCellId) 62 | 63 | tableView.backgroundColor = RedditConstants.backgroundColor 64 | 65 | allComments = RandomDiscussion.generate().comments 66 | 67 | currentlyDisplayed = allComments 68 | 69 | self.swipeToHide = true 70 | self.swipeActionAppearance.swipeActionColor = RedditConstants.flashyColor 71 | } 72 | 73 | override open func commentsView(_ tableView: UITableView, commentCellForModel commentModel: AbstractComment, atIndexPath indexPath: IndexPath) -> CommentCell { 74 | let commentCell = tableView.dequeueReusableCell(withIdentifier: commentCellId, for: indexPath) as! RedditCommentCell 75 | let comment = currentlyDisplayed[indexPath.row] as! RichComment 76 | commentCell.level = comment.level 77 | commentCell.commentContent = comment.body 78 | commentCell.posterName = comment.posterName 79 | commentCell.date = comment.soMuchTimeAgo() 80 | commentCell.upvotes = comment.upvotes 81 | commentCell.isFolded = comment.isFolded && !isCellExpanded(indexPath: indexPath) 82 | return commentCell 83 | } 84 | 85 | override func viewWillAppear(_ animated: Bool) { 86 | super.viewWillAppear(animated) 87 | self.navigationController?.navigationBar.barTintColor = RedditConstants.flashyColor 88 | self.navigationController?.navigationBar.tintColor = .white 89 | } 90 | 91 | override var preferredStatusBarStyle: UIStatusBarStyle { 92 | return .lightContent 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Example/SwiftyComments/SimpleCommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCommentView.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 24/04/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyComments 11 | 12 | class SimpleCommentCell: CommentCell { 13 | var content:SimpleCommentView { 14 | get { 15 | return self.commentViewContent as! SimpleCommentView 16 | } 17 | } 18 | open var commentContent: String! = "content" { 19 | didSet { 20 | (self.commentViewContent as! SimpleCommentView).commentContent = commentContent 21 | } 22 | } 23 | open var posterName: String! { 24 | get { 25 | return self.content.posterName 26 | } set(value) { 27 | self.content.posterName = value 28 | } 29 | } 30 | 31 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 32 | super.init(style: style, reuseIdentifier: reuseIdentifier) 33 | self.commentViewContent = SimpleCommentView() 34 | 35 | 36 | self.rootCommentMarginColor = #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1) 37 | self.rootCommentMargin = 20 38 | self.commentMarginColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1) 39 | self.commentMargin = 10 40 | self.indentationColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1) 41 | self.indentationIndicatorColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1) 42 | self.indentationIndicatorThickness = 5 43 | self.indentationUnit = 40 44 | 45 | self.commentViewContent?.backgroundColor = .gray 46 | 47 | } 48 | required init?(coder aDecoder: NSCoder) { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | } 52 | 53 | class SimpleCommentView: UIView { 54 | open var commentContent: String! = "content" { 55 | didSet { 56 | contentLabel.text = commentContent 57 | } 58 | } 59 | open var posterName: String! = "username" { 60 | didSet { 61 | posterLabel.text = posterName 62 | } 63 | } 64 | 65 | 66 | convenience init() { 67 | self.init(frame: CGRect.zero) 68 | } 69 | override init(frame: CGRect) { 70 | super.init(frame: frame) 71 | setLayout() 72 | } 73 | 74 | required init?(coder aDecoder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | private func setLayout() { 79 | let margin: CGFloat = 10 80 | 81 | addSubview(posterLabel) 82 | posterLabel.translatesAutoresizingMaskIntoConstraints = false 83 | posterLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: margin).isActive = true 84 | posterLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: margin).isActive = true 85 | 86 | addSubview(contentLabel) 87 | contentLabel.translatesAutoresizingMaskIntoConstraints = false 88 | contentLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: margin).isActive = true 89 | contentLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -margin).isActive = true 90 | contentLabel.topAnchor.constraint(equalTo: posterLabel.bottomAnchor).isActive = true 91 | 92 | 93 | addSubview(controlView) 94 | controlView.translatesAutoresizingMaskIntoConstraints = false 95 | controlView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -margin).isActive = true 96 | controlView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor).isActive = true 97 | controlView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true 98 | 99 | } 100 | var contentLabel: UILabel = { 101 | let lbl = UILabel() 102 | lbl.text = "No content" 103 | lbl.textColor = UIColor.black 104 | lbl.lineBreakMode = .byWordWrapping 105 | lbl.font = UIFont.systemFont(ofSize: 13, weight: UIFont.Weight.regular) 106 | lbl.numberOfLines = 0 107 | lbl.textAlignment = .left 108 | return lbl 109 | }() 110 | var posterLabel: UILabel = { 111 | let lbl = UILabel() 112 | lbl.text = "annonymous" 113 | lbl.textColor = #colorLiteral(red: 0.6470588235, green: 0.6470588235, blue: 0.6470588235, alpha: 1) 114 | lbl.font = UIFont.systemFont(ofSize: 11, weight: UIFont.Weight.regular) 115 | lbl.textAlignment = .left 116 | return lbl 117 | }() 118 | var controlView: UIView = { 119 | let v = UIView() 120 | let actionBtn = UIButton(type: UIButton.ButtonType.infoDark) 121 | actionBtn.setTitle("Like", for: .normal) 122 | 123 | v.addSubview(actionBtn) 124 | 125 | actionBtn.translatesAutoresizingMaskIntoConstraints = false 126 | 127 | actionBtn.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -10).isActive = true 128 | actionBtn.bottomAnchor.constraint(equalTo: v.bottomAnchor).isActive = true 129 | actionBtn.topAnchor.constraint(equalTo: v.topAnchor).isActive = true 130 | actionBtn.leadingAnchor.constraint(equalTo: v.leadingAnchor).isActive = true 131 | 132 | return v 133 | }() 134 | } 135 | -------------------------------------------------------------------------------- /Example/SwiftyComments/SimpleCommentsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 15/04/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyComments 11 | 12 | /// Simplest implementation, colored to show the different zones in a commentCell. 13 | class SimpleCommentsViewController: CommentsViewController, UITextViewDelegate { 14 | private let commentCellId = "simpleCommentCellId" 15 | var allComments: [RichComment] = [] // All the comments (nested, not in a linear format) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | tableView.register(SimpleCommentCell.self, forCellReuseIdentifier: commentCellId) 20 | 21 | allComments = RandomDiscussion.generate().comments 22 | currentlyDisplayed = allComments 23 | 24 | } 25 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> CommentCell { 26 | let commentCell = tableView.dequeueReusableCell(withIdentifier: commentCellId, for: indexPath) as! SimpleCommentCell 27 | let comment = currentlyDisplayed[indexPath.row] as! RichComment 28 | commentCell.level = comment.level 29 | commentCell.commentContent = comment.body 30 | commentCell.posterName = comment.posterName 31 | return commentCell 32 | } 33 | 34 | } 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Example/SwiftyComments/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyComments 4 | // 5 | // Created by tsucres on 10/09/2017. 6 | // Copyright (c) 2017 tsucres. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Stéphane Sercu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Screenshots/HN.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/HN.mov -------------------------------------------------------------------------------- /Screenshots/HNExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/HNExample.gif -------------------------------------------------------------------------------- /Screenshots/HNExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/HNExample.png -------------------------------------------------------------------------------- /Screenshots/HNSwipe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/HNSwipe.gif -------------------------------------------------------------------------------- /Screenshots/HNSwipe.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/HNSwipe.mov -------------------------------------------------------------------------------- /Screenshots/Imgur.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/Imgur.mov -------------------------------------------------------------------------------- /Screenshots/ImgurExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/ImgurExample.gif -------------------------------------------------------------------------------- /Screenshots/ImgurExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/ImgurExample.png -------------------------------------------------------------------------------- /Screenshots/Reddit.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/Reddit.mov -------------------------------------------------------------------------------- /Screenshots/RedditExample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/RedditExample.gif -------------------------------------------------------------------------------- /Screenshots/RedditExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/RedditExample.png -------------------------------------------------------------------------------- /Screenshots/RedditSwipe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/RedditSwipe.gif -------------------------------------------------------------------------------- /Screenshots/RedditSwipe.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/RedditSwipe.mov -------------------------------------------------------------------------------- /Screenshots/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/schema.png -------------------------------------------------------------------------------- /Screenshots/schema_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/Screenshots/schema_small.png -------------------------------------------------------------------------------- /SwiftyComments.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = 'SwiftyComments' 4 | s.version = '0.2.1' 5 | s.summary = 'UITableView based component designed to display a hierarchy of expandable/foldable comments.' 6 | 7 | s.description = <<-DESC 8 | SwiftyComments is a UITableView based component designed to display a hierarchy of expandable/foldable comments. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/tsucres/SwiftyComments' 12 | s.screenshots = 'https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/ImgurExample.png', 'https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/HNExample.png', "https://github.com/tsucres/SwiftyComments/raw/master/Screenshots/RedditExample.png" 13 | s.license = { :type => 'MIT', :file => 'LICENSE' } 14 | s.author = { 'Stéphane Sercu' => 'stefsercu@gmail.com' } 15 | s.source = { :git => 'https://github.com/tsucres/SwiftyComments.git', :tag => s.version.to_s } 16 | 17 | s.ios.deployment_target = '9.0' 18 | 19 | s.source_files = 'SwiftyComments/Classes/**/*' 20 | s.resources = "SwiftyComments/Assets/*.xcassets" 21 | 22 | s.dependency 'SwipeCellKit', '2.6.0' 23 | 24 | s.frameworks = 'UIKit' 25 | s.swift_version = '4.1' 26 | end 27 | -------------------------------------------------------------------------------- /SwiftyComments/Assets/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftyComments/Assets/Media.xcassets/collapse.imageset/Collapse.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsucres/SwiftyComments/e79768d6376a5494af59eedb207b4ac1dd4ad5e4/SwiftyComments/Assets/Media.xcassets/collapse.imageset/Collapse.pdf -------------------------------------------------------------------------------- /SwiftyComments/Assets/Media.xcassets/collapse.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Collapse.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftyComments/Classes/AbstractComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractComment.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 9/10/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Defines the minimal properties a comment model must have (requiered by the CommentsViewController) 12 | public protocol AbstractComment: class { 13 | var replies: [AbstractComment]! { get set } 14 | var level: Int! { get set } 15 | /*weak*/ var replyTo: AbstractComment? { get set } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftyComments/Classes/CommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentCell.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 24/04/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwipeCellKit 11 | 12 | struct DefaultValues { 13 | static let rootCommentMarginColor = UIColor(red: 247/255, green: 247/255, blue: 245/255, alpha: 1) 14 | static let rootCommentMargin: CGFloat = 10 15 | static let commentMarginColor = UIColor.black 16 | static let commentMargin: CGFloat = 1 17 | static let identationColor = UIColor.black 18 | static let commentBackgroundColor = UIColor.red 19 | static let indentationIndicatorColor = UIColor.gray 20 | static let indentationIndicatorThickness: CGFloat = 1 21 | } 22 | 23 | 24 | /** 25 | This class manages everything relating to the level of the 26 | comment: identations, background colors, spacings, etc... 27 | */ 28 | open class CommentCell: SwipeTableViewCell { 29 | /// Color of the separation between 2 root comments 30 | open var rootCommentMarginColor: UIColor! = DefaultValues.rootCommentMarginColor { 31 | didSet { 32 | updateCommentMargin() 33 | } 34 | } 35 | /// Space between 2 root comments 36 | open var rootCommentMargin: CGFloat! = DefaultValues.rootCommentMargin { 37 | didSet { 38 | updateCommentMargin() 39 | } 40 | } 41 | /// Color of the separation between 2 indented comments 42 | open var commentMarginColor: UIColor! = DefaultValues.commentMarginColor { 43 | didSet { 44 | updateCommentMargin() 45 | } 46 | } 47 | /// Space between 2 indented comments 48 | open var commentMargin: CGFloat! = DefaultValues.commentMargin { 49 | didSet { 50 | updateCommentMargin() 51 | } 52 | } 53 | /// Color of the space above an indented comment 54 | open var indentationColor: UIColor! {//= DefaultValues.identationColor { 55 | get { 56 | return backgroundColor 57 | } set(value) { 58 | backgroundColor = value 59 | } 60 | } 61 | 62 | /// Color of the vertical indentation indicators 63 | open var indentationIndicatorColor: UIColor! = DefaultValues.indentationIndicatorColor { 64 | didSet { 65 | updateIndentationIndicators() 66 | } 67 | } 68 | 69 | /// Thickness of the vertical indentation indicators 70 | open var indentationIndicatorThickness: CGFloat! = CGFloat(DefaultValues.indentationIndicatorThickness) { 71 | didSet { 72 | updateIndentationIndicators() 73 | } 74 | } 75 | 76 | /// Indicates weither the vertical indentation indicators extends to the replies 77 | open var isIndentationIndicatorsExtended: Bool! = false { 78 | didSet { 79 | updateIndentationIndicators() 80 | } 81 | } 82 | 83 | /// Defines the identation per level 84 | open var indentationUnit = 10 { 85 | didSet { 86 | indentationConstraint?.constant = CGFloat(self.level*indentationUnit) 87 | updateIndentationIndicators() 88 | updateCommentMargin() 89 | } 90 | } 91 | 92 | open var level = 0 { 93 | didSet { 94 | contentView.autoresizingMask = UIView.AutoresizingMask.flexibleHeight // solves a warning (http://stackoverflow.com/questions/26100053/uitableviewcells-contentview-gets-unwanted-height-44-constraint) 95 | indentationConstraint?.constant = CGFloat(self.level*indentationUnit) 96 | updateIndentationIndicators() 97 | updateCommentMargin() 98 | } 99 | } 100 | 101 | 102 | 103 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 104 | super.init(style: style, reuseIdentifier: reuseIdentifier) 105 | selectionStyle = .none 106 | indentationColor = DefaultValues.identationColor 107 | setupView() 108 | } 109 | 110 | required public init?(coder aDecoder: NSCoder) { 111 | fatalError("init(coder:) has not been implemented") 112 | } 113 | 114 | 115 | 116 | private func setupView() { 117 | updateIndentationIndicators() 118 | setupCommentMargin() 119 | 120 | contentView.addSubview(commentView) 121 | commentView.translatesAutoresizingMaskIntoConstraints = false 122 | commentView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true 123 | indentationConstraint = commentView.leadingAnchor.constraint( 124 | equalTo: contentView.leadingAnchor, 125 | constant: CGFloat(self.level*indentationUnit)) 126 | indentationConstraint?.isActive = true 127 | commentView.topAnchor.constraint(equalTo: hSeparator.bottomAnchor).isActive = true 128 | commentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true 129 | } 130 | 131 | 132 | private let commentView:UIView! = UIView() 133 | 134 | 135 | /// This is the key element of the class. It's the actual view of a comment. 136 | open var commentViewContent: UIView? { 137 | get { 138 | if self.commentView.subviews.count > 0 { 139 | return self.commentView.subviews[0] 140 | } 141 | return nil 142 | } set(v) { 143 | commentView.subviews.forEach({ $0.removeFromSuperview() }) 144 | if v != nil { 145 | commentView.addSubview(v!) 146 | v!.translatesAutoresizingMaskIntoConstraints = false 147 | v!.trailingAnchor.constraint(equalTo: commentView.trailingAnchor).isActive = true 148 | v!.leadingAnchor.constraint(equalTo: commentView.leadingAnchor).isActive = true 149 | v!.topAnchor.constraint(equalTo: commentView.topAnchor).isActive = true 150 | v!.bottomAnchor.constraint(equalTo: commentView.bottomAnchor).isActive = true 151 | } 152 | } 153 | } 154 | 155 | // Indentation 156 | private var indentationConstraint: NSLayoutConstraint? 157 | 158 | // Vertical indentation indicators 159 | private var vSeparators: [UIView] = [] 160 | private func updateIndentationIndicators() { 161 | // Remove the eventual existing ones 162 | vSeparators.forEach({(body) in 163 | body.removeFromSuperview() 164 | }) 165 | if self.level > 0 { // No indicators for root comments 166 | let n = self.isIndentationIndicatorsExtended! ? self.level : 1 // number of indicators to draw 167 | for i in 1...n { 168 | let sep = UIView() 169 | vSeparators.append(sep) 170 | contentView.addSubview(sep) 171 | sep.translatesAutoresizingMaskIntoConstraints = false 172 | sep.topAnchor.constraint(equalTo: contentView.topAnchor, constant:commentMargin).isActive = true 173 | sep.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true 174 | sep.widthAnchor.constraint(equalToConstant: indentationIndicatorThickness).isActive = true 175 | sep.leadingAnchor.constraint( 176 | equalTo: contentView.leadingAnchor, 177 | constant: CGFloat((self.level-i+1)*indentationUnit)).isActive = true 178 | sep.backgroundColor = indentationIndicatorColor 179 | } 180 | } 181 | } 182 | 183 | // Horizontal separator 184 | private var hSeparator = UIView() 185 | private var hSepHeightConstraint: NSLayoutConstraint? 186 | private var hSepLeadingConstraint: NSLayoutConstraint? 187 | private func setupCommentMargin() { 188 | contentView.addSubview(hSeparator) 189 | hSeparator.translatesAutoresizingMaskIntoConstraints = false 190 | hSeparator.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 191 | hSepHeightConstraint = hSeparator.leadingAnchor.constraint( 192 | equalTo: contentView.leadingAnchor, 193 | constant: CGFloat(self.level*indentationUnit)) 194 | hSepHeightConstraint?.isActive = true 195 | hSeparator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true 196 | 197 | if self.level == 0 { 198 | hSeparator.backgroundColor = self.rootCommentMarginColor 199 | hSepHeightConstraint = hSeparator.heightAnchor.constraint(equalToConstant: self.rootCommentMargin) 200 | 201 | } else { 202 | hSeparator.backgroundColor = self.commentMarginColor 203 | hSepHeightConstraint = hSeparator.heightAnchor.constraint(equalToConstant: self.commentMargin) 204 | } 205 | hSepHeightConstraint?.isActive = true 206 | } 207 | private func updateCommentMargin() { 208 | hSepHeightConstraint?.constant = CGFloat(self.level*indentationUnit) 209 | if self.level == 0 { 210 | hSeparator.backgroundColor = self.rootCommentMarginColor 211 | hSepHeightConstraint?.constant = self.rootCommentMargin 212 | 213 | } else { 214 | hSeparator.backgroundColor = self.commentMarginColor 215 | hSepHeightConstraint?.constant = self.commentMargin 216 | } 217 | } 218 | 219 | } 220 | 221 | -------------------------------------------------------------------------------- /SwiftyComments/Classes/CommentsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentsViewController.swift 3 | // Commenting 4 | // 5 | // Created by Stéphane Sercu on 26/06/17. 6 | // Copyright © 2017 Stéphane Sercu. All rights reserved. 7 | // 8 | import UIKit 9 | import SwipeCellKit 10 | 11 | /// Encapsulates all the visual properties of a SwipeCellKit's SwipeAction 12 | open class SwipeActionAppearance { 13 | open var swipeActionColor: UIColor = #colorLiteral(red: 0.4147516489, green: 0.8618777394, blue: 0.8051461577, alpha: 1) 14 | open var swipeActionHighlightedColor: UIColor = #colorLiteral(red: 0.7551190257, green: 0.8365258574, blue: 0.8273909688, alpha: 1) 15 | 16 | open var swipeActionText: String = "Collapse" 17 | open var swipeActionTextColor: UIColor = .white 18 | open var swipeActionHighlightedTextColor: UIColor = .white 19 | open var swipeActionTextFont: UIFont = UIFont.systemFont(ofSize: 17.0, weight: .bold) 20 | 21 | open var swipeActionImage: UIImage = SwipeActionAppearance.getCollapseImage() 22 | open var swipeActionHighlightedImage: UIImage = SwipeActionAppearance.getCollapseImage() 23 | static func getCollapseImage() -> UIImage { 24 | let bundle = Bundle(for: CommentsViewController.self) 25 | return UIImage(named: "collapse", in: bundle, compatibleWith: nil)! 26 | } 27 | 28 | } 29 | /// ViewController displaying expandable comments. 30 | open class CommentsViewController: UITableViewController, SwipeTableViewCellDelegate { 31 | 32 | /// The list of comments correctly displayed in the tableView (in linearized form) 33 | var _currentlyDisplayed: [AbstractComment] = [] 34 | open var currentlyDisplayed: [AbstractComment] { 35 | get { 36 | return _currentlyDisplayed 37 | } set(value) { 38 | if fullyExpanded { 39 | linearizeComments(comments: value, linearizedComments: &_currentlyDisplayed) 40 | } else { 41 | _currentlyDisplayed = value 42 | } 43 | } 44 | } 45 | 46 | /// If true, when a cell is expanded, the tableView will scroll to make the new cells visible 47 | open var makeExpandedCellsVisible: Bool = true 48 | 49 | /// Enable/Disable the "swipe to hide" gesture 50 | open var swipeToHide: Bool = true 51 | 52 | open var swipeActionAppearance = SwipeActionAppearance() 53 | 54 | open var fullyExpanded: Bool = false { 55 | didSet { 56 | if fullyExpanded { 57 | self.linearizeCurrentlyDisplayedComs() 58 | } 59 | } 60 | } 61 | 62 | open weak var delegate: CommentsViewDelegate? = nil 63 | 64 | deinit { 65 | //print("CommentsVC deinited!") 66 | } 67 | override open func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | // Tableview style 71 | tableView.separatorStyle = UITableViewCell.SeparatorStyle.none 72 | 73 | if #available(iOS 11.0, *) { 74 | tableView.estimatedRowHeight = 0 75 | tableView.estimatedSectionFooterHeight = 0 76 | tableView.estimatedSectionHeaderHeight = 0 77 | } else { 78 | tableView.rowHeight = UITableView.automaticDimension 79 | tableView.estimatedRowHeight = 400.0 80 | } 81 | } 82 | 83 | /** 84 | Helper function that takes a list of root comments and turn it in 85 | a linearized list of all the root + children comments. 86 | - Parameters: 87 | - comments: The input, nested, list of comments 88 | - linearizedComments: a reference to the list that will contain the comments 89 | - sort: a function that is applied recursively on each sub-list of comments 90 | */ 91 | public func linearizeComments(comments: [AbstractComment], linearizedComments: inout [AbstractComment], sort: ((inout [AbstractComment])->Void)? = nil) { 92 | var coms = comments 93 | if sort != nil { 94 | sort!(&coms) 95 | } 96 | for c in coms { 97 | linearizedComments.append(c) 98 | linearizeComments(comments: c.replies, linearizedComments: &linearizedComments, sort: sort) 99 | } 100 | } 101 | /// Linearize the comments in _currentlyDisplayed. 102 | public func linearizeCurrentlyDisplayedComs() { 103 | var linearizedComs: [AbstractComment] = [] 104 | linearizeComments(comments: _currentlyDisplayed, linearizedComments: &linearizedComs) 105 | _currentlyDisplayed = linearizedComs 106 | } 107 | 108 | override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 109 | return currentlyDisplayed.count 110 | } 111 | 112 | override open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 113 | return UITableView.automaticDimension 114 | } 115 | override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 116 | return UITableView.automaticDimension 117 | } 118 | 119 | override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 120 | let selectedCom: AbstractComment = _currentlyDisplayed[indexPath.row] 121 | let selectedIndex = indexPath.row 122 | 123 | if selectedCom.replies.count > 0 { // if expandable 124 | if isCellExpanded(indexPath: indexPath) { 125 | // collapse 126 | var nCellsToDelete = 0 127 | repeat { 128 | nCellsToDelete += 1 129 | } while (_currentlyDisplayed.count > selectedIndex+nCellsToDelete+1 && _currentlyDisplayed[selectedIndex+nCellsToDelete+1].level > selectedCom.level) 130 | 131 | _currentlyDisplayed.removeSubrange(Range(uncheckedBounds: (lower: selectedIndex+1 , upper: selectedIndex+nCellsToDelete+1))) 132 | var indexPaths: [IndexPath] = [] 133 | for i in 0.. UITableViewCell { 162 | let comment = currentlyDisplayed[indexPath.row] 163 | let commentCell = commentsView(tableView, commentCellForModel: comment, atIndexPath: indexPath) 164 | if swipeToHide { 165 | commentCell.delegate = self 166 | } 167 | return commentCell 168 | } 169 | 170 | open func commentsView(_ tableView: UITableView, commentCellForModel commentModel: AbstractComment, atIndexPath indexPath: IndexPath) -> CommentCell { 171 | return CommentCell() 172 | } 173 | 174 | open func isCellExpanded(indexPath: IndexPath) -> Bool { 175 | let com: AbstractComment = _currentlyDisplayed[indexPath.row] 176 | return _currentlyDisplayed.count > indexPath.row+1 && // if not last cell 177 | _currentlyDisplayed[indexPath.row+1].level > com.level // if replies are displayed 178 | } 179 | 180 | open func commentsView(_ tableView: UITableView, isCommentExpandable commentModel: AbstractComment, atIndexPath indexPath: IndexPath) -> Bool { 181 | return swipeToHide && isCellExpanded(indexPath: indexPath) 182 | } 183 | 184 | public func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? { 185 | guard orientation == .right else { return nil } 186 | guard commentsView(tableView, isCommentExpandable: _currentlyDisplayed[indexPath.row], atIndexPath: indexPath) else { return nil } 187 | 188 | let collapseAction = SwipeAction(style: .destructive, 189 | title: swipeActionAppearance.swipeActionText) 190 | { [weak self](action, indexPath) in 191 | if self != nil { 192 | self!.tableView(self!.tableView, didSelectRowAt: indexPath) 193 | action.fulfill(with: .reset) 194 | } 195 | } 196 | 197 | // customize the action appearance 198 | collapseAction.backgroundColor = swipeActionAppearance.swipeActionColor 199 | collapseAction.highlightedBackgroundColor = swipeActionAppearance.swipeActionHighlightedColor 200 | collapseAction.textColor = swipeActionAppearance.swipeActionTextColor 201 | collapseAction.highlightedTextColor = swipeActionAppearance.swipeActionHighlightedTextColor 202 | collapseAction.font = swipeActionAppearance.swipeActionTextFont 203 | collapseAction.image = swipeActionAppearance.swipeActionImage 204 | collapseAction.highlightedImage = swipeActionAppearance.swipeActionHighlightedImage 205 | return [collapseAction] 206 | } 207 | 208 | public func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeTableOptions { 209 | var options = SwipeTableOptions() 210 | options.expansionStyle = .destructive(automaticallyDelete: false) 211 | return options 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /SwiftyComments/Classes/CommentsViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentsViewDelegate.swift 3 | // SwiftyComments 4 | // 5 | // Created by Stéphane Sercu on 12/02/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CommentsViewDelegate: class { 11 | func commentCellExpanded(atIndex index: Int) 12 | func commentCellFolded(atIndex index: Int) 13 | } 14 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------