├── .gitignore ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── Package.swift ├── PanModal.podspec ├── PanModal ├── Animator │ ├── PanModalAnimator.swift │ └── PanModalPresentationAnimator.swift ├── Controller │ └── PanModalPresentationController.swift ├── Delegate │ └── PanModalPresentationDelegate.swift ├── Info.plist ├── PanModal.h ├── Presentable │ ├── PanModalHeight.swift │ ├── PanModalPresentable+Defaults.swift │ ├── PanModalPresentable+LayoutHelpers.swift │ ├── PanModalPresentable+UIViewController.swift │ └── PanModalPresentable.swift ├── Presenter │ ├── PanModalPresenter.swift │ └── UIViewController+PanModalPresenter.swift └── View │ ├── DimmedView.swift │ └── PanContainerView.swift ├── PanModalDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── PanModal.xcscheme │ └── PanModalDemo.xcscheme ├── README.md ├── Sample ├── AppDelegate.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ ├── Icon-Small-50x50@1x.png │ │ │ ├── Icon-Small-50x50@2x.png │ │ │ └── ItunesArtwork@2x.png │ │ ├── Contents.json │ │ └── LaunchImage.launchimage │ │ │ ├── Contents.json │ │ │ ├── iPhone4_LaunchScreen_Placeholder.png │ │ │ ├── iPhone5_LaunchScreen_Placeholder.png │ │ │ ├── iPhone6Plus_LaunchScreen_Placeholder.png │ │ │ ├── iPhone6_LaunchScreen_Placeholder.png │ │ │ └── iPhoneX_LaunchScreen_Placeholder.png │ ├── Fonts │ │ ├── Lato-Bold.ttf │ │ └── Lato-Regular.ttf │ ├── Info.plist │ └── Preview.png ├── SampleViewController.swift └── View Controllers │ ├── Alert (Transient) │ └── TransientAlertViewController.swift │ ├── Alert │ ├── AlertView.swift │ └── AlertViewController.swift │ ├── Basic │ └── BasicViewController.swift │ ├── Full Screen │ └── FullScreenNavController.swift │ ├── User Groups (Navigation Controller) │ ├── NavigationController.swift │ └── ProfileViewController.swift │ ├── User Groups (Stacked) │ ├── StackedProfileViewController.swift │ └── UserGroupStackedViewController.swift │ └── User Groups │ ├── Presentables │ ├── UserGroupHeaderPresentable.swift │ └── UserGroupMemberPresentable.swift │ ├── UserGroupViewController.swift │ └── Views │ ├── UserGroupHeaderView.swift │ └── UserGroupMemberCell.swift ├── Screenshots ├── documentation.png └── panModal.gif └── Tests ├── Info.plist └── PanModalTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | .DS_Store 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | # 52 | # Add this line if you want to avoid checking in source code from the Xcode workspace 53 | # *.xcworkspace 54 | 55 | # Carthage 56 | # 57 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 58 | # Carthage/Checkouts 59 | 60 | Carthage/Build 61 | 62 | # fastlane 63 | # 64 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 65 | # screenshots whenever they are needed. 66 | # For more information about the recommended setup visit: 67 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 68 | 69 | fastlane/report.xml 70 | fastlane/Preview.html 71 | fastlane/screenshots/**/*.png 72 | fastlane/test_output 73 | 74 | # Code Injection 75 | # 76 | # After new code Injection tools there's a generated folder /iOSInjectionProject 77 | # https://github.com/johnno1962/injectionforxcode 78 | 79 | iOSInjectionProject/ 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | Interested in contributing? Awesome! Before you do though, please read our 4 | [Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as 5 | well. 6 | 7 | There are many ways you can contribute! :heart: 8 | 9 | ### Bug Reports and Fixes :bug: 10 | - If you find a bug, please search for it in the [Issues](https://github.com/slackhq/PanModal/issues), and if it isn't already tracked, 11 | [create a new issue](https://github.com/slackhq/PanModal/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still 12 | be reviewed. 13 | - Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. 14 | - If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. 15 | - Include tests that isolate the bug and verifies that it was fixed. 16 | 17 | ### New Features :bulb: 18 | - If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackhq/PanModal/issues/new). 19 | - Issues that have been identified as a feature request will be labelled `enhancement`. 20 | - If you'd like to implement the new feature, please wait for feedback from the project 21 | maintainers before spending too much time writing the code. In some cases, `enhancement`s may 22 | not align well with the project objectives at the time. 23 | 24 | ### Tests :mag:, Documentation :books:, Miscellaneous :sparkles: 25 | - If you'd like to improve the tests, you want to make the documentation clearer, you have an 26 | alternative implementation of something that may have advantages over the way its currently 27 | done, or you have any other change, we would be happy to hear about it! 28 | - If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. 29 | - If not, [open an Issue](https://github.com/slackhq/PanModal/issues/new) to discuss the idea first. 30 | 31 | If you're new to our project and looking for some way to make your first contribution, look for 32 | Issues labelled `good first contribution`. 33 | 34 | ## Requirements 35 | 36 | For your contribution to be accepted: 37 | 38 | - [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackHQ/PanModal). 39 | - [x] The test suite must be complete and pass. 40 | - [x] The changes must be approved by code review. 41 | - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. 42 | 43 | If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. 44 | 45 | [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) 46 | 47 | ## Creating a Pull Request 48 | 49 | 1. :fork_and_knife: Fork the repository on GitHub. 50 | 2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just 51 | to make sure everything is in order. 52 | 3. :herb: Create a new branch and check it out. 53 | 4. :crystal_ball: Make your changes and commit them locally. Magic happens here! 54 | 5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`). 55 | 6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this 56 | repository. 57 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Describe your issue here. 4 | 5 | ### What type of issue is this? (place an `x` in one of the `[ ]`) 6 | - [ ] bug 7 | - [ ] enhancement (feature request) 8 | - [ ] question 9 | - [ ] documentation related 10 | - [ ] testing related 11 | - [ ] discussion 12 | 13 | ### Requirements (place an `x` in each of the `[ ]`) 14 | * [ ] I've read and understood the [Contributing guidelines](https://github.com/slackhq/PanModal/blob/master/CONTRIBUTING.md) and have done my best effort to follow them. 15 | * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). 16 | * [ ] I've searched for any related issues and avoided creating a duplicate issue. 17 | 18 | --- 19 | 20 | ### Bug Report 21 | 22 | Filling out the following details about bugs will help us solve your issue sooner. 23 | 24 | #### Reproducible in: 25 | 26 | PanModal version: 27 | 28 | iOS version: 29 | 30 | #### Steps to reproduce: 31 | 32 | 1. 33 | 2. 34 | 3. 35 | 36 | #### Expected result: 37 | 38 | What you expected to happen 39 | 40 | #### Actual result: 41 | 42 | What actually happened 43 | 44 | #### Attachments: 45 | 46 | Logs, screenshots, screencast, sample project, funny gif, etc. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2018 Tiny Speck, Inc. 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | Describe the goal of this PR. Mention any related Issue numbers. 4 | 5 | ### Requirements (place an `x` in each `[ ]`) 6 | 7 | * [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackhq/PanModal/blob/master/CONTRIBUTING.md) and have done my best effort to follow them. 8 | * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). 9 | 10 | * [ ] I've written tests to cover the new code and functionality included in this PR. 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PanModal", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library( 11 | name: "PanModal", 12 | targets: ["PanModal"]), 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target( 17 | name: "PanModal", 18 | dependencies: [], 19 | path: "PanModal") 20 | ], 21 | swiftLanguageVersions: [.version("5.0")] 22 | ) 23 | -------------------------------------------------------------------------------- /PanModal.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint PanModal.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'PanModal' 11 | s.version = '1.2.7' 12 | s.summary = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.' 21 | s.homepage = 'https://github.com/slackhq/PanModal' 22 | s.license = { :type => 'MIT', :file => 'LICENSE' } 23 | s.author = { 'slack' => 'opensource@slack.com' } 24 | s.source = { :git => 'https://github.com/slackhq/PanModal.git', :tag => s.version.to_s } 25 | s.social_media_url = 'https://twitter.com/slackhq' 26 | s.ios.deployment_target = '10.0' 27 | s.swift_version = '5.0' 28 | s.source_files = 'PanModal/**/*.{swift,h,m}' 29 | end 30 | -------------------------------------------------------------------------------- /PanModal/Animator/PanModalAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalAnimator.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | Helper animation function to keep animations consistent. 13 | */ 14 | struct PanModalAnimator { 15 | 16 | /** 17 | Constant Animation Properties 18 | */ 19 | struct Constants { 20 | static let defaultTransitionDuration: TimeInterval = 0.5 21 | } 22 | 23 | static func animate(_ animations: @escaping PanModalPresentable.AnimationBlockType, 24 | config: PanModalPresentable?, 25 | _ completion: PanModalPresentable.AnimationCompletionType? = nil) { 26 | 27 | let transitionDuration = config?.transitionDuration ?? Constants.defaultTransitionDuration 28 | let springDamping = config?.springDamping ?? 1.0 29 | let animationOptions = config?.transitionAnimationOptions ?? [] 30 | 31 | UIView.animate(withDuration: transitionDuration, 32 | delay: 0, 33 | usingSpringWithDamping: springDamping, 34 | initialSpringVelocity: 0, 35 | options: animationOptions, 36 | animations: animations, 37 | completion: completion) 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /PanModal/Animator/PanModalPresentationAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentationAnimator.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | Handles the animation of the presentedViewController as it is presented or dismissed. 13 | 14 | This is a vertical animation that 15 | - Animates up from the bottom of the screen 16 | - Dismisses from the top to the bottom of the screen 17 | 18 | This can be used as a standalone object for transition animation, 19 | but is primarily used in the PanModalPresentationDelegate for handling pan modal transitions. 20 | 21 | - Note: The presentedViewController can conform to PanModalPresentable to adjust 22 | it's starting position through manipulating the shortFormHeight 23 | */ 24 | 25 | public class PanModalPresentationAnimator: NSObject { 26 | 27 | /** 28 | Enum representing the possible transition styles 29 | */ 30 | public enum TransitionStyle { 31 | case presentation 32 | case dismissal 33 | } 34 | 35 | // MARK: - Properties 36 | 37 | /** 38 | The transition style 39 | */ 40 | private let transitionStyle: TransitionStyle 41 | 42 | /** 43 | Haptic feedback generator (during presentation) 44 | */ 45 | private var feedbackGenerator: UISelectionFeedbackGenerator? 46 | 47 | // MARK: - Initializers 48 | 49 | required public init(transitionStyle: TransitionStyle) { 50 | self.transitionStyle = transitionStyle 51 | super.init() 52 | 53 | /** 54 | Prepare haptic feedback, only during the presentation state 55 | */ 56 | if case .presentation = transitionStyle { 57 | feedbackGenerator = UISelectionFeedbackGenerator() 58 | feedbackGenerator?.prepare() 59 | } 60 | } 61 | 62 | /** 63 | Animate presented view controller presentation 64 | */ 65 | private func animatePresentation(transitionContext: UIViewControllerContextTransitioning) { 66 | 67 | guard 68 | let toVC = transitionContext.viewController(forKey: .to), 69 | let fromVC = transitionContext.viewController(forKey: .from) 70 | else { return } 71 | 72 | let presentable = panModalLayoutType(from: transitionContext) 73 | 74 | // Calls viewWillAppear and viewWillDisappear 75 | fromVC.beginAppearanceTransition(false, animated: true) 76 | 77 | // Presents the view in shortForm position, initially 78 | let yPos: CGFloat = presentable?.shortFormYPos ?? 0.0 79 | 80 | // Use panView as presentingView if it already exists within the containerView 81 | let panView: UIView = transitionContext.containerView.panContainerView ?? toVC.view 82 | 83 | // Move presented view offscreen (from the bottom) 84 | panView.frame = transitionContext.finalFrame(for: toVC) 85 | panView.frame.origin.y = transitionContext.containerView.frame.height 86 | 87 | // Haptic feedback 88 | if presentable?.isHapticFeedbackEnabled == true { 89 | feedbackGenerator?.selectionChanged() 90 | } 91 | 92 | PanModalAnimator.animate({ 93 | panView.frame.origin.y = yPos 94 | }, config: presentable) { [weak self] didComplete in 95 | // Calls viewDidAppear and viewDidDisappear 96 | fromVC.endAppearanceTransition() 97 | transitionContext.completeTransition(didComplete) 98 | self?.feedbackGenerator = nil 99 | } 100 | } 101 | 102 | /** 103 | Animate presented view controller dismissal 104 | */ 105 | private func animateDismissal(transitionContext: UIViewControllerContextTransitioning) { 106 | 107 | guard 108 | let toVC = transitionContext.viewController(forKey: .to), 109 | let fromVC = transitionContext.viewController(forKey: .from) 110 | else { return } 111 | 112 | // Calls viewWillAppear and viewWillDisappear 113 | toVC.beginAppearanceTransition(true, animated: true) 114 | 115 | let presentable = panModalLayoutType(from: transitionContext) 116 | let panView: UIView = transitionContext.containerView.panContainerView ?? fromVC.view 117 | 118 | PanModalAnimator.animate({ 119 | panView.frame.origin.y = transitionContext.containerView.frame.height 120 | }, config: presentable) { didComplete in 121 | fromVC.view.removeFromSuperview() 122 | // Calls viewDidAppear and viewDidDisappear 123 | toVC.endAppearanceTransition() 124 | transitionContext.completeTransition(didComplete) 125 | } 126 | } 127 | 128 | /** 129 | Extracts the PanModal from the transition context, if it exists 130 | */ 131 | private func panModalLayoutType(from context: UIViewControllerContextTransitioning) -> PanModalPresentable.LayoutType? { 132 | switch transitionStyle { 133 | case .presentation: 134 | return context.viewController(forKey: .to) as? PanModalPresentable.LayoutType 135 | case .dismissal: 136 | return context.viewController(forKey: .from) as? PanModalPresentable.LayoutType 137 | } 138 | } 139 | 140 | } 141 | 142 | // MARK: - UIViewControllerAnimatedTransitioning Delegate 143 | 144 | extension PanModalPresentationAnimator: UIViewControllerAnimatedTransitioning { 145 | 146 | /** 147 | Returns the transition duration 148 | */ 149 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 150 | 151 | guard 152 | let context = transitionContext, 153 | let presentable = panModalLayoutType(from: context) 154 | else { return PanModalAnimator.Constants.defaultTransitionDuration } 155 | 156 | return presentable.transitionDuration 157 | } 158 | 159 | /** 160 | Performs the appropriate animation based on the transition style 161 | */ 162 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 163 | switch transitionStyle { 164 | case .presentation: 165 | animatePresentation(transitionContext: transitionContext) 166 | case .dismissal: 167 | animateDismissal(transitionContext: transitionContext) 168 | } 169 | } 170 | 171 | } 172 | #endif 173 | -------------------------------------------------------------------------------- /PanModal/Controller/PanModalPresentationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentationController.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | The PanModalPresentationController is the middle layer between the presentingViewController 13 | and the presentedViewController. 14 | 15 | It controls the coordination between the individual transition classes as well as 16 | provides an abstraction over how the presented view is presented & displayed. 17 | 18 | For example, we add a drag indicator view above the presented view and 19 | a background overlay between the presenting & presented view. 20 | 21 | The presented view's layout configuration & presentation is defined using the PanModalPresentable. 22 | 23 | By conforming to the PanModalPresentable protocol & overriding values 24 | the presented view can define its layout configuration & presentation. 25 | */ 26 | open class PanModalPresentationController: UIPresentationController { 27 | 28 | /** 29 | Enum representing the possible presentation states 30 | */ 31 | public enum PresentationState { 32 | case shortForm 33 | case longForm 34 | } 35 | 36 | /** 37 | Constants 38 | */ 39 | struct Constants { 40 | static let indicatorYOffset = CGFloat(8.0) 41 | static let snapMovementSensitivity = CGFloat(0.7) 42 | static let dragIndicatorSize = CGSize(width: 36.0, height: 5.0) 43 | } 44 | 45 | // MARK: - Properties 46 | 47 | /** 48 | A flag to track if the presented view is animating 49 | */ 50 | private var isPresentedViewAnimating = false 51 | 52 | /** 53 | A flag to determine if scrolling should seamlessly transition 54 | from the pan modal container view to the scroll view 55 | once the scroll limit has been reached. 56 | */ 57 | private var extendsPanScrolling = true 58 | 59 | /** 60 | A flag to determine if scrolling should be limited to the longFormHeight. 61 | Return false to cap scrolling at .max height. 62 | */ 63 | private var anchorModalToLongForm = true 64 | 65 | /** 66 | The y content offset value of the embedded scroll view 67 | */ 68 | private var scrollViewYOffset: CGFloat = 0.0 69 | 70 | /** 71 | An observer for the scroll view content offset 72 | */ 73 | private var scrollObserver: NSKeyValueObservation? 74 | 75 | // store the y positions so we don't have to keep re-calculating 76 | 77 | /** 78 | The y value for the short form presentation state 79 | */ 80 | private var shortFormYPosition: CGFloat = 0 81 | 82 | /** 83 | The y value for the long form presentation state 84 | */ 85 | private var longFormYPosition: CGFloat = 0 86 | 87 | /** 88 | Determine anchored Y postion based on the `anchorModalToLongForm` flag 89 | */ 90 | private var anchoredYPosition: CGFloat { 91 | let defaultTopOffset = presentable?.topOffset ?? 0 92 | return anchorModalToLongForm ? longFormYPosition : defaultTopOffset 93 | } 94 | 95 | /** 96 | Configuration object for PanModalPresentationController 97 | */ 98 | private var presentable: PanModalPresentable? { 99 | return presentedViewController as? PanModalPresentable 100 | } 101 | 102 | // MARK: - Views 103 | 104 | /** 105 | Background view used as an overlay over the presenting view 106 | */ 107 | private lazy var backgroundView: DimmedView = { 108 | let view: DimmedView 109 | if let color = presentable?.panModalBackgroundColor { 110 | view = DimmedView(dimColor: color) 111 | } else { 112 | view = DimmedView() 113 | } 114 | view.didTap = { [weak self] _ in 115 | if self?.presentable?.allowsTapToDismiss == true { 116 | self?.presentedViewController.dismiss(animated: true) 117 | } 118 | } 119 | return view 120 | }() 121 | 122 | /** 123 | A wrapper around the presented view so that we can modify 124 | the presented view apperance without changing 125 | the presented view's properties 126 | */ 127 | private lazy var panContainerView: PanContainerView = { 128 | let frame = containerView?.frame ?? .zero 129 | return PanContainerView(presentedView: presentedViewController.view, frame: frame) 130 | }() 131 | 132 | /** 133 | Drag Indicator View 134 | */ 135 | private lazy var dragIndicatorView: UIView = { 136 | let view = UIView() 137 | view.backgroundColor = presentable?.dragIndicatorBackgroundColor 138 | view.layer.cornerRadius = Constants.dragIndicatorSize.height / 2.0 139 | return view 140 | }() 141 | 142 | /** 143 | Override presented view to return the pan container wrapper 144 | */ 145 | public override var presentedView: UIView { 146 | return panContainerView 147 | } 148 | 149 | // MARK: - Gesture Recognizers 150 | 151 | /** 152 | Gesture recognizer to detect & track pan gestures 153 | */ 154 | private lazy var panGestureRecognizer: UIPanGestureRecognizer = { 155 | let gesture = UIPanGestureRecognizer(target: self, action: #selector(didPanOnPresentedView(_ :))) 156 | gesture.minimumNumberOfTouches = 1 157 | gesture.maximumNumberOfTouches = 1 158 | gesture.delegate = self 159 | return gesture 160 | }() 161 | 162 | // MARK: - Deinitializers 163 | 164 | deinit { 165 | scrollObserver?.invalidate() 166 | } 167 | 168 | // MARK: - Lifecycle 169 | 170 | override public func containerViewWillLayoutSubviews() { 171 | super.containerViewWillLayoutSubviews() 172 | configureViewLayout() 173 | } 174 | 175 | override public func presentationTransitionWillBegin() { 176 | 177 | guard let containerView = containerView 178 | else { return } 179 | 180 | layoutBackgroundView(in: containerView) 181 | layoutPresentedView(in: containerView) 182 | configureScrollViewInsets() 183 | 184 | guard let coordinator = presentedViewController.transitionCoordinator else { 185 | backgroundView.dimState = .max 186 | return 187 | } 188 | 189 | coordinator.animate(alongsideTransition: { [weak self] _ in 190 | self?.backgroundView.dimState = .max 191 | self?.presentedViewController.setNeedsStatusBarAppearanceUpdate() 192 | }) 193 | } 194 | 195 | override public func presentationTransitionDidEnd(_ completed: Bool) { 196 | if completed { return } 197 | 198 | backgroundView.removeFromSuperview() 199 | } 200 | 201 | override public func dismissalTransitionWillBegin() { 202 | presentable?.panModalWillDismiss() 203 | 204 | guard let coordinator = presentedViewController.transitionCoordinator else { 205 | backgroundView.dimState = .off 206 | return 207 | } 208 | 209 | /** 210 | Drag indicator is drawn outside of view bounds 211 | so hiding it on view dismiss means avoiding visual bugs 212 | */ 213 | coordinator.animate(alongsideTransition: { [weak self] _ in 214 | self?.dragIndicatorView.alpha = 0.0 215 | self?.backgroundView.dimState = .off 216 | self?.presentingViewController.setNeedsStatusBarAppearanceUpdate() 217 | }) 218 | } 219 | 220 | override public func dismissalTransitionDidEnd(_ completed: Bool) { 221 | if !completed { return } 222 | 223 | presentable?.panModalDidDismiss() 224 | } 225 | 226 | /** 227 | Update presented view size in response to size class changes 228 | */ 229 | override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 230 | super.viewWillTransition(to: size, with: coordinator) 231 | 232 | coordinator.animate(alongsideTransition: { [weak self] _ in 233 | guard 234 | let self = self, 235 | let presentable = self.presentable 236 | else { return } 237 | 238 | self.adjustPresentedViewFrame() 239 | if presentable.shouldRoundTopCorners { 240 | self.addRoundedCorners(to: self.presentedView) 241 | } 242 | }) 243 | } 244 | 245 | } 246 | 247 | // MARK: - Public Methods 248 | 249 | public extension PanModalPresentationController { 250 | 251 | /** 252 | Transition the PanModalPresentationController 253 | to the given presentation state 254 | */ 255 | func transition(to state: PresentationState) { 256 | 257 | guard presentable?.shouldTransition(to: state) == true 258 | else { return } 259 | 260 | presentable?.willTransition(to: state) 261 | 262 | switch state { 263 | case .shortForm: 264 | snap(toYPosition: shortFormYPosition) 265 | case .longForm: 266 | snap(toYPosition: longFormYPosition) 267 | } 268 | } 269 | 270 | /** 271 | Operations on the scroll view, such as content height changes, 272 | or when inserting/deleting rows can cause the pan modal to jump, 273 | caused by the pan modal responding to content offset changes. 274 | 275 | To avoid this, you can call this method to perform scroll view updates, 276 | with scroll observation temporarily disabled. 277 | */ 278 | func performUpdates(_ updates: () -> Void) { 279 | 280 | guard let scrollView = presentable?.panScrollable 281 | else { return } 282 | 283 | // Pause scroll observer 284 | scrollObserver?.invalidate() 285 | scrollObserver = nil 286 | 287 | // Perform updates 288 | updates() 289 | 290 | // Resume scroll observer 291 | trackScrolling(scrollView) 292 | observe(scrollView: scrollView) 293 | } 294 | 295 | /** 296 | Updates the PanModalPresentationController layout 297 | based on values in the PanModalPresentable 298 | 299 | - Note: This should be called whenever any 300 | pan modal presentable value changes after the initial presentation 301 | */ 302 | func setNeedsLayoutUpdate() { 303 | configureViewLayout() 304 | adjustPresentedViewFrame() 305 | observe(scrollView: presentable?.panScrollable) 306 | configureScrollViewInsets() 307 | } 308 | 309 | } 310 | 311 | // MARK: - Presented View Layout Configuration 312 | 313 | private extension PanModalPresentationController { 314 | 315 | /** 316 | Boolean flag to determine if the presented view is anchored 317 | */ 318 | var isPresentedViewAnchored: Bool { 319 | if !isPresentedViewAnimating 320 | && extendsPanScrolling 321 | && presentedView.frame.minY.rounded() <= anchoredYPosition.rounded() { 322 | return true 323 | } 324 | 325 | return false 326 | } 327 | 328 | /** 329 | Adds the presented view to the given container view 330 | & configures the view elements such as drag indicator, rounded corners 331 | based on the pan modal presentable. 332 | */ 333 | func layoutPresentedView(in containerView: UIView) { 334 | 335 | /** 336 | If the presented view controller does not conform to pan modal presentable 337 | don't configure 338 | */ 339 | guard let presentable = presentable 340 | else { return } 341 | 342 | /** 343 | ⚠️ If this class is NOT used in conjunction with the PanModalPresentationAnimator 344 | & PanModalPresentable, the presented view should be added to the container view 345 | in the presentation animator instead of here 346 | */ 347 | containerView.addSubview(presentedView) 348 | containerView.addGestureRecognizer(panGestureRecognizer) 349 | 350 | if presentable.showDragIndicator { 351 | addDragIndicatorView(to: presentedView) 352 | } 353 | 354 | if presentable.shouldRoundTopCorners { 355 | addRoundedCorners(to: presentedView) 356 | } 357 | 358 | setNeedsLayoutUpdate() 359 | adjustPanContainerBackgroundColor() 360 | } 361 | 362 | /** 363 | Reduce height of presentedView so that it sits at the bottom of the screen 364 | */ 365 | func adjustPresentedViewFrame() { 366 | 367 | guard let frame = containerView?.frame 368 | else { return } 369 | 370 | let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition) 371 | let panFrame = panContainerView.frame 372 | panContainerView.frame.size = frame.size 373 | 374 | if ![shortFormYPosition, longFormYPosition].contains(panFrame.origin.y) { 375 | // if the container is already in the correct position, no need to adjust positioning 376 | // (rotations & size changes cause positioning to be out of sync) 377 | let yPosition = panFrame.origin.y - panFrame.height + frame.height 378 | presentedView.frame.origin.y = max(yPosition, anchoredYPosition) 379 | } 380 | panContainerView.frame.origin.x = frame.origin.x 381 | presentedViewController.view.frame = CGRect(origin: .zero, size: adjustedSize) 382 | } 383 | 384 | /** 385 | Adds a background color to the pan container view 386 | in order to avoid a gap at the bottom 387 | during initial view presentation in longForm (when view bounces) 388 | */ 389 | func adjustPanContainerBackgroundColor() { 390 | panContainerView.backgroundColor = presentedViewController.view.backgroundColor 391 | ?? presentable?.panScrollable?.backgroundColor 392 | } 393 | 394 | /** 395 | Adds the background view to the view hierarchy 396 | & configures its layout constraints. 397 | */ 398 | func layoutBackgroundView(in containerView: UIView) { 399 | containerView.addSubview(backgroundView) 400 | backgroundView.translatesAutoresizingMaskIntoConstraints = false 401 | backgroundView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true 402 | backgroundView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true 403 | backgroundView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true 404 | backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true 405 | } 406 | 407 | /** 408 | Adds the drag indicator view to the view hierarchy 409 | & configures its layout constraints. 410 | */ 411 | func addDragIndicatorView(to view: UIView) { 412 | view.addSubview(dragIndicatorView) 413 | dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false 414 | dragIndicatorView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: -Constants.indicatorYOffset).isActive = true 415 | dragIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 416 | dragIndicatorView.widthAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.width).isActive = true 417 | dragIndicatorView.heightAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.height).isActive = true 418 | } 419 | 420 | /** 421 | Calculates & stores the layout anchor points & options 422 | */ 423 | func configureViewLayout() { 424 | 425 | guard let layoutPresentable = presentedViewController as? PanModalPresentable.LayoutType 426 | else { return } 427 | 428 | shortFormYPosition = layoutPresentable.shortFormYPos 429 | longFormYPosition = layoutPresentable.longFormYPos 430 | anchorModalToLongForm = layoutPresentable.anchorModalToLongForm 431 | extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling 432 | 433 | containerView?.isUserInteractionEnabled = layoutPresentable.isUserInteractionEnabled 434 | } 435 | 436 | /** 437 | Configures the scroll view insets 438 | */ 439 | func configureScrollViewInsets() { 440 | 441 | guard 442 | let scrollView = presentable?.panScrollable, 443 | !scrollView.isScrolling 444 | else { return } 445 | 446 | /** 447 | Disable vertical scroll indicator until we start to scroll 448 | to avoid visual bugs 449 | */ 450 | scrollView.showsVerticalScrollIndicator = false 451 | scrollView.scrollIndicatorInsets = presentable?.scrollIndicatorInsets ?? .zero 452 | 453 | /** 454 | Set the appropriate contentInset as the configuration within this class 455 | offsets it 456 | */ 457 | scrollView.contentInset.bottom = presentingViewController.bottomLayoutGuide.length 458 | 459 | /** 460 | As we adjust the bounds during `handleScrollViewTopBounce` 461 | we should assume that contentInsetAdjustmentBehavior will not be correct 462 | */ 463 | if #available(iOS 11.0, *) { 464 | scrollView.contentInsetAdjustmentBehavior = .never 465 | } 466 | } 467 | 468 | } 469 | 470 | // MARK: - Pan Gesture Event Handler 471 | 472 | private extension PanModalPresentationController { 473 | 474 | /** 475 | The designated function for handling pan gesture events 476 | */ 477 | @objc func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) { 478 | 479 | guard 480 | shouldRespond(to: recognizer), 481 | let containerView = containerView 482 | else { 483 | recognizer.setTranslation(.zero, in: recognizer.view) 484 | return 485 | } 486 | 487 | switch recognizer.state { 488 | case .began, .changed: 489 | 490 | /** 491 | Respond accordingly to pan gesture translation 492 | */ 493 | respond(to: recognizer) 494 | 495 | /** 496 | If presentedView is translated above the longForm threshold, treat as transition 497 | */ 498 | if presentedView.frame.origin.y == anchoredYPosition && extendsPanScrolling { 499 | presentable?.willTransition(to: .longForm) 500 | } 501 | 502 | default: 503 | 504 | /** 505 | Use velocity sensitivity value to restrict snapping 506 | */ 507 | let velocity = recognizer.velocity(in: presentedView) 508 | 509 | if isVelocityWithinSensitivityRange(velocity.y) { 510 | 511 | /** 512 | If velocity is within the sensitivity range, 513 | transition to a presentation state or dismiss entirely. 514 | 515 | This allows the user to dismiss directly from long form 516 | instead of going to the short form state first. 517 | */ 518 | if velocity.y < 0 { 519 | transition(to: .longForm) 520 | 521 | } else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition 522 | && presentedView.frame.minY < shortFormYPosition) || presentable?.allowsDragToDismiss == false { 523 | transition(to: .shortForm) 524 | 525 | } else { 526 | presentedViewController.dismiss(animated: true) 527 | } 528 | 529 | } else { 530 | 531 | /** 532 | The `containerView.bounds.height` is used to determine 533 | how close the presented view is to the bottom of the screen 534 | */ 535 | let position = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, longFormYPosition]) 536 | 537 | if position == longFormYPosition { 538 | transition(to: .longForm) 539 | 540 | } else if position == shortFormYPosition || presentable?.allowsDragToDismiss == false { 541 | transition(to: .shortForm) 542 | 543 | } else { 544 | presentedViewController.dismiss(animated: true) 545 | } 546 | } 547 | } 548 | } 549 | 550 | /** 551 | Determine if the pan modal should respond to the gesture recognizer. 552 | 553 | If the pan modal is already being dragged & the delegate returns false, ignore until 554 | the recognizer is back to it's original state (.began) 555 | 556 | ⚠️ This is the only time we should be cancelling the pan modal gesture recognizer 557 | */ 558 | func shouldRespond(to panGestureRecognizer: UIPanGestureRecognizer) -> Bool { 559 | guard 560 | presentable?.shouldRespond(to: panGestureRecognizer) == true || 561 | !(panGestureRecognizer.state == .began || panGestureRecognizer.state == .cancelled) 562 | else { 563 | panGestureRecognizer.isEnabled = false 564 | panGestureRecognizer.isEnabled = true 565 | return false 566 | } 567 | return !shouldFail(panGestureRecognizer: panGestureRecognizer) 568 | } 569 | 570 | /** 571 | Communicate intentions to presentable and adjust subviews in containerView 572 | */ 573 | func respond(to panGestureRecognizer: UIPanGestureRecognizer) { 574 | presentable?.willRespond(to: panGestureRecognizer) 575 | 576 | var yDisplacement = panGestureRecognizer.translation(in: presentedView).y 577 | 578 | /** 579 | If the presentedView is not anchored to long form, reduce the rate of movement 580 | above the threshold 581 | */ 582 | if presentedView.frame.origin.y < longFormYPosition { 583 | yDisplacement /= 2.0 584 | } 585 | adjust(toYPosition: presentedView.frame.origin.y + yDisplacement) 586 | 587 | panGestureRecognizer.setTranslation(.zero, in: presentedView) 588 | } 589 | 590 | /** 591 | Determines if we should fail the gesture recognizer based on certain conditions 592 | 593 | We fail the presented view's pan gesture recognizer if we are actively scrolling on the scroll view. 594 | This allows the user to drag whole view controller from outside scrollView touch area. 595 | 596 | Unfortunately, cancelling a gestureRecognizer means that we lose the effect of transition scrolling 597 | from one view to another in the same pan gesture so don't cancel 598 | */ 599 | func shouldFail(panGestureRecognizer: UIPanGestureRecognizer) -> Bool { 600 | 601 | /** 602 | Allow api consumers to override the internal conditions & 603 | decide if the pan gesture recognizer should be prioritized. 604 | 605 | ⚠️ This is the only time we should be cancelling the panScrollable recognizer, 606 | for the purpose of ensuring we're no longer tracking the scrollView 607 | */ 608 | guard !shouldPrioritize(panGestureRecognizer: panGestureRecognizer) else { 609 | presentable?.panScrollable?.panGestureRecognizer.isEnabled = false 610 | presentable?.panScrollable?.panGestureRecognizer.isEnabled = true 611 | return false 612 | } 613 | 614 | guard 615 | isPresentedViewAnchored, 616 | let scrollView = presentable?.panScrollable, 617 | scrollView.contentOffset.y > 0 618 | else { 619 | return false 620 | } 621 | 622 | let loc = panGestureRecognizer.location(in: presentedView) 623 | return (scrollView.frame.contains(loc) || scrollView.isScrolling) 624 | } 625 | 626 | /** 627 | Determine if the presented view's panGestureRecognizer should be prioritized over 628 | embedded scrollView's panGestureRecognizer. 629 | */ 630 | func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool { 631 | return panGestureRecognizer.state == .began && 632 | presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) == true 633 | } 634 | 635 | /** 636 | Check if the given velocity is within the sensitivity range 637 | */ 638 | func isVelocityWithinSensitivityRange(_ velocity: CGFloat) -> Bool { 639 | return (abs(velocity) - (1000 * (1 - Constants.snapMovementSensitivity))) > 0 640 | } 641 | 642 | func snap(toYPosition yPos: CGFloat) { 643 | PanModalAnimator.animate({ [weak self] in 644 | self?.adjust(toYPosition: yPos) 645 | self?.isPresentedViewAnimating = true 646 | }, config: presentable) { [weak self] didComplete in 647 | self?.isPresentedViewAnimating = !didComplete 648 | } 649 | } 650 | 651 | /** 652 | Sets the y position of the presentedView & adjusts the backgroundView. 653 | */ 654 | func adjust(toYPosition yPos: CGFloat) { 655 | presentedView.frame.origin.y = max(yPos, anchoredYPosition) 656 | 657 | guard presentedView.frame.origin.y > shortFormYPosition else { 658 | backgroundView.dimState = .max 659 | return 660 | } 661 | 662 | let yDisplacementFromShortForm = presentedView.frame.origin.y - shortFormYPosition 663 | 664 | /** 665 | Once presentedView is translated below shortForm, calculate yPos relative to bottom of screen 666 | and apply percentage to backgroundView alpha 667 | */ 668 | backgroundView.dimState = .percent(1.0 - (yDisplacementFromShortForm / presentedView.frame.height)) 669 | } 670 | 671 | /** 672 | Finds the nearest value to a given number out of a given array of float values 673 | 674 | - Parameters: 675 | - number: reference float we are trying to find the closest value to 676 | - values: array of floats we would like to compare against 677 | */ 678 | func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat { 679 | guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) }) 680 | else { return number } 681 | return nearestVal 682 | } 683 | } 684 | 685 | // MARK: - UIScrollView Observer 686 | 687 | private extension PanModalPresentationController { 688 | 689 | /** 690 | Creates & stores an observer on the given scroll view's content offset. 691 | This allows us to track scrolling without overriding the scrollView delegate 692 | */ 693 | func observe(scrollView: UIScrollView?) { 694 | scrollObserver?.invalidate() 695 | scrollObserver = scrollView?.observe(\.contentOffset, options: .old) { [weak self] scrollView, change in 696 | 697 | /** 698 | Incase we have a situation where we have two containerViews in the same presentation 699 | */ 700 | guard self?.containerView != nil 701 | else { return } 702 | 703 | self?.didPanOnScrollView(scrollView, change: change) 704 | } 705 | } 706 | 707 | /** 708 | Scroll view content offset change event handler 709 | 710 | Also when scrollView is scrolled to the top, we disable the scroll indicator 711 | otherwise glitchy behaviour occurs 712 | 713 | This is also shown in Apple Maps (reverse engineering) 714 | which allows us to seamlessly transition scrolling from the panContainerView to the scrollView 715 | */ 716 | func didPanOnScrollView(_ scrollView: UIScrollView, change: NSKeyValueObservedChange) { 717 | 718 | guard 719 | !presentedViewController.isBeingDismissed, 720 | !presentedViewController.isBeingPresented 721 | else { return } 722 | 723 | if !isPresentedViewAnchored && scrollView.contentOffset.y > 0 { 724 | 725 | /** 726 | Hold the scrollView in place if we're actively scrolling and not handling top bounce 727 | */ 728 | haltScrolling(scrollView) 729 | 730 | } else if scrollView.isScrolling || isPresentedViewAnimating { 731 | 732 | if isPresentedViewAnchored { 733 | /** 734 | While we're scrolling upwards on the scrollView, 735 | store the last content offset position 736 | */ 737 | trackScrolling(scrollView) 738 | } else { 739 | /** 740 | Keep scroll view in place while we're panning on main view 741 | */ 742 | haltScrolling(scrollView) 743 | } 744 | 745 | } else if presentedViewController.view.isKind(of: UIScrollView.self) 746 | && !isPresentedViewAnimating && scrollView.contentOffset.y <= 0 { 747 | 748 | /** 749 | In the case where we drag down quickly on the scroll view and let go, 750 | `handleScrollViewTopBounce` adds a nice elegant touch. 751 | */ 752 | handleScrollViewTopBounce(scrollView: scrollView, change: change) 753 | } else { 754 | trackScrolling(scrollView) 755 | } 756 | } 757 | 758 | /** 759 | Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset` 760 | */ 761 | func haltScrolling(_ scrollView: UIScrollView) { 762 | scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewYOffset), animated: false) 763 | scrollView.showsVerticalScrollIndicator = false 764 | } 765 | 766 | /** 767 | As the user scrolls, track & save the scroll view y offset. 768 | This helps halt scrolling when we want to hold the scroll view in place. 769 | */ 770 | func trackScrolling(_ scrollView: UIScrollView) { 771 | scrollViewYOffset = max(scrollView.contentOffset.y, 0) 772 | scrollView.showsVerticalScrollIndicator = true 773 | } 774 | 775 | /** 776 | To ensure that the scroll transition between the scrollView & the modal 777 | is completely seamless, we need to handle the case where content offset is negative. 778 | 779 | In this case, we follow the curve of the decelerating scroll view. 780 | This gives the effect that the modal view and the scroll view are one view entirely. 781 | 782 | - Note: This works best where the view behind view controller is a UIScrollView. 783 | So, for example, a UITableViewController. 784 | */ 785 | func handleScrollViewTopBounce(scrollView: UIScrollView, change: NSKeyValueObservedChange) { 786 | 787 | guard let oldYValue = change.oldValue?.y, scrollView.isDecelerating 788 | else { return } 789 | 790 | let yOffset = scrollView.contentOffset.y 791 | let presentedSize = containerView?.frame.size ?? .zero 792 | 793 | /** 794 | Decrease the view bounds by the y offset so the scroll view stays in place 795 | and we can still get updates on its content offset 796 | */ 797 | presentedView.bounds.size = CGSize(width: presentedSize.width, height: presentedSize.height + yOffset) 798 | 799 | if oldYValue > yOffset { 800 | /** 801 | Move the view in the opposite direction to the decreasing bounds 802 | until half way through the deceleration so that it appears 803 | as if we're transferring the scrollView drag momentum to the entire view 804 | */ 805 | presentedView.frame.origin.y = longFormYPosition - yOffset 806 | } else { 807 | scrollViewYOffset = 0 808 | snap(toYPosition: longFormYPosition) 809 | } 810 | 811 | scrollView.showsVerticalScrollIndicator = false 812 | } 813 | } 814 | 815 | // MARK: - UIGestureRecognizerDelegate 816 | 817 | extension PanModalPresentationController: UIGestureRecognizerDelegate { 818 | 819 | /** 820 | Do not require any other gesture recognizers to fail 821 | */ 822 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 823 | return false 824 | } 825 | 826 | /** 827 | Allow simultaneous gesture recognizers only when the other gesture recognizer's view 828 | is the pan scrollable view 829 | */ 830 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 831 | return otherGestureRecognizer.view == presentable?.panScrollable 832 | } 833 | } 834 | 835 | // MARK: - UIBezierPath 836 | 837 | private extension PanModalPresentationController { 838 | 839 | /** 840 | Draws top rounded corners on a given view 841 | We have to set a custom path for corner rounding 842 | because we render the dragIndicator outside of view bounds 843 | */ 844 | func addRoundedCorners(to view: UIView) { 845 | let radius = presentable?.cornerRadius ?? 0 846 | let path = UIBezierPath(roundedRect: view.bounds, 847 | byRoundingCorners: [.topLeft, .topRight], 848 | cornerRadii: CGSize(width: radius, height: radius)) 849 | 850 | // Draw around the drag indicator view, if displayed 851 | if presentable?.showDragIndicator == true { 852 | let indicatorLeftEdgeXPos = view.bounds.width/2.0 - Constants.dragIndicatorSize.width/2.0 853 | drawAroundDragIndicator(currentPath: path, indicatorLeftEdgeXPos: indicatorLeftEdgeXPos) 854 | } 855 | 856 | // Set path as a mask to display optional drag indicator view & rounded corners 857 | let mask = CAShapeLayer() 858 | mask.path = path.cgPath 859 | view.layer.mask = mask 860 | 861 | // Improve performance by rasterizing the layer 862 | view.layer.shouldRasterize = true 863 | view.layer.rasterizationScale = UIScreen.main.scale 864 | } 865 | 866 | /** 867 | Draws a path around the drag indicator view 868 | */ 869 | func drawAroundDragIndicator(currentPath path: UIBezierPath, indicatorLeftEdgeXPos: CGFloat) { 870 | 871 | let totalIndicatorOffset = Constants.indicatorYOffset + Constants.dragIndicatorSize.height 872 | 873 | // Draw around drag indicator starting from the left 874 | path.addLine(to: CGPoint(x: indicatorLeftEdgeXPos, y: path.currentPoint.y)) 875 | path.addLine(to: CGPoint(x: path.currentPoint.x, y: path.currentPoint.y - totalIndicatorOffset)) 876 | path.addLine(to: CGPoint(x: path.currentPoint.x + Constants.dragIndicatorSize.width, y: path.currentPoint.y)) 877 | path.addLine(to: CGPoint(x: path.currentPoint.x, y: path.currentPoint.y + totalIndicatorOffset)) 878 | } 879 | } 880 | 881 | // MARK: - Helper Extensions 882 | 883 | private extension UIScrollView { 884 | 885 | /** 886 | A flag to determine if a scroll view is scrolling 887 | */ 888 | var isScrolling: Bool { 889 | return isDragging && !isDecelerating || isTracking 890 | } 891 | } 892 | #endif 893 | -------------------------------------------------------------------------------- /PanModal/Delegate/PanModalPresentationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentationDelegate.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | The PanModalPresentationDelegate conforms to the various transition delegates 13 | and vends the appropriate object for each transition controller requested. 14 | 15 | Usage: 16 | ``` 17 | viewController.modalPresentationStyle = .custom 18 | viewController.transitioningDelegate = PanModalPresentationDelegate.default 19 | ``` 20 | */ 21 | public class PanModalPresentationDelegate: NSObject { 22 | 23 | /** 24 | Returns an instance of the delegate, retained for the duration of presentation 25 | */ 26 | public static var `default`: PanModalPresentationDelegate = { 27 | return PanModalPresentationDelegate() 28 | }() 29 | 30 | } 31 | 32 | extension PanModalPresentationDelegate: UIViewControllerTransitioningDelegate { 33 | 34 | /** 35 | Returns a modal presentation animator configured for the presenting state 36 | */ 37 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 38 | return PanModalPresentationAnimator(transitionStyle: .presentation) 39 | } 40 | 41 | /** 42 | Returns a modal presentation animator configured for the dismissing state 43 | */ 44 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 45 | return PanModalPresentationAnimator(transitionStyle: .dismissal) 46 | } 47 | 48 | /** 49 | Returns a modal presentation controller to coordinate the transition from the presenting 50 | view controller to the presented view controller. 51 | 52 | Changes in size class during presentation are handled via the adaptive presentation delegate 53 | */ 54 | public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 55 | let controller = PanModalPresentationController(presentedViewController: presented, presenting: presenting) 56 | controller.delegate = self 57 | return controller 58 | } 59 | 60 | } 61 | 62 | extension PanModalPresentationDelegate: UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate { 63 | 64 | /** 65 | - Note: We do not adapt to size classes due to the introduction of the UIPresentationController 66 | & deprecation of UIPopoverController (iOS 9), there is no way to have more than one 67 | presentation controller in use during the same presentation 68 | 69 | This is essential when transitioning from .popover to .custom on iPad split view... unless a custom popover view is also implemented 70 | (popover uses UIPopoverPresentationController & we use PanModalPresentationController) 71 | */ 72 | 73 | /** 74 | Dismisses the presented view controller 75 | */ 76 | public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { 77 | return .none 78 | } 79 | 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /PanModal/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /PanModal/PanModal.h: -------------------------------------------------------------------------------- 1 | // 2 | // PanModal.h 3 | // PanModal 4 | // 5 | // Created by Tosin A on 3/13/19. 6 | // Copyright © 2019 Detail. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for PanModal. 12 | FOUNDATION_EXPORT double PanModalVersionNumber; 13 | 14 | //! Project version string for PanModal. 15 | FOUNDATION_EXPORT const unsigned char PanModalVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /PanModal/Presentable/PanModalHeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalHeight.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | An enum that defines the possible states of the height of a pan modal container view 13 | for a given presentation state (shortForm, longForm) 14 | */ 15 | public enum PanModalHeight: Equatable { 16 | 17 | /** 18 | Sets the height to be the maximum height (+ topOffset) 19 | */ 20 | case maxHeight 21 | 22 | /** 23 | Sets the height to be the max height with a specified top inset. 24 | - Note: A value of 0 is equivalent to .maxHeight 25 | */ 26 | case maxHeightWithTopInset(CGFloat) 27 | 28 | /** 29 | Sets the height to be the specified content height 30 | */ 31 | case contentHeight(CGFloat) 32 | 33 | /** 34 | Sets the height to be the specified content height 35 | & also ignores the bottomSafeAreaInset 36 | */ 37 | case contentHeightIgnoringSafeArea(CGFloat) 38 | 39 | /** 40 | Sets the height to be the intrinsic content height 41 | */ 42 | case intrinsicHeight 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /PanModal/Presentable/PanModalPresentable+Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentable+Defaults.swift 3 | // PanModal 4 | // 5 | // Copyright © 2018 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | Default values for the PanModalPresentable. 13 | */ 14 | public extension PanModalPresentable where Self: UIViewController { 15 | 16 | var topOffset: CGFloat { 17 | return topLayoutOffset + 21.0 18 | } 19 | 20 | var shortFormHeight: PanModalHeight { 21 | return longFormHeight 22 | } 23 | 24 | var longFormHeight: PanModalHeight { 25 | 26 | guard let scrollView = panScrollable 27 | else { return .maxHeight } 28 | 29 | // called once during presentation and stored 30 | scrollView.layoutIfNeeded() 31 | return .contentHeight(scrollView.contentSize.height) 32 | } 33 | 34 | var cornerRadius: CGFloat { 35 | return 8.0 36 | } 37 | 38 | var springDamping: CGFloat { 39 | return 0.8 40 | } 41 | 42 | var transitionDuration: Double { 43 | return PanModalAnimator.Constants.defaultTransitionDuration 44 | } 45 | 46 | var transitionAnimationOptions: UIView.AnimationOptions { 47 | return [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState] 48 | } 49 | 50 | var panModalBackgroundColor: UIColor { 51 | return UIColor.black.withAlphaComponent(0.7) 52 | } 53 | 54 | var dragIndicatorBackgroundColor: UIColor { 55 | return UIColor.lightGray 56 | } 57 | 58 | var scrollIndicatorInsets: UIEdgeInsets { 59 | let top = shouldRoundTopCorners ? cornerRadius : 0 60 | return UIEdgeInsets(top: CGFloat(top), left: 0, bottom: bottomLayoutOffset, right: 0) 61 | } 62 | 63 | var anchorModalToLongForm: Bool { 64 | return true 65 | } 66 | 67 | var allowsExtendedPanScrolling: Bool { 68 | 69 | guard let scrollView = panScrollable 70 | else { return false } 71 | 72 | scrollView.layoutIfNeeded() 73 | return scrollView.contentSize.height > (scrollView.frame.height - bottomLayoutOffset) 74 | } 75 | 76 | var allowsDragToDismiss: Bool { 77 | return true 78 | } 79 | 80 | var allowsTapToDismiss: Bool { 81 | return true 82 | } 83 | 84 | var isUserInteractionEnabled: Bool { 85 | return true 86 | } 87 | 88 | var isHapticFeedbackEnabled: Bool { 89 | return true 90 | } 91 | 92 | var shouldRoundTopCorners: Bool { 93 | return isPanModalPresented 94 | } 95 | 96 | var showDragIndicator: Bool { 97 | return shouldRoundTopCorners 98 | } 99 | 100 | func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool { 101 | return true 102 | } 103 | 104 | func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) { 105 | 106 | } 107 | 108 | func shouldTransition(to state: PanModalPresentationController.PresentationState) -> Bool { 109 | return true 110 | } 111 | 112 | func shouldPrioritize(panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool { 113 | return false 114 | } 115 | 116 | func willTransition(to state: PanModalPresentationController.PresentationState) { 117 | 118 | } 119 | 120 | func panModalWillDismiss() { 121 | 122 | } 123 | 124 | func panModalDidDismiss() { 125 | 126 | } 127 | } 128 | #endif 129 | -------------------------------------------------------------------------------- /PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentable+LayoutHelpers.swift 3 | // PanModal 4 | // 5 | // Copyright © 2018 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | ⚠️ [Internal Only] ⚠️ 13 | Helper extensions that handle layout in the PanModalPresentationController 14 | */ 15 | extension PanModalPresentable where Self: UIViewController { 16 | 17 | /** 18 | Cast the presentation controller to PanModalPresentationController 19 | so we can access PanModalPresentationController properties and methods 20 | */ 21 | var presentedVC: PanModalPresentationController? { 22 | return presentationController as? PanModalPresentationController 23 | } 24 | 25 | /** 26 | Length of the top layout guide of the presenting view controller. 27 | Gives us the safe area inset from the top. 28 | */ 29 | var topLayoutOffset: CGFloat { 30 | 31 | guard let rootVC = rootViewController 32 | else { return 0} 33 | 34 | if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.top } else { return rootVC.topLayoutGuide.length } 35 | } 36 | 37 | /** 38 | Length of the bottom layout guide of the presenting view controller. 39 | Gives us the safe area inset from the bottom. 40 | */ 41 | var bottomLayoutOffset: CGFloat { 42 | 43 | guard let rootVC = rootViewController 44 | else { return 0} 45 | 46 | if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.bottom } else { return rootVC.bottomLayoutGuide.length } 47 | } 48 | 49 | /** 50 | Returns the short form Y position 51 | 52 | - Note: If voiceover is on, the `longFormYPos` is returned. 53 | We do not support short form when voiceover is on as it would make it difficult for user to navigate. 54 | */ 55 | var shortFormYPos: CGFloat { 56 | 57 | guard !UIAccessibility.isVoiceOverRunning 58 | else { return longFormYPos } 59 | 60 | let shortFormYPos = topMargin(from: shortFormHeight) + topOffset 61 | 62 | // shortForm shouldn't exceed longForm 63 | return max(shortFormYPos, longFormYPos) 64 | } 65 | 66 | /** 67 | Returns the long form Y position 68 | 69 | - Note: We cap this value to the max possible height 70 | to ensure content is not rendered outside of the view bounds 71 | */ 72 | var longFormYPos: CGFloat { 73 | return max(topMargin(from: longFormHeight), topMargin(from: .maxHeight)) + topOffset 74 | } 75 | 76 | /** 77 | Use the container view for relative positioning as this view's frame 78 | is adjusted in PanModalPresentationController 79 | */ 80 | var bottomYPos: CGFloat { 81 | 82 | guard let container = presentedVC?.containerView 83 | else { return view.bounds.height } 84 | 85 | return container.bounds.size.height - topOffset 86 | } 87 | 88 | /** 89 | Converts a given pan modal height value into a y position value 90 | calculated from top of view 91 | */ 92 | func topMargin(from: PanModalHeight) -> CGFloat { 93 | switch from { 94 | case .maxHeight: 95 | return 0.0 96 | case .maxHeightWithTopInset(let inset): 97 | return inset 98 | case .contentHeight(let height): 99 | return bottomYPos - (height + bottomLayoutOffset) 100 | case .contentHeightIgnoringSafeArea(let height): 101 | return bottomYPos - height 102 | case .intrinsicHeight: 103 | view.layoutIfNeeded() 104 | let targetSize = CGSize(width: (presentedVC?.containerView?.bounds ?? UIScreen.main.bounds).width, 105 | height: UIView.layoutFittingCompressedSize.height) 106 | let intrinsicHeight = view.systemLayoutSizeFitting(targetSize).height 107 | return bottomYPos - (intrinsicHeight + bottomLayoutOffset) 108 | } 109 | } 110 | 111 | private var rootViewController: UIViewController? { 112 | 113 | guard let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication 114 | else { return nil } 115 | 116 | return application.keyWindow?.rootViewController 117 | } 118 | 119 | } 120 | #endif 121 | -------------------------------------------------------------------------------- /PanModal/Presentable/PanModalPresentable+UIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentable+UIViewController.swift 3 | // PanModal 4 | // 5 | // Copyright © 2018 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | Extends PanModalPresentable with helper methods 13 | when the conforming object is a UIViewController 14 | */ 15 | public extension PanModalPresentable where Self: UIViewController { 16 | 17 | typealias AnimationBlockType = () -> Void 18 | typealias AnimationCompletionType = (Bool) -> Void 19 | 20 | /** 21 | For Presentation, the object must be a UIViewController & confrom to the PanModalPresentable protocol. 22 | */ 23 | typealias LayoutType = UIViewController & PanModalPresentable 24 | 25 | /** 26 | A function wrapper over the `transition(to state: PanModalPresentationController.PresentationState)` 27 | function in the PanModalPresentationController. 28 | */ 29 | func panModalTransition(to state: PanModalPresentationController.PresentationState) { 30 | presentedVC?.transition(to: state) 31 | } 32 | 33 | /** 34 | A function wrapper over the `setNeedsLayoutUpdate()` 35 | function in the PanModalPresentationController. 36 | 37 | - Note: This should be called whenever any of the values for the PanModalPresentable protocol are changed. 38 | */ 39 | func panModalSetNeedsLayoutUpdate() { 40 | presentedVC?.setNeedsLayoutUpdate() 41 | } 42 | 43 | /** 44 | Operations on the scroll view, such as content height changes, or when inserting/deleting rows can cause the pan modal to jump, 45 | caused by the pan modal responding to content offset changes. 46 | 47 | To avoid this, you can call this method to perform scroll view updates, with scroll observation temporarily disabled. 48 | */ 49 | func panModalPerformUpdates(_ updates: () -> Void) { 50 | presentedVC?.performUpdates(updates) 51 | } 52 | 53 | /** 54 | A function wrapper over the animate function in PanModalAnimator. 55 | 56 | This can be used for animation consistency on views within the presented view controller. 57 | */ 58 | func panModalAnimate(_ animationBlock: @escaping AnimationBlockType, _ completion: AnimationCompletionType? = nil) { 59 | PanModalAnimator.animate(animationBlock, config: self, completion) 60 | } 61 | 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /PanModal/Presentable/PanModalPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresentable.swift 3 | // PanModal 4 | // 5 | // Copyright © 2017 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | This is the configuration object for a view controller 13 | that will be presented using the PanModal transition. 14 | 15 | Usage: 16 | ``` 17 | extension YourViewController: PanModalPresentable { 18 | func shouldRoundTopCorners: Bool { return false } 19 | } 20 | ``` 21 | */ 22 | public protocol PanModalPresentable: AnyObject { 23 | 24 | /** 25 | The scroll view embedded in the view controller. 26 | Setting this value allows for seamless transition scrolling between the embedded scroll view 27 | and the pan modal container view. 28 | */ 29 | var panScrollable: UIScrollView? { get } 30 | 31 | /** 32 | The offset between the top of the screen and the top of the pan modal container view. 33 | 34 | Default value is the topLayoutGuide.length + 21.0. 35 | */ 36 | var topOffset: CGFloat { get } 37 | 38 | /** 39 | The height of the pan modal container view 40 | when in the shortForm presentation state. 41 | 42 | This value is capped to .max, if provided value exceeds the space available. 43 | 44 | Default value is the longFormHeight. 45 | */ 46 | var shortFormHeight: PanModalHeight { get } 47 | 48 | /** 49 | The height of the pan modal container view 50 | when in the longForm presentation state. 51 | 52 | This value is capped to .max, if provided value exceeds the space available. 53 | 54 | Default value is .max. 55 | */ 56 | var longFormHeight: PanModalHeight { get } 57 | 58 | /** 59 | The corner radius used when `shouldRoundTopCorners` is enabled. 60 | 61 | Default Value is 8.0. 62 | */ 63 | var cornerRadius: CGFloat { get } 64 | 65 | /** 66 | The springDamping value used to determine the amount of 'bounce' 67 | seen when transitioning to short/long form. 68 | 69 | Default Value is 0.8. 70 | */ 71 | var springDamping: CGFloat { get } 72 | 73 | /** 74 | The transitionDuration value is used to set the speed of animation during a transition, 75 | including initial presentation. 76 | 77 | Default value is 0.5. 78 | */ 79 | var transitionDuration: Double { get } 80 | 81 | /** 82 | The animation options used when performing animations on the PanModal, utilized mostly 83 | during a transition. 84 | 85 | Default value is [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState]. 86 | */ 87 | var transitionAnimationOptions: UIView.AnimationOptions { get } 88 | 89 | /** 90 | The background view color. 91 | 92 | - Note: This is only utilized at the very start of the transition. 93 | 94 | Default Value is black with alpha component 0.7. 95 | */ 96 | var panModalBackgroundColor: UIColor { get } 97 | 98 | /** 99 | The drag indicator view color. 100 | 101 | Default value is light gray. 102 | */ 103 | var dragIndicatorBackgroundColor: UIColor { get } 104 | 105 | /** 106 | We configure the panScrollable's scrollIndicatorInsets interally so override this value 107 | to set custom insets. 108 | 109 | - Note: Use `panModalSetNeedsLayoutUpdate()` when updating insets. 110 | */ 111 | var scrollIndicatorInsets: UIEdgeInsets { get } 112 | 113 | /** 114 | A flag to determine if scrolling should be limited to the longFormHeight. 115 | Return false to cap scrolling at .max height. 116 | 117 | Default value is true. 118 | */ 119 | var anchorModalToLongForm: Bool { get } 120 | 121 | /** 122 | A flag to determine if scrolling should seamlessly transition from the pan modal container view to 123 | the embedded scroll view once the scroll limit has been reached. 124 | 125 | Default value is false. Unless a scrollView is provided and the content height exceeds the longForm height. 126 | */ 127 | var allowsExtendedPanScrolling: Bool { get } 128 | 129 | /** 130 | A flag to determine if dismissal should be initiated when swiping down on the presented view. 131 | 132 | Return false to fallback to the short form state instead of dismissing. 133 | 134 | Default value is true. 135 | */ 136 | var allowsDragToDismiss: Bool { get } 137 | 138 | /** 139 | A flag to determine if dismissal should be initiated when tapping on the dimmed background view. 140 | 141 | Default value is true. 142 | */ 143 | var allowsTapToDismiss: Bool { get } 144 | 145 | /** 146 | A flag to toggle user interactions on the container view. 147 | 148 | - Note: Return false to forward touches to the presentingViewController. 149 | 150 | Default is true. 151 | */ 152 | var isUserInteractionEnabled: Bool { get } 153 | 154 | /** 155 | A flag to determine if haptic feedback should be enabled during presentation. 156 | 157 | Default value is true. 158 | */ 159 | var isHapticFeedbackEnabled: Bool { get } 160 | 161 | /** 162 | A flag to determine if the top corners should be rounded. 163 | 164 | Default value is true. 165 | */ 166 | var shouldRoundTopCorners: Bool { get } 167 | 168 | /** 169 | A flag to determine if a drag indicator should be shown 170 | above the pan modal container view. 171 | 172 | Default value is true. 173 | */ 174 | var showDragIndicator: Bool { get } 175 | 176 | /** 177 | Asks the delegate if the pan modal should respond to the pan modal gesture recognizer. 178 | 179 | Return false to disable movement on the pan modal but maintain gestures on the presented view. 180 | 181 | Default value is true. 182 | */ 183 | func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool 184 | 185 | /** 186 | Notifies the delegate when the pan modal gesture recognizer state is either 187 | `began` or `changed`. This method gives the delegate a chance to prepare 188 | for the gesture recognizer state change. 189 | 190 | For example, when the pan modal view is about to scroll. 191 | 192 | Default value is an empty implementation. 193 | */ 194 | func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) 195 | 196 | /** 197 | Asks the delegate if the pan modal gesture recognizer should be prioritized. 198 | 199 | For example, you can use this to define a region 200 | where you would like to restrict where the pan gesture can start. 201 | 202 | If false, then we rely solely on the internal conditions of when a pan gesture 203 | should succeed or fail, such as, if we're actively scrolling on the scrollView. 204 | 205 | Default return value is false. 206 | */ 207 | func shouldPrioritize(panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool 208 | 209 | /** 210 | Asks the delegate if the pan modal should transition to a new state. 211 | 212 | Default value is true. 213 | */ 214 | func shouldTransition(to state: PanModalPresentationController.PresentationState) -> Bool 215 | 216 | /** 217 | Notifies the delegate that the pan modal is about to transition to a new state. 218 | 219 | Default value is an empty implementation. 220 | */ 221 | func willTransition(to state: PanModalPresentationController.PresentationState) 222 | 223 | /** 224 | Notifies the delegate that the pan modal is about to be dismissed. 225 | 226 | Default value is an empty implementation. 227 | */ 228 | func panModalWillDismiss() 229 | 230 | /** 231 | Notifies the delegate after the pan modal is dismissed. 232 | 233 | Default value is an empty implementation. 234 | */ 235 | func panModalDidDismiss() 236 | } 237 | #endif 238 | -------------------------------------------------------------------------------- /PanModal/Presenter/PanModalPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalPresenter.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | A protocol for objects that will present a view controller as a PanModal 13 | 14 | - Usage: 15 | ``` 16 | viewController.presentPanModal(viewControllerToPresent: presentingVC, 17 | sourceView: presentingVC.view, 18 | sourceRect: .zero) 19 | ``` 20 | */ 21 | protocol PanModalPresenter: AnyObject { 22 | 23 | /** 24 | A flag that returns true if the current presented view controller 25 | is using the PanModalPresentationDelegate 26 | */ 27 | var isPanModalPresented: Bool { get } 28 | 29 | /** 30 | Presents a view controller that conforms to the PanModalPresentable protocol 31 | */ 32 | func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType, 33 | sourceView: UIView?, 34 | sourceRect: CGRect, 35 | completion: (() -> Void)?) 36 | 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /PanModal/Presenter/UIViewController+PanModalPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+PanModalPresenterProtocol.swift 3 | // PanModal 4 | // 5 | // Copyright © 2019 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | Extends the UIViewController to conform to the PanModalPresenter protocol 13 | */ 14 | extension UIViewController: PanModalPresenter { 15 | 16 | /** 17 | A flag that returns true if the topmost view controller in the navigation stack 18 | was presented using the custom PanModal transition 19 | 20 | - Warning: ⚠️ Calling `presentationController` in this function may cause a memory leak. ⚠️ 21 | 22 | In most cases, this check will be used early in the view lifecycle and unfortunately, 23 | there's an Apple bug that causes multiple presentationControllers to be created if 24 | the presentationController is referenced here and called too early resulting in 25 | a strong reference to this view controller and in turn, creating a memory leak. 26 | */ 27 | public var isPanModalPresented: Bool { 28 | return (transitioningDelegate as? PanModalPresentationDelegate) != nil 29 | } 30 | 31 | /** 32 | Configures a view controller for presentation using the PanModal transition 33 | 34 | - Parameters: 35 | - viewControllerToPresent: The view controller to be presented 36 | - sourceView: The view containing the anchor rectangle for the popover. 37 | - sourceRect: The rectangle in the specified view in which to anchor the popover. 38 | - completion: The block to execute after the presentation finishes. You may specify nil for this parameter. 39 | 40 | - Note: sourceView & sourceRect are only required for presentation on an iPad. 41 | */ 42 | public func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType, 43 | sourceView: UIView? = nil, 44 | sourceRect: CGRect = .zero, 45 | completion: (() -> Void)? = nil) { 46 | 47 | /** 48 | Here, we deliberately do not check for size classes. More info in `PanModalPresentationDelegate` 49 | */ 50 | 51 | if UIDevice.current.userInterfaceIdiom == .pad { 52 | viewControllerToPresent.modalPresentationStyle = .popover 53 | viewControllerToPresent.popoverPresentationController?.sourceRect = sourceRect 54 | viewControllerToPresent.popoverPresentationController?.sourceView = sourceView ?? view 55 | viewControllerToPresent.popoverPresentationController?.delegate = PanModalPresentationDelegate.default 56 | } else { 57 | viewControllerToPresent.modalPresentationStyle = .custom 58 | viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true 59 | viewControllerToPresent.transitioningDelegate = PanModalPresentationDelegate.default 60 | } 61 | 62 | present(viewControllerToPresent, animated: true, completion: completion) 63 | } 64 | 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /PanModal/View/DimmedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DimmedView.swift 3 | // PanModal 4 | // 5 | // Copyright © 2017 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | A dim view for use as an overlay over content you want dimmed. 13 | */ 14 | public class DimmedView: UIView { 15 | 16 | /** 17 | Represents the possible states of the dimmed view. 18 | max, off or a percentage of dimAlpha. 19 | */ 20 | enum DimState { 21 | case max 22 | case off 23 | case percent(CGFloat) 24 | } 25 | 26 | // MARK: - Properties 27 | 28 | /** 29 | The state of the dimmed view 30 | */ 31 | var dimState: DimState = .off { 32 | didSet { 33 | switch dimState { 34 | case .max: 35 | alpha = 1.0 36 | case .off: 37 | alpha = 0.0 38 | case .percent(let percentage): 39 | alpha = max(0.0, min(1.0, percentage)) 40 | } 41 | } 42 | } 43 | 44 | /** 45 | The closure to be executed when a tap occurs 46 | */ 47 | var didTap: ((_ recognizer: UIGestureRecognizer) -> Void)? 48 | 49 | /** 50 | Tap gesture recognizer 51 | */ 52 | private lazy var tapGesture: UIGestureRecognizer = { 53 | return UITapGestureRecognizer(target: self, action: #selector(didTapView)) 54 | }() 55 | 56 | // MARK: - Initializers 57 | 58 | init(dimColor: UIColor = UIColor.black.withAlphaComponent(0.7)) { 59 | super.init(frame: .zero) 60 | alpha = 0.0 61 | backgroundColor = dimColor 62 | addGestureRecognizer(tapGesture) 63 | } 64 | 65 | required public init?(coder aDecoder: NSCoder) { 66 | fatalError() 67 | } 68 | 69 | // MARK: - Event Handlers 70 | 71 | @objc private func didTapView() { 72 | didTap?(tapGesture) 73 | } 74 | 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /PanModal/View/PanContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanContainerView.swift 3 | // PanModal 4 | // 5 | // Copyright © 2018 Tiny Speck, Inc. All rights reserved. 6 | // 7 | 8 | #if os(iOS) 9 | import UIKit 10 | 11 | /** 12 | A view wrapper around the presented view in a PanModal transition. 13 | 14 | This allows us to make modifications to the presented view without 15 | having to do those changes directly on the view 16 | */ 17 | class PanContainerView: UIView { 18 | 19 | init(presentedView: UIView, frame: CGRect) { 20 | super.init(frame: frame) 21 | addSubview(presentedView) 22 | } 23 | 24 | @available(*, unavailable) 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | } 30 | 31 | extension UIView { 32 | 33 | /** 34 | Convenience property for retrieving a PanContainerView instance 35 | from the view hierachy 36 | */ 37 | var panContainerView: PanContainerView? { 38 | return subviews.first(where: { view -> Bool in 39 | view is PanContainerView 40 | }) as? PanContainerView 41 | } 42 | 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /PanModalDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0F2A2C552239C119003BDB2F /* PanModal.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F2A2C532239C119003BDB2F /* PanModal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 0F2A2C582239C119003BDB2F /* PanModal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F2A2C512239C119003BDB2F /* PanModal.framework */; }; 12 | 0F2A2C592239C119003BDB2F /* PanModal.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0F2A2C512239C119003BDB2F /* PanModal.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 13 | 0F2A2C5E2239C137003BDB2F /* PanModalAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A2220BA6E500124CE1 /* PanModalAnimator.swift */; }; 14 | 0F2A2C5F2239C139003BDB2F /* PanModalPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139066216D9458007A3E64 /* PanModalPresentationAnimator.swift */; }; 15 | 0F2A2C602239C13C003BDB2F /* PanModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906C216D9458007A3E64 /* PanModalPresentationController.swift */; }; 16 | 0F2A2C612239C140003BDB2F /* PanModalPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */; }; 17 | 0F2A2C622239C148003BDB2F /* PanModalHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A4220BA76D00124CE1 /* PanModalHeight.swift */; }; 18 | 0F2A2C632239C14B003BDB2F /* PanModalPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139068216D9458007A3E64 /* PanModalPresentable.swift */; }; 19 | 0F2A2C642239C14E003BDB2F /* PanModalPresentable+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC0EE7B21917F2500208DBC /* PanModalPresentable+Defaults.swift */; }; 20 | 0F2A2C652239C151003BDB2F /* PanModalPresentable+UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139069216D9458007A3E64 /* PanModalPresentable+UIViewController.swift */; }; 21 | 0F2A2C662239C153003BDB2F /* PanModalPresentable+LayoutHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A6220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift */; }; 22 | 0F2A2C672239C157003BDB2F /* PanModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906A216D9458007A3E64 /* PanModalPresenter.swift */; }; 23 | 0F2A2C682239C15D003BDB2F /* UIViewController+PanModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A9220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift */; }; 24 | 0F2A2C692239C162003BDB2F /* DimmedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906E216D9458007A3E64 /* DimmedView.swift */; }; 25 | 0F2A2C6A2239C165003BDB2F /* PanContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9C21F03368008045A0 /* PanContainerView.swift */; }; 26 | 743CABB02225FC9F00634A5A /* UserGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABAF2225FC9F00634A5A /* UserGroupViewController.swift */; }; 27 | 743CABB22225FD1100634A5A /* UserGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB12225FD1100634A5A /* UserGroupHeaderView.swift */; }; 28 | 743CABB42225FE7700634A5A /* UserGroupMemberPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB32225FE7700634A5A /* UserGroupMemberPresentable.swift */; }; 29 | 743CABB62225FEEE00634A5A /* UserGroupHeaderPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB52225FEEE00634A5A /* UserGroupHeaderPresentable.swift */; }; 30 | 743CABB8222600C600634A5A /* UserGroupMemberCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB7222600C600634A5A /* UserGroupMemberCell.swift */; }; 31 | 743CABBC22260A0300634A5A /* Lato-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 743CABBA22260A0300634A5A /* Lato-Bold.ttf */; }; 32 | 743CABBD22260A0300634A5A /* Lato-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 743CABBB22260A0300634A5A /* Lato-Regular.ttf */; }; 33 | 743CABC72226171500634A5A /* PanModalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABC62226171500634A5A /* PanModalTests.swift */; }; 34 | 743CABD322265F2E00634A5A /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABD222265F2E00634A5A /* ProfileViewController.swift */; }; 35 | 743CB2AA222660D100665A55 /* StackedProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CB2A9222660D100665A55 /* StackedProfileViewController.swift */; }; 36 | 74C072A3220BA6E500124CE1 /* PanModalAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A2220BA6E500124CE1 /* PanModalAnimator.swift */; }; 37 | 74C072A5220BA76D00124CE1 /* PanModalHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A4220BA76D00124CE1 /* PanModalHeight.swift */; }; 38 | 74C072A7220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A6220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift */; }; 39 | 74C072AA220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C072A9220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift */; }; 40 | 943904EB2226354100859537 /* BasicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904EA2226354100859537 /* BasicViewController.swift */; }; 41 | 943904ED2226366700859537 /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904EC2226366700859537 /* AlertViewController.swift */; }; 42 | 943904EF2226383700859537 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904EE2226383700859537 /* NavigationController.swift */; }; 43 | 943904F32226484F00859537 /* UserGroupStackedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904F22226484F00859537 /* UserGroupStackedViewController.swift */; }; 44 | 944EBA2E227BB7F400C4C97B /* FullScreenNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */; }; 45 | 94795C9B21F0335D008045A0 /* PanModalPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */; }; 46 | 94795C9D21F03368008045A0 /* PanContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9C21F03368008045A0 /* PanContainerView.swift */; }; 47 | DC13905E216D90D5007A3E64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC13905D216D90D5007A3E64 /* Assets.xcassets */; }; 48 | DC139061216D93ED007A3E64 /* SampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139060216D93ED007A3E64 /* SampleViewController.swift */; }; 49 | DC139070216D9458007A3E64 /* PanModalPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139066216D9458007A3E64 /* PanModalPresentationAnimator.swift */; }; 50 | DC139071216D9458007A3E64 /* PanModalPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139068216D9458007A3E64 /* PanModalPresentable.swift */; }; 51 | DC139072216D9458007A3E64 /* PanModalPresentable+UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC139069216D9458007A3E64 /* PanModalPresentable+UIViewController.swift */; }; 52 | DC139073216D9458007A3E64 /* PanModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906A216D9458007A3E64 /* PanModalPresenter.swift */; }; 53 | DC139074216D9458007A3E64 /* PanModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906C216D9458007A3E64 /* PanModalPresentationController.swift */; }; 54 | DC139075216D9458007A3E64 /* DimmedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC13906E216D9458007A3E64 /* DimmedView.swift */; }; 55 | DC3B2EBA222A560A000C8A4A /* TransientAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3B2EB9222A560A000C8A4A /* TransientAlertViewController.swift */; }; 56 | DC3B2EBE222A58C9000C8A4A /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3B2EBD222A58C9000C8A4A /* AlertView.swift */; }; 57 | DCA741AE216D90410021F2F2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA741AD216D90410021F2F2 /* AppDelegate.swift */; }; 58 | DCC0EE7C21917F2500208DBC /* PanModalPresentable+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC0EE7B21917F2500208DBC /* PanModalPresentable+Defaults.swift */; }; 59 | /* End PBXBuildFile section */ 60 | 61 | /* Begin PBXContainerItemProxy section */ 62 | 0F2A2C562239C119003BDB2F /* PBXContainerItemProxy */ = { 63 | isa = PBXContainerItemProxy; 64 | containerPortal = DCA741A2216D90410021F2F2 /* Project object */; 65 | proxyType = 1; 66 | remoteGlobalIDString = 0F2A2C502239C119003BDB2F; 67 | remoteInfo = PanModal; 68 | }; 69 | 743CABC92226171500634A5A /* PBXContainerItemProxy */ = { 70 | isa = PBXContainerItemProxy; 71 | containerPortal = DCA741A2216D90410021F2F2 /* Project object */; 72 | proxyType = 1; 73 | remoteGlobalIDString = DCA741A9216D90410021F2F2; 74 | remoteInfo = PanModal; 75 | }; 76 | /* End PBXContainerItemProxy section */ 77 | 78 | /* Begin PBXCopyFilesBuildPhase section */ 79 | 0F2A2C5D2239C119003BDB2F /* Embed Frameworks */ = { 80 | isa = PBXCopyFilesBuildPhase; 81 | buildActionMask = 2147483647; 82 | dstPath = ""; 83 | dstSubfolderSpec = 10; 84 | files = ( 85 | 0F2A2C592239C119003BDB2F /* PanModal.framework in Embed Frameworks */, 86 | ); 87 | name = "Embed Frameworks"; 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXCopyFilesBuildPhase section */ 91 | 92 | /* Begin PBXFileReference section */ 93 | 0F2A2C512239C119003BDB2F /* PanModal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PanModal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 94 | 0F2A2C532239C119003BDB2F /* PanModal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PanModal.h; sourceTree = ""; }; 95 | 0F2A2C542239C119003BDB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96 | 743CABAF2225FC9F00634A5A /* UserGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupViewController.swift; sourceTree = ""; }; 97 | 743CABB12225FD1100634A5A /* UserGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupHeaderView.swift; sourceTree = ""; }; 98 | 743CABB32225FE7700634A5A /* UserGroupMemberPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupMemberPresentable.swift; sourceTree = ""; }; 99 | 743CABB52225FEEE00634A5A /* UserGroupHeaderPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupHeaderPresentable.swift; sourceTree = ""; }; 100 | 743CABB7222600C600634A5A /* UserGroupMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupMemberCell.swift; sourceTree = ""; }; 101 | 743CABBA22260A0300634A5A /* Lato-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Lato-Bold.ttf"; sourceTree = ""; }; 102 | 743CABBB22260A0300634A5A /* Lato-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Lato-Regular.ttf"; sourceTree = ""; }; 103 | 743CABC42226171500634A5A /* PanModalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PanModalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 104 | 743CABC62226171500634A5A /* PanModalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalTests.swift; sourceTree = ""; }; 105 | 743CABC82226171500634A5A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106 | 743CABD222265F2E00634A5A /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; 107 | 743CB2A9222660D100665A55 /* StackedProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedProfileViewController.swift; sourceTree = ""; }; 108 | 74C072A2220BA6E500124CE1 /* PanModalAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalAnimator.swift; sourceTree = ""; }; 109 | 74C072A4220BA76D00124CE1 /* PanModalHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanModalHeight.swift; sourceTree = ""; }; 110 | 74C072A6220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PanModalPresentable+LayoutHelpers.swift"; sourceTree = ""; }; 111 | 74C072A9220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+PanModalPresenter.swift"; sourceTree = ""; }; 112 | 943904EA2226354100859537 /* BasicViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicViewController.swift; sourceTree = ""; }; 113 | 943904EC2226366700859537 /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 114 | 943904EE2226383700859537 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 115 | 943904F22226484F00859537 /* UserGroupStackedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupStackedViewController.swift; sourceTree = ""; }; 116 | 944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenNavController.swift; sourceTree = ""; }; 117 | 94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresentationDelegate.swift; sourceTree = ""; }; 118 | 94795C9C21F03368008045A0 /* PanContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanContainerView.swift; sourceTree = ""; }; 119 | DC13905D216D90D5007A3E64 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 120 | DC139060216D93ED007A3E64 /* SampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleViewController.swift; sourceTree = ""; }; 121 | DC139066216D9458007A3E64 /* PanModalPresentationAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresentationAnimator.swift; sourceTree = ""; }; 122 | DC139068216D9458007A3E64 /* PanModalPresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresentable.swift; sourceTree = ""; }; 123 | DC139069216D9458007A3E64 /* PanModalPresentable+UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PanModalPresentable+UIViewController.swift"; sourceTree = ""; }; 124 | DC13906A216D9458007A3E64 /* PanModalPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresenter.swift; sourceTree = ""; }; 125 | DC13906C216D9458007A3E64 /* PanModalPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresentationController.swift; sourceTree = ""; }; 126 | DC13906E216D9458007A3E64 /* DimmedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DimmedView.swift; sourceTree = ""; }; 127 | DC3B2EB9222A560A000C8A4A /* TransientAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientAlertViewController.swift; sourceTree = ""; }; 128 | DC3B2EBD222A58C9000C8A4A /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; 129 | DCA741AA216D90410021F2F2 /* PanModalDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PanModalDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 130 | DCA741AD216D90410021F2F2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 131 | DCA741B9216D90420021F2F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 132 | DCC0EE7B21917F2500208DBC /* PanModalPresentable+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PanModalPresentable+Defaults.swift"; sourceTree = ""; }; 133 | /* End PBXFileReference section */ 134 | 135 | /* Begin PBXFrameworksBuildPhase section */ 136 | 0F2A2C4E2239C119003BDB2F /* Frameworks */ = { 137 | isa = PBXFrameworksBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | 743CABC12226171500634A5A /* Frameworks */ = { 144 | isa = PBXFrameworksBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | DCA741A7216D90410021F2F2 /* Frameworks */ = { 151 | isa = PBXFrameworksBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 0F2A2C582239C119003BDB2F /* PanModal.framework in Frameworks */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXFrameworksBuildPhase section */ 159 | 160 | /* Begin PBXGroup section */ 161 | 0F2A2C522239C119003BDB2F /* PanModal */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 0F2A2C532239C119003BDB2F /* PanModal.h */, 165 | 0F2A2C542239C119003BDB2F /* Info.plist */, 166 | ); 167 | path = PanModal; 168 | sourceTree = ""; 169 | }; 170 | 743CABAE2225FC4A00634A5A /* User Groups */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 743CABAF2225FC9F00634A5A /* UserGroupViewController.swift */, 174 | 743CABBF2226105700634A5A /* Presentables */, 175 | 743CABBE2226105200634A5A /* Views */, 176 | ); 177 | path = "User Groups"; 178 | sourceTree = ""; 179 | }; 180 | 743CABB92226093700634A5A /* Fonts */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 743CABBA22260A0300634A5A /* Lato-Bold.ttf */, 184 | 743CABBB22260A0300634A5A /* Lato-Regular.ttf */, 185 | ); 186 | path = Fonts; 187 | sourceTree = ""; 188 | }; 189 | 743CABBE2226105200634A5A /* Views */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 743CABB12225FD1100634A5A /* UserGroupHeaderView.swift */, 193 | 743CABB7222600C600634A5A /* UserGroupMemberCell.swift */, 194 | ); 195 | path = Views; 196 | sourceTree = ""; 197 | }; 198 | 743CABBF2226105700634A5A /* Presentables */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 743CABB52225FEEE00634A5A /* UserGroupHeaderPresentable.swift */, 202 | 743CABB32225FE7700634A5A /* UserGroupMemberPresentable.swift */, 203 | ); 204 | path = Presentables; 205 | sourceTree = ""; 206 | }; 207 | 743CABC52226171500634A5A /* Tests */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | 743CABC62226171500634A5A /* PanModalTests.swift */, 211 | 743CABC82226171500634A5A /* Info.plist */, 212 | ); 213 | path = Tests; 214 | sourceTree = ""; 215 | }; 216 | 743CABD122265F0400634A5A /* User Groups (Navigation Controller) */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 943904EE2226383700859537 /* NavigationController.swift */, 220 | 743CABD222265F2E00634A5A /* ProfileViewController.swift */, 221 | ); 222 | path = "User Groups (Navigation Controller)"; 223 | sourceTree = ""; 224 | }; 225 | 743CB2AB222661EA00665A55 /* User Groups (Stacked) */ = { 226 | isa = PBXGroup; 227 | children = ( 228 | 943904F22226484F00859537 /* UserGroupStackedViewController.swift */, 229 | 743CB2A9222660D100665A55 /* StackedProfileViewController.swift */, 230 | ); 231 | path = "User Groups (Stacked)"; 232 | sourceTree = ""; 233 | }; 234 | 74C072A8220BA80700124CE1 /* Presenter */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | DC13906A216D9458007A3E64 /* PanModalPresenter.swift */, 238 | 74C072A9220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift */, 239 | ); 240 | path = Presenter; 241 | sourceTree = ""; 242 | }; 243 | 944EBA2B227BB7D900C4C97B /* Basic */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | 943904EA2226354100859537 /* BasicViewController.swift */, 247 | ); 248 | path = Basic; 249 | sourceTree = ""; 250 | }; 251 | 944EBA2C227BB7E100C4C97B /* Full Screen */ = { 252 | isa = PBXGroup; 253 | children = ( 254 | 944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */, 255 | ); 256 | path = "Full Screen"; 257 | sourceTree = ""; 258 | }; 259 | DC13905F216D93AB007A3E64 /* Resources */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | 743CABB92226093700634A5A /* Fonts */, 263 | DC13905D216D90D5007A3E64 /* Assets.xcassets */, 264 | DCA741B9216D90420021F2F2 /* Info.plist */, 265 | ); 266 | path = Resources; 267 | sourceTree = ""; 268 | }; 269 | DC139062216D9431007A3E64 /* PanModal */ = { 270 | isa = PBXGroup; 271 | children = ( 272 | DC139065216D9458007A3E64 /* Animator */, 273 | DC13906B216D9458007A3E64 /* Controller */, 274 | DC139063216D9458007A3E64 /* Delegate */, 275 | DC139067216D9458007A3E64 /* Presentable */, 276 | 74C072A8220BA80700124CE1 /* Presenter */, 277 | DC13906D216D9458007A3E64 /* View */, 278 | ); 279 | path = PanModal; 280 | sourceTree = ""; 281 | }; 282 | DC139063216D9458007A3E64 /* Delegate */ = { 283 | isa = PBXGroup; 284 | children = ( 285 | 94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */, 286 | ); 287 | path = Delegate; 288 | sourceTree = ""; 289 | }; 290 | DC139065216D9458007A3E64 /* Animator */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | 74C072A2220BA6E500124CE1 /* PanModalAnimator.swift */, 294 | DC139066216D9458007A3E64 /* PanModalPresentationAnimator.swift */, 295 | ); 296 | path = Animator; 297 | sourceTree = ""; 298 | }; 299 | DC139067216D9458007A3E64 /* Presentable */ = { 300 | isa = PBXGroup; 301 | children = ( 302 | 74C072A4220BA76D00124CE1 /* PanModalHeight.swift */, 303 | DC139068216D9458007A3E64 /* PanModalPresentable.swift */, 304 | DCC0EE7B21917F2500208DBC /* PanModalPresentable+Defaults.swift */, 305 | DC139069216D9458007A3E64 /* PanModalPresentable+UIViewController.swift */, 306 | 74C072A6220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift */, 307 | ); 308 | path = Presentable; 309 | sourceTree = ""; 310 | }; 311 | DC13906B216D9458007A3E64 /* Controller */ = { 312 | isa = PBXGroup; 313 | children = ( 314 | DC13906C216D9458007A3E64 /* PanModalPresentationController.swift */, 315 | ); 316 | path = Controller; 317 | sourceTree = ""; 318 | }; 319 | DC13906D216D9458007A3E64 /* View */ = { 320 | isa = PBXGroup; 321 | children = ( 322 | DC13906E216D9458007A3E64 /* DimmedView.swift */, 323 | 94795C9C21F03368008045A0 /* PanContainerView.swift */, 324 | ); 325 | path = View; 326 | sourceTree = ""; 327 | }; 328 | DC139079216D9AAA007A3E64 /* View Controllers */ = { 329 | isa = PBXGroup; 330 | children = ( 331 | 944EBA2B227BB7D900C4C97B /* Basic */, 332 | 944EBA2C227BB7E100C4C97B /* Full Screen */, 333 | DC3B2EBB222A5882000C8A4A /* Alert */, 334 | DC3B2EBC222A5893000C8A4A /* Alert (Transient) */, 335 | 743CB2AB222661EA00665A55 /* User Groups (Stacked) */, 336 | 743CABD122265F0400634A5A /* User Groups (Navigation Controller) */, 337 | 743CABAE2225FC4A00634A5A /* User Groups */, 338 | ); 339 | path = "View Controllers"; 340 | sourceTree = ""; 341 | }; 342 | DC3B2EBB222A5882000C8A4A /* Alert */ = { 343 | isa = PBXGroup; 344 | children = ( 345 | 943904EC2226366700859537 /* AlertViewController.swift */, 346 | DC3B2EBD222A58C9000C8A4A /* AlertView.swift */, 347 | ); 348 | path = Alert; 349 | sourceTree = ""; 350 | }; 351 | DC3B2EBC222A5893000C8A4A /* Alert (Transient) */ = { 352 | isa = PBXGroup; 353 | children = ( 354 | DC3B2EB9222A560A000C8A4A /* TransientAlertViewController.swift */, 355 | ); 356 | path = "Alert (Transient)"; 357 | sourceTree = ""; 358 | }; 359 | DCA741A1216D90410021F2F2 = { 360 | isa = PBXGroup; 361 | children = ( 362 | DCA741AC216D90410021F2F2 /* Sample */, 363 | DC139062216D9431007A3E64 /* PanModal */, 364 | 743CABC52226171500634A5A /* Tests */, 365 | 0F2A2C522239C119003BDB2F /* PanModal */, 366 | DCA741AB216D90410021F2F2 /* Products */, 367 | ); 368 | sourceTree = ""; 369 | }; 370 | DCA741AB216D90410021F2F2 /* Products */ = { 371 | isa = PBXGroup; 372 | children = ( 373 | DCA741AA216D90410021F2F2 /* PanModalDemo.app */, 374 | 743CABC42226171500634A5A /* PanModalTests.xctest */, 375 | 0F2A2C512239C119003BDB2F /* PanModal.framework */, 376 | ); 377 | name = Products; 378 | sourceTree = ""; 379 | }; 380 | DCA741AC216D90410021F2F2 /* Sample */ = { 381 | isa = PBXGroup; 382 | children = ( 383 | DCA741AD216D90410021F2F2 /* AppDelegate.swift */, 384 | DC139060216D93ED007A3E64 /* SampleViewController.swift */, 385 | DC139079216D9AAA007A3E64 /* View Controllers */, 386 | DC13905F216D93AB007A3E64 /* Resources */, 387 | ); 388 | path = Sample; 389 | sourceTree = ""; 390 | }; 391 | /* End PBXGroup section */ 392 | 393 | /* Begin PBXHeadersBuildPhase section */ 394 | 0F2A2C4C2239C119003BDB2F /* Headers */ = { 395 | isa = PBXHeadersBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | 0F2A2C552239C119003BDB2F /* PanModal.h in Headers */, 399 | ); 400 | runOnlyForDeploymentPostprocessing = 0; 401 | }; 402 | /* End PBXHeadersBuildPhase section */ 403 | 404 | /* Begin PBXNativeTarget section */ 405 | 0F2A2C502239C119003BDB2F /* PanModal */ = { 406 | isa = PBXNativeTarget; 407 | buildConfigurationList = 0F2A2C5A2239C119003BDB2F /* Build configuration list for PBXNativeTarget "PanModal" */; 408 | buildPhases = ( 409 | 0F2A2C4C2239C119003BDB2F /* Headers */, 410 | 0F2A2C4D2239C119003BDB2F /* Sources */, 411 | 0F2A2C4E2239C119003BDB2F /* Frameworks */, 412 | 0F2A2C4F2239C119003BDB2F /* Resources */, 413 | ); 414 | buildRules = ( 415 | ); 416 | dependencies = ( 417 | ); 418 | name = PanModal; 419 | productName = PanModal; 420 | productReference = 0F2A2C512239C119003BDB2F /* PanModal.framework */; 421 | productType = "com.apple.product-type.framework"; 422 | }; 423 | 743CABC32226171500634A5A /* PanModalTests */ = { 424 | isa = PBXNativeTarget; 425 | buildConfigurationList = 743CABCB2226171500634A5A /* Build configuration list for PBXNativeTarget "PanModalTests" */; 426 | buildPhases = ( 427 | 743CABC02226171500634A5A /* Sources */, 428 | 743CABC12226171500634A5A /* Frameworks */, 429 | 743CABC22226171500634A5A /* Resources */, 430 | ); 431 | buildRules = ( 432 | ); 433 | dependencies = ( 434 | 743CABCA2226171500634A5A /* PBXTargetDependency */, 435 | ); 436 | name = PanModalTests; 437 | productName = PanModalTests; 438 | productReference = 743CABC42226171500634A5A /* PanModalTests.xctest */; 439 | productType = "com.apple.product-type.bundle.unit-test"; 440 | }; 441 | DCA741A9216D90410021F2F2 /* PanModalDemo */ = { 442 | isa = PBXNativeTarget; 443 | buildConfigurationList = DCA741BC216D90420021F2F2 /* Build configuration list for PBXNativeTarget "PanModalDemo" */; 444 | buildPhases = ( 445 | DCA741A6216D90410021F2F2 /* Sources */, 446 | DCA741A7216D90410021F2F2 /* Frameworks */, 447 | DCA741A8216D90410021F2F2 /* Resources */, 448 | 0F2A2C5D2239C119003BDB2F /* Embed Frameworks */, 449 | ); 450 | buildRules = ( 451 | ); 452 | dependencies = ( 453 | 0F2A2C572239C119003BDB2F /* PBXTargetDependency */, 454 | ); 455 | name = PanModalDemo; 456 | productName = PanModal; 457 | productReference = DCA741AA216D90410021F2F2 /* PanModalDemo.app */; 458 | productType = "com.apple.product-type.application"; 459 | }; 460 | /* End PBXNativeTarget section */ 461 | 462 | /* Begin PBXProject section */ 463 | DCA741A2216D90410021F2F2 /* Project object */ = { 464 | isa = PBXProject; 465 | attributes = { 466 | LastSwiftUpdateCheck = 1010; 467 | LastUpgradeCheck = 1000; 468 | ORGANIZATIONNAME = Detail; 469 | TargetAttributes = { 470 | 0F2A2C502239C119003BDB2F = { 471 | CreatedOnToolsVersion = 10.1; 472 | }; 473 | 743CABC32226171500634A5A = { 474 | CreatedOnToolsVersion = 10.1; 475 | TestTargetID = DCA741A9216D90410021F2F2; 476 | }; 477 | DCA741A9216D90410021F2F2 = { 478 | CreatedOnToolsVersion = 10.0; 479 | }; 480 | }; 481 | }; 482 | buildConfigurationList = DCA741A5216D90410021F2F2 /* Build configuration list for PBXProject "PanModalDemo" */; 483 | compatibilityVersion = "Xcode 9.3"; 484 | developmentRegion = en; 485 | hasScannedForEncodings = 0; 486 | knownRegions = ( 487 | en, 488 | Base, 489 | ); 490 | mainGroup = DCA741A1216D90410021F2F2; 491 | productRefGroup = DCA741AB216D90410021F2F2 /* Products */; 492 | projectDirPath = ""; 493 | projectRoot = ""; 494 | targets = ( 495 | DCA741A9216D90410021F2F2 /* PanModalDemo */, 496 | 743CABC32226171500634A5A /* PanModalTests */, 497 | 0F2A2C502239C119003BDB2F /* PanModal */, 498 | ); 499 | }; 500 | /* End PBXProject section */ 501 | 502 | /* Begin PBXResourcesBuildPhase section */ 503 | 0F2A2C4F2239C119003BDB2F /* Resources */ = { 504 | isa = PBXResourcesBuildPhase; 505 | buildActionMask = 2147483647; 506 | files = ( 507 | ); 508 | runOnlyForDeploymentPostprocessing = 0; 509 | }; 510 | 743CABC22226171500634A5A /* Resources */ = { 511 | isa = PBXResourcesBuildPhase; 512 | buildActionMask = 2147483647; 513 | files = ( 514 | ); 515 | runOnlyForDeploymentPostprocessing = 0; 516 | }; 517 | DCA741A8216D90410021F2F2 /* Resources */ = { 518 | isa = PBXResourcesBuildPhase; 519 | buildActionMask = 2147483647; 520 | files = ( 521 | DC13905E216D90D5007A3E64 /* Assets.xcassets in Resources */, 522 | 743CABBD22260A0300634A5A /* Lato-Regular.ttf in Resources */, 523 | 743CABBC22260A0300634A5A /* Lato-Bold.ttf in Resources */, 524 | ); 525 | runOnlyForDeploymentPostprocessing = 0; 526 | }; 527 | /* End PBXResourcesBuildPhase section */ 528 | 529 | /* Begin PBXSourcesBuildPhase section */ 530 | 0F2A2C4D2239C119003BDB2F /* Sources */ = { 531 | isa = PBXSourcesBuildPhase; 532 | buildActionMask = 2147483647; 533 | files = ( 534 | 0F2A2C5E2239C137003BDB2F /* PanModalAnimator.swift in Sources */, 535 | 0F2A2C5F2239C139003BDB2F /* PanModalPresentationAnimator.swift in Sources */, 536 | 0F2A2C602239C13C003BDB2F /* PanModalPresentationController.swift in Sources */, 537 | 0F2A2C612239C140003BDB2F /* PanModalPresentationDelegate.swift in Sources */, 538 | 0F2A2C622239C148003BDB2F /* PanModalHeight.swift in Sources */, 539 | 0F2A2C632239C14B003BDB2F /* PanModalPresentable.swift in Sources */, 540 | 0F2A2C642239C14E003BDB2F /* PanModalPresentable+Defaults.swift in Sources */, 541 | 0F2A2C652239C151003BDB2F /* PanModalPresentable+UIViewController.swift in Sources */, 542 | 0F2A2C662239C153003BDB2F /* PanModalPresentable+LayoutHelpers.swift in Sources */, 543 | 0F2A2C672239C157003BDB2F /* PanModalPresenter.swift in Sources */, 544 | 0F2A2C682239C15D003BDB2F /* UIViewController+PanModalPresenter.swift in Sources */, 545 | 0F2A2C692239C162003BDB2F /* DimmedView.swift in Sources */, 546 | 0F2A2C6A2239C165003BDB2F /* PanContainerView.swift in Sources */, 547 | ); 548 | runOnlyForDeploymentPostprocessing = 0; 549 | }; 550 | 743CABC02226171500634A5A /* Sources */ = { 551 | isa = PBXSourcesBuildPhase; 552 | buildActionMask = 2147483647; 553 | files = ( 554 | 743CABC72226171500634A5A /* PanModalTests.swift in Sources */, 555 | ); 556 | runOnlyForDeploymentPostprocessing = 0; 557 | }; 558 | DCA741A6216D90410021F2F2 /* Sources */ = { 559 | isa = PBXSourcesBuildPhase; 560 | buildActionMask = 2147483647; 561 | files = ( 562 | DC3B2EBA222A560A000C8A4A /* TransientAlertViewController.swift in Sources */, 563 | 743CABB42225FE7700634A5A /* UserGroupMemberPresentable.swift in Sources */, 564 | 74C072A3220BA6E500124CE1 /* PanModalAnimator.swift in Sources */, 565 | 743CABB8222600C600634A5A /* UserGroupMemberCell.swift in Sources */, 566 | 743CABB62225FEEE00634A5A /* UserGroupHeaderPresentable.swift in Sources */, 567 | 743CB2AA222660D100665A55 /* StackedProfileViewController.swift in Sources */, 568 | 943904EB2226354100859537 /* BasicViewController.swift in Sources */, 569 | DC139073216D9458007A3E64 /* PanModalPresenter.swift in Sources */, 570 | 943904EF2226383700859537 /* NavigationController.swift in Sources */, 571 | DC3B2EBE222A58C9000C8A4A /* AlertView.swift in Sources */, 572 | 74C072A5220BA76D00124CE1 /* PanModalHeight.swift in Sources */, 573 | 94795C9B21F0335D008045A0 /* PanModalPresentationDelegate.swift in Sources */, 574 | 943904F32226484F00859537 /* UserGroupStackedViewController.swift in Sources */, 575 | 74C072AA220BA82A00124CE1 /* UIViewController+PanModalPresenter.swift in Sources */, 576 | 943904ED2226366700859537 /* AlertViewController.swift in Sources */, 577 | 743CABB22225FD1100634A5A /* UserGroupHeaderView.swift in Sources */, 578 | DC139072216D9458007A3E64 /* PanModalPresentable+UIViewController.swift in Sources */, 579 | DC139074216D9458007A3E64 /* PanModalPresentationController.swift in Sources */, 580 | DC139071216D9458007A3E64 /* PanModalPresentable.swift in Sources */, 581 | DC139061216D93ED007A3E64 /* SampleViewController.swift in Sources */, 582 | 743CABB02225FC9F00634A5A /* UserGroupViewController.swift in Sources */, 583 | DCC0EE7C21917F2500208DBC /* PanModalPresentable+Defaults.swift in Sources */, 584 | 94795C9D21F03368008045A0 /* PanContainerView.swift in Sources */, 585 | 74C072A7220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift in Sources */, 586 | DC139075216D9458007A3E64 /* DimmedView.swift in Sources */, 587 | 743CABD322265F2E00634A5A /* ProfileViewController.swift in Sources */, 588 | DC139070216D9458007A3E64 /* PanModalPresentationAnimator.swift in Sources */, 589 | 944EBA2E227BB7F400C4C97B /* FullScreenNavController.swift in Sources */, 590 | DCA741AE216D90410021F2F2 /* AppDelegate.swift in Sources */, 591 | ); 592 | runOnlyForDeploymentPostprocessing = 0; 593 | }; 594 | /* End PBXSourcesBuildPhase section */ 595 | 596 | /* Begin PBXTargetDependency section */ 597 | 0F2A2C572239C119003BDB2F /* PBXTargetDependency */ = { 598 | isa = PBXTargetDependency; 599 | target = 0F2A2C502239C119003BDB2F /* PanModal */; 600 | targetProxy = 0F2A2C562239C119003BDB2F /* PBXContainerItemProxy */; 601 | }; 602 | 743CABCA2226171500634A5A /* PBXTargetDependency */ = { 603 | isa = PBXTargetDependency; 604 | target = DCA741A9216D90410021F2F2 /* PanModalDemo */; 605 | targetProxy = 743CABC92226171500634A5A /* PBXContainerItemProxy */; 606 | }; 607 | /* End PBXTargetDependency section */ 608 | 609 | /* Begin XCBuildConfiguration section */ 610 | 0F2A2C5B2239C119003BDB2F /* Debug */ = { 611 | isa = XCBuildConfiguration; 612 | buildSettings = { 613 | CODE_SIGN_IDENTITY = ""; 614 | CODE_SIGN_STYLE = Automatic; 615 | CURRENT_PROJECT_VERSION = 1; 616 | DEFINES_MODULE = YES; 617 | DEVELOPMENT_TEAM = 6UF7FN999R; 618 | DYLIB_COMPATIBILITY_VERSION = 1; 619 | DYLIB_CURRENT_VERSION = 1; 620 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 621 | INFOPLIST_FILE = PanModal/Info.plist; 622 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 623 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 624 | LD_RUNPATH_SEARCH_PATHS = ( 625 | "$(inherited)", 626 | "@executable_path/Frameworks", 627 | "@loader_path/Frameworks", 628 | ); 629 | PRODUCT_BUNDLE_IDENTIFIER = com.slack.PanModal; 630 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 631 | SKIP_INSTALL = YES; 632 | SWIFT_VERSION = 5.0; 633 | TARGETED_DEVICE_FAMILY = "1,2"; 634 | VERSIONING_SYSTEM = "apple-generic"; 635 | VERSION_INFO_PREFIX = ""; 636 | }; 637 | name = Debug; 638 | }; 639 | 0F2A2C5C2239C119003BDB2F /* Release */ = { 640 | isa = XCBuildConfiguration; 641 | buildSettings = { 642 | CODE_SIGN_IDENTITY = ""; 643 | CODE_SIGN_STYLE = Automatic; 644 | CURRENT_PROJECT_VERSION = 1; 645 | DEFINES_MODULE = YES; 646 | DEVELOPMENT_TEAM = 6UF7FN999R; 647 | DYLIB_COMPATIBILITY_VERSION = 1; 648 | DYLIB_CURRENT_VERSION = 1; 649 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 650 | INFOPLIST_FILE = PanModal/Info.plist; 651 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 652 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 653 | LD_RUNPATH_SEARCH_PATHS = ( 654 | "$(inherited)", 655 | "@executable_path/Frameworks", 656 | "@loader_path/Frameworks", 657 | ); 658 | PRODUCT_BUNDLE_IDENTIFIER = com.slack.PanModal; 659 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 660 | SKIP_INSTALL = YES; 661 | SWIFT_VERSION = 5.0; 662 | TARGETED_DEVICE_FAMILY = "1,2"; 663 | VERSIONING_SYSTEM = "apple-generic"; 664 | VERSION_INFO_PREFIX = ""; 665 | }; 666 | name = Release; 667 | }; 668 | 743CABCC2226171500634A5A /* Debug */ = { 669 | isa = XCBuildConfiguration; 670 | buildSettings = { 671 | BUNDLE_LOADER = "$(TEST_HOST)"; 672 | CODE_SIGN_STYLE = Automatic; 673 | INFOPLIST_FILE = Tests/Info.plist; 674 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 675 | LD_RUNPATH_SEARCH_PATHS = ( 676 | "$(inherited)", 677 | "@executable_path/Frameworks", 678 | "@loader_path/Frameworks", 679 | ); 680 | PRODUCT_BUNDLE_IDENTIFIER = slack.PanModalTests; 681 | PRODUCT_NAME = "$(TARGET_NAME)"; 682 | SWIFT_VERSION = 5.0; 683 | TARGETED_DEVICE_FAMILY = "1,2"; 684 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo"; 685 | }; 686 | name = Debug; 687 | }; 688 | 743CABCD2226171500634A5A /* Release */ = { 689 | isa = XCBuildConfiguration; 690 | buildSettings = { 691 | BUNDLE_LOADER = "$(TEST_HOST)"; 692 | CODE_SIGN_STYLE = Automatic; 693 | INFOPLIST_FILE = Tests/Info.plist; 694 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 695 | LD_RUNPATH_SEARCH_PATHS = ( 696 | "$(inherited)", 697 | "@executable_path/Frameworks", 698 | "@loader_path/Frameworks", 699 | ); 700 | PRODUCT_BUNDLE_IDENTIFIER = slack.PanModalTests; 701 | PRODUCT_NAME = "$(TARGET_NAME)"; 702 | SWIFT_VERSION = 5.0; 703 | TARGETED_DEVICE_FAMILY = "1,2"; 704 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo"; 705 | }; 706 | name = Release; 707 | }; 708 | DCA741BA216D90420021F2F2 /* Debug */ = { 709 | isa = XCBuildConfiguration; 710 | buildSettings = { 711 | ALWAYS_SEARCH_USER_PATHS = NO; 712 | CLANG_ANALYZER_NONNULL = YES; 713 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 714 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 715 | CLANG_CXX_LIBRARY = "libc++"; 716 | CLANG_ENABLE_MODULES = YES; 717 | CLANG_ENABLE_OBJC_ARC = YES; 718 | CLANG_ENABLE_OBJC_WEAK = YES; 719 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 720 | CLANG_WARN_BOOL_CONVERSION = YES; 721 | CLANG_WARN_COMMA = YES; 722 | CLANG_WARN_CONSTANT_CONVERSION = YES; 723 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 724 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 725 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 726 | CLANG_WARN_EMPTY_BODY = YES; 727 | CLANG_WARN_ENUM_CONVERSION = YES; 728 | CLANG_WARN_INFINITE_RECURSION = YES; 729 | CLANG_WARN_INT_CONVERSION = YES; 730 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 731 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 732 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 733 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 734 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 735 | CLANG_WARN_STRICT_PROTOTYPES = YES; 736 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 737 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 738 | CLANG_WARN_UNREACHABLE_CODE = YES; 739 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 740 | CODE_SIGN_IDENTITY = "iPhone Developer"; 741 | COPY_PHASE_STRIP = NO; 742 | DEBUG_INFORMATION_FORMAT = dwarf; 743 | ENABLE_STRICT_OBJC_MSGSEND = YES; 744 | ENABLE_TESTABILITY = YES; 745 | GCC_C_LANGUAGE_STANDARD = gnu11; 746 | GCC_DYNAMIC_NO_PIC = NO; 747 | GCC_NO_COMMON_BLOCKS = YES; 748 | GCC_OPTIMIZATION_LEVEL = 0; 749 | GCC_PREPROCESSOR_DEFINITIONS = ( 750 | "DEBUG=1", 751 | "$(inherited)", 752 | ); 753 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 754 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 755 | GCC_WARN_UNDECLARED_SELECTOR = YES; 756 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 757 | GCC_WARN_UNUSED_FUNCTION = YES; 758 | GCC_WARN_UNUSED_VARIABLE = YES; 759 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 760 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 761 | MTL_FAST_MATH = YES; 762 | ONLY_ACTIVE_ARCH = YES; 763 | SDKROOT = iphoneos; 764 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 765 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 766 | SWIFT_VERSION = 5.0; 767 | }; 768 | name = Debug; 769 | }; 770 | DCA741BB216D90420021F2F2 /* Release */ = { 771 | isa = XCBuildConfiguration; 772 | buildSettings = { 773 | ALWAYS_SEARCH_USER_PATHS = NO; 774 | CLANG_ANALYZER_NONNULL = YES; 775 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 776 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 777 | CLANG_CXX_LIBRARY = "libc++"; 778 | CLANG_ENABLE_MODULES = YES; 779 | CLANG_ENABLE_OBJC_ARC = YES; 780 | CLANG_ENABLE_OBJC_WEAK = YES; 781 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 782 | CLANG_WARN_BOOL_CONVERSION = YES; 783 | CLANG_WARN_COMMA = YES; 784 | CLANG_WARN_CONSTANT_CONVERSION = YES; 785 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 786 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 787 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 788 | CLANG_WARN_EMPTY_BODY = YES; 789 | CLANG_WARN_ENUM_CONVERSION = YES; 790 | CLANG_WARN_INFINITE_RECURSION = YES; 791 | CLANG_WARN_INT_CONVERSION = YES; 792 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 793 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 794 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 795 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 796 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 797 | CLANG_WARN_STRICT_PROTOTYPES = YES; 798 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 799 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 800 | CLANG_WARN_UNREACHABLE_CODE = YES; 801 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 802 | CODE_SIGN_IDENTITY = "iPhone Developer"; 803 | COPY_PHASE_STRIP = NO; 804 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 805 | ENABLE_NS_ASSERTIONS = NO; 806 | ENABLE_STRICT_OBJC_MSGSEND = YES; 807 | GCC_C_LANGUAGE_STANDARD = gnu11; 808 | GCC_NO_COMMON_BLOCKS = YES; 809 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 810 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 811 | GCC_WARN_UNDECLARED_SELECTOR = YES; 812 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 813 | GCC_WARN_UNUSED_FUNCTION = YES; 814 | GCC_WARN_UNUSED_VARIABLE = YES; 815 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 816 | MTL_ENABLE_DEBUG_INFO = NO; 817 | MTL_FAST_MATH = YES; 818 | SDKROOT = iphoneos; 819 | SWIFT_COMPILATION_MODE = wholemodule; 820 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 821 | SWIFT_VERSION = 5.0; 822 | VALIDATE_PRODUCT = YES; 823 | }; 824 | name = Release; 825 | }; 826 | DCA741BD216D90420021F2F2 /* Debug */ = { 827 | isa = XCBuildConfiguration; 828 | buildSettings = { 829 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 830 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 831 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 832 | CODE_SIGN_STYLE = Automatic; 833 | DEVELOPMENT_TEAM = 6UF7FN999R; 834 | INFOPLIST_FILE = "$(SRCROOT)/Sample/Resources/Info.plist"; 835 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 836 | LD_RUNPATH_SEARCH_PATHS = ( 837 | "$(inherited)", 838 | "@executable_path/Frameworks", 839 | ); 840 | PRODUCT_BUNDLE_IDENTIFIER = com.PanModal; 841 | PRODUCT_NAME = "$(TARGET_NAME)"; 842 | SWIFT_VERSION = 5.0; 843 | TARGETED_DEVICE_FAMILY = "1,2"; 844 | }; 845 | name = Debug; 846 | }; 847 | DCA741BE216D90420021F2F2 /* Release */ = { 848 | isa = XCBuildConfiguration; 849 | buildSettings = { 850 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 851 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 852 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 853 | CODE_SIGN_STYLE = Automatic; 854 | DEVELOPMENT_TEAM = 6UF7FN999R; 855 | INFOPLIST_FILE = "$(SRCROOT)/Sample/Resources/Info.plist"; 856 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 857 | LD_RUNPATH_SEARCH_PATHS = ( 858 | "$(inherited)", 859 | "@executable_path/Frameworks", 860 | ); 861 | PRODUCT_BUNDLE_IDENTIFIER = com.PanModal; 862 | PRODUCT_NAME = "$(TARGET_NAME)"; 863 | SWIFT_VERSION = 5.0; 864 | TARGETED_DEVICE_FAMILY = "1,2"; 865 | }; 866 | name = Release; 867 | }; 868 | /* End XCBuildConfiguration section */ 869 | 870 | /* Begin XCConfigurationList section */ 871 | 0F2A2C5A2239C119003BDB2F /* Build configuration list for PBXNativeTarget "PanModal" */ = { 872 | isa = XCConfigurationList; 873 | buildConfigurations = ( 874 | 0F2A2C5B2239C119003BDB2F /* Debug */, 875 | 0F2A2C5C2239C119003BDB2F /* Release */, 876 | ); 877 | defaultConfigurationIsVisible = 0; 878 | defaultConfigurationName = Release; 879 | }; 880 | 743CABCB2226171500634A5A /* Build configuration list for PBXNativeTarget "PanModalTests" */ = { 881 | isa = XCConfigurationList; 882 | buildConfigurations = ( 883 | 743CABCC2226171500634A5A /* Debug */, 884 | 743CABCD2226171500634A5A /* Release */, 885 | ); 886 | defaultConfigurationIsVisible = 0; 887 | defaultConfigurationName = Release; 888 | }; 889 | DCA741A5216D90410021F2F2 /* Build configuration list for PBXProject "PanModalDemo" */ = { 890 | isa = XCConfigurationList; 891 | buildConfigurations = ( 892 | DCA741BA216D90420021F2F2 /* Debug */, 893 | DCA741BB216D90420021F2F2 /* Release */, 894 | ); 895 | defaultConfigurationIsVisible = 0; 896 | defaultConfigurationName = Release; 897 | }; 898 | DCA741BC216D90420021F2F2 /* Build configuration list for PBXNativeTarget "PanModalDemo" */ = { 899 | isa = XCConfigurationList; 900 | buildConfigurations = ( 901 | DCA741BD216D90420021F2F2 /* Debug */, 902 | DCA741BE216D90420021F2F2 /* Release */, 903 | ); 904 | defaultConfigurationIsVisible = 0; 905 | defaultConfigurationName = Release; 906 | }; 907 | /* End XCConfigurationList section */ 908 | }; 909 | rootObject = DCA741A2216D90410021F2F2 /* Project object */; 910 | } 911 | -------------------------------------------------------------------------------- /PanModalDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PanModalDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PanModalDemo.xcodeproj/xcshareddata/xcschemes/PanModal.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /PanModalDemo.xcodeproj/xcshareddata/xcschemes/PanModalDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS. 3 | 4 |

5 | Screenshot Preview 6 |

7 | 8 |

9 | Platform: iOS 10.0+ 10 | Language: Swift 5 11 | CocoaPods compatible 12 | Carthage compatible 13 | License: MIT 14 |

15 | 16 |

17 | Features 18 | • Compatibility 19 | • Installation 20 | • Usage 21 | • Documentation 22 | • Contributing 23 | • Authors 24 | • License 25 |

26 | 27 |

28 | Read our blog on how Slack is getting more :thumbsup: with PanModal 29 | 30 | Swift 4.2 support can be found on the `Swift4.2` branch. 31 |

32 | 33 | ## Features 34 | 35 | * Supports any type of `UIViewController` 36 | * Seamless transition between modal and content 37 | * Maintains 60 fps performance 38 | 39 | ## Compatibility 40 | 41 | PanModal requires **iOS 10+** and is compatible with **Swift 4.2** projects. 42 | 43 | ## Installation 44 | 45 | * CocoaPods: 46 | 47 | ```ruby 48 | pod 'PanModal' 49 | ``` 50 | 51 | * Carthage: 52 | 53 | ```ruby 54 | github "slackhq/PanModal" 55 | ``` 56 | 57 | * Swift Package Manager: 58 | 59 | ```swift 60 | dependencies: [ 61 | .package(url: "https://github.com/slackhq/PanModal.git", .exact("1.2.6")), 62 | ], 63 | ``` 64 | 65 | ## Usage 66 | 67 | PanModal was designed to be used effortlessly. Simply call `presentPanModal` in the same way you would expect to present a `UIViewController` 68 | 69 | ```swift 70 | .presentPanModal(yourViewController) 71 | ``` 72 | 73 | The presented view controller must conform to `PanModalPresentable` to take advantage of the customizable options 74 | 75 | ```swift 76 | extension YourViewController: PanModalPresentable { 77 | 78 | var panScrollable: UIScrollView? { 79 | return nil 80 | } 81 | } 82 | ``` 83 | 84 | ### PanScrollable 85 | 86 | If the presented view controller has an embedded `UIScrollView` e.g. as is the case with `UITableViewController`, panModal will seamlessly transition pan gestures between the modal and the scroll view 87 | 88 | ```swift 89 | class TableViewController: UITableViewController, PanModalPresentable { 90 | 91 | var panScrollable: UIScrollView? { 92 | return tableView 93 | } 94 | } 95 | ``` 96 | 97 | ### Adjusting Heights 98 | 99 | Height values of the panModal can be adjusted by overriding `shortFormHeight` or `longFormHeight` 100 | 101 | ```swift 102 | var shortFormHeight: PanModalHeight { 103 | return .contentHeight(300) 104 | } 105 | 106 | var longFormHeight: PanModalHeight { 107 | return .maxHeightWithTopInset(40) 108 | } 109 | ``` 110 | 111 | ### Updates at Runtime 112 | 113 | Values are stored during presentation, so when adjusting at runtime you should call `panModalSetNeedsLayoutUpdate()` 114 | 115 | ```swift 116 | func viewDidLoad() { 117 | hasLoaded = true 118 | 119 | panModalSetNeedsLayoutUpdate() 120 | panModalTransition(to: .shortForm) 121 | } 122 | 123 | var shortFormHeight: PanModalHeight { 124 | if hasLoaded { 125 | return .contentHeight(200) 126 | } 127 | return .maxHeight 128 | } 129 | ``` 130 | 131 | ### Sample App 132 | 133 | Check out the [Sample App](https://github.com/slackhq/PanModal/tree/master/Sample) for more complex configurations of `PanModalPresentable`, including navigation controllers and stacked modals. 134 | 135 | ## Documentation 136 | Option + click on any of PanModal's methods or notes for detailed documentation. 137 | 138 |

139 | Screenshot Preview 140 |

141 | 142 | ## Contributing 143 | 144 | We're glad to be open sourcing this library. We use it in numerous places within the slack app and expect it to be easy to use as well as modify; we've added extensive documentation within the code to support that. 145 | 146 | We will only be fixing critical bugs, thus, for any non-critical issues or feature requests we hope to be able to rely on the community using the library to add what they need. For more information, please read the [contributing guidelines](https://github.com/slackhq/PanModal/blob/master/CONTRIBUTING.md). 147 | 148 | ## Authors 149 | 150 | [Stephen Sowole](https://github.com/ste57) • [Tosin Afolabi](https://github.com/tosinaf) 151 | 152 | ## License 153 | 154 | PanModal is released under a MIT License. See LICENSE file for details. 155 | -------------------------------------------------------------------------------- /Sample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 10/9/18. 6 | // Copyright © 2018 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | window?.rootViewController = UINavigationController(rootViewController: SampleViewController()) 19 | window?.makeKeyAndVisible() 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-57x57@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-57x57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-App-60x60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-App-60x60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-20x20@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-20x20@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-29x29@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-29x29@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-40x40@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-40x40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50x50@1x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50x50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-App-72x72@1x.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-App-72x72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-App-76x76@1x.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-App-76x76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-App-83.5x83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "size" : "1024x1024", 149 | "idiom" : "ios-marketing", 150 | "filename" : "ItunesArtwork@2x.png", 151 | "scale" : "1x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "2436h", 7 | "filename" : "iPhoneX_LaunchScreen_Placeholder.png", 8 | "minimum-system-version" : "11.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "736h", 16 | "filename" : "iPhone6Plus_LaunchScreen_Placeholder.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "portrait", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "extent" : "full-screen", 23 | "idiom" : "iphone", 24 | "subtype" : "667h", 25 | "filename" : "iPhone6_LaunchScreen_Placeholder.png", 26 | "minimum-system-version" : "8.0", 27 | "orientation" : "portrait", 28 | "scale" : "2x" 29 | }, 30 | { 31 | "orientation" : "portrait", 32 | "idiom" : "iphone", 33 | "filename" : "iPhone4_LaunchScreen_Placeholder.png", 34 | "extent" : "full-screen", 35 | "minimum-system-version" : "7.0", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "extent" : "full-screen", 40 | "idiom" : "iphone", 41 | "subtype" : "retina4", 42 | "filename" : "iPhone5_LaunchScreen_Placeholder.png", 43 | "minimum-system-version" : "7.0", 44 | "orientation" : "portrait", 45 | "scale" : "2x" 46 | } 47 | ], 48 | "info" : { 49 | "version" : 1, 50 | "author" : "xcode" 51 | } 52 | } -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone4_LaunchScreen_Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone4_LaunchScreen_Placeholder.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone5_LaunchScreen_Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone5_LaunchScreen_Placeholder.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone6Plus_LaunchScreen_Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone6Plus_LaunchScreen_Placeholder.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone6_LaunchScreen_Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhone6_LaunchScreen_Placeholder.png -------------------------------------------------------------------------------- /Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhoneX_LaunchScreen_Placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Assets.xcassets/LaunchImage.launchimage/iPhoneX_LaunchScreen_Placeholder.png -------------------------------------------------------------------------------- /Sample/Resources/Fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /Sample/Resources/Fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /Sample/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIAppFonts 24 | 25 | Lato-Regular.ttf 26 | Lato-Bold.ttf 27 | 28 | UIRequiredDeviceCapabilities 29 | 30 | armv7 31 | 32 | UIRequiresFullScreen 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Sample/Resources/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Sample/Resources/Preview.png -------------------------------------------------------------------------------- /Sample/SampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleViewController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 10/9/18. 6 | // Copyright © 2018 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SampleViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | setupView() 16 | } 17 | 18 | private func setupView() { 19 | title = "PanModal" 20 | 21 | navigationController?.navigationBar.titleTextAttributes = [ 22 | .font: UIFont(name: "Lato-Bold", size: 17)! 23 | ] 24 | 25 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: String(describing: UITableViewCell.self)) 26 | tableView.tableFooterView = UIView() 27 | tableView.separatorInset = .zero 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 31 | return 60.0 32 | } 33 | 34 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return RowType.allCases.count 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UITableViewCell.self), for: indexPath) 40 | 41 | guard let rowType = RowType(rawValue: indexPath.row) else { 42 | return cell 43 | } 44 | cell.textLabel?.textAlignment = .center 45 | cell.textLabel?.text = rowType.presentable.string 46 | cell.textLabel?.font = UIFont(name: "Lato-Regular", size: 17.0) 47 | return cell 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 51 | tableView.deselectRow(at: indexPath, animated: true) 52 | 53 | guard let rowType = RowType(rawValue: indexPath.row) else { 54 | return 55 | } 56 | dismiss(animated: true, completion: nil) 57 | presentPanModal(rowType.presentable.rowVC) 58 | } 59 | } 60 | 61 | protocol RowPresentable { 62 | var string: String { get } 63 | var rowVC: UIViewController & PanModalPresentable { get } 64 | } 65 | 66 | private extension SampleViewController { 67 | 68 | enum RowType: Int, CaseIterable { 69 | case basic 70 | case fullScreen 71 | case alert 72 | case transientAlert 73 | case userGroups 74 | case stacked 75 | case navController 76 | 77 | 78 | var presentable: RowPresentable { 79 | switch self { 80 | case .basic: return Basic() 81 | case .fullScreen: return FullScreen() 82 | case .alert: return Alert() 83 | case .transientAlert: return TransientAlert() 84 | case .userGroups: return UserGroup() 85 | case .stacked: return Stacked() 86 | case .navController: return Navigation() 87 | } 88 | } 89 | 90 | struct Basic: RowPresentable { 91 | let string: String = "Basic" 92 | let rowVC: PanModalPresentable.LayoutType = BasicViewController() 93 | } 94 | 95 | struct FullScreen: RowPresentable { 96 | let string: String = "Full Screen" 97 | let rowVC: PanModalPresentable.LayoutType = FullScreenNavController() 98 | } 99 | 100 | struct Alert: RowPresentable { 101 | let string: String = "Alert" 102 | let rowVC: PanModalPresentable.LayoutType = AlertViewController() 103 | } 104 | 105 | struct TransientAlert: RowPresentable { 106 | let string: String = "Alert (Transient)" 107 | let rowVC: PanModalPresentable.LayoutType = TransientAlertViewController() 108 | } 109 | 110 | struct UserGroup: RowPresentable { 111 | let string: String = "User Groups" 112 | let rowVC: PanModalPresentable.LayoutType = UserGroupViewController() 113 | } 114 | 115 | struct Navigation: RowPresentable { 116 | let string: String = "User Groups (NavigationController)" 117 | let rowVC: PanModalPresentable.LayoutType = NavigationController() 118 | } 119 | 120 | struct Stacked: RowPresentable { 121 | let string: String = "User Groups (Stacked)" 122 | let rowVC: PanModalPresentable.LayoutType = UserGroupStackedViewController() 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sample/View Controllers/Alert (Transient)/TransientAlertViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransientAlertViewController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 3/1/19. 6 | // Copyright © 2019 Detail. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TransientAlertViewController: AlertViewController { 12 | 13 | private weak var timer: Timer? 14 | private var countdown: Int = 5 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | alertView.titleLabel.text = "Transient Alert" 19 | updateMessage() 20 | } 21 | 22 | override func viewDidAppear(_ animated: Bool) { 23 | super.viewDidAppear(animated) 24 | startTimer() 25 | } 26 | 27 | private func startTimer() { 28 | timer?.invalidate() 29 | timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in 30 | self?.countdown -= 1 31 | self?.updateMessage() 32 | } 33 | } 34 | 35 | @objc func updateMessage() { 36 | guard countdown > 0 else { 37 | invalidateTimer() 38 | dismiss(animated: true, completion: nil) 39 | return 40 | } 41 | alertView.message.text = "Message disppears in \(countdown) seconds" 42 | } 43 | 44 | func invalidateTimer() { 45 | timer?.invalidate() 46 | } 47 | 48 | deinit { 49 | invalidateTimer() 50 | } 51 | 52 | // MARK: - Pan Modal Presentable 53 | 54 | override var showDragIndicator: Bool { 55 | return false 56 | } 57 | 58 | override var anchorModalToLongForm: Bool { 59 | return true 60 | } 61 | 62 | override var panModalBackgroundColor: UIColor { 63 | return .clear 64 | } 65 | 66 | override var isUserInteractionEnabled: Bool { 67 | return false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sample/View Controllers/Alert/AlertView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertView.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 3/1/19. 6 | // Copyright © 2019 Detail. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlertView: UIView { 12 | 13 | // MARK: - Views 14 | 15 | private let colors = [#colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1), #colorLiteral(red: 0.7176470588, green: 0.8784313725, blue: 0.9882352941, alpha: 1), #colorLiteral(red: 0.9725490196, green: 0.937254902, blue: 0.4666666667, alpha: 1), #colorLiteral(red: 0.9490196078, green: 0.7568627451, blue: 0.9803921569, alpha: 1), #colorLiteral(red: 0.9960784314, green: 0.8823529412, blue: 0.6980392157, alpha: 1)] 16 | 17 | private lazy var icon: UIView = { 18 | let icon = UIView() 19 | icon.backgroundColor = colors.randomElement() 20 | icon.layer.cornerRadius = 6.0 21 | return icon 22 | }() 23 | 24 | let titleLabel: UILabel = { 25 | let label = UILabel() 26 | label.text = "Incoming Message" 27 | label.font = UIFont(name: "Lato-Bold", size: 17.0) 28 | label.textColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1) 29 | return label 30 | }() 31 | 32 | let message: UILabel = { 33 | let label = UILabel() 34 | label.text = "This is an example alert..." 35 | label.font = UIFont(name: "Lato-Regular", size: 13.0) 36 | label.textColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 37 | return label 38 | }() 39 | 40 | private lazy var alertStackView: UIStackView = { 41 | let stackView = UIStackView(arrangedSubviews: [titleLabel, message]) 42 | stackView.axis = .vertical 43 | stackView.alignment = .leading 44 | stackView.spacing = 4.0 45 | return stackView 46 | }() 47 | 48 | init() { 49 | super.init(frame: .zero) 50 | setupView() 51 | } 52 | 53 | required init?(coder aDecoder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | // MARK: - Layout 58 | 59 | private func setupView() { 60 | backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 61 | layoutIcon() 62 | layoutStackView() 63 | } 64 | 65 | private func layoutIcon() { 66 | addSubview(icon) 67 | icon.translatesAutoresizingMaskIntoConstraints = false 68 | icon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14).isActive = true 69 | icon.topAnchor.constraint(equalTo: topAnchor, constant: 14).isActive = true 70 | icon.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -14).isActive = true 71 | icon.widthAnchor.constraint(equalTo: icon.heightAnchor).isActive = true 72 | } 73 | 74 | private func layoutStackView() { 75 | addSubview(alertStackView) 76 | alertStackView.translatesAutoresizingMaskIntoConstraints = false 77 | alertStackView.topAnchor.constraint(equalTo: icon.topAnchor).isActive = true 78 | alertStackView.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10).isActive = true 79 | alertStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14).isActive = true 80 | alertStackView.bottomAnchor.constraint(equalTo: icon.bottomAnchor).isActive = true 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sample/View Controllers/Alert/AlertViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertViewController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AlertViewController: UIViewController, PanModalPresentable { 12 | 13 | private let alertViewHeight: CGFloat = 68 14 | 15 | let alertView: AlertView = { 16 | let alertView = AlertView() 17 | alertView.layer.cornerRadius = 10 18 | return alertView 19 | }() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | setupView() 24 | } 25 | 26 | private func setupView() { 27 | view.addSubview(alertView) 28 | alertView.translatesAutoresizingMaskIntoConstraints = false 29 | alertView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 30 | alertView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true 31 | alertView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true 32 | alertView.heightAnchor.constraint(equalToConstant: alertViewHeight).isActive = true 33 | } 34 | 35 | // MARK: - PanModalPresentable 36 | 37 | var panScrollable: UIScrollView? { 38 | return nil 39 | } 40 | 41 | var shortFormHeight: PanModalHeight { 42 | return .contentHeight(alertViewHeight) 43 | } 44 | 45 | var longFormHeight: PanModalHeight { 46 | return shortFormHeight 47 | } 48 | 49 | var panModalBackgroundColor: UIColor { 50 | return UIColor.black.withAlphaComponent(0.1) 51 | } 52 | 53 | var shouldRoundTopCorners: Bool { 54 | return false 55 | } 56 | 57 | var showDragIndicator: Bool { 58 | return true 59 | } 60 | 61 | var anchorModalToLongForm: Bool { 62 | return false 63 | } 64 | 65 | var isUserInteractionEnabled: Bool { 66 | return true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sample/View Controllers/Basic/BasicViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicViewController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BasicViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | view.backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 16 | } 17 | } 18 | 19 | extension BasicViewController: PanModalPresentable { 20 | 21 | override var preferredStatusBarStyle: UIStatusBarStyle { 22 | return .lightContent 23 | } 24 | 25 | var panScrollable: UIScrollView? { 26 | return nil 27 | } 28 | 29 | var longFormHeight: PanModalHeight { 30 | return .maxHeightWithTopInset(200) 31 | } 32 | 33 | var anchorModalToLongForm: Bool { 34 | return false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sample/View Controllers/Full Screen/FullScreenNavController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullScreenNavController.swift 3 | // PanModalDemo 4 | // 5 | // Created by Stephen Sowole on 5/2/19. 6 | // Copyright © 2019 Detail. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FullScreenNavController: UINavigationController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | pushViewController(FullScreenViewController(), animated: false) 16 | } 17 | } 18 | 19 | extension FullScreenNavController: PanModalPresentable { 20 | 21 | var panScrollable: UIScrollView? { 22 | return nil 23 | } 24 | 25 | var topOffset: CGFloat { 26 | return 0.0 27 | } 28 | 29 | var springDamping: CGFloat { 30 | return 1.0 31 | } 32 | 33 | var transitionDuration: Double { 34 | return 0.4 35 | } 36 | 37 | var transitionAnimationOptions: UIView.AnimationOptions { 38 | return [.allowUserInteraction, .beginFromCurrentState] 39 | } 40 | 41 | var shouldRoundTopCorners: Bool { 42 | return false 43 | } 44 | 45 | var showDragIndicator: Bool { 46 | return false 47 | } 48 | } 49 | 50 | private class FullScreenViewController: UIViewController { 51 | 52 | let textLabel: UILabel = { 53 | let label = UILabel() 54 | label.text = "Drag downwards to dismiss" 55 | label.font = UIFont(name: "Lato-Bold", size: 17) 56 | label.translatesAutoresizingMaskIntoConstraints = false 57 | return label 58 | }() 59 | 60 | override func viewDidLoad() { 61 | super.viewDidLoad() 62 | title = "Full Screen" 63 | view.backgroundColor = .white 64 | setupConstraints() 65 | } 66 | 67 | private func setupConstraints() { 68 | view.addSubview(textLabel) 69 | textLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 70 | textLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups (Navigation Controller)/NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NavigationController: UINavigationController, PanModalPresentable { 12 | 13 | private let navGroups = NavUserGroups() 14 | 15 | init() { 16 | super.init(nibName: nil, bundle: nil) 17 | viewControllers = [navGroups] 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError() 22 | } 23 | 24 | override var preferredStatusBarStyle: UIStatusBarStyle { 25 | return .lightContent 26 | } 27 | 28 | override func popViewController(animated: Bool) -> UIViewController? { 29 | let vc = super.popViewController(animated: animated) 30 | panModalSetNeedsLayoutUpdate() 31 | return vc 32 | } 33 | 34 | override func pushViewController(_ viewController: UIViewController, animated: Bool) { 35 | super.pushViewController(viewController, animated: animated) 36 | panModalSetNeedsLayoutUpdate() 37 | } 38 | 39 | // MARK: - Pan Modal Presentable 40 | 41 | var panScrollable: UIScrollView? { 42 | return (topViewController as? PanModalPresentable)?.panScrollable 43 | } 44 | 45 | var longFormHeight: PanModalHeight { 46 | return .maxHeight 47 | } 48 | 49 | var shortFormHeight: PanModalHeight { 50 | return longFormHeight 51 | } 52 | } 53 | 54 | private class NavUserGroups: UserGroupViewController { 55 | 56 | override func viewDidLoad() { 57 | super.viewDidLoad() 58 | 59 | title = "iOS Engineers" 60 | 61 | navigationController?.navigationBar.isTranslucent = false 62 | navigationController?.navigationBar.titleTextAttributes = [ 63 | .font: UIFont(name: "Lato-Bold", size: 17)!, 64 | .foregroundColor: #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 65 | ] 66 | navigationController?.navigationBar.tintColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 67 | navigationController?.navigationBar.barTintColor = #colorLiteral(red: 0.1294117647, green: 0.1411764706, blue: 0.1568627451, alpha: 1) 68 | 69 | navigationItem.backBarButtonItem = UIBarButtonItem(title:"", style: .plain, target: nil, action: nil) 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 73 | tableView.deselectRow(at: indexPath, animated: true) 74 | 75 | let presentable = members[indexPath.row] 76 | let viewController = ProfileViewController(presentable: presentable) 77 | 78 | navigationController?.pushViewController(viewController, animated: true) 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups (Navigation Controller)/ProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewController.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ProfileViewController: UIViewController { 12 | 13 | // MARK: - Properties 14 | 15 | let presentable: UserGroupMemberPresentable 16 | 17 | // MARK: - Views 18 | 19 | let avatarView: UIView = { 20 | let view = UIView() 21 | view.layer.cornerRadius = 6.0 22 | view.translatesAutoresizingMaskIntoConstraints = false 23 | return view 24 | }() 25 | 26 | let nameLabel: UILabel = { 27 | let label = UILabel() 28 | label.textColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1) 29 | label.font = UIFont(name: "Lato-Bold", size: 20.0) 30 | label.backgroundColor = .clear 31 | label.translatesAutoresizingMaskIntoConstraints = false 32 | return label 33 | }() 34 | 35 | let roleLabel: UILabel = { 36 | let label = UILabel() 37 | label.textColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 38 | label.backgroundColor = .clear 39 | label.font = UIFont(name: "Lato-Regular", size: 16.0) 40 | label.translatesAutoresizingMaskIntoConstraints = false 41 | return label 42 | }() 43 | 44 | // MARK: - Initializers 45 | 46 | init(presentable: UserGroupMemberPresentable) { 47 | self.presentable = presentable 48 | super.init(nibName: nil, bundle: nil) 49 | } 50 | 51 | required init?(coder aDecoder: NSCoder) { 52 | fatalError("init(coder:) has not been implemented") 53 | } 54 | 55 | // MARK: - View Lifecycle 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | 60 | title = "Profile" 61 | view.backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 62 | 63 | view.addSubview(avatarView) 64 | view.addSubview(nameLabel) 65 | view.addSubview(roleLabel) 66 | 67 | nameLabel.text = presentable.name 68 | roleLabel.text = presentable.role 69 | avatarView.backgroundColor = presentable.avatarBackgroundColor 70 | 71 | setupConstraints() 72 | } 73 | 74 | // MARK: - Layoutt 75 | 76 | func setupConstraints() { 77 | 78 | avatarView.widthAnchor.constraint(equalToConstant: 200.0).isActive = true 79 | avatarView.heightAnchor.constraint(equalToConstant: 200.0).isActive = true 80 | avatarView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 81 | avatarView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0).isActive = true 82 | 83 | nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 84 | nameLabel.topAnchor.constraint(equalTo: avatarView.bottomAnchor, constant: 60.0).isActive = true 85 | 86 | roleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 87 | roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8.0).isActive = true 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups (Stacked)/StackedProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackedProfileViewController.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StackedProfileViewController: UIViewController, PanModalPresentable { 12 | 13 | // MARK: - Properties 14 | 15 | let presentable: UserGroupMemberPresentable 16 | 17 | override var preferredStatusBarStyle: UIStatusBarStyle { 18 | return .lightContent 19 | } 20 | 21 | // MARK: - Views 22 | 23 | let avatarView: UIView = { 24 | let view = UIView() 25 | view.layer.cornerRadius = 6.0 26 | view.translatesAutoresizingMaskIntoConstraints = false 27 | return view 28 | }() 29 | 30 | let nameLabel: UILabel = { 31 | let label = UILabel() 32 | label.textColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1) 33 | label.font = UIFont(name: "Lato-Bold", size: 20.0) 34 | label.backgroundColor = .clear 35 | label.translatesAutoresizingMaskIntoConstraints = false 36 | return label 37 | }() 38 | 39 | let roleLabel: UILabel = { 40 | let label = UILabel() 41 | label.textColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 42 | label.backgroundColor = .clear 43 | label.font = UIFont(name: "Lato-Regular", size: 15.0) 44 | label.translatesAutoresizingMaskIntoConstraints = false 45 | return label 46 | }() 47 | 48 | // MARK: - Initializers 49 | 50 | init(presentable: UserGroupMemberPresentable) { 51 | self.presentable = presentable 52 | super.init(nibName: nil, bundle: nil) 53 | } 54 | 55 | required init?(coder aDecoder: NSCoder) { 56 | fatalError("init(coder:) has not been implemented") 57 | } 58 | 59 | // MARK: - View Lifecycle 60 | 61 | override func viewDidLoad() { 62 | super.viewDidLoad() 63 | 64 | title = "Profile" 65 | view.backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 66 | 67 | view.addSubview(avatarView) 68 | view.addSubview(nameLabel) 69 | view.addSubview(roleLabel) 70 | 71 | nameLabel.text = presentable.name 72 | roleLabel.text = presentable.role 73 | avatarView.backgroundColor = presentable.avatarBackgroundColor 74 | 75 | setupConstraints() 76 | } 77 | 78 | // MARK: - Layoutt 79 | 80 | func setupConstraints() { 81 | 82 | avatarView.widthAnchor.constraint(equalToConstant: 200.0).isActive = true 83 | avatarView.heightAnchor.constraint(equalToConstant: 200.0).isActive = true 84 | avatarView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 85 | avatarView.topAnchor.constraint(equalTo: view.topAnchor, constant: 25.0).isActive = true 86 | 87 | nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 88 | nameLabel.topAnchor.constraint(equalTo: avatarView.bottomAnchor, constant: 20.0).isActive = true 89 | 90 | roleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 91 | roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4.0).isActive = true 92 | bottomLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: roleLabel.bottomAnchor).isActive = true 93 | } 94 | 95 | // MARK: - Pan Modal Presentable 96 | 97 | var panScrollable: UIScrollView? { 98 | return nil 99 | } 100 | 101 | var longFormHeight: PanModalHeight { 102 | return .intrinsicHeight 103 | } 104 | 105 | var anchorModalToLongForm: Bool { 106 | return false 107 | } 108 | 109 | var shouldRoundTopCorners: Bool { 110 | return true 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups (Stacked)/UserGroupStackedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupStackedViewController.swift 3 | // PanModal 4 | // 5 | // Created by Stephen Sowole on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserGroupStackedViewController: UserGroupViewController { 12 | 13 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 14 | tableView.deselectRow(at: indexPath, animated: true) 15 | 16 | let presentable = members[indexPath.row] 17 | let viewController = StackedProfileViewController(presentable: presentable) 18 | 19 | presentPanModal(viewController) 20 | } 21 | 22 | override var shortFormHeight: PanModalHeight { 23 | return longFormHeight 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups/Presentables/UserGroupHeaderPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupHeaderPresentable.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct UserGroupHeaderPresentable: Equatable { 12 | 13 | let handle: String 14 | let description: String 15 | let memberCount: Int 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups/Presentables/UserGroupMemberPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupMemberPresentable.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct UserGroupMemberPresentable: Equatable { 12 | 13 | let name: String 14 | let role: String 15 | let avatarBackgroundColor: UIColor 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups/UserGroupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupViewController.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserGroupViewController: UITableViewController, PanModalPresentable { 12 | 13 | let members: [UserGroupMemberPresentable] = [ 14 | UserGroupMemberPresentable(name: "Naida Schill ✈️", role: "Staff Engineer - Mobile DevXP", avatarBackgroundColor: #colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1)), 15 | UserGroupMemberPresentable(name: "Annalisa Doty", role: "iOS Engineer - NewXP", avatarBackgroundColor: #colorLiteral(red: 0.7176470588, green: 0.8784313725, blue: 0.9882352941, alpha: 1)), 16 | UserGroupMemberPresentable(name: "Petra Gazaway 🏡", role: "Senior iOS Product Engineer - Enterprise", avatarBackgroundColor: #colorLiteral(red: 0.9725490196, green: 0.937254902, blue: 0.4666666667, alpha: 1)), 17 | UserGroupMemberPresentable(name: "Jermaine Gill ⛷", role: "Staff Engineer - Mobile Infra", avatarBackgroundColor: #colorLiteral(red: 0.9490196078, green: 0.7568627451, blue: 0.9803921569, alpha: 1)), 18 | UserGroupMemberPresentable(name: "Juana Brooks 🚌", role: "Staff Software Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9960784314, green: 0.8823529412, blue: 0.6980392157, alpha: 1)), 19 | UserGroupMemberPresentable(name: "Stacey Francis 🛳", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.8784313725, green: 0.8745098039, blue: 0.9921568627, alpha: 1)), 20 | UserGroupMemberPresentable(name: "Frederick Vargas", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1)), 21 | UserGroupMemberPresentable(name: "Michele Owens", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.7176470588, green: 0.8784313725, blue: 0.9882352941, alpha: 1)), 22 | UserGroupMemberPresentable(name: "Freda Ramsey", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9725490196, green: 0.937254902, blue: 0.4666666667, alpha: 1)), 23 | UserGroupMemberPresentable(name: "Anita Thomas", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9490196078, green: 0.7568627451, blue: 0.9803921569, alpha: 1)), 24 | UserGroupMemberPresentable(name: "Leona Lane", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9960784314, green: 0.8823529412, blue: 0.6980392157, alpha: 1)), 25 | UserGroupMemberPresentable(name: "Chad Roy", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.8784313725, green: 0.8745098039, blue: 0.9921568627, alpha: 1)), 26 | UserGroupMemberPresentable(name: "Joan Guzman", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1)), 27 | UserGroupMemberPresentable(name: "Mike Yates", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.7176470588, green: 0.8784313725, blue: 0.9882352941, alpha: 1)), 28 | UserGroupMemberPresentable(name: "Elbert Wilson", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9725490196, green: 0.937254902, blue: 0.4666666667, alpha: 1)), 29 | UserGroupMemberPresentable(name: "Anita Thomas", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9490196078, green: 0.7568627451, blue: 0.9803921569, alpha: 1)), 30 | UserGroupMemberPresentable(name: "Leona Lane", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.9960784314, green: 0.8823529412, blue: 0.6980392157, alpha: 1)), 31 | UserGroupMemberPresentable(name: "Chad Roy", role: "Senior iOS Engineer", avatarBackgroundColor: #colorLiteral(red: 0.8784313725, green: 0.8745098039, blue: 0.9921568627, alpha: 1)), 32 | UserGroupMemberPresentable(name: "Naida Schill", role: "Staff Engineer - Mobile DevXP", avatarBackgroundColor: #colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1)) 33 | ] 34 | 35 | var isShortFormEnabled = true 36 | 37 | override var preferredStatusBarStyle: UIStatusBarStyle { 38 | return .lightContent 39 | } 40 | 41 | let headerView = UserGroupHeaderView() 42 | 43 | let headerPresentable = UserGroupHeaderPresentable.init(handle: "ios-engs", description: "iOS Engineers", memberCount: 10) 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | setupTableView() 49 | } 50 | 51 | // MARK: - View Configurations 52 | 53 | func setupTableView() { 54 | 55 | tableView.separatorStyle = .none 56 | tableView.backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 57 | tableView.register(UserGroupMemberCell.self, forCellReuseIdentifier: "cell") 58 | } 59 | 60 | // MARK: - UITableViewDataSource 61 | 62 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 63 | return members.count 64 | } 65 | 66 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 67 | 68 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? UserGroupMemberCell 69 | else { return UITableViewCell() } 70 | 71 | cell.configure(with: members[indexPath.row]) 72 | return cell 73 | } 74 | 75 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 76 | return 60.0 77 | } 78 | 79 | // MARK: - UITableViewDelegate 80 | 81 | override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 82 | headerView.configure(with: headerPresentable) 83 | return headerView 84 | } 85 | 86 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 87 | tableView.deselectRow(at: indexPath, animated: true) 88 | } 89 | 90 | // MARK: - Pan Modal Presentable 91 | 92 | var panScrollable: UIScrollView? { 93 | return tableView 94 | } 95 | 96 | var shortFormHeight: PanModalHeight { 97 | return isShortFormEnabled ? .contentHeight(300.0) : longFormHeight 98 | } 99 | 100 | var scrollIndicatorInsets: UIEdgeInsets { 101 | let bottomOffset = presentingViewController?.bottomLayoutGuide.length ?? 0 102 | return UIEdgeInsets(top: headerView.frame.size.height, left: 0, bottom: bottomOffset, right: 0) 103 | } 104 | 105 | var anchorModalToLongForm: Bool { 106 | return false 107 | } 108 | 109 | func shouldPrioritize(panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool { 110 | let location = panModalGestureRecognizer.location(in: view) 111 | return headerView.frame.contains(location) 112 | } 113 | 114 | func willTransition(to state: PanModalPresentationController.PresentationState) { 115 | guard isShortFormEnabled, case .longForm = state 116 | else { return } 117 | 118 | isShortFormEnabled = false 119 | panModalSetNeedsLayoutUpdate() 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups/Views/UserGroupHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupHeaderView.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserGroupHeaderView: UIView { 12 | 13 | struct Constants { 14 | static let contentInsets = UIEdgeInsets(top: 12.0, left: 16.0, bottom: 12.0, right: 16.0) 15 | } 16 | 17 | // MARK: - Views 18 | 19 | let titleLabel: UILabel = { 20 | let label = UILabel() 21 | label.font = UIFont(name: "Lato-Bold", size: 17.0) 22 | label.textColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1) 23 | return label 24 | }() 25 | 26 | let subtitleLabel: UILabel = { 27 | let label = UILabel() 28 | label.numberOfLines = 2 29 | label.textColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 30 | label.font = UIFont(name: "Lato-Regular", size: 13.0) 31 | return label 32 | }() 33 | 34 | lazy var stackView: UIStackView = { 35 | let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 36 | stackView.axis = .vertical 37 | stackView.alignment = .leading 38 | stackView.spacing = 4.0 39 | stackView.translatesAutoresizingMaskIntoConstraints = false 40 | return stackView 41 | }() 42 | 43 | let seperatorView: UIView = { 44 | let view = UIView() 45 | view.backgroundColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1).withAlphaComponent(0.11) 46 | view.translatesAutoresizingMaskIntoConstraints = false 47 | return view 48 | }() 49 | 50 | // MARK: - Initializers 51 | 52 | override init(frame: CGRect) { 53 | super.init(frame: frame) 54 | 55 | backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 56 | 57 | addSubview(stackView) 58 | addSubview(seperatorView) 59 | 60 | setupConstraints() 61 | } 62 | 63 | required init?(coder aDecoder: NSCoder) { 64 | fatalError("init(coder:) has not been implemented") 65 | } 66 | 67 | // MARK: - Layout 68 | 69 | func setupConstraints() { 70 | 71 | stackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.contentInsets.top).isActive = true 72 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.contentInsets.left).isActive = true 73 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.contentInsets.right).isActive = true 74 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.contentInsets.bottom).isActive = true 75 | 76 | seperatorView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 77 | seperatorView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 78 | seperatorView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 79 | seperatorView.heightAnchor.constraint(equalToConstant: 1.0).isActive = true 80 | } 81 | 82 | // MARK: - View Configuration 83 | 84 | func configure(with presentable: UserGroupHeaderPresentable) { 85 | titleLabel.text = "@\(presentable.handle)" 86 | subtitleLabel.text = "\(presentable.memberCount) members | \(presentable.description)" 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Sample/View Controllers/User Groups/Views/UserGroupMemberCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserGroupMemberCell.swift 3 | // PanModal 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserGroupMemberCell: UITableViewCell { 12 | 13 | struct Constants { 14 | static let contentInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 8.0, right: 16.0) 15 | static let avatarSize = CGSize(width: 36.0, height: 36.0) 16 | } 17 | 18 | // MARK: - Properties 19 | 20 | var presentable = UserGroupMemberPresentable(name: "", role: "", avatarBackgroundColor: .black) 21 | 22 | // MARK: - Views 23 | 24 | let avatarView: UIView = { 25 | let view = UIView() 26 | view.layer.cornerRadius = 8.0 27 | return view 28 | }() 29 | 30 | let nameLabel: UILabel = { 31 | let label = UILabel() 32 | label.textColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1) 33 | label.font = UIFont(name: "Lato-Bold", size: 17.0) 34 | label.backgroundColor = .clear 35 | return label 36 | }() 37 | 38 | let roleLabel: UILabel = { 39 | let label = UILabel() 40 | label.textColor = #colorLiteral(red: 0.7019607843, green: 0.7058823529, blue: 0.7137254902, alpha: 1) 41 | label.backgroundColor = .clear 42 | label.font = UIFont(name: "Lato-Regular", size: 13.0) 43 | return label 44 | }() 45 | 46 | lazy var memberDetailsStackView: UIStackView = { 47 | let stackView = UIStackView(arrangedSubviews: [nameLabel, roleLabel]) 48 | stackView.axis = .vertical 49 | stackView.alignment = .leading 50 | stackView.translatesAutoresizingMaskIntoConstraints = false 51 | return stackView 52 | }() 53 | 54 | lazy var stackView: UIStackView = { 55 | let stackView = UIStackView(arrangedSubviews: [avatarView, memberDetailsStackView]) 56 | stackView.alignment = .center 57 | stackView.spacing = 16.0 58 | stackView.translatesAutoresizingMaskIntoConstraints = false 59 | return stackView 60 | }() 61 | 62 | // MARK: - Initializers 63 | 64 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 65 | super.init(style: style, reuseIdentifier: reuseIdentifier) 66 | 67 | backgroundColor = #colorLiteral(red: 0.1019607843, green: 0.1137254902, blue: 0.1294117647, alpha: 1) 68 | isAccessibilityElement = true 69 | 70 | let backgroundView = UIView() 71 | backgroundView.backgroundColor = #colorLiteral(red: 0.8196078431, green: 0.8235294118, blue: 0.8274509804, alpha: 1).withAlphaComponent(0.11) 72 | selectedBackgroundView = backgroundView 73 | 74 | contentView.addSubview(stackView) 75 | 76 | setupConstraints() 77 | } 78 | 79 | required init?(coder aDecoder: NSCoder) { 80 | fatalError("init(coder:) has not been implemented") 81 | } 82 | 83 | // MARK: - Layout 84 | 85 | func setupConstraints() { 86 | 87 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.contentInsets.top).isActive = true 88 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.contentInsets.left).isActive = true 89 | stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.contentInsets.right).isActive = true 90 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.contentInsets.bottom).isActive = true 91 | 92 | let avatarWidthConstriant = avatarView.widthAnchor.constraint(equalToConstant: Constants.avatarSize.width) 93 | let avatarHeightConstraint = avatarView.heightAnchor.constraint(equalToConstant: Constants.avatarSize.height) 94 | 95 | [avatarWidthConstriant, avatarHeightConstraint].forEach { 96 | $0.priority = UILayoutPriority(UILayoutPriority.required.rawValue - 1) 97 | $0.isActive = true 98 | } 99 | } 100 | 101 | // MARK: - Highlight 102 | 103 | /** 104 | On cell selection or highlight, iOS makes all vies have a clear background 105 | the below methods address the issue for the avatar view 106 | */ 107 | 108 | override func setHighlighted(_ highlighted: Bool, animated: Bool) { 109 | super.setHighlighted(highlighted, animated: animated) 110 | avatarView.backgroundColor = presentable.avatarBackgroundColor 111 | } 112 | 113 | override func setSelected(_ selected: Bool, animated: Bool) { 114 | super.setSelected(selected, animated: animated) 115 | avatarView.backgroundColor = presentable.avatarBackgroundColor 116 | } 117 | 118 | // MARK: - View Configuration 119 | 120 | func configure(with presentable: UserGroupMemberPresentable) { 121 | self.presentable = presentable 122 | nameLabel.text = presentable.name 123 | roleLabel.text = presentable.role 124 | avatarView.backgroundColor = presentable.avatarBackgroundColor 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /Screenshots/documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Screenshots/documentation.png -------------------------------------------------------------------------------- /Screenshots/panModal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/PanModal/b2f5bd7d169cba564d5c5b7203864f79be3fe826/Screenshots/panModal.gif -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/PanModalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanModalTests.swift 3 | // PanModalTests 4 | // 5 | // Created by Tosin Afolabi on 2/26/19. 6 | // Copyright © 2019 PanModal. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PanModal 11 | 12 | /** 13 | ⚠️ Run tests on iPhone 8 iOS (12.1) Sim 14 | */ 15 | 16 | class PanModalTests: XCTestCase { 17 | 18 | class MockViewController: UIViewController, PanModalPresentable { 19 | var panScrollable: UIScrollView? { return nil } 20 | } 21 | 22 | class AdjustedMockViewController: UITableViewController, PanModalPresentable { 23 | var panScrollable: UIScrollView? { return tableView } 24 | var shortFormHeight: PanModalHeight { return .contentHeight(300) } 25 | var longFormHeight: PanModalHeight { return .maxHeightWithTopInset(50) } 26 | // for testing purposes - to mimic safe area insets 27 | var topLayoutOffset: CGFloat { return 20 } 28 | var bottomLayoutOffset: CGFloat { return 44 } 29 | } 30 | 31 | private var vc: AdjustedMockViewController! 32 | 33 | override func setUp() { 34 | super.setUp() 35 | vc = AdjustedMockViewController() 36 | } 37 | 38 | override func tearDown() { 39 | super.tearDown() 40 | vc = nil 41 | } 42 | 43 | func testPresentableDefaults() { 44 | 45 | let vc = MockViewController() 46 | 47 | XCTAssertEqual(vc.topOffset, 41.0) 48 | XCTAssertEqual(vc.shortFormHeight, PanModalHeight.maxHeight) 49 | XCTAssertEqual(vc.longFormHeight, PanModalHeight.maxHeight) 50 | XCTAssertEqual(vc.springDamping, 0.8) 51 | XCTAssertEqual(vc.panModalBackgroundColor, UIColor.black.withAlphaComponent(0.7)) 52 | XCTAssertEqual(vc.dragIndicatorBackgroundColor, UIColor.lightGray) 53 | XCTAssertEqual(vc.scrollIndicatorInsets, .zero) 54 | XCTAssertEqual(vc.anchorModalToLongForm, true) 55 | XCTAssertEqual(vc.allowsExtendedPanScrolling, false) 56 | XCTAssertEqual(vc.allowsDragToDismiss, true) 57 | XCTAssertEqual(vc.allowsTapToDismiss, true) 58 | XCTAssertEqual(vc.isUserInteractionEnabled, true) 59 | XCTAssertEqual(vc.isHapticFeedbackEnabled, true) 60 | XCTAssertEqual(vc.shouldRoundTopCorners, false) 61 | XCTAssertEqual(vc.showDragIndicator, false) 62 | XCTAssertEqual(vc.shouldRoundTopCorners, false) 63 | XCTAssertEqual(vc.cornerRadius, 8.0) 64 | XCTAssertEqual(vc.transitionDuration, PanModalAnimator.Constants.defaultTransitionDuration) 65 | XCTAssertEqual(vc.transitionAnimationOptions, [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState]) 66 | } 67 | 68 | func testPresentableYValues() { 69 | 70 | XCTAssertEqual(vc.topLayoutOffset, 20) 71 | XCTAssertEqual(vc.bottomLayoutOffset, 44) 72 | 73 | XCTAssertEqual(vc.topMargin(from: .maxHeight), 0) 74 | XCTAssertEqual(vc.topMargin(from: .maxHeightWithTopInset(40)), 40) 75 | XCTAssertEqual(vc.topMargin(from: .contentHeight(200)), 447) 76 | XCTAssertEqual(vc.topMargin(from: .contentHeightIgnoringSafeArea(200)), 447) 77 | 78 | XCTAssertEqual(vc.shortFormYPos, 388) 79 | XCTAssertEqual(vc.longFormYPos, 91) 80 | XCTAssertEqual(vc.bottomYPos, vc.view.frame.height) 81 | 82 | XCTAssertEqual(vc.view.frame.height, UIScreen.main.bounds.size.height - 20) 83 | } 84 | } 85 | --------------------------------------------------------------------------------