├── .codeclimate.yml ├── .codecov.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .swift-version ├── .tailor.yml ├── .travis.yml ├── CHANGELOG.md ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Panels │ ├── MapPanelContentViewController.swift │ ├── Text2PanelContentViewController.swift │ └── TextPanelContentViewController.swift │ └── ViewController.swift ├── LICENSE ├── PanelKit Test Host ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── PanelKit UI Test Host ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── Panels │ ├── MapPanelContentViewController.swift │ └── TextPanelContentViewController.swift └── ViewController.swift ├── PanelKit UI Tests ├── Info.plist └── PanelKit_UI_Tests.swift ├── PanelKit.podspec ├── PanelKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── PanelKit UI Test Host.xcscheme │ ├── PanelKit UI Tests.xcscheme │ └── PanelKit.xcscheme ├── PanelKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── PanelKit ├── Controller │ ├── PanelNavigationController.swift │ ├── PanelViewController+Appearance.swift │ ├── PanelViewController+Dragging.swift │ ├── PanelViewController+Keyboard.swift │ ├── PanelViewController+Offscreen.swift │ ├── PanelViewController+Resize.swift │ ├── PanelViewController+States.swift │ └── PanelViewController.swift ├── Info.plist ├── Model │ ├── AssociatedObject.swift │ ├── BlockGestureRecognizer.swift │ ├── Constants.swift │ ├── LogLevel.swift │ ├── PanelFloatingState.swift │ ├── PanelPinSide.swift │ ├── PanelPinnedMetadata.swift │ ├── PinnedPosition.swift │ ├── ResizeStart.swift │ └── UnpinningMetadata.swift ├── PanelContentDelegate │ ├── PanelContentDelegate+Default.swift │ ├── PanelContentDelegate+Navigation.swift │ └── PanelContentDelegate.swift ├── PanelKit.h ├── PanelManager │ ├── PanelManager+AutoLayout.swift │ ├── PanelManager+Closing.swift │ ├── PanelManager+Default.swift │ ├── PanelManager+Dragging.swift │ ├── PanelManager+Expose.swift │ ├── PanelManager+Floating.swift │ ├── PanelManager+Offscreen.swift │ ├── PanelManager+Pinning.swift │ ├── PanelManager+State.swift │ └── PanelManager.swift ├── State Restoration │ ├── PanelState.swift │ └── PanelStateCoder.swift ├── Utils │ ├── BlockBarButtonItem.swift │ ├── CGRect+Center.swift │ └── UIViewController+Popover.swift └── View │ └── CornerHandleView.swift ├── PanelKitTests ├── Info.plist ├── MainTests.swift ├── Panels │ ├── MapPanelContentViewController.swift │ └── TextPanelContentViewController.swift ├── StateTests.swift ├── StateViewController.swift └── ViewController.swift ├── README.md ├── SHOWCASE.md ├── docs ├── Expose.md ├── MultiPinning.md ├── Resizing.md └── States.md ├── readme-resources ├── example.gif └── hero.png └── showcase-resources └── pixure.gif /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" # required to adjust maintainability checks 2 | plugins: 3 | tailor: 4 | enabled: true 5 | exclude_patterns: 6 | - "*.md" 7 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "PanelKitTests" 3 | - "PanelKit UI Test Host" 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | PanelKit.podspec linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | .DS_Store 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | 21 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.1 -------------------------------------------------------------------------------- /.tailor.yml: -------------------------------------------------------------------------------- 1 | except: 2 | - forced-type-cast 3 | - function-whitespace -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.3 3 | branches: 4 | only: 5 | - master 6 | env: 7 | global: 8 | - LC_CTYPE=en_US.UTF-8 9 | - LANG=en_US.UTF-8 10 | - WORKSPACE=PanelKit.xcworkspace 11 | - IOS_FRAMEWORK_SCHEME="PanelKit" 12 | - EXAMPLE_SCHEME="Example" 13 | matrix: 14 | 15 | - DESTINATION="OS=10.0,name=iPhone 7" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="NO" BUILD_EXAMPLE="YES" CODE_COV="NO" 16 | - DESTINATION="OS=11.3,name=iPad Pro (9.7-inch)" SCHEME="$IOS_FRAMEWORK_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" CODE_COV="YES" 17 | 18 | script: 19 | - set -o pipefail 20 | - xcodebuild -version 21 | - xcodebuild -showsdks 22 | 23 | # Build Framework in Debug and Run Tests if specified 24 | - if [ $RUN_TESTS == "YES" ]; then 25 | travis_retry xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 26 | else 27 | travis_retry xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty; 28 | fi 29 | 30 | # Build Framework in Release and Run Tests if specified 31 | - if [ $RUN_TESTS == "YES" ]; then 32 | travis_retry xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 33 | else 34 | travis_retry xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty; 35 | fi 36 | 37 | # Build Example in Debug if specified 38 | - if [ $BUILD_EXAMPLE == "YES" ]; then 39 | travis_retry xcodebuild -workspace "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty; 40 | fi 41 | 42 | # Build and report code coverage if specified 43 | - if [ $CODE_COV == "YES" ]; then 44 | xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=YES ENABLE_TESTABILITY=YES -enableCodeCoverage YES test; 45 | fi 46 | 47 | after_success: 48 | - bash <(curl -s https://codecov.io/bash) 49 | 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 |
5 | Note: This is in reverse chronological order, so newer entries are added to the top. 6 | 7 | | Contents | 8 | | :------------------------------ | 9 | | [2.0.1](#201-2017-12-14) | 10 | | [2.0.0](#200-2017-12-05) | 11 | | [1.0.0](#100-2017-03-20) | 12 | | [0.9.0](#090-2017-03-08) | 13 | | [0.8.2](#082-2017-02-23) | 14 | | [0.8.1](#081-2017-02-21) | 15 | | [0.8](#08-2017-02-20) | 16 | 17 |
18 | 19 | [2.0.1](https://github.com/louisdh/panelkit/tree/2.0.1) (2017-12-14) 20 | -------------- 21 | * Fixed an animation glitch when unpinning a panel 22 | 23 | [2.0.0](https://github.com/louisdh/panelkit/tree/2.0.0) (2017-12-05) 24 | -------------- 25 | * Multi-pinning, pin multiple panels to a side 26 | * Panel resizing 27 | * State restoring, save and load panel states 28 | * Added APIs to pin or float a panel, without the use of a popover 29 | * Improved documentation 30 | * Updated to Swift 4.0 31 | * Added PanelViewController convenience initializer 32 | * Maintain panel at drag position when unpinned 33 | * Respect dragInsets when adjusting panel position for keyboard 34 | * Added preferredPanelPinnedWidth API: specifies width for panel while pinned, which can now differ from the panel width while floating 35 | * Fixes UITableViewCell swipe actions on iOS 11 36 | * PanelContentDelegate: add panelDragGestureRecognizer(shouldReceive: touch) API 37 | * Improved debug logging 38 | * Improved performance 39 | * Removed iOS 9 support (iOS 10.0 or newer is now required) 40 | 41 | [1.0.0](https://github.com/louisdh/panelkit/tree/1.0.0) (2017-03-20) 42 | -------------- 43 | * Replaced ```PanelContentViewController``` with ```PanelContentDelegate``` protocol. 44 | * Fixed memory leaks. 45 | * Added unit tests. 46 | 47 | [0.9.0](https://github.com/louisdh/panelkit/tree/0.9.0) (2017-03-08) 48 | -------------- 49 | * Introduced exposé with optional double 3 finger tap gesture recognizer to active. 50 | * Reduced public API. 51 | * Moved panel state properties from ```PanelContentViewController``` to ```PanelViewController```. 52 | 53 | [0.8.2](https://github.com/louisdh/panelkit/tree/0.8.2) (2017-02-23) 54 | -------------- 55 | 56 | * Fixed pinned panel preview views that weren't ever removed 57 | * ```panelContentView``` now supports a top and bottom margin other than 0 58 | 59 | [0.8.1](https://github.com/louisdh/panelkit/tree/0.8.1) (2017-02-21) 60 | -------------- 61 | 62 | * Updated documentation 63 | 64 | [0.8](https://github.com/louisdh/panelkit/tree/0.8) (2017-02-20) 65 | ------------ 66 | 67 | * Initial release with support for floating and pinned panels. 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Louis D'hauwe on 12/02/2017. 6 | // Copyright © 2017 Silver Fox. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/Panels/MapPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapPanelContentViewController.swift 3 | // Example 4 | // 5 | // Created by Louis D'hauwe on 12/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PanelKit 11 | import MapKit 12 | 13 | class MapPanelContentViewController: UIViewController, PanelContentDelegate { 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | self.title = "Map" 19 | 20 | } 21 | 22 | var preferredPanelContentSize: CGSize { 23 | return CGSize(width: 320, height: 500) 24 | } 25 | 26 | var minimumPanelContentSize: CGSize { 27 | return CGSize(width: 240, height: 260) 28 | } 29 | 30 | var maximumPanelContentSize: CGSize { 31 | return CGSize(width: 512, height: 600) 32 | } 33 | 34 | var preferredPanelPinnedWidth: CGFloat { 35 | return 500 36 | } 37 | 38 | var preferredPanelPinnedHeight: CGFloat { 39 | return 260 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Example/Example/Panels/Text2PanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text2PanelContentViewController.swift 3 | // Example 4 | // 5 | // Created by Louis D'hauwe on 28/05/2018. 6 | // Copyright © 2018 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PanelKit 11 | 12 | class Text2PanelContentViewController: UIViewController, PanelContentDelegate { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | self.title = "TextView 2" 20 | 21 | } 22 | 23 | var shouldAdjustForKeyboard: Bool { 24 | return textView.isFirstResponder 25 | } 26 | 27 | var minimumPanelContentSize: CGSize { 28 | return CGSize(width: 240, height: 260) 29 | } 30 | 31 | let preferredPanelContentSize = CGSize(width: 320, height: 400) 32 | 33 | var preferredPanelPinnedHeight: CGFloat { 34 | return 260 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/Example/Panels/TextPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextPanelContentViewController.swift 3 | // Example 4 | // 5 | // Created by Louis D'hauwe on 12/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PanelKit 11 | 12 | class TextPanelContentViewController: UIViewController, PanelContentDelegate { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | self.title = "TextView" 20 | 21 | } 22 | 23 | var shouldAdjustForKeyboard: Bool { 24 | return textView.isFirstResponder 25 | } 26 | 27 | var minimumPanelContentSize: CGSize { 28 | return CGSize(width: 240, height: 260) 29 | } 30 | 31 | let preferredPanelContentSize = CGSize(width: 320, height: 400) 32 | 33 | var preferredPanelPinnedHeight: CGFloat { 34 | return 260 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Louis D'hauwe on 12/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PanelKit 11 | 12 | class ViewController: UIViewController { 13 | 14 | var mapPanelContentVC: MapPanelContentViewController! 15 | var mapPanelVC: PanelViewController! 16 | 17 | var textPanelContentVC: TextPanelContentViewController! 18 | var textPanelVC: PanelViewController! 19 | 20 | var text2PanelContentVC: Text2PanelContentViewController! 21 | var text2PanelVC: PanelViewController! 22 | 23 | @IBOutlet weak var contentWrapperView: UIView! 24 | @IBOutlet weak var contentView: UIView! 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | mapPanelContentVC = storyboard?.instantiateViewController(withIdentifier: "MapPanelContentViewController") as! MapPanelContentViewController 30 | 31 | mapPanelVC = PanelViewController(with: mapPanelContentVC, in: self) 32 | 33 | textPanelContentVC = storyboard?.instantiateViewController(withIdentifier: "TextPanelContentViewController") as! TextPanelContentViewController 34 | 35 | textPanelVC = PanelViewController(with: textPanelContentVC, in: self) 36 | 37 | text2PanelContentVC = storyboard?.instantiateViewController(withIdentifier: "Text2PanelContentViewController") as! Text2PanelContentViewController 38 | 39 | text2PanelVC = PanelViewController(with: text2PanelContentVC, in: self) 40 | 41 | } 42 | 43 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 44 | super.viewWillTransition(to: size, with: coordinator) 45 | 46 | coordinator.animate(alongsideTransition: { (context) in 47 | 48 | }) { (context) in 49 | 50 | if !self.allowFloatingPanels { 51 | self.closeAllFloatingPanels() 52 | } 53 | 54 | if !self.allowPanelPinning { 55 | self.closeAllPinnedPanels() 56 | } 57 | 58 | } 59 | 60 | } 61 | 62 | // MARK: - Popover 63 | 64 | @IBAction func showMap(_ sender: UIBarButtonItem) { 65 | 66 | showPopover(mapPanelVC, from: sender) 67 | 68 | } 69 | 70 | @IBAction func showTextViewPanel(_ sender: UIBarButtonItem) { 71 | 72 | showPopover(textPanelVC, from: sender) 73 | 74 | } 75 | 76 | @IBAction func showTextView2Panel(_ sender: UIBarButtonItem) { 77 | 78 | showPopover(text2PanelVC, from: sender) 79 | 80 | } 81 | 82 | func showPopover(_ vc: UIViewController, from barButtonItem: UIBarButtonItem) { 83 | 84 | vc.modalPresentationStyle = .popover 85 | vc.popoverPresentationController?.barButtonItem = barButtonItem 86 | 87 | present(vc, animated: true, completion: nil) 88 | 89 | } 90 | 91 | } 92 | 93 | extension ViewController: PanelManager { 94 | 95 | var panelContentWrapperView: UIView { 96 | return contentWrapperView 97 | } 98 | 99 | var panelContentView: UIView { 100 | return contentView 101 | } 102 | 103 | var panels: [PanelViewController] { 104 | return [mapPanelVC, textPanelVC, text2PanelVC] 105 | } 106 | 107 | func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int { 108 | return 2 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Louis D'hauwe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PanelKit Test Host/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PanelKit Test Host 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /PanelKit Test Host/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /PanelKit Test Host/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /PanelKit Test Host/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /PanelKit Test Host/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | UISupportedInterfaceOrientations~ipad 37 | 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /PanelKit Test Host/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PanelKit Test Host 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PanelKit UI Test Host 4 | // 5 | // Created by Louis D'hauwe on 01/03/2017. 6 | // Copyright © 2017 Silver Fox. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /PanelKit UI Test Host/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/Panels/MapPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapPanelContentViewController.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 01/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PanelKit 11 | import MapKit 12 | import UIKit 13 | 14 | class MapPanelContentViewController: UIViewController, PanelContentDelegate { 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | self.title = "Map" 20 | 21 | } 22 | 23 | var preferredPanelContentSize: CGSize { 24 | return CGSize(width: 320, height: 500) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/Panels/TextPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextPanelContentViewController.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 01/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PanelKit 11 | import UIKit 12 | 13 | class TextPanelContentViewController: UIViewController, PanelContentDelegate { 14 | 15 | @IBOutlet weak var textView: UITextView! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | self.title = "TextView" 21 | 22 | } 23 | 24 | var shouldAdjustForKeyboard: Bool { 25 | return textView.isFirstResponder 26 | } 27 | 28 | var preferredPanelContentSize: CGSize { 29 | return CGSize(width: 320, height: 400) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /PanelKit UI Test Host/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PanelKit UI Test Host 4 | // 5 | // Created by Louis D'hauwe on 01/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PanelKit 11 | 12 | class ViewController: UIViewController, PanelManager { 13 | 14 | var mapPanelContentVC: MapPanelContentViewController! 15 | var mapPanelVC: PanelViewController! 16 | 17 | var textPanelContentVC: TextPanelContentViewController! 18 | var textPanelVC: PanelViewController! 19 | 20 | @IBOutlet weak var contentWrapperView: UIView! 21 | @IBOutlet weak var contentView: UIView! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | mapPanelContentVC = storyboard?.instantiateViewController(withIdentifier: "MapPanelContentViewController") as! MapPanelContentViewController 27 | 28 | mapPanelVC = PanelViewController(with: mapPanelContentVC, contentDelegate: mapPanelContentVC, in: self) 29 | 30 | textPanelContentVC = storyboard?.instantiateViewController(withIdentifier: "TextPanelContentViewController") as! TextPanelContentViewController 31 | 32 | textPanelVC = PanelViewController(with: textPanelContentVC, contentDelegate: textPanelContentVC, in: self) 33 | 34 | enableTripleTapExposeActivation() 35 | } 36 | 37 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 38 | super.viewWillTransition(to: size, with: coordinator) 39 | 40 | coordinator.animate(alongsideTransition: { (context) in 41 | 42 | }) { (context) in 43 | 44 | if !self.allowFloatingPanels { 45 | self.closeAllFloatingPanels() 46 | } 47 | 48 | if !self.allowPanelPinning { 49 | self.closeAllPinnedPanels() 50 | } 51 | 52 | } 53 | 54 | } 55 | 56 | // MARK: - Exposé 57 | 58 | @IBAction func toggleExpose(_ sender: UIBarButtonItem) { 59 | 60 | toggleExpose() 61 | 62 | } 63 | 64 | // MARK: - Popover 65 | 66 | @IBAction func showMap(_ sender: UIBarButtonItem) { 67 | 68 | showPopover(mapPanelVC, from: sender) 69 | 70 | } 71 | 72 | @IBAction func showTextViewPanel(_ sender: UIBarButtonItem) { 73 | 74 | showPopover(textPanelVC, from: sender) 75 | 76 | } 77 | 78 | func showPopover(_ vc: UIViewController, from barButtonItem: UIBarButtonItem) { 79 | 80 | vc.modalPresentationStyle = .popover 81 | vc.popoverPresentationController?.barButtonItem = barButtonItem 82 | 83 | present(vc, animated: true, completion: nil) 84 | 85 | } 86 | 87 | // MARK: - PanelManager 88 | 89 | var panelContentWrapperView: UIView { 90 | return contentWrapperView 91 | } 92 | 93 | var panelContentView: UIView { 94 | return contentView 95 | } 96 | 97 | var panels: [PanelViewController] { 98 | return [mapPanelVC, textPanelVC] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /PanelKit UI Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PanelKit UI Tests/PanelKit_UI_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelKit_UI_Tests.swift 3 | // PanelKit UI Tests 4 | // 5 | // Created by Louis D'hauwe on 01/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PanelKit 11 | 12 | class PanelKit_UI_Tests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | 19 | // In UI tests it is usually best to stop immediately when a failure occurs. 20 | continueAfterFailure = false 21 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 22 | XCUIApplication().launch() 23 | 24 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 25 | 26 | if UIDevice.current.userInterfaceIdiom == .phone { 27 | XCTFail("Test does not work on an iPhone") 28 | } 29 | } 30 | 31 | override func tearDown() { 32 | // Put teardown code here. This method is called after the invocation of each test method in the class. 33 | super.tearDown() 34 | } 35 | 36 | func testFloating() { 37 | 38 | XCUIDevice.shared.orientation = .landscapeLeft 39 | 40 | let app = XCUIApplication() 41 | let mapButton = app.navigationBars["PanelKit Example"].buttons["Map"] 42 | let mapNavigationBar = app.navigationBars["Map"] 43 | 44 | mapButton.tap() 45 | 46 | mapNavigationBar.buttons["⬇︎"].tap() 47 | 48 | // let mapStaticText = mapNavigationBar.staticTexts["Map"] 49 | // mapStaticText.tap() 50 | 51 | mapNavigationBar.buttons["Close"].tap() 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /PanelKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PanelKit' 3 | s.version = '2.0.1' 4 | s.license = 'MIT' 5 | s.summary = 'A UI framework that enables panels on iOS.' 6 | s.homepage = 'https://github.com/louisdh/panelkit' 7 | s.social_media_url = 'http://twitter.com/LouisDhauwe' 8 | s.authors = { 'Louis D\'hauwe' => 'louisdhauwe@silverfox.be' } 9 | s.source = { :git => 'https://github.com/louisdh/panelkit.git', :tag => s.version } 10 | 11 | s.ios.deployment_target = '10.0' 12 | 13 | s.source_files = 'PanelKit/**/*.swift' 14 | end 15 | -------------------------------------------------------------------------------- /PanelKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PanelKit.xcodeproj/xcshareddata/xcschemes/PanelKit UI Test Host.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /PanelKit.xcodeproj/xcshareddata/xcschemes/PanelKit UI Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /PanelKit.xcodeproj/xcshareddata/xcschemes/PanelKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /PanelKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PanelKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelNavigationController.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 10/09/16. 6 | // Copyright © 2016-2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public class PanelNavigationController: UINavigationController { 12 | 13 | public weak var panelViewController: PanelViewController? 14 | 15 | override public func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapBar(_ :))) 19 | tapGestureRecognizer.cancelsTouchesInView = false 20 | self.view.addGestureRecognizer(tapGestureRecognizer) 21 | 22 | } 23 | 24 | deinit { 25 | if panelViewController?.logLevel == .full { 26 | print("deinit \(self)") 27 | } 28 | } 29 | 30 | @objc private func didTapBar(_ gestureRecognizer: UITapGestureRecognizer) { 31 | 32 | if self.panelViewController?.isPinned != true { 33 | self.panelViewController?.bringToFront() 34 | } 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+Appearance.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension PanelViewController { 13 | 14 | var tintColor: UIColor { 15 | return panelNavigationController.navigationBar.tintColor 16 | } 17 | 18 | var shadowLayer: CALayer { 19 | return self.view.layer 20 | } 21 | 22 | func disableShadow(animated: Bool = false, duration: Double = 0.3) { 23 | 24 | if animated { 25 | 26 | let anim = CABasicAnimation(keyPath: #keyPath(CALayer.shadowOpacity)) 27 | anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) 28 | anim.fromValue = shadowLayer.shadowOpacity 29 | anim.toValue = 0.0 30 | anim.duration = duration 31 | shadowLayer.add(anim, forKey: #keyPath(CALayer.shadowOpacity)) 32 | 33 | } 34 | 35 | shadowLayer.shadowOpacity = 0.0 36 | 37 | isShadowForceDisabled = true 38 | } 39 | 40 | func enableShadow(animated: Bool = false, duration: Double = 0.3) { 41 | 42 | if animated { 43 | 44 | let anim = CABasicAnimation(keyPath: #keyPath(CALayer.shadowOpacity)) 45 | anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) 46 | anim.fromValue = shadowLayer.shadowOpacity 47 | anim.toValue = shadowOpacity 48 | anim.duration = duration 49 | shadowLayer.add(anim, forKey: #keyPath(CALayer.shadowOpacity)) 50 | 51 | } 52 | 53 | shadowLayer.shadowOpacity = shadowOpacity 54 | 55 | isShadowForceDisabled = false 56 | 57 | } 58 | 59 | func disableCornerRadius(animated: Bool = false, duration: Double = 0.3) { 60 | 61 | if animated { 62 | 63 | let anim = CABasicAnimation(keyPath: #keyPath(CALayer.cornerRadius)) 64 | anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) 65 | anim.fromValue = panelNavigationController.view.layer.cornerRadius 66 | anim.toValue = 0.0 67 | anim.duration = duration 68 | panelNavigationController.view.layer.add(anim, forKey: #keyPath(CALayer.cornerRadius)) 69 | 70 | } 71 | 72 | panelNavigationController.view.layer.cornerRadius = 0.0 73 | 74 | panelNavigationController.view.clipsToBounds = true 75 | 76 | } 77 | 78 | func enableCornerRadius(animated: Bool = false, duration: Double = 0.3) { 79 | 80 | if animated { 81 | 82 | let anim = CABasicAnimation(keyPath: #keyPath(CALayer.cornerRadius)) 83 | anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) 84 | anim.fromValue = panelNavigationController.view.layer.cornerRadius 85 | anim.toValue = cornerRadius 86 | anim.duration = duration 87 | panelNavigationController.view.layer.add(anim, forKey: #keyPath(CALayer.cornerRadius)) 88 | 89 | } 90 | 91 | panelNavigationController.view.layer.cornerRadius = cornerRadius 92 | 93 | panelNavigationController.view.clipsToBounds = true 94 | 95 | } 96 | 97 | var shadowEnabled: Bool { 98 | return manager?.enablePanelShadow(for: self) == true 99 | } 100 | 101 | func updateShadow() { 102 | 103 | if isShadowForceDisabled { 104 | return 105 | } 106 | 107 | if shadowEnabled { 108 | 109 | shadowLayer.shadowRadius = shadowRadius 110 | shadowLayer.shadowOpacity = shadowOpacity 111 | shadowLayer.shadowOffset = shadowOffset 112 | shadowLayer.shadowColor = shadowColor 113 | 114 | } else { 115 | 116 | shadowLayer.shadowRadius = 0.0 117 | shadowLayer.shadowOpacity = 0.0 118 | 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+Dragging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+Dragging.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelViewController { 12 | 13 | func didDrag(at point: CGPoint) { 14 | 15 | guard isFloating || isPinned else { 16 | return 17 | } 18 | 19 | guard let manager = self.manager else { 20 | return 21 | } 22 | 23 | let panelContentView = manager.panelContentView 24 | 25 | if self.view.frame.maxX >= panelContentView.frame.maxX && !isPinned { 26 | 27 | manager.didDrag(self, toEdgeOf: .right) 28 | 29 | } else if self.view.frame.minX <= panelContentView.frame.minX && !isPinned { 30 | 31 | manager.didDrag(self, toEdgeOf: .left) 32 | 33 | } else if self.view.frame.minY <= panelContentView.frame.minY && !isPinned { 34 | 35 | manager.didDrag(self, toEdgeOf: .top) 36 | 37 | } else if self.view.frame.maxY >= panelContentView.frame.maxY && !isPinned { 38 | 39 | manager.didDrag(self, toEdgeOf: .bottom) 40 | 41 | } else { 42 | 43 | if let pinnedSide = pinnedMetadata?.side { 44 | if !isUnpinning { 45 | self.unpinningMetadata = UnpinningMetadata(side: pinnedSide) 46 | } 47 | } 48 | 49 | manager.didDragFree(self, from: point) 50 | 51 | } 52 | 53 | } 54 | 55 | func didEndDrag() { 56 | 57 | self.unpinningMetadata = nil 58 | 59 | guard isFloating || isPinned else { 60 | return 61 | } 62 | 63 | guard let manager = self.manager else { 64 | return 65 | } 66 | 67 | let panelContentView = manager.panelContentView 68 | 69 | if self.view.frame.maxX >= panelContentView.frame.maxX { 70 | 71 | manager.didEndDrag(self, toEdgeOf: .right) 72 | 73 | } else if self.view.frame.minX <= panelContentView.frame.minX { 74 | 75 | manager.didEndDrag(self, toEdgeOf: .left) 76 | 77 | } else if self.view.frame.minY <= panelContentView.frame.minY { 78 | 79 | manager.didEndDrag(self, toEdgeOf: .top) 80 | 81 | } else if self.view.frame.maxY >= panelContentView.frame.maxY { 82 | 83 | manager.didEndDrag(self, toEdgeOf: .bottom) 84 | 85 | } else { 86 | 87 | manager.didEndDragFree(self) 88 | 89 | } 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+Keyboard.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelViewController { 12 | 13 | func keyboardWillChangeFrame(_ notification: Notification) { 14 | 15 | } 16 | 17 | @objc func willShowKeyboard(_ notification: Notification) { 18 | 19 | guard let contentDelegate = contentDelegate else { 20 | return 21 | } 22 | 23 | guard contentDelegate.shouldAdjustForKeyboard else { 24 | return 25 | } 26 | 27 | guard let userInfo = notification.userInfo else { 28 | return 29 | } 30 | 31 | let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber 32 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions().rawValue 33 | let animationCurve = UIViewAnimationOptions(rawValue: animationCurveRaw) 34 | 35 | let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?? 0.3 36 | 37 | guard var keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { 38 | return 39 | } 40 | 41 | guard let viewToMove = self.view else { 42 | return 43 | } 44 | 45 | guard let superView = viewToMove.superview else { 46 | return 47 | } 48 | 49 | var keyboardFrameInSuperView = superView.convert(keyboardFrame, from: nil) 50 | keyboardFrameInSuperView = keyboardFrameInSuperView.intersection(superView.bounds) 51 | 52 | keyboardFrame = viewToMove.convert(keyboardFrame, from: nil) 53 | keyboardFrame = keyboardFrame.intersection(viewToMove.bounds) 54 | 55 | if isFloating || isPinned { 56 | 57 | if keyboardFrame.intersects(viewToMove.bounds) { 58 | 59 | let maxHeight = superView.bounds.height - keyboardFrameInSuperView.height 60 | 61 | var height = min(viewToMove.frame.height, maxHeight) 62 | 63 | var y = keyboardFrameInSuperView.origin.y - height 64 | 65 | if let dragInsets = self.manager?.dragInsets(for: self) { 66 | y += dragInsets.top 67 | height -= dragInsets.top 68 | } 69 | 70 | let updatedFrame = CGRect(x: viewToMove.frame.origin.x, y: y, width: viewToMove.frame.width, height: height) 71 | 72 | manager?.updateFrame(for: self, to: updatedFrame, keyboardShown: true) 73 | 74 | self.bringToFront() 75 | 76 | UIView.animate(withDuration: duration, delay: 0.0, options: [animationCurve], animations: { 77 | 78 | self.manager?.panelContentWrapperView.layoutIfNeeded() 79 | 80 | }, completion: nil) 81 | 82 | } 83 | 84 | } 85 | 86 | contentDelegate.updateConstraintsForKeyboardShow(with: keyboardFrame) 87 | 88 | UIView.animate(withDuration: duration, delay: 0.0, options: animationCurve, animations: { 89 | 90 | self.view.layoutIfNeeded() 91 | contentDelegate.updateUIForKeyboardShow(with: keyboardFrame) 92 | 93 | }, completion: nil) 94 | 95 | } 96 | 97 | @objc func willHideKeyboard(_ notification: Notification) { 98 | 99 | guard let contentDelegate = contentDelegate else { 100 | return 101 | } 102 | 103 | guard let viewToMove = self.view else { 104 | return 105 | } 106 | 107 | guard let userInfo = notification.userInfo else { 108 | return 109 | } 110 | 111 | let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber 112 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions().rawValue 113 | let animationCurve = UIViewAnimationOptions(rawValue: animationCurveRaw) 114 | 115 | let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?? 0.3 116 | 117 | let currentFrame = viewToMove.frame 118 | 119 | // Currently uses a slight hack to prevent navigation bar height from bugging out (height became 64, instead of the normal 44) 120 | 121 | // 1: change panel size height to actual height + 1 122 | 123 | var newFrame = currentFrame 124 | newFrame.size = contentDelegate.preferredPanelContentSize 125 | 126 | if isFloating { 127 | if let floatingSize = self.floatingSize { 128 | newFrame.size = floatingSize 129 | } 130 | } 131 | 132 | if isPinned { 133 | if pinnedMetadata?.side == .left || pinnedMetadata?.side == .right { 134 | newFrame.size.width = view.frame.width 135 | } else { 136 | newFrame.size.height = view.frame.height 137 | } 138 | } 139 | 140 | newFrame.size.height += 1 141 | 142 | self.manager?.updateFrame(for: self, to: newFrame, keyboardShown: true) 143 | 144 | contentDelegate.updateConstraintsForKeyboardHide() 145 | 146 | UIView.animate(withDuration: duration, delay: 0.0, options: animationCurve, animations: { 147 | 148 | self.view.layoutIfNeeded() 149 | 150 | self.manager?.panelContentWrapperView.layoutIfNeeded() 151 | 152 | contentDelegate.updateUIForKeyboardHide() 153 | 154 | }, completion: nil) 155 | 156 | // 2: change panel size height to actual height 157 | 158 | var newFrame2 = currentFrame 159 | newFrame2.size = contentDelegate.preferredPanelContentSize 160 | 161 | if isFloating { 162 | if let floatingSize = self.floatingSize { 163 | newFrame2.size = floatingSize 164 | } 165 | } 166 | 167 | if isPinned { 168 | if pinnedMetadata?.side == .left || pinnedMetadata?.side == .right { 169 | newFrame2.size.width = view.frame.width 170 | } else { 171 | newFrame2.size.height = view.frame.height 172 | } 173 | } 174 | 175 | self.manager?.updateFrame(for: self, to: newFrame2) 176 | self.manager?.panelContentWrapperView.layoutIfNeeded() 177 | 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+Offscreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+Offscreen.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelViewController { 12 | 13 | func prepareMoveOffScreen() { 14 | 15 | position = view?.center 16 | 17 | } 18 | 19 | func movePanelOffScreen() { 20 | 21 | guard let viewToMove = self.view else { 22 | return 23 | } 24 | 25 | guard let superView = viewToMove.superview else { 26 | return 27 | } 28 | 29 | let deltaToMoveOffscreen: CGFloat = viewToMove.frame.width + shadowRadius + max(0, -shadowOffset.width) 30 | 31 | var frame = viewToMove.frame 32 | 33 | if viewToMove.center.x < superView.frame.size.width/2.0 { 34 | frame.center = CGPoint(x: -deltaToMoveOffscreen, y: viewToMove.center.y) 35 | } else { 36 | frame.center = CGPoint(x: superView.frame.size.width + deltaToMoveOffscreen, y: viewToMove.center.y) 37 | } 38 | 39 | manager?.updateFrame(for: self, to: frame) 40 | 41 | } 42 | 43 | func completeMoveOffScreen() { 44 | 45 | positionInFullscreen = view?.center 46 | 47 | } 48 | 49 | // MARK: - Move on screen 50 | 51 | func prepareMoveOnScreen() { 52 | 53 | guard let position = position else { 54 | return 55 | } 56 | 57 | guard let positionInFullscreen = positionInFullscreen else { 58 | return 59 | } 60 | 61 | guard let viewToMove = self.view else { 62 | return 63 | } 64 | 65 | let x = position.x - (positionInFullscreen.x - viewToMove.center.x) 66 | let y = position.y - (positionInFullscreen.y - viewToMove.center.y) 67 | 68 | self.position = CGPoint(x: x, y: y) 69 | } 70 | 71 | func movePanelOnScreen() { 72 | 73 | guard let position = position else { 74 | return 75 | } 76 | 77 | guard let viewToMove = self.view else { 78 | return 79 | } 80 | 81 | var frame = viewToMove.frame 82 | frame.center = position 83 | 84 | manager?.updateFrame(for: self, to: frame) 85 | 86 | } 87 | 88 | func completeMoveOnScreen() { 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+Resize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+Resize.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 08/10/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelViewController { 12 | 13 | private var canResize: Bool { 14 | 15 | guard isFloating else { 16 | return false 17 | } 18 | 19 | guard let contentDelegate = contentDelegate else { 20 | return false 21 | } 22 | 23 | let preferredSize = contentDelegate.preferredPanelContentSize 24 | let minSize = contentDelegate.minimumPanelContentSize 25 | let maxSize = contentDelegate.maximumPanelContentSize 26 | 27 | if preferredSize == minSize && preferredSize == maxSize { 28 | return false 29 | } 30 | 31 | return true 32 | } 33 | 34 | func showResizeHandleIfNeeded(animated: Bool = true) { 35 | 36 | guard canResize else { 37 | return 38 | } 39 | 40 | func show() { 41 | 42 | self.resizeCornerHandle.transform = .identity 43 | 44 | } 45 | 46 | self.resizeCornerHandle.alpha = 1 47 | 48 | if animated { 49 | 50 | UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, options: [], animations: { 51 | 52 | self.resizeCornerHandle.transform = .identity 53 | 54 | }, completion: nil) 55 | 56 | } else { 57 | 58 | show() 59 | } 60 | 61 | } 62 | 63 | func hideResizeHandle(animated: Bool = true) { 64 | 65 | func hide() { 66 | 67 | var transform = CGAffineTransform.identity 68 | transform = transform.translatedBy(x: -44, y: -44) 69 | 70 | self.resizeCornerHandle.transform = transform 71 | self.resizeCornerHandle.alpha = 0 72 | 73 | } 74 | 75 | if animated { 76 | 77 | UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, options: [], animations: { 78 | 79 | hide() 80 | 81 | }, completion: nil) 82 | 83 | } else { 84 | 85 | hide() 86 | } 87 | 88 | } 89 | 90 | func configureResizeHandle() { 91 | 92 | let resizeHandle = resizeCornerHandle 93 | resizeHandle.backgroundColor = .clear 94 | resizeHandle.translatesAutoresizingMaskIntoConstraints = false 95 | 96 | resizeHandle.tintColor = .white 97 | resizeHandle.transform = CGAffineTransform(rotationAngle: .pi/2 * 2) 98 | 99 | let recognizer = UIPanGestureRecognizer(target: self, action: #selector(didDragCornerHandle(_:))) 100 | resizeHandle.addGestureRecognizer(recognizer) 101 | 102 | let resizeHandleTapGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didTapCornerHandle(_ :))) 103 | resizeHandleTapGestureRecognizer.minimumPressDuration = 0 104 | resizeHandleTapGestureRecognizer.delegate = self 105 | 106 | resizeHandle.addGestureRecognizer(resizeHandleTapGestureRecognizer) 107 | 108 | cornerHandleDidBecomeInactive(animated: false) 109 | hideResizeHandle(animated: false) 110 | 111 | } 112 | 113 | private func cornerHandleDidBecomeActive() { 114 | 115 | resizeCornerHandle.cornerHandleDidBecomeActive() 116 | 117 | } 118 | 119 | private func cornerHandleDidBecomeInactive(animated: Bool = true) { 120 | 121 | resizeCornerHandle.cornerHandleDidBecomeInactive(animated: animated) 122 | 123 | } 124 | 125 | @objc private func didTapCornerHandle(_ recognizer: UILongPressGestureRecognizer) { 126 | 127 | if recognizer.state == .began { 128 | 129 | cornerHandleDidBecomeActive() 130 | 131 | } else if recognizer.state == .failed || recognizer.state == .ended { 132 | 133 | cornerHandleDidBecomeInactive() 134 | 135 | } 136 | 137 | } 138 | 139 | @objc private func didDragCornerHandle(_ recognizer: UIPanGestureRecognizer) { 140 | 141 | guard let contentDelegate = contentDelegate else { 142 | return 143 | } 144 | 145 | guard let manager = self.manager else { 146 | return 147 | } 148 | 149 | guard let viewToMove = self.view else { 150 | return 151 | } 152 | 153 | if recognizer.state == .began { 154 | 155 | cornerHandleDidBecomeActive() 156 | 157 | let position = recognizer.location(in: self.view) 158 | 159 | resizeStart = ResizeStart(dragPosition: position, frame: viewToMove.frame) 160 | 161 | } else if recognizer.state == .changed, let resizeStart = resizeStart { 162 | 163 | let newPosition = recognizer.location(in: self.view) 164 | 165 | var newFrame = resizeStart.frame 166 | 167 | let proposedWidth = newFrame.size.width + (newPosition.x - resizeStart.dragPosition.x) 168 | let proposedHeight = newFrame.size.height + (newPosition.y - resizeStart.dragPosition.y) 169 | 170 | let maxWidth: CGFloat 171 | 172 | if let panelPinnedRight = manager.panelPinnedRight { 173 | 174 | // Prevent a panel from intersecting with a pinned panel when resizing. 175 | 176 | let wrapperWidth = manager.panelContentWrapperView.frame.width 177 | 178 | let theoreticalMaxWidth = (wrapperWidth - self.view.frame.minX) - panelPinnedRight.view.frame.width 179 | 180 | maxWidth = min(contentDelegate.maximumPanelContentSize.width, theoreticalMaxWidth) 181 | 182 | } else { 183 | 184 | maxWidth = contentDelegate.maximumPanelContentSize.width 185 | 186 | } 187 | 188 | let minWidth = contentDelegate.minimumPanelContentSize.width 189 | 190 | let maxHeight = contentDelegate.maximumPanelContentSize.height 191 | let minHeight = contentDelegate.minimumPanelContentSize.height 192 | 193 | let newWidth: CGFloat 194 | 195 | if proposedWidth > maxWidth { 196 | newWidth = maxWidth 197 | } else if proposedWidth < minWidth { 198 | newWidth = minWidth 199 | } else { 200 | newWidth = proposedWidth 201 | } 202 | 203 | let newHeight: CGFloat 204 | 205 | if proposedHeight > maxHeight { 206 | newHeight = maxHeight 207 | } else if proposedHeight < minHeight { 208 | newHeight = minHeight 209 | } else { 210 | newHeight = proposedHeight 211 | } 212 | 213 | newFrame.size.width = newWidth 214 | newFrame.size.height = newHeight 215 | 216 | manager.updateFrame(for: self, to: newFrame) 217 | 218 | floatingSize = newFrame.size 219 | 220 | } else if recognizer.state == .ended { 221 | 222 | cornerHandleDidBecomeInactive() 223 | 224 | } 225 | 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /PanelKit/Controller/PanelViewController+States.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelViewController+States.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension PanelViewController { 12 | 13 | var wasPinned: Bool { 14 | return !isPinned && pinnedMetadata != nil 15 | } 16 | 17 | @objc public var isUnpinning: Bool { 18 | return unpinningMetadata != nil 19 | } 20 | 21 | @objc public var isPinned: Bool { 22 | 23 | if isPresentedAsPopover { 24 | return false 25 | } 26 | 27 | if isPresentedModally { 28 | return false 29 | } 30 | 31 | guard view.superview != nil else { 32 | return false 33 | } 34 | 35 | return pinnedMetadata != nil 36 | } 37 | 38 | @objc public var isFloating: Bool { 39 | 40 | if isPresentedAsPopover { 41 | return false 42 | } 43 | 44 | if isPresentedModally { 45 | return false 46 | } 47 | 48 | if isPinned { 49 | return false 50 | } 51 | 52 | guard view.superview == self.manager?.panelContentWrapperView else { 53 | return false 54 | } 55 | 56 | return true 57 | } 58 | 59 | var isPresentedModally: Bool { 60 | 61 | if isPresentedAsPopover { 62 | return false 63 | } 64 | 65 | return presentingViewController != nil 66 | } 67 | 68 | public var isInExpose: Bool { 69 | return frameBeforeExpose != nil 70 | } 71 | 72 | /// A panel can't float when it is presented modally. 73 | public var canFloat: Bool { 74 | 75 | guard manager?.allowFloatingPanels == true else { 76 | return false 77 | } 78 | 79 | if isPresentedAsPopover { 80 | return true 81 | } 82 | 83 | // Modal 84 | if isPresentedModally { 85 | return false 86 | } 87 | 88 | return true 89 | } 90 | 91 | } 92 | 93 | // MARK: - State updating 94 | 95 | extension PanelViewController { 96 | 97 | func updateState() { 98 | 99 | if wasPinned { 100 | manager?.didDragFree(self, from: nil) 101 | } 102 | 103 | if isFloating || isPinned { 104 | self.view.translatesAutoresizingMaskIntoConstraints = false 105 | 106 | if !isPinned { 107 | enableCornerRadius() 108 | if shadowEnabled { 109 | enableShadow() 110 | } 111 | } 112 | 113 | } else { 114 | self.view.translatesAutoresizingMaskIntoConstraints = true 115 | 116 | disableShadow() 117 | disableCornerRadius() 118 | 119 | } 120 | 121 | contentDelegate?.updateNavigationButtons() 122 | 123 | } 124 | 125 | func didUpdateFloatingState() { 126 | 127 | updateState() 128 | 129 | self.updateShadow() 130 | 131 | if !(isFloating || isPinned) { 132 | widthConstraint?.isActive = false 133 | heightConstraint?.isActive = false 134 | } 135 | 136 | if isFloating { 137 | showResizeHandleIfNeeded() 138 | } else { 139 | hideResizeHandle() 140 | } 141 | 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /PanelKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.0.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /PanelKit/Model/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObject.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 25/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // From: https://medium.com/@ttikitu/swift-extensions-can-add-stored-properties-92db66bce6cd#.a3wql3oiw 12 | 13 | func associatedObject( 14 | _ base: AnyObject, 15 | key: UnsafePointer, 16 | initialiser: () -> ValueType) 17 | -> ValueType { 18 | if let associated = objc_getAssociatedObject(base, key) as? ValueType { 19 | return associated 20 | } 21 | 22 | let associated = initialiser() 23 | objc_setAssociatedObject(base, key, associated, .OBJC_ASSOCIATION_RETAIN) 24 | return associated 25 | } 26 | 27 | func associateObject( 28 | _ base: AnyObject, 29 | key: UnsafePointer, 30 | value: ValueType) { 31 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN) 32 | } 33 | -------------------------------------------------------------------------------- /PanelKit/Model/BlockGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockGestureRecognizer.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 25/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class BlockGestureRecognizer: NSObject { 13 | 14 | let closure: () -> Void 15 | 16 | init(view: UIView, recognizer: UIGestureRecognizer, closure: @escaping () -> Void) { 17 | self.closure = closure 18 | super.init() 19 | view.addGestureRecognizer(recognizer) 20 | recognizer.addTarget(self, action: #selector(invokeTarget(_ :))) 21 | } 22 | 23 | @objc func invokeTarget(_ recognizer: UIGestureRecognizer) { 24 | self.closure() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PanelKit/Model/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | import UIKit 12 | 13 | // Exposé 14 | 15 | let exposeOuterPadding: CGFloat = 44.0 16 | let exposeExitDuration: TimeInterval = 0.3 17 | let exposeEnterDuration: TimeInterval = 0.3 18 | let exposePanelHorizontalSpacing: CGFloat = 20.0 19 | let exposePanelVerticalSpacing: CGFloat = 20.0 20 | 21 | // Pinned 22 | 23 | let pinnedPanelPreviewAlpha: CGFloat = 0.4 24 | let pinnedPanelPreviewGrowDuration: TimeInterval = 0.3 25 | let pinnedPanelPreviewFadeDuration: TimeInterval = 0.3 26 | let panelGrowDuration: TimeInterval = 0.3 27 | 28 | // Floating 29 | 30 | let panelPopDuration: TimeInterval = 0.2 31 | let panelPopYOffset: CGFloat = 12.0 32 | 33 | // PanelViewController 34 | 35 | let cornerRadius: CGFloat = 16.0 36 | 37 | // Panel shadow 38 | 39 | let shadowRadius: CGFloat = 8.0 40 | let shadowOpacity: Float = 0.3 41 | let shadowOffset: CGSize = CGSize(width: 0, height: 10.0) 42 | let shadowColor = UIColor.black.cgColor 43 | -------------------------------------------------------------------------------- /PanelKit/Model/LogLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogLevel.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 16/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Log level for common events, such as "viewWillAppear" or "panel did pin". 12 | /// Helpful while debugging. 13 | public enum LogLevel { 14 | 15 | /// Log nothing. 16 | case none 17 | 18 | /// Log all events. 19 | case full 20 | 21 | } 22 | -------------------------------------------------------------------------------- /PanelKit/Model/PanelFloatingState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelFloatingState.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 14/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | public struct PanelFloatingState: Codable { 13 | 14 | let relativePosition: CGPoint 15 | let zIndex: Int 16 | 17 | } 18 | 19 | extension PanelFloatingState: Hashable { 20 | 21 | static public func ==(lhs: PanelFloatingState, rhs: PanelFloatingState) -> Bool { 22 | return lhs.relativePosition == rhs.relativePosition && lhs.zIndex == rhs.zIndex 23 | } 24 | 25 | public var hashValue: Int { 26 | return zIndex.hashValue 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /PanelKit/Model/PanelPinSide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelPinSide.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 11/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The sides that a panel can be pinned to. 12 | @objc public enum PanelPinSide: Int, Codable { 13 | case left 14 | case right 15 | case top 16 | case bottom 17 | } 18 | 19 | extension PanelPinSide: CustomStringConvertible { 20 | 21 | public var description: String { 22 | switch self { 23 | case .left: 24 | return "left" 25 | case .right: 26 | return "right" 27 | case .top: 28 | return "top" 29 | case .bottom: 30 | return "bottom" 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /PanelKit/Model/PanelPinnedMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelPinnedMetadata.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 04/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct PanelPinnedMetadata: Codable, Hashable { 12 | public var side: PanelPinSide 13 | public var index: Int 14 | let date = Date() 15 | 16 | public init(side: PanelPinSide, index: Int) { 17 | self.side = side 18 | self.index = index 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PanelKit/Model/PinnedPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinnedMetadata.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 04/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | struct PinnedPosition { 13 | let frame: CGRect 14 | let index: Int 15 | } 16 | -------------------------------------------------------------------------------- /PanelKit/Model/ResizeStart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResizeStart.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 10/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | struct ResizeStart { 12 | 13 | let dragPosition: CGPoint 14 | let frame: CGRect 15 | 16 | } 17 | -------------------------------------------------------------------------------- /PanelKit/Model/UnpinningMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnpinningMetadata.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 10/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct UnpinningMetadata { 12 | 13 | let side: PanelPinSide 14 | 15 | } 16 | -------------------------------------------------------------------------------- /PanelKit/PanelContentDelegate/PanelContentDelegate+Default.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelContentDelegate+Default.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 12/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension PanelContentDelegate { 12 | 13 | var hideCloseButtonWhileFloating: Bool { 14 | return false 15 | } 16 | 17 | var hideCloseButtonWhilePinned: Bool { 18 | return false 19 | } 20 | 21 | var closeButtonTitle: String { 22 | return "Close" 23 | } 24 | var popButtonTitle: String { 25 | return "⬇︎" 26 | } 27 | 28 | var modalCloseButtonTitle: String { 29 | return "Back" 30 | } 31 | 32 | func updateConstraintsForKeyboardShow(with frame: CGRect) { 33 | 34 | } 35 | 36 | func updateUIForKeyboardShow(with frame: CGRect) { 37 | 38 | } 39 | 40 | func updateConstraintsForKeyboardHide() { 41 | 42 | } 43 | 44 | func updateUIForKeyboardHide() { 45 | 46 | } 47 | 48 | /// Defaults to false 49 | var shouldAdjustForKeyboard: Bool { 50 | return false 51 | } 52 | 53 | /// Excludes potential "close" or "pop" buttons 54 | var leftBarButtonItems: [UIBarButtonItem] { 55 | return [] 56 | } 57 | 58 | /// Excludes potential "close" or "pop" buttons 59 | var rightBarButtonItems: [UIBarButtonItem] { 60 | return [] 61 | } 62 | 63 | func panelDragGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 64 | return true 65 | } 66 | 67 | var preferredPanelPinnedWidth: CGFloat { 68 | return preferredPanelContentSize.width 69 | } 70 | 71 | var preferredPanelPinnedHeight: CGFloat { 72 | return preferredPanelContentSize.height 73 | } 74 | 75 | var minimumPanelContentSize: CGSize { 76 | return preferredPanelContentSize 77 | } 78 | 79 | var maximumPanelContentSize: CGSize { 80 | return preferredPanelContentSize 81 | } 82 | 83 | } 84 | 85 | public extension PanelContentDelegate where Self: UIViewController { 86 | 87 | func updateNavigationButtons() { 88 | 89 | guard let panel = panelNavigationController?.panelViewController else { 90 | return 91 | } 92 | 93 | if panel.isPresentedModally { 94 | 95 | let backBtn = getBackBtn() 96 | 97 | navigationItem.leftBarButtonItems = [backBtn] + leftBarButtonItems 98 | 99 | } else { 100 | 101 | if !panel.canFloat { 102 | 103 | navigationItem.leftBarButtonItems = leftBarButtonItems 104 | 105 | } else { 106 | 107 | if panel.contentDelegate?.hideCloseButtonWhileFloating == true, panel.isFloating { 108 | 109 | navigationItem.leftBarButtonItems = leftBarButtonItems 110 | 111 | } else if panel.contentDelegate?.hideCloseButtonWhilePinned == true, panel.isPinned { 112 | 113 | navigationItem.leftBarButtonItems = leftBarButtonItems 114 | 115 | } else { 116 | 117 | let panelToggleBtn = getPanelToggleBtn() 118 | 119 | navigationItem.leftBarButtonItems = [panelToggleBtn] + leftBarButtonItems 120 | 121 | } 122 | 123 | 124 | } 125 | 126 | } 127 | 128 | navigationItem.rightBarButtonItems = rightBarButtonItems 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /PanelKit/PanelContentDelegate/PanelContentDelegate+Navigation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelContentDelegate+Navigation.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 12/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension PanelContentDelegate where Self: UIViewController { 12 | 13 | weak var panelNavigationController: PanelNavigationController? { 14 | return navigationController as? PanelNavigationController 15 | } 16 | 17 | } 18 | 19 | extension PanelContentDelegate { 20 | 21 | func didUpdateFloatingState() { 22 | 23 | updateNavigationButtons() 24 | 25 | } 26 | 27 | } 28 | 29 | extension PanelContentDelegate where Self: UIViewController { 30 | 31 | func dismissPanel() { 32 | panelNavigationController?.panelViewController?.dismiss(animated: true, completion: nil) 33 | } 34 | 35 | func popPanel() { 36 | 37 | guard let panel = panelNavigationController?.panelViewController else { 38 | return 39 | } 40 | 41 | panel.manager?.toggleFloatStatus(for: panel) 42 | 43 | } 44 | 45 | func panelFloatToggleBtnTitle() -> String { 46 | 47 | guard let panel = panelNavigationController?.panelViewController else { 48 | return closeButtonTitle 49 | } 50 | 51 | if panel.isFloating || panel.isPinned { 52 | return closeButtonTitle 53 | } else { 54 | return popButtonTitle 55 | } 56 | } 57 | 58 | func getBackBtn() -> UIBarButtonItem { 59 | 60 | let button = BlockBarButtonItem(title: modalCloseButtonTitle, style: .done) { [weak self] in 61 | self?.dismissPanel() 62 | } 63 | 64 | return button 65 | } 66 | 67 | func getPanelToggleBtn() -> UIBarButtonItem { 68 | 69 | let panel = panelNavigationController?.panelViewController 70 | 71 | if let button = panel?.popBarButtonItem { 72 | button.title = panelFloatToggleBtnTitle() 73 | return button 74 | } 75 | 76 | let button = BlockBarButtonItem(title: "", style: .done) { [weak self] in 77 | self?.popPanel() 78 | } 79 | 80 | panel?.popBarButtonItem = button 81 | 82 | button.title = panelFloatToggleBtnTitle() 83 | 84 | return button 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /PanelKit/PanelContentDelegate/PanelContentDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelContentDelegate.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 12/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// PanelContentDelegate determines the panel size and allows 12 | /// you to get notified for certain events. 13 | public protocol PanelContentDelegate: class { 14 | 15 | /// The title for the close button in the navigation bar. 16 | var closeButtonTitle: String { get } 17 | 18 | /// The title for the pop button in the navigation bar. 19 | /// This is the button that will make the panel float when tapped. 20 | var popButtonTitle: String { get } 21 | 22 | /// The close button title for the panel when it is presented modally. 23 | var modalCloseButtonTitle: String { get } 24 | 25 | /// Return true to make the panel manager resize the panel 26 | /// when a keyboard is shown. 27 | /// 28 | /// Typically you would only want to return true when something 29 | /// in the panel is first responser. 30 | /// 31 | /// Returns false by default. 32 | var shouldAdjustForKeyboard: Bool { get } 33 | 34 | /// The size the panel should have while floating. 35 | /// The panel manager will try to maintain the size specified by 36 | /// this property. The panel manager may deviate from this size, 37 | /// for example when the keyboard is shown. 38 | var preferredPanelContentSize: CGSize { get } 39 | 40 | /// The width the panel should have when it is pinned left or right. 41 | /// 42 | /// Returns `preferredPanelContentSize.width` by default. 43 | var preferredPanelPinnedWidth: CGFloat { get } 44 | 45 | /// The height the panel should have when it is pinned at the top or bottom. 46 | /// 47 | /// Returns `preferredPanelContentSize.height` by default. 48 | var preferredPanelPinnedHeight: CGFloat { get } 49 | 50 | /// The `minimumPanelContentSize` controls the minimum size 51 | /// a panel may have while floating. 52 | /// If this property differs from `preferredPanelContentSize`, it will 53 | /// allow the user to resize the panel. 54 | /// 55 | /// Returns `preferredPanelContentSize` by default. 56 | var minimumPanelContentSize: CGSize { get } 57 | 58 | /// The `maximumPanelContentSize` controls the maximum size 59 | /// a panel may have while floating. 60 | /// If this property differs from `preferredPanelContentSize`, it will 61 | /// allow the user to resize the panel. 62 | /// 63 | /// Returns `preferredPanelContentSize` by default. 64 | var maximumPanelContentSize: CGSize { get } 65 | 66 | /// Notifies you that the keyboard will be shown. 67 | /// Use this to update any constraints that descend from the panel's view. 68 | /// The constraints will be updated with an animation automatically. 69 | /// 70 | /// - Parameter frame: the keyboard frame, 71 | /// in the panel's coordinate space. 72 | func updateConstraintsForKeyboardShow(with frame: CGRect) 73 | 74 | /// Notifies you that the keyboard will be shown. 75 | /// Use this to change any view frames (when not using Auto Layout). 76 | /// This function will be invoked in a UIView animation block. 77 | /// 78 | /// - Parameter frame: the keyboard frame, 79 | /// in the panel's coordinate space. 80 | func updateUIForKeyboardShow(with frame: CGRect) 81 | 82 | /// Notifies you that the keyboard will hide. 83 | /// Use this to update any constraints that descend from the panel's view. 84 | /// The constraints will be updated with an animation automatically. 85 | func updateConstraintsForKeyboardHide() 86 | 87 | /// Notifies you that the keyboard will hide. 88 | /// Use this to change any view frames (when not using Auto Layout). 89 | /// This function will be invoked in a UIView animation block. 90 | func updateUIForKeyboardHide() 91 | 92 | /// Excludes potential "close" or "pop" buttons. 93 | /// Default implementation is an empty array. 94 | var leftBarButtonItems: [UIBarButtonItem] { get } 95 | 96 | /// Excludes potential "close" or "pop" buttons. 97 | /// Default implementation is an empty array. 98 | var rightBarButtonItems: [UIBarButtonItem] { get } 99 | 100 | /// This is called when the state of the panel changes. 101 | /// The default implementation provides the default close or pop button. 102 | /// Only implement yourself if you wish to use your own close and pop button. 103 | func updateNavigationButtons() 104 | 105 | /// Return true to make the drag gesture recognizer receive its touch. 106 | /// This is only applicable when a panel is in a floating state. 107 | /// Returning false will prevent the panel from being dragged. 108 | /// 109 | /// This can be used to prevent the panel from dragging in certain areas. 110 | func panelDragGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool 111 | 112 | /// When true: the close UIBarButtonItem will be hidden when the panel is floating. 113 | /// Default is false. 114 | var hideCloseButtonWhileFloating: Bool { get } 115 | 116 | /// When true: the close UIBarButtonItem will be hidden when the panel is pinned. 117 | /// Default is false. 118 | var hideCloseButtonWhilePinned: Bool { get } 119 | } 120 | -------------------------------------------------------------------------------- /PanelKit/PanelKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // PanelKit.h 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 11/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+AutoLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+AutoLayout.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelManager { 12 | 13 | func updateContentViewFrame(to frame: CGRect) { 14 | 15 | // First remove constraints that will be recreated 16 | 17 | var constraintsToCheck = [NSLayoutConstraint]() 18 | constraintsToCheck.append(contentsOf: panelContentWrapperView.constraints) 19 | constraintsToCheck.append(contentsOf: panelContentView.constraints) 20 | 21 | for c in constraintsToCheck { 22 | 23 | if (c.firstItem === panelContentView && c.secondItem === panelContentWrapperView) || 24 | (c.secondItem === panelContentView && c.firstItem === panelContentWrapperView) { 25 | 26 | if panelContentView.constraints.contains(c) { 27 | panelContentView.removeConstraint(c) 28 | } else if panelContentWrapperView.constraints.contains(c) { 29 | panelContentWrapperView.removeConstraint(c) 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | // Recreate them 37 | 38 | panelContentView.topAnchor.constraint(equalTo: panelContentWrapperView.topAnchor, constant: frame.origin.y).isActive = true 39 | panelContentView.bottomAnchor.constraint(equalTo: panelContentWrapperView.bottomAnchor, constant: frame.maxY - panelContentWrapperView.bounds.height).isActive = true 40 | 41 | panelContentView.leadingAnchor.constraint(equalTo: panelContentWrapperView.leadingAnchor, constant: frame.origin.x).isActive = true 42 | 43 | panelContentView.trailingAnchor.constraint(equalTo: panelContentWrapperView.trailingAnchor, constant: frame.maxX - panelContentWrapperView.bounds.width).isActive = true 44 | 45 | } 46 | 47 | /// Updates the panel's constraints to match the specified frame 48 | func updateFrame(for panel: PanelViewController, to frame: CGRect, keyboardShown: Bool = false) { 49 | 50 | guard panel.view.superview == panelContentWrapperView else { 51 | return 52 | } 53 | 54 | if panel.topConstraint == nil { 55 | panel.topConstraint = panel.view.topAnchor.constraint(equalTo: panelContentWrapperView.topAnchor, constant: 0.0) 56 | } 57 | 58 | if panel.bottomConstraint == nil { 59 | panel.bottomConstraint = panel.view.bottomAnchor.constraint(equalTo: panelContentWrapperView.bottomAnchor, constant: 0.0) 60 | } 61 | 62 | panel.leadingConstraint?.isActive = false 63 | panel.leadingConstraint = panel.view.leadingAnchor.constraint(equalTo: panelContentWrapperView.leadingAnchor, constant: 0.0) 64 | 65 | panel.trailingConstraint?.isActive = false 66 | panel.trailingConstraint = panel.view.trailingAnchor.constraint(equalTo: panelContentWrapperView.trailingAnchor, constant: 0.0) 67 | 68 | if let pinnedSide = panel.pinnedMetadata?.side, !keyboardShown && !isInExpose { 69 | 70 | if pinnedSide == .left || pinnedSide == .right { 71 | 72 | panel.heightConstraint?.isActive = false 73 | 74 | let insets = dragInsets(for: panel) 75 | 76 | let multiplier = 1.0 / CGFloat(numberOfPanelsPinned(at: pinnedSide)) 77 | panel.heightConstraint = panel.view.heightAnchor.constraint(equalTo: panelContentWrapperView.heightAnchor, multiplier: multiplier, constant: -insets.top - insets.bottom) 78 | panel.heightConstraint?.isActive = true 79 | 80 | panel.widthConstraint?.isActive = true 81 | panel.widthConstraint?.constant = frame.width 82 | 83 | } else { 84 | 85 | panel.widthConstraint?.isActive = false 86 | 87 | let multiplier = 1.0 / CGFloat(numberOfPanelsPinned(at: pinnedSide)) 88 | panel.widthConstraint = panel.view.widthAnchor.constraint(equalTo: panelContentView.widthAnchor, multiplier: multiplier) 89 | panel.widthConstraint?.isActive = true 90 | 91 | panel.heightConstraint?.isActive = true 92 | panel.heightConstraint?.constant = frame.height 93 | } 94 | 95 | } else { 96 | 97 | panel.heightConstraint?.isActive = false 98 | panel.heightConstraint = panel.view.heightAnchor.constraint(equalToConstant: frame.height) 99 | panel.heightConstraint?.isActive = true 100 | panel.heightConstraint?.constant = frame.height 101 | 102 | panel.widthConstraint?.isActive = false 103 | panel.widthConstraint = panel.view.widthAnchor.constraint(equalToConstant: frame.width) 104 | panel.widthConstraint?.isActive = true 105 | panel.widthConstraint?.constant = frame.width 106 | 107 | } 108 | 109 | if let pinnedMetadata = panel.pinnedMetadata, pinnedMetadata.side == .top || pinnedMetadata.side == .bottom { 110 | 111 | panel.topConstraint?.constant = frame.origin.y 112 | panel.bottomConstraint?.constant = frame.maxY - panelContentWrapperView.bounds.maxY 113 | 114 | if frame.center.y > panelContentView.frame.center.y { 115 | 116 | panel.topConstraint?.isActive = false 117 | panel.bottomConstraint?.isActive = true 118 | 119 | } else { 120 | 121 | panel.topConstraint?.isActive = true 122 | panel.bottomConstraint?.isActive = false 123 | 124 | } 125 | 126 | var useLeadingConstraint = false 127 | 128 | if pinnedMetadata.index > 0, !keyboardShown { 129 | 130 | var panelsPinned = self.panelsPinned(at: pinnedMetadata.side).sorted { (p1, p2) -> Bool in 131 | return p1.pinnedMetadata?.index ?? 0 < p2.pinnedMetadata?.index ?? 0 132 | } 133 | 134 | let panelPinnedLeft = panelsPinned[pinnedMetadata.index - 1] 135 | 136 | assert(panelPinnedLeft != panel, "Panel logic error") 137 | 138 | panel.leadingConstraint?.isActive = false 139 | panel.leadingConstraint = panel.view.leadingAnchor.constraint(equalTo: panelPinnedLeft.view.trailingAnchor, constant: 0.0) 140 | 141 | useLeadingConstraint = true 142 | 143 | } else { 144 | 145 | panel.leadingConstraint?.isActive = false 146 | panel.leadingConstraint = panel.view.leadingAnchor.constraint(equalTo: panelContentView.leadingAnchor, constant: 0.0) 147 | 148 | if let pinnedSide = panel.pinnedMetadata?.side, numberOfPanelsPinned(at: pinnedSide) == 1, !isInExpose { 149 | 150 | panel.leadingConstraint?.constant = 0 151 | 152 | } else { 153 | 154 | panel.leadingConstraint?.constant = 0 155 | 156 | } 157 | 158 | if pinnedMetadata.side == .bottom, !keyboardShown { 159 | 160 | panel.bottomConstraint?.isActive = false 161 | panel.bottomConstraint = panel.view.bottomAnchor.constraint(equalTo: panelContentWrapperView.bottomAnchor, constant: 0.0) 162 | panel.topConstraint?.isActive = false 163 | panel.bottomConstraint?.isActive = true 164 | 165 | } 166 | 167 | } 168 | 169 | panel.trailingConstraint?.constant = frame.maxX - panelContentWrapperView.bounds.maxX 170 | 171 | if !useLeadingConstraint && frame.center.x > panelContentWrapperView.bounds.center.x { 172 | 173 | panel.leadingConstraint?.isActive = false 174 | panel.trailingConstraint?.isActive = true 175 | 176 | } else { 177 | 178 | panel.leadingConstraint?.isActive = true 179 | panel.trailingConstraint?.isActive = false 180 | 181 | } 182 | 183 | } else { 184 | 185 | panel.leadingConstraint?.constant = frame.origin.x 186 | panel.trailingConstraint?.constant = frame.maxX - panelContentWrapperView.bounds.maxX 187 | 188 | if frame.center.x > panelContentView.frame.center.x { 189 | 190 | panel.leadingConstraint?.isActive = false 191 | panel.trailingConstraint?.isActive = true 192 | 193 | } else { 194 | 195 | panel.leadingConstraint?.isActive = true 196 | panel.trailingConstraint?.isActive = false 197 | 198 | } 199 | 200 | var useTopConstraint = false 201 | 202 | if let pinnedMetadata = panel.pinnedMetadata, pinnedMetadata.index > 0, !keyboardShown { 203 | 204 | var panelsPinned = self.panelsPinned(at: pinnedMetadata.side).sorted { (p1, p2) -> Bool in 205 | return p1.pinnedMetadata?.index ?? 0 < p2.pinnedMetadata?.index ?? 0 206 | } 207 | 208 | let panelPinnedAbove = panelsPinned[pinnedMetadata.index - 1] 209 | 210 | assert(panelPinnedAbove != panel, "Panel logic error") 211 | 212 | panel.topConstraint?.isActive = false 213 | panel.topConstraint = panel.view.topAnchor.constraint(equalTo: panelPinnedAbove.view.bottomAnchor, constant: 0.0) 214 | 215 | useTopConstraint = true 216 | 217 | } else { 218 | 219 | panel.topConstraint?.isActive = false 220 | panel.topConstraint = panel.view.topAnchor.constraint(equalTo: panelContentWrapperView.topAnchor, constant: 0.0) 221 | 222 | if let pinnedSide = panel.pinnedMetadata?.side, numberOfPanelsPinned(at: pinnedSide) == 1, !isInExpose { 223 | 224 | panel.topConstraint?.constant = 0 225 | 226 | } else { 227 | 228 | panel.topConstraint?.constant = frame.origin.y 229 | 230 | } 231 | 232 | } 233 | 234 | panel.bottomConstraint?.constant = frame.maxY - panelContentWrapperView.bounds.maxY 235 | 236 | if !useTopConstraint && frame.center.y > panelContentWrapperView.bounds.center.y { 237 | 238 | panel.topConstraint?.isActive = false 239 | panel.bottomConstraint?.isActive = true 240 | 241 | } else { 242 | 243 | panel.topConstraint?.isActive = true 244 | panel.bottomConstraint?.isActive = false 245 | 246 | } 247 | 248 | } 249 | 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Closing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Closing.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 08/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelManager { 12 | 13 | public func close(_ panel: PanelViewController) { 14 | 15 | panel.resizeCornerHandle.removeFromSuperview() 16 | panel.view.removeFromSuperview() 17 | 18 | panel.contentDelegate?.didUpdateFloatingState() 19 | 20 | panel.viewWillDisappear(false) 21 | panel.viewDidDisappear(false) 22 | 23 | if panel.isPinned || panel.wasPinned { 24 | didDragFree(panel, from: nil) 25 | } 26 | 27 | } 28 | 29 | } 30 | 31 | public extension PanelManager { 32 | 33 | func closeAllPinnedPanels() { 34 | 35 | for panel in panels { 36 | 37 | guard panel.view.superview == panelContentWrapperView else { 38 | continue 39 | } 40 | 41 | guard panel.isPinned || panel.wasPinned else { 42 | continue 43 | } 44 | 45 | close(panel) 46 | 47 | } 48 | 49 | } 50 | 51 | func closeAllFloatingPanels() { 52 | 53 | for panel in panels { 54 | 55 | guard panel.view.superview == panelContentWrapperView else { 56 | continue 57 | } 58 | 59 | guard panel.isFloating else { 60 | continue 61 | } 62 | 63 | close(panel) 64 | 65 | } 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Default.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Default.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public extension PanelManager { 13 | 14 | func didUpdatePinnedPanels() { 15 | 16 | } 17 | 18 | func enablePanelShadow(for panel: PanelViewController) -> Bool { 19 | return true 20 | } 21 | 22 | var allowFloatingPanels: Bool { 23 | return panelContentWrapperView.bounds.width > 800 24 | } 25 | 26 | var allowPanelPinning: Bool { 27 | return panelContentWrapperView.bounds.width > 800 28 | } 29 | 30 | func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int { 31 | return 1 32 | } 33 | 34 | var panelManagerLogLevel: LogLevel { 35 | return .none 36 | } 37 | 38 | func dragInsets(for panel: PanelViewController) -> UIEdgeInsets { 39 | return .zero 40 | } 41 | 42 | func willEnterExpose() { 43 | 44 | } 45 | 46 | func willExitExpose() { 47 | 48 | } 49 | 50 | var exposeOverlayBlurEffect: UIBlurEffect { 51 | return UIBlurEffect(style: .light) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Dragging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Dragging.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension PanelManager { 13 | 14 | func didDragFree(_ panel: PanelViewController, from point: CGPoint?) { 15 | 16 | fadePinnedPreviewOut(for: panel) 17 | 18 | guard let pinnedMetadata = panel.pinnedMetadata else { 19 | return 20 | } 21 | 22 | let isPinned = panel.isPinned 23 | 24 | guard isPinned || panel.wasPinned else { 25 | return 26 | } 27 | 28 | guard let panelView = panel.view else { 29 | return 30 | } 31 | 32 | guard let contentDelegate = panel.contentDelegate else { 33 | return 34 | } 35 | 36 | if panel.logLevel == .full { 37 | print("did drag \(panel) free from \(String(describing: point))") 38 | } 39 | 40 | var prevPinnedPanels = panelsPinned(at: pinnedMetadata.side).sorted { (p1, p2) -> Bool in 41 | return p1.pinnedMetadata?.index ?? 0 < p2.pinnedMetadata?.index ?? 0 42 | } 43 | 44 | prevPinnedPanels.remove(at: pinnedMetadata.index) 45 | 46 | panel.pinnedMetadata = nil 47 | 48 | panel.bringToFront() 49 | 50 | panel.enableCornerRadius(animated: true, duration: panelGrowDuration) 51 | panel.enableShadow(animated: true, duration: panelGrowDuration) 52 | 53 | let side = pinnedMetadata.side 54 | 55 | let currentFrame = panelView.frame 56 | 57 | var newFrame = currentFrame 58 | 59 | let preferredPanelPinnedWidth = contentDelegate.preferredPanelPinnedWidth 60 | let preferredPanelPinnedHeight = contentDelegate.preferredPanelPinnedWidth 61 | let preferredPanelContentSize = contentDelegate.preferredPanelContentSize 62 | newFrame.size = panel.floatingSize ?? preferredPanelContentSize 63 | 64 | if side == .right { 65 | if newFrame.width > preferredPanelPinnedWidth { 66 | let delta = newFrame.width - preferredPanelPinnedWidth 67 | newFrame.origin.x -= delta 68 | } 69 | } 70 | 71 | if side == .bottom { 72 | if newFrame.height > preferredPanelPinnedHeight { 73 | let delta = newFrame.height - preferredPanelPinnedHeight 74 | newFrame.origin.y -= delta 75 | } 76 | } 77 | 78 | if let point = point { 79 | 80 | if !newFrame.contains(point) { 81 | 82 | if side == .left || side == .right { 83 | 84 | if newFrame.minY > point.y || newFrame.maxY < point.y { 85 | newFrame.origin.y += point.y - newFrame.maxY 86 | } 87 | 88 | } else { 89 | 90 | if newFrame.minX > point.x || newFrame.maxX < point.x { 91 | newFrame.origin.x += point.x - newFrame.maxX 92 | } 93 | 94 | } 95 | 96 | } 97 | 98 | } 99 | 100 | newFrame = panel.allowedFrame(for: newFrame) 101 | 102 | updateFrame(for: panel, to: newFrame) 103 | 104 | if numberOfPanelsPinned(at: side) > 0 { 105 | 106 | for pinnedPanel in panelsPinned(at: side) { 107 | 108 | if pinnedPanel == panel { 109 | continue 110 | } 111 | 112 | pinnedPanel.pinnedMetadata?.index = prevPinnedPanels.index(of: pinnedPanel) ?? 0 113 | 114 | } 115 | 116 | for pinnedPanel in panelsPinned(at: side) { 117 | 118 | if pinnedPanel == panel { 119 | continue 120 | } 121 | 122 | guard let newPosition = pinnedPanelPosition(for: pinnedPanel, at: side) else { 123 | assertionFailure("Expected a valid position") 124 | continue 125 | } 126 | 127 | self.updateFrame(for: pinnedPanel, to: newPosition.frame) 128 | 129 | } 130 | 131 | } 132 | 133 | updateContentViewFrame(to: updatedContentViewFrame()) 134 | 135 | UIView.animate(withDuration: panelGrowDuration, delay: 0.0, options: [.allowAnimatedContent, .allowUserInteraction], animations: { 136 | 137 | self.panelContentWrapperView.layoutIfNeeded() 138 | 139 | self.didUpdatePinnedPanels() 140 | 141 | }, completion: { (_) in 142 | 143 | }) 144 | 145 | panel.showResizeHandleIfNeeded() 146 | 147 | } 148 | 149 | func didDrag(_ panel: PanelViewController, toEdgeOf side: PanelPinSide) { 150 | 151 | guard allowPanelPinning else { 152 | return 153 | } 154 | 155 | guard numberOfPanelsPinned(at: side) < maximumNumberOfPanelsPinned(at: side) else { 156 | return 157 | } 158 | 159 | guard !panel.isPinned else { 160 | return 161 | } 162 | 163 | guard let panelView = panel.view else { 164 | return 165 | } 166 | 167 | guard let previewTargetPosition = pinnedPanelPosition(for: panel, at: side) else { 168 | return 169 | } 170 | 171 | if let currentPreviewView = panel.panelPinnedPreviewView { 172 | 173 | if currentPreviewView.frame == previewTargetPosition.frame { 174 | return 175 | } 176 | 177 | } 178 | 179 | if panel.logLevel == .full { 180 | print("did drag \(panel) to edge of \(side) side") 181 | } 182 | 183 | let previewStartFrame = panelView.layer.presentation()?.frame ?? panelView.frame 184 | 185 | let previewView = panel.panelPinnedPreviewView ?? UIView(frame: previewStartFrame) 186 | previewView.isUserInteractionEnabled = false 187 | 188 | previewView.backgroundColor = panel.tintColor 189 | previewView.alpha = pinnedPanelPreviewAlpha 190 | 191 | panelContentWrapperView.addSubview(previewView) 192 | panelContentWrapperView.insertSubview(previewView, belowSubview: panelView) 193 | 194 | UIView.animate(withDuration: pinnedPanelPreviewGrowDuration) { 195 | 196 | previewView.frame = previewTargetPosition.frame 197 | 198 | } 199 | 200 | panel.panelPinnedPreviewView = previewView 201 | } 202 | 203 | func didEndDrag(_ panel: PanelViewController, toEdgeOf side: PanelPinSide) { 204 | 205 | let pinnedPreviewView = panel.panelPinnedPreviewView 206 | 207 | fadePinnedPreviewOut(for: panel) 208 | 209 | guard allowPanelPinning else { 210 | return 211 | } 212 | 213 | guard numberOfPanelsPinned(at: side) < maximumNumberOfPanelsPinned(at: side) else { 214 | return 215 | } 216 | 217 | guard !panel.isPinned else { 218 | return 219 | } 220 | 221 | guard let panelView = panel.view else { 222 | return 223 | } 224 | 225 | guard let position = pinnedPanelPosition(for: panel, at: side) else { 226 | return 227 | } 228 | 229 | if panel.logLevel == .full { 230 | print("did pin \(panel) to edge of \(side) side") 231 | } 232 | 233 | var prevPinnedPanels = panelsPinned(at: side).sorted { (p1, p2) -> Bool in 234 | return p1.pinnedMetadata?.index ?? 0 < p2.pinnedMetadata?.index ?? 0 235 | } 236 | 237 | panel.pinnedMetadata = PanelPinnedMetadata(side: side, index: position.index) 238 | 239 | prevPinnedPanels.insert(panel, at: position.index) 240 | 241 | panel.disableCornerRadius(animated: true, duration: panelGrowDuration) 242 | panel.disableShadow(animated: true, duration: panelGrowDuration) 243 | 244 | panel.floatingSize = panel.view.frame.size 245 | 246 | if numberOfPanelsPinned(at: side) > 1 { 247 | 248 | for pinnedPanel in panelsPinned(at: side) { 249 | 250 | if pinnedPanel == panel { 251 | continue 252 | } 253 | 254 | pinnedPanel.pinnedMetadata?.index = prevPinnedPanels.index(of: pinnedPanel) ?? 0 255 | 256 | } 257 | 258 | for pinnedPanel in panelsPinned(at: side) { 259 | 260 | if pinnedPanel == panel { 261 | continue 262 | } 263 | 264 | guard let newPosition = pinnedPanelPosition(for: pinnedPanel, at: side) else { 265 | assertionFailure("Expected a valid position") 266 | continue 267 | } 268 | 269 | self.updateFrame(for: pinnedPanel, to: newPosition.frame) 270 | 271 | } 272 | 273 | } 274 | 275 | self.updateFrame(for: panel, to: position.frame) 276 | 277 | updateContentViewFrame(to: updatedContentViewFrame()) 278 | 279 | UIView.animate(withDuration: panelGrowDuration, delay: 0.0, options: [.allowAnimatedContent, .allowUserInteraction], animations: { 280 | 281 | self.panelContentWrapperView.layoutIfNeeded() 282 | 283 | self.didUpdatePinnedPanels() 284 | 285 | }, completion: { (_) in 286 | 287 | // Send panel and preview view to back, so (shadows of) non-pinned panels are on top 288 | self.panelContentWrapperView.insertSubview(panelView, aboveSubview: self.panelContentView) 289 | 290 | if let pinnedPreviewView = pinnedPreviewView, pinnedPreviewView.superview != nil { 291 | self.panelContentWrapperView.insertSubview(pinnedPreviewView, aboveSubview: self.panelContentView) 292 | } 293 | 294 | }) 295 | 296 | self.moveAllPanelsToValidPositions() 297 | 298 | UIView.animate(withDuration: panelGrowDuration, delay: 0.0, options: [.allowAnimatedContent, .allowUserInteraction], animations: { 299 | 300 | self.panelContentWrapperView.layoutIfNeeded() 301 | 302 | }) 303 | 304 | panel.hideResizeHandle() 305 | 306 | } 307 | 308 | func didEndDragFree(_ panel: PanelViewController) { 309 | 310 | fadePinnedPreviewOut(for: panel) 311 | 312 | } 313 | 314 | } 315 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Expose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Expose.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 24/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | private var exposeOverlayViewKey: UInt8 = 0 13 | private var exposeOverlayTapRecognizerKey: UInt8 = 0 14 | private var exposeEnterTapRecognizerKey: UInt8 = 0 15 | 16 | extension PanelManager { 17 | 18 | var exposeOverlayView: UIVisualEffectView { 19 | get { 20 | return associatedObject(self, key: &exposeOverlayViewKey) { 21 | return UIVisualEffectView(effect: nil) 22 | } 23 | } 24 | set { 25 | associateObject(self, key: &exposeOverlayViewKey, value: newValue) 26 | } 27 | } 28 | 29 | var exposeOverlayTapRecognizer: BlockGestureRecognizer { 30 | get { 31 | return associatedObject(self, key: &exposeOverlayTapRecognizerKey) { 32 | 33 | let gestureRecognizer = UITapGestureRecognizer() 34 | 35 | let blockRecognizer = BlockGestureRecognizer(view: exposeOverlayView, recognizer: gestureRecognizer, closure: { [weak self] in 36 | 37 | if self?.isInExpose == true { 38 | self?.exitExpose() 39 | } 40 | }) 41 | 42 | return blockRecognizer 43 | } 44 | } 45 | set { 46 | associateObject(self, key: &exposeOverlayTapRecognizerKey, value: newValue) 47 | } 48 | } 49 | 50 | var exposeEnterTapRecognizer: BlockGestureRecognizer { 51 | get { 52 | return associatedObject(self, key: &exposeEnterTapRecognizerKey) { 53 | 54 | let tapGestureRecognizer = UITapGestureRecognizer() 55 | tapGestureRecognizer.numberOfTapsRequired = 2 56 | tapGestureRecognizer.numberOfTouchesRequired = 3 57 | 58 | let blockRecognizer = BlockGestureRecognizer(view: panelContentWrapperView, recognizer: tapGestureRecognizer) { [weak self] in 59 | 60 | self?.toggleExpose() 61 | 62 | } 63 | 64 | return blockRecognizer 65 | } 66 | } 67 | set { 68 | associateObject(self, key: &exposeEnterTapRecognizerKey, value: newValue) 69 | } 70 | } 71 | 72 | } 73 | 74 | public extension PanelManager { 75 | 76 | func enableTripleTapExposeActivation() { 77 | 78 | _ = exposeEnterTapRecognizer 79 | 80 | } 81 | 82 | func toggleExpose() { 83 | 84 | if isInExpose { 85 | exitExpose() 86 | } else { 87 | enterExpose() 88 | } 89 | 90 | } 91 | 92 | var isInExpose: Bool { 93 | 94 | for panel in panels { 95 | if panel.isInExpose { 96 | return true 97 | } 98 | } 99 | 100 | return false 101 | } 102 | 103 | func enterExpose() { 104 | 105 | guard !isInExpose else { 106 | return 107 | } 108 | 109 | addExposeOverlayViewIfNeeded() 110 | 111 | let exposePanels = panels.filter { (p) -> Bool in 112 | return p.isPinned || p.isFloating 113 | } 114 | 115 | guard !exposePanels.isEmpty else { 116 | return 117 | } 118 | 119 | willEnterExpose() 120 | 121 | let (panelFrames, scale) = calculateExposeFrames(with: exposePanels) 122 | 123 | for panelFrame in panelFrames { 124 | panelFrame.panel.frameBeforeExpose = panelFrame.panel.view.frame 125 | updateFrame(for: panelFrame.panel, to: panelFrame.exposeFrame) 126 | } 127 | 128 | panelContentWrapperView.insertSubview(exposeOverlayView, aboveSubview: panelContentView) 129 | exposeOverlayView.isUserInteractionEnabled = true 130 | 131 | UIView.animate(withDuration: exposeEnterDuration, delay: 0.0, options: [], animations: { 132 | 133 | self.exposeOverlayView.effect = self.exposeOverlayBlurEffect 134 | 135 | self.panelContentWrapperView.layoutIfNeeded() 136 | 137 | for panelFrame in panelFrames { 138 | 139 | panelFrame.panel.view.transform = CGAffineTransform(scaleX: scale, y: scale) 140 | 141 | } 142 | 143 | }) 144 | 145 | for panel in panels { 146 | panel.hideResizeHandle() 147 | } 148 | 149 | } 150 | 151 | func exitExpose() { 152 | 153 | guard isInExpose else { 154 | return 155 | } 156 | 157 | let exposePanels = panels.filter { (p) -> Bool in 158 | return p.isInExpose 159 | } 160 | 161 | guard !exposePanels.isEmpty else { 162 | return 163 | } 164 | 165 | willExitExpose() 166 | 167 | for panel in exposePanels { 168 | if let frameBeforeExpose = panel.frameBeforeExpose { 169 | updateFrame(for: panel, to: frameBeforeExpose) 170 | panel.frameBeforeExpose = nil 171 | } 172 | } 173 | 174 | exposeOverlayView.isUserInteractionEnabled = false 175 | 176 | UIView.animate(withDuration: exposeExitDuration, delay: 0.0, options: [], animations: { 177 | 178 | self.exposeOverlayView.effect = nil 179 | 180 | self.panelContentWrapperView.layoutIfNeeded() 181 | 182 | for panel in exposePanels { 183 | 184 | panel.view.transform = .identity 185 | 186 | } 187 | 188 | }) 189 | 190 | for panel in panels { 191 | panel.showResizeHandleIfNeeded() 192 | } 193 | 194 | } 195 | 196 | } 197 | 198 | extension PanelManager { 199 | 200 | func addExposeOverlayViewIfNeeded() { 201 | 202 | if exposeOverlayView.superview == nil { 203 | 204 | exposeOverlayView.translatesAutoresizingMaskIntoConstraints = false 205 | 206 | panelContentWrapperView.addSubview(exposeOverlayView) 207 | panelContentWrapperView.insertSubview(exposeOverlayView, aboveSubview: panelContentView) 208 | 209 | exposeOverlayView.topAnchor.constraint(equalTo: panelContentView.topAnchor).isActive = true 210 | exposeOverlayView.bottomAnchor.constraint(equalTo: panelContentView.bottomAnchor).isActive = true 211 | exposeOverlayView.leadingAnchor.constraint(equalTo: panelContentWrapperView.leadingAnchor).isActive = true 212 | exposeOverlayView.trailingAnchor.constraint(equalTo: panelContentWrapperView.trailingAnchor).isActive = true 213 | 214 | exposeOverlayView.alpha = 1.0 215 | 216 | exposeOverlayView.isUserInteractionEnabled = false 217 | 218 | panelContentWrapperView.layoutIfNeeded() 219 | 220 | _ = exposeOverlayTapRecognizer 221 | } 222 | 223 | } 224 | 225 | func calculateExposeFrames(with panels: [PanelViewController]) -> ([PanelExposeFrame], CGFloat) { 226 | 227 | let panelFrames: [PanelExposeFrame] = panels.map { (p) -> PanelExposeFrame in 228 | return PanelExposeFrame(panel: p) 229 | } 230 | 231 | distribute(panelFrames) 232 | 233 | guard let unionFrame = unionRect(with: panelFrames) else { 234 | return (panelFrames, 1.0) 235 | } 236 | 237 | if panelManagerLogLevel == .full { 238 | print("[Exposé] unionFrame: \(unionFrame)") 239 | } 240 | 241 | for r in panelFrames { 242 | 243 | r.exposeFrame.origin.x -= unionFrame.origin.x 244 | r.exposeFrame.origin.y -= unionFrame.origin.y 245 | 246 | } 247 | 248 | var normalizedUnionFrame = unionFrame 249 | normalizedUnionFrame.origin.x = 0.0 250 | normalizedUnionFrame.origin.y = 0.0 251 | 252 | if panelManagerLogLevel == .full { 253 | print("[Exposé] normalizedUnionFrame: \(normalizedUnionFrame)") 254 | } 255 | 256 | var exposeContainmentFrame = panelContentView.frame 257 | exposeContainmentFrame.size.width = panelContentWrapperView.frame.width 258 | exposeContainmentFrame.origin.x = 0 259 | 260 | let padding: CGFloat = exposeOuterPadding 261 | 262 | let scale = min(1.0, min(((exposeContainmentFrame.width - padding) / unionFrame.width), ((exposeContainmentFrame.height - padding) / unionFrame.height))) 263 | 264 | if panelManagerLogLevel == .full { 265 | print("[Exposé] scale: \(scale)") 266 | } 267 | 268 | var scaledNormalizedUnionFrame = normalizedUnionFrame 269 | scaledNormalizedUnionFrame.size.width *= scale 270 | scaledNormalizedUnionFrame.size.height *= scale 271 | 272 | if panelManagerLogLevel == .full { 273 | print("[Exposé] scaledNormalizedUnionFrame: \(scaledNormalizedUnionFrame)") 274 | } 275 | 276 | for r in panelFrames { 277 | 278 | r.exposeFrame.origin.x *= scale 279 | r.exposeFrame.origin.y *= scale 280 | 281 | let width = r.exposeFrame.size.width 282 | let height = r.exposeFrame.size.height 283 | 284 | r.exposeFrame.origin.x -= width * (1.0 - scale) / 2 285 | r.exposeFrame.origin.y -= height * (1.0 - scale) / 2 286 | 287 | // Center 288 | 289 | r.exposeFrame.origin.x += (max(exposeContainmentFrame.width - scaledNormalizedUnionFrame.width, 0.0)) / 2.0 290 | r.exposeFrame.origin.y += (max(exposeContainmentFrame.height - scaledNormalizedUnionFrame.height, 0.0)) / 2.0 291 | r.exposeFrame.origin.y += exposeContainmentFrame.origin.y 292 | 293 | } 294 | 295 | return (panelFrames, scale) 296 | 297 | } 298 | 299 | func doFramesIntersect(_ frames: [PanelExposeFrame]) -> Bool { 300 | 301 | for r1 in frames { 302 | 303 | for r2 in frames { 304 | if r1 === r2 { 305 | continue 306 | } 307 | 308 | if numberOfIntersections(of: r1, with: [r2]) > 0 { 309 | return true 310 | } 311 | 312 | } 313 | 314 | } 315 | 316 | return false 317 | 318 | } 319 | 320 | func numberOfIntersections(of frame: PanelExposeFrame, with frames: [PanelExposeFrame]) -> Int { 321 | 322 | var intersections = 0 323 | 324 | let r1 = frame 325 | 326 | for r2 in frames { 327 | if r1 === r2 { 328 | continue 329 | } 330 | 331 | let r1InsetFrame = r1.exposeFrame.insetBy(dx: -exposePanelHorizontalSpacing, dy: -exposePanelVerticalSpacing) 332 | if r1InsetFrame.intersects(r2.exposeFrame) { 333 | intersections += 1 334 | } 335 | 336 | } 337 | 338 | return intersections 339 | } 340 | 341 | func unionRect(with frames: [PanelExposeFrame]) -> CGRect? { 342 | 343 | guard var rect = frames.first?.exposeFrame else { 344 | return nil 345 | } 346 | 347 | for r in frames { 348 | 349 | rect = rect.union(r.exposeFrame) 350 | 351 | } 352 | 353 | return rect 354 | 355 | } 356 | 357 | func distribute(_ frames: [PanelExposeFrame]) { 358 | 359 | var frames = frames 360 | 361 | var stack = [PanelExposeFrame]() 362 | 363 | while doFramesIntersect(frames) { 364 | 365 | let sortedFrames = frames.sorted(by: { (r1, r2) -> Bool in 366 | let n1 = numberOfIntersections(of: r1, with: frames) 367 | let n2 = numberOfIntersections(of: r2, with: frames) 368 | return n1 > n2 369 | }) 370 | 371 | guard let mostIntersected = sortedFrames.first else { 372 | break 373 | } 374 | 375 | stack.append(mostIntersected) 376 | 377 | guard let index = frames.index(where: { (r) -> Bool in 378 | r === mostIntersected 379 | }) else { 380 | break 381 | } 382 | 383 | frames.remove(at: index) 384 | 385 | } 386 | 387 | while !stack.isEmpty { 388 | 389 | guard let last = stack.popLast() else { 390 | break 391 | } 392 | 393 | frames.append(last) 394 | 395 | guard let unionRect = self.unionRect(with: frames) else { 396 | break 397 | } 398 | 399 | let g = CGPoint(x: unionRect.midX, y: unionRect.midY) 400 | 401 | let deltaX = max(1.0, last.panel.view.center.x - g.x) 402 | let deltaY = max(1.0, last.panel.view.center.y - g.y) 403 | 404 | while numberOfIntersections(of: last, with: frames) > 0 { 405 | 406 | last.exposeFrame.origin.x += deltaX / 20.0 407 | last.exposeFrame.origin.y += deltaY / 20.0 408 | 409 | } 410 | 411 | } 412 | 413 | } 414 | 415 | } 416 | 417 | class PanelExposeFrame { 418 | 419 | let panel: PanelViewController 420 | var exposeFrame: CGRect 421 | 422 | init(panel: PanelViewController) { 423 | self.panel = panel 424 | self.exposeFrame = panel.view.frame 425 | } 426 | 427 | } 428 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Floating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Floating.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension PanelManager where Self: UIViewController { 12 | 13 | var managerViewController: UIViewController { 14 | return self 15 | } 16 | 17 | } 18 | 19 | public extension PanelManager { 20 | 21 | func toggleFloatStatus(for panel: PanelViewController, animated: Bool = true, completion: (() -> Void)? = nil) { 22 | 23 | let panelNavCon = panel.panelNavigationController 24 | 25 | if (panel.isFloating || panel.isPinned) && !panelNavCon.isPresentedAsPopover { 26 | 27 | close(panel) 28 | completion?() 29 | 30 | } else if panelNavCon.isPresentedAsPopover { 31 | 32 | let rect = panel.view.convert(panel.view.frame, to: panelContentWrapperView) 33 | 34 | panel.dismiss(animated: false, completion: { 35 | 36 | self.floatPanel(panel, toRect: rect, animated: animated) 37 | 38 | completion?() 39 | 40 | }) 41 | 42 | } else { 43 | 44 | let rect = CGRect(origin: .zero, size: panel.preferredContentSize) 45 | floatPanel(panel, toRect: rect, animated: animated) 46 | 47 | } 48 | 49 | } 50 | 51 | internal func floatPanel(_ panel: PanelViewController, toRect rect: CGRect, animated: Bool) { 52 | 53 | self.panelContentWrapperView.addSubview(panel.resizeCornerHandle) 54 | 55 | self.panelContentWrapperView.addSubview(panel.view) 56 | 57 | panel.resizeCornerHandle.bottomAnchor.constraint(equalTo: panel.view.bottomAnchor, constant: 16).isActive = true 58 | panel.resizeCornerHandle.trailingAnchor.constraint(equalTo: panel.view.trailingAnchor, constant: 16).isActive = true 59 | 60 | panel.didUpdateFloatingState() 61 | 62 | self.updateFrame(for: panel, to: rect) 63 | self.panelContentWrapperView.layoutIfNeeded() 64 | 65 | let x = rect.origin.x 66 | let y = rect.origin.y + panelPopYOffset 67 | 68 | let width = panel.view.frame.size.width 69 | let height = panel.view.frame.size.height 70 | 71 | var newFrame = CGRect(x: x, y: y, width: width, height: height) 72 | newFrame.center = panel.allowedCenter(for: newFrame.center) 73 | 74 | self.updateFrame(for: panel, to: newFrame) 75 | 76 | if animated { 77 | 78 | UIView.animate(withDuration: panelPopDuration, delay: 0.0, options: [.allowUserInteraction, .curveEaseOut], animations: { 79 | 80 | self.panelContentWrapperView.layoutIfNeeded() 81 | 82 | }, completion: nil) 83 | 84 | } else { 85 | 86 | self.panelContentWrapperView.layoutIfNeeded() 87 | 88 | } 89 | 90 | if panel.view.superview == self.panelContentWrapperView { 91 | panel.contentDelegate?.didUpdateFloatingState() 92 | } 93 | 94 | } 95 | 96 | } 97 | 98 | public extension PanelManager { 99 | 100 | func float(_ panel: PanelViewController, at frame: CGRect) { 101 | 102 | guard !panel.isFloating else { 103 | return 104 | } 105 | 106 | guard panel.canFloat else { 107 | return 108 | } 109 | 110 | toggleFloatStatus(for: panel, animated: false) 111 | 112 | updateFrame(for: panel, to: frame) 113 | 114 | self.panelContentWrapperView.layoutIfNeeded() 115 | 116 | panel.viewWillAppear(false) 117 | panel.viewDidAppear(false) 118 | 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Offscreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Offscreen.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 13/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension PanelManager { 12 | 13 | func panelsPrepareMoveOffScreen() { 14 | 15 | for panel in panels { 16 | panel.prepareMoveOffScreen() 17 | } 18 | 19 | } 20 | 21 | func panelsPrepareMoveOnScreen() { 22 | 23 | for panel in panels { 24 | panel.prepareMoveOnScreen() 25 | } 26 | 27 | } 28 | 29 | func panelsMoveOnScreen() { 30 | 31 | for panel in panels { 32 | 33 | guard panel.isFloating || panel.isPinned else { 34 | continue 35 | } 36 | 37 | panel.movePanelOnScreen() 38 | 39 | } 40 | 41 | } 42 | 43 | func panelsMoveOffScreen() { 44 | 45 | for panel in panels { 46 | 47 | guard panel.isFloating || panel.isPinned else { 48 | continue 49 | } 50 | 51 | panel.movePanelOffScreen() 52 | } 53 | 54 | } 55 | 56 | func panelsCompleteMoveOnScreen() { 57 | 58 | for panel in panels { 59 | panel.completeMoveOnScreen() 60 | } 61 | 62 | } 63 | 64 | func panelsCompleteMoveOffScreen() { 65 | 66 | for panel in panels { 67 | panel.completeMoveOffScreen() 68 | } 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+Pinning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+Pinning.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 07/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension PanelManager { 12 | 13 | var panelPinnedLeft: PanelViewController? { 14 | return panelsPinnedLeft.first 15 | } 16 | 17 | var panelsPinnedLeft: [PanelViewController] { 18 | return panelsPinned(at: .left) 19 | } 20 | 21 | var numberOfPanelsPinnedLeft: Int { 22 | return numberOfPanelsPinned(at: .left) 23 | } 24 | 25 | var panelPinnedRight: PanelViewController? { 26 | return panelsPinnedRight.first 27 | } 28 | 29 | var panelsPinnedRight: [PanelViewController] { 30 | return panelsPinned(at: .right) 31 | } 32 | 33 | var numberOfPanelsPinnedRight: Int { 34 | return numberOfPanelsPinned(at: .right) 35 | } 36 | 37 | var panelPinnedTop: PanelViewController? { 38 | return panelsPinnedTop.first 39 | } 40 | 41 | var panelsPinnedTop: [PanelViewController] { 42 | return panelsPinned(at: .top) 43 | } 44 | 45 | var numberOfPanelsPinnedTop: Int { 46 | return numberOfPanelsPinned(at: .top) 47 | } 48 | 49 | var panelPinnedBottom: PanelViewController? { 50 | return panelsPinnedBottom.first 51 | } 52 | 53 | var panelsPinnedBottom: [PanelViewController] { 54 | return panelsPinned(at: .bottom) 55 | } 56 | 57 | var numberOfPanelsPinnedBottom: Int { 58 | return numberOfPanelsPinned(at: .bottom) 59 | } 60 | 61 | func panelsPinned(at side: PanelPinSide) -> [PanelViewController] { 62 | return panels.filter { $0.pinnedMetadata?.side == side }.sorted(by: { (p1, p2) -> Bool in 63 | 64 | guard let date1 = p1.pinnedMetadata?.date else { 65 | return true 66 | } 67 | 68 | guard let date2 = p2.pinnedMetadata?.date else { 69 | return true 70 | } 71 | 72 | return date1 < date2 73 | }) 74 | } 75 | 76 | func numberOfPanelsPinned(at side: PanelPinSide) -> Int { 77 | return panelsPinned(at: side).count 78 | } 79 | 80 | } 81 | 82 | extension PanelManager { 83 | 84 | func pinnedPanelPosition(for panel: PanelViewController, at side: PanelPinSide) -> PinnedPosition? { 85 | 86 | guard let panelView = panel.view else { 87 | return nil 88 | } 89 | 90 | guard let contentDelegate = panel.contentDelegate else { 91 | return nil 92 | } 93 | 94 | var previewTargetFrame = panelView.bounds 95 | 96 | if let panelPinned = panelsPinned(at: side).first { 97 | 98 | if side == .left || side == .right { 99 | 100 | if let preferredPanelPinnedWidth = panelPinned.contentDelegate?.preferredPanelPinnedWidth { 101 | previewTargetFrame.size.width = preferredPanelPinnedWidth 102 | } 103 | 104 | } else { 105 | 106 | if let preferredPanelPinnedHeight = panelPinned.contentDelegate?.preferredPanelPinnedHeight { 107 | previewTargetFrame.size.height = preferredPanelPinnedHeight 108 | } 109 | } 110 | 111 | } else { 112 | 113 | if side == .left || side == .right { 114 | 115 | previewTargetFrame.size.width = contentDelegate.preferredPanelPinnedWidth 116 | 117 | } else { 118 | 119 | previewTargetFrame.size.height = contentDelegate.preferredPanelPinnedHeight 120 | 121 | } 122 | 123 | } 124 | 125 | if side == .left || side == .right { 126 | 127 | previewTargetFrame.origin.y = panelContentWrapperView.bounds.origin.y 128 | 129 | let totalAvailableHeight = panelContentWrapperView.bounds.height 130 | 131 | previewTargetFrame.size.height = totalAvailableHeight 132 | 133 | } else { 134 | 135 | previewTargetFrame.origin.x = panelContentView.frame.origin.x 136 | 137 | let totalAvailableWidth = panelContentView.bounds.width 138 | 139 | previewTargetFrame.size.width = totalAvailableWidth 140 | 141 | } 142 | 143 | let index: Int 144 | 145 | switch side { 146 | 147 | case .top: 148 | previewTargetFrame.origin.y = 0.0 149 | 150 | if panel.isPinned { 151 | 152 | if numberOfPanelsPinnedTop > 1 { 153 | 154 | previewTargetFrame.size.width /= CGFloat(numberOfPanelsPinnedTop) 155 | 156 | index = panel.pinnedMetadata?.index ?? 0 157 | 158 | } else { 159 | index = 0 160 | } 161 | 162 | } else { 163 | 164 | if numberOfPanelsPinnedTop > 0 { 165 | 166 | previewTargetFrame.size.width /= CGFloat(numberOfPanelsPinnedTop + 1) 167 | 168 | index = Int(floor((panelView.frame.center.x - panelContentView.frame.origin.x) / previewTargetFrame.size.width)) 169 | 170 | } else { 171 | index = 0 172 | } 173 | 174 | } 175 | 176 | case .bottom: 177 | previewTargetFrame.origin.y = panelContentWrapperView.bounds.height - previewTargetFrame.size.height 178 | 179 | if panel.isPinned { 180 | 181 | if numberOfPanelsPinnedBottom > 1 { 182 | 183 | previewTargetFrame.size.width /= CGFloat(numberOfPanelsPinnedBottom) 184 | 185 | index = panel.pinnedMetadata?.index ?? 0 186 | 187 | } else { 188 | index = 0 189 | } 190 | 191 | } else { 192 | 193 | if numberOfPanelsPinnedBottom > 0 { 194 | 195 | previewTargetFrame.size.width /= CGFloat(numberOfPanelsPinnedBottom + 1) 196 | 197 | index = Int(floor((panelView.frame.center.x - panelContentView.frame.origin.x) / previewTargetFrame.size.width)) 198 | 199 | } else { 200 | index = 0 201 | } 202 | 203 | } 204 | 205 | case .left: 206 | previewTargetFrame.origin.x = 0.0 207 | 208 | if panel.isPinned { 209 | 210 | if numberOfPanelsPinnedLeft > 1 { 211 | 212 | previewTargetFrame.size.height /= CGFloat(numberOfPanelsPinnedLeft) 213 | 214 | index = panel.pinnedMetadata?.index ?? 0 215 | 216 | } else { 217 | index = 0 218 | } 219 | 220 | } else { 221 | 222 | if numberOfPanelsPinnedLeft > 0 { 223 | 224 | previewTargetFrame.size.height /= CGFloat(numberOfPanelsPinnedLeft + 1) 225 | 226 | index = Int(floor((panelView.frame.center.y - panelContentWrapperView.bounds.origin.y) / previewTargetFrame.size.height)) 227 | 228 | } else { 229 | index = 0 230 | } 231 | 232 | } 233 | 234 | case .right: 235 | previewTargetFrame.origin.x = panelContentWrapperView.bounds.width - previewTargetFrame.size.width 236 | 237 | if panel.isPinned { 238 | 239 | if numberOfPanelsPinnedRight > 1 { 240 | 241 | previewTargetFrame.size.height /= CGFloat(numberOfPanelsPinnedRight) 242 | 243 | index = panel.pinnedMetadata?.index ?? 0 244 | 245 | } else { 246 | index = 0 247 | } 248 | 249 | } else { 250 | 251 | if numberOfPanelsPinnedRight > 0 { 252 | 253 | previewTargetFrame.size.height /= CGFloat(numberOfPanelsPinnedRight + 1) 254 | 255 | index = Int(floor((panelView.frame.center.y - panelContentWrapperView.bounds.origin.y) / previewTargetFrame.size.height)) 256 | 257 | } else { 258 | index = 0 259 | } 260 | 261 | } 262 | 263 | } 264 | 265 | if index > 0 { 266 | if side == .left || side == .right { 267 | previewTargetFrame.origin.y += previewTargetFrame.size.height * CGFloat(index) 268 | } else { 269 | previewTargetFrame.origin.x += previewTargetFrame.size.width * CGFloat(index) 270 | } 271 | } 272 | 273 | return PinnedPosition(frame: previewTargetFrame, index: index) 274 | } 275 | 276 | func updatedContentViewFrame() -> CGRect { 277 | 278 | var updatedContentViewFrame = panelContentView.frame 279 | 280 | updatedContentViewFrame.size.width = panelContentWrapperView.bounds.width 281 | 282 | updatedContentViewFrame.origin.x = 0.0 283 | 284 | updatedContentViewFrame.size.height = panelContentWrapperView.bounds.height 285 | 286 | updatedContentViewFrame.origin.y = 0.0 287 | 288 | if let leftPanelWidth = panelPinnedLeft?.contentDelegate?.preferredPanelPinnedWidth { 289 | 290 | updatedContentViewFrame.size.width -= leftPanelWidth 291 | 292 | updatedContentViewFrame.origin.x = leftPanelWidth 293 | } 294 | 295 | if let rightPanelWidth = panelPinnedRight?.contentDelegate?.preferredPanelPinnedWidth { 296 | 297 | updatedContentViewFrame.size.width -= rightPanelWidth 298 | 299 | } 300 | 301 | if let topPanelHeight = panelPinnedTop?.contentDelegate?.preferredPanelPinnedHeight { 302 | 303 | updatedContentViewFrame.size.height -= topPanelHeight 304 | 305 | updatedContentViewFrame.origin.y = topPanelHeight 306 | } 307 | 308 | if let bottomPanelHeight = panelPinnedBottom?.contentDelegate?.preferredPanelPinnedHeight { 309 | 310 | updatedContentViewFrame.size.height -= bottomPanelHeight 311 | 312 | } 313 | 314 | return updatedContentViewFrame 315 | } 316 | 317 | func fadePinnedPreviewOut(for panel: PanelViewController) { 318 | 319 | if let panelPinnedPreviewView = panel.panelPinnedPreviewView { 320 | 321 | UIView.animate(withDuration: pinnedPanelPreviewFadeDuration, animations: { 322 | panelPinnedPreviewView.alpha = 0.0 323 | }, completion: { (_) in 324 | panelPinnedPreviewView.removeFromSuperview() 325 | }) 326 | 327 | panel.panelPinnedPreviewView = nil 328 | } 329 | 330 | } 331 | 332 | } 333 | 334 | public extension PanelManager { 335 | 336 | func pin(_ panel: PanelViewController, to side: PanelPinSide, atIndex index: Int) { 337 | 338 | guard allowPanelPinning else { 339 | return 340 | } 341 | 342 | guard numberOfPanelsPinned(at: side) < maximumNumberOfPanelsPinned(at: side) else { 343 | return 344 | } 345 | 346 | if !panel.isFloating { 347 | toggleFloatStatus(for: panel, animated: false) 348 | } 349 | 350 | guard panel.isFloating || panel.isPinned else { 351 | return 352 | } 353 | 354 | let pinnedPreviewView = panel.panelPinnedPreviewView 355 | 356 | fadePinnedPreviewOut(for: panel) 357 | 358 | guard !panel.isPinned else { 359 | return 360 | } 361 | 362 | guard let panelView = panel.view else { 363 | return 364 | } 365 | 366 | if panel.logLevel == .full { 367 | print("did pin \(panel) to edge of \(side) side") 368 | } 369 | 370 | var prevPinnedPanels = panelsPinned(at: side).sorted { (p1, p2) -> Bool in 371 | return p1.pinnedMetadata?.index ?? 0 < p2.pinnedMetadata?.index ?? 0 372 | } 373 | 374 | panel.pinnedMetadata = PanelPinnedMetadata(side: side, index: index) 375 | 376 | prevPinnedPanels.insert(panel, at: index) 377 | 378 | panel.disableCornerRadius(animated: false, duration: panelGrowDuration) 379 | panel.disableShadow(animated: false, duration: panelGrowDuration) 380 | 381 | guard let position = pinnedPanelPosition(for: panel, at: side) else { 382 | assertionFailure("Expected a valid position") 383 | return 384 | } 385 | 386 | self.updateFrame(for: panel, to: position.frame) 387 | 388 | if numberOfPanelsPinned(at: side) > 1 { 389 | 390 | for pinnedPanel in panelsPinned(at: side) { 391 | 392 | if pinnedPanel == panel { 393 | continue 394 | } 395 | 396 | pinnedPanel.pinnedMetadata?.index = prevPinnedPanels.index(of: pinnedPanel) ?? 0 397 | 398 | guard let newPosition = pinnedPanelPosition(for: pinnedPanel, at: side) else { 399 | assertionFailure("Expected a valid position") 400 | continue 401 | } 402 | 403 | self.updateFrame(for: pinnedPanel, to: newPosition.frame) 404 | 405 | } 406 | 407 | } 408 | 409 | updateContentViewFrame(to: updatedContentViewFrame()) 410 | 411 | self.panelContentWrapperView.layoutIfNeeded() 412 | 413 | self.didUpdatePinnedPanels() 414 | 415 | // Send panel and preview view to back, so (shadows of) non-pinned panels are on top 416 | self.panelContentWrapperView.insertSubview(panelView, aboveSubview: self.panelContentView) 417 | 418 | if let pinnedPreviewView = pinnedPreviewView, pinnedPreviewView.superview != nil { 419 | self.panelContentWrapperView.insertSubview(pinnedPreviewView, aboveSubview: self.panelContentView) 420 | } 421 | 422 | self.moveAllPanelsToValidPositions() 423 | 424 | self.panelContentWrapperView.layoutIfNeeded() 425 | 426 | panel.hideResizeHandle(animated: false) 427 | 428 | panel.viewWillAppear(false) 429 | panel.viewDidAppear(false) 430 | 431 | } 432 | 433 | } 434 | 435 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager+State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager+State.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 14/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | extension PanelManager { 13 | 14 | public var panelStates: [Int: PanelState] { 15 | 16 | var states = [Int: PanelState]() 17 | 18 | for panel in panels { 19 | 20 | if let id = (panel.contentViewController as? PanelStateCoder)?.panelId { 21 | 22 | states[id] = PanelState(panel) 23 | 24 | } 25 | 26 | } 27 | 28 | return states 29 | } 30 | 31 | func panelForId(_ id: Int) -> PanelViewController? { 32 | 33 | for panel in panels { 34 | 35 | if let panelId = (panel.contentViewController as? PanelStateCoder)?.panelId, panelId == id { 36 | 37 | return panel 38 | 39 | } 40 | 41 | } 42 | 43 | return nil 44 | } 45 | 46 | public func restorePanelStates(_ states: [Int: PanelState]) { 47 | 48 | var pinnedTable = [PanelPinnedMetadata: PanelViewController]() 49 | 50 | var pinnedMetadatas = [PanelPinnedMetadata]() 51 | 52 | 53 | var floatTable = [PanelFloatingState: PanelViewController]() 54 | 55 | var floatStates = [PanelFloatingState]() 56 | 57 | for (id, state) in states { 58 | 59 | guard let panel = panelForId(id) else { 60 | continue 61 | } 62 | 63 | panel.floatingSize = state.floatingSize 64 | 65 | if let pinnedMetadata = state.pinnedMetadata { 66 | 67 | pinnedTable[pinnedMetadata] = panel 68 | pinnedMetadatas.append(pinnedMetadata) 69 | 70 | } else if let floatingState = state.floatingState { 71 | 72 | floatTable[floatingState] = panel 73 | floatStates.append(floatingState) 74 | 75 | } 76 | 77 | } 78 | 79 | pinnedMetadatas.sort { (lhs, rhs) -> Bool in 80 | return lhs.index < rhs.index 81 | } 82 | 83 | for pinnedMetadata in pinnedMetadatas { 84 | 85 | guard let panel = pinnedTable[pinnedMetadata] else { 86 | continue 87 | } 88 | 89 | pin(panel, to: pinnedMetadata.side, atIndex: pinnedMetadata.index) 90 | 91 | } 92 | 93 | floatStates.sort { (lhs, rhs) -> Bool in 94 | return lhs.zIndex < rhs.zIndex 95 | } 96 | 97 | for floatingState in floatStates { 98 | 99 | guard let panel = floatTable[floatingState] else { 100 | continue 101 | } 102 | 103 | var pos = floatingState.relativePosition 104 | 105 | pos.x *= panelContentWrapperView.frame.width 106 | pos.y *= panelContentWrapperView.frame.height 107 | 108 | let size: CGSize 109 | 110 | if let floatingSize = panel.floatingSize { 111 | 112 | size = floatingSize 113 | 114 | } else { 115 | 116 | size = panel.preferredContentSize 117 | 118 | } 119 | 120 | let frame = CGRect(origin: pos, size: size) 121 | 122 | float(panel, at: frame) 123 | 124 | } 125 | 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /PanelKit/PanelManager/PanelManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelManager.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 11/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The PanelManager protocol contains the necessary settings for letting 12 | /// panels float and pin. It also contains callbacks for certain actions triggered by panels. 13 | /// 14 | /// Typically the `PanelManager` protocol is implemented on a `UIViewController` subclass. 15 | /// If not, you should specify the `managerViewController` property. 16 | public protocol PanelManager: class { 17 | 18 | /// The ```UIViewController``` that manages the panels and contains 19 | /// ```panelContentWrapperView``` and ```panelContentView```. 20 | /// 21 | /// When the PanelManager protocol is implemented on a `UIViewController` subclass 22 | /// this property returns "self" by default. 23 | var managerViewController: UIViewController { get } 24 | 25 | /// The panels to be managed. 26 | var panels: [PanelViewController] { get } 27 | 28 | /// Controls wether panels are allowed to float (be dragged around). 29 | /// If this property returns true: the panel will automatically provide a UIBarButtonItem 30 | /// to make itself float when shown in a popover, as well as a close button (to close itself) while it's floating. 31 | /// 32 | /// The default implementation returns true if `panelContentWrapperView.bounds.width > 800`. 33 | var allowFloatingPanels: Bool { get } 34 | 35 | /// Controls wether panels are allowed to be pinned to either the left or right side. 36 | /// The ```panelContentView``` is resized when a panel is pinned. 37 | /// 38 | /// The default implementation returns true if `panelContentWrapperView.bounds.width > 800`. 39 | var allowPanelPinning: Bool { get } 40 | 41 | /// Controls the number of panels that may be pinned to a side. 42 | /// 43 | /// The default implementation returns 1. 44 | /// - Parameter side: A side where panels can be pinned to. 45 | /// - Returns: Maximum number of panels that may be pinned to `side`. 46 | func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int 47 | 48 | /// The view in which the panels may be dragged around. 49 | var panelContentWrapperView: UIView { get } 50 | 51 | /// The content view, which will be moved/resized when panels pin. 52 | var panelContentView: UIView { get } 53 | 54 | /// Default implementation is ```LogLevel.none```. 55 | var panelManagerLogLevel: LogLevel { get } 56 | 57 | /// This will be called when a panel is pinned or unpinned. 58 | /// The default implementation is an empty function. 59 | func didUpdatePinnedPanels() 60 | 61 | /// Drag insets for panel. 62 | /// 63 | /// E.g. a positive top inset will change the minimum y value 64 | /// a panel can be dragged to inside ```panelContentWrapperView```. 65 | /// 66 | /// - Parameter panel: The panel for which to provide insets. 67 | /// - Returns: Edge insets. 68 | func dragInsets(for panel: PanelViewController) -> UIEdgeInsets 69 | 70 | /// Blur effect for content overlay view when exposé is active. 71 | var exposeOverlayBlurEffect: UIBlurEffect { get } 72 | 73 | /// Called when exposé is about to be entered. 74 | /// The default implementation is an empty function. 75 | func willEnterExpose() 76 | 77 | /// Called when exposé is about to be exited. 78 | /// The default implementation is an empty function. 79 | func willExitExpose() 80 | 81 | } 82 | 83 | // MARK: - 84 | 85 | extension PanelManager { 86 | 87 | func totalDragInsets(for panel: PanelViewController) -> UIEdgeInsets { 88 | 89 | let insets = dragInsets(for: panel) 90 | 91 | let left = panelPinnedLeft?.view?.bounds.width ?? 0.0 92 | let right = panelPinnedRight?.view?.bounds.width ?? 0.0 93 | let top = panelPinnedTop?.view?.bounds.height ?? 0.0 94 | let bottom = panelPinnedBottom?.view?.bounds.height ?? 0.0 95 | 96 | return UIEdgeInsets(top: insets.top + top, 97 | left: insets.left + left, 98 | bottom: insets.bottom + bottom, 99 | right: insets.right + right) 100 | 101 | } 102 | 103 | } 104 | 105 | // MARK: - 106 | 107 | public extension PanelManager { 108 | 109 | /// E.g. to move after a panel pins 110 | func moveAllPanelsToValidPositions() { 111 | 112 | for panel in panels { 113 | 114 | guard panel.isFloating else { 115 | continue 116 | } 117 | 118 | var newPanelFrame = panel.view.frame 119 | newPanelFrame.center = panel.allowedCenter(for: newPanelFrame.center) 120 | 121 | updateFrame(for: panel, to: newPanelFrame) 122 | 123 | } 124 | 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /PanelKit/State Restoration/PanelState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelState.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 10/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | public struct PanelState: Codable, Equatable { 13 | 14 | public let floatingState: PanelFloatingState? 15 | 16 | public let pinnedMetadata: PanelPinnedMetadata? 17 | 18 | public let floatingSize: CGSize? 19 | 20 | public init(floatingState: PanelFloatingState? = nil, pinnedMetadata: PanelPinnedMetadata? = nil, floatingSize: CGSize? = nil) { 21 | 22 | self.floatingState = floatingState 23 | self.pinnedMetadata = pinnedMetadata 24 | self.floatingSize = floatingSize 25 | } 26 | 27 | init(_ panel: PanelViewController) { 28 | 29 | if panel.isFloating { 30 | 31 | if let panelContentWrapperView = panel.manager?.panelContentWrapperView { 32 | 33 | let x = panel.view.frame.origin.x / panelContentWrapperView.frame.width 34 | let y = panel.view.frame.origin.y / panelContentWrapperView.frame.height 35 | let relPosition = CGPoint(x: x, y: y) 36 | 37 | if let zIndex = panelContentWrapperView.subviews.index(of: panel.view) { 38 | floatingState = PanelFloatingState(relativePosition: relPosition, zIndex: zIndex) 39 | } else { 40 | floatingState = nil 41 | } 42 | 43 | } else { 44 | 45 | floatingState = nil 46 | 47 | } 48 | 49 | } else { 50 | floatingState = nil 51 | } 52 | 53 | floatingSize = panel.floatingSize 54 | 55 | pinnedMetadata = panel.pinnedMetadata 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /PanelKit/State Restoration/PanelStateCoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PanelStateCoder.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 10/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol PanelStateCoder { 12 | 13 | /// Unique id to identify a panel. 14 | /// Used when restoring the panel's state. 15 | /// 16 | /// A panel's id should be the same across app launches 17 | /// to successfully restore its state. 18 | var panelId: Int { get } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /PanelKit/Utils/BlockBarButtonItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockBarButtonItem.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 11/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BlockBarButtonItem: UIBarButtonItem { 12 | 13 | private var actionHandler: (() -> Void)? 14 | 15 | convenience init(title: String?, style: UIBarButtonItemStyle, actionHandler: (() -> Void)?) { 16 | self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed)) 17 | self.target = self 18 | self.actionHandler = actionHandler 19 | } 20 | 21 | convenience init(image: UIImage?, style: UIBarButtonItemStyle, actionHandler: (() -> Void)?) { 22 | self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed)) 23 | self.target = self 24 | self.actionHandler = actionHandler 25 | } 26 | 27 | @objc func barButtonItemPressed(sender: UIBarButtonItem) { 28 | actionHandler?() 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /PanelKit/Utils/CGRect+Center.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+Center.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 13/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension CGRect { 12 | 13 | var center: CGPoint { 14 | get { 15 | 16 | return CGPoint(x: midX, y: midY) 17 | } 18 | set { 19 | 20 | let x = newValue.x - width / 2.0 21 | let y = newValue.y - height / 2.0 22 | 23 | let newOrigin = CGPoint(x: x, y: y) 24 | 25 | self.origin = newOrigin 26 | 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /PanelKit/Utils/UIViewController+Popover.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Popover.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 12/02/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIViewController { 13 | 14 | /// Returns true if the `UIViewController` instance is presented as a popover. 15 | @objc public var isPresentedAsPopover: Bool { 16 | 17 | // Checking for a "UIPopoverView" seems to be deemed trustworthy, 18 | // as explained here: 19 | // http://petersteinberger.com/blog/2015/uipresentationcontroller-popover-detection/ 20 | 21 | var currentView: UIView? = self.view 22 | 23 | while currentView != nil { 24 | let classNameOfCurrentView = NSStringFromClass(type(of: currentView!)) as NSString 25 | 26 | let searchString = "UIPopoverView" 27 | 28 | if classNameOfCurrentView.range(of: searchString, options: .caseInsensitive).location != NSNotFound { 29 | return true 30 | } 31 | 32 | currentView = currentView?.superview 33 | } 34 | 35 | return false 36 | 37 | // The "popoverPresentationController" way of checking if presented as popover 38 | // causes a memory leak :/ 39 | // Possibly because "popoverPresentationController" is lazily created? 40 | 41 | // guard let p = self.popoverPresentationController else { 42 | // return false 43 | // } 44 | // 45 | // // FIXME: presentedViewController can never be nil? 46 | // let c = p.presentedViewController as UIViewController? 47 | // 48 | // return c != nil && p.arrowDirection != .unknown 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /PanelKit/View/CornerHandleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerHandleView.swift 3 | // HandleViewTest 4 | // 5 | // Created by Louis D'hauwe on 01/10/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import CoreGraphics 12 | 13 | class CornerHandleView: UIView { 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | 18 | setup() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | private let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) 26 | 27 | private func setup() { 28 | 29 | let glyphView = CornerHandleGlyphView() 30 | glyphView.frame = CGRect(x: 0, y: 0, width: 38, height: 38) 31 | 32 | glyphView.backgroundColor = .clear 33 | glyphView.isOpaque = false 34 | glyphView.tintColor = self.tintColor 35 | 36 | let drawRect = CGRect(origin: .zero, size: glyphView.bounds.size) 37 | UIGraphicsBeginImageContextWithOptions(drawRect.size, false, 0.0) 38 | 39 | if let context = UIGraphicsGetCurrentContext() { 40 | glyphView.draw(in: context, rect: drawRect) 41 | } 42 | 43 | let img = UIGraphicsGetImageFromCurrentImageContext() 44 | 45 | UIGraphicsEndImageContext() 46 | 47 | self.tintColor = .white 48 | 49 | visualEffectView.translatesAutoresizingMaskIntoConstraints = false 50 | 51 | self.addSubview(visualEffectView) 52 | 53 | visualEffectView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true 54 | visualEffectView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true 55 | visualEffectView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true 56 | visualEffectView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true 57 | 58 | self.widthAnchor.constraint(equalToConstant: 38).isActive = true 59 | self.heightAnchor.constraint(equalToConstant: 38).isActive = true 60 | 61 | let imgView = UIImageView(image: img) 62 | imgView.frame = CGRect(x: 0, y: 0, width: 38, height: 38) 63 | visualEffectView.mask = imgView 64 | 65 | self.layer.shadowColor = UIColor.black.cgColor 66 | self.layer.shadowRadius = 8.0 67 | self.layer.shadowOpacity = 0.4 68 | self.layer.shadowOffset = .zero 69 | 70 | self.visualEffectView.transform = CGAffineTransform(rotationAngle: .pi/2 * 2) 71 | } 72 | 73 | func cornerHandleDidBecomeActive() { 74 | 75 | UIView.animate(withDuration: 0.15) { 76 | self.visualEffectView.alpha = 0.5 77 | } 78 | 79 | } 80 | 81 | func cornerHandleDidBecomeInactive(animated: Bool = true) { 82 | 83 | func setState() { 84 | self.visualEffectView.alpha = 1.0 85 | } 86 | 87 | if animated { 88 | UIView.animate(withDuration: 0.15) { 89 | setState() 90 | } 91 | } else { 92 | setState() 93 | } 94 | 95 | } 96 | 97 | } 98 | 99 | @IBDesignable 100 | private class CornerHandleGlyphView: UIView { 101 | 102 | private let handleWidth: CGFloat = 6 103 | private let innerRadius: CGFloat = 24 104 | private let outerRadius: CGFloat = 28 105 | 106 | override func draw(_ rect: CGRect) { 107 | 108 | guard let context = UIGraphicsGetCurrentContext() else { 109 | return 110 | } 111 | 112 | self.tintColor.setFill() 113 | 114 | draw(in: context, rect: rect) 115 | 116 | } 117 | 118 | func draw(in context: CGContext, rect: CGRect) { 119 | 120 | context.saveGState() 121 | 122 | let outerRadii = CGSize(width: outerRadius, height: outerRadius) 123 | let innerRadii = CGSize(width: innerRadius, height: innerRadius) 124 | 125 | let outerRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width * 2, height: rect.height * 2) 126 | 127 | let outerRoundedRect = UIBezierPath(roundedRect: outerRect, byRoundingCorners: .topLeft, cornerRadii: outerRadii) 128 | 129 | let clipRect = UIBezierPath(rect: CGRect(x: 0, y: 0, width: rect.width - handleWidth/2, height: rect.height - handleWidth/2)) 130 | 131 | clipRect.addClip() 132 | 133 | let innerRect = CGRect(x: rect.origin.x + handleWidth, y: rect.origin.y + handleWidth, width: rect.width*2, height: rect.height*2) 134 | 135 | let innerRoundedRect = UIBezierPath(roundedRect: innerRect, byRoundingCorners: .topLeft, cornerRadii: innerRadii) 136 | 137 | outerRoundedRect.append(innerRoundedRect) 138 | outerRoundedRect.usesEvenOddFillRule = true 139 | 140 | outerRoundedRect.addClip() 141 | 142 | context.fill(rect) 143 | 144 | context.restoreGState() 145 | 146 | context.fillEllipse(in: CGRect(x: 0, y: rect.height - handleWidth, width: handleWidth, height: handleWidth)) 147 | 148 | context.fillEllipse(in: CGRect(x: rect.width - handleWidth, y: 0, width: handleWidth, height: handleWidth)) 149 | 150 | } 151 | 152 | override var intrinsicContentSize: CGSize { 153 | return CGSize(width: 38, height: 38) 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /PanelKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PanelKitTests/MainTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTests.swift 3 | // PanelKitTests 4 | // 5 | // Created by Louis D'hauwe on 16/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import PanelKit 12 | 13 | class MainTests: XCTestCase { 14 | 15 | var viewController: ViewController! 16 | var navigationController: UINavigationController! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | viewController = ViewController() 22 | 23 | navigationController = UINavigationController(rootViewController: viewController) 24 | navigationController.view.frame = CGRect(origin: .zero, size: CGSize(width: 1024, height: 768)) 25 | 26 | let window = UIWindow(frame: UIScreen.main.bounds) 27 | window.rootViewController = navigationController 28 | window.makeKeyAndVisible() 29 | 30 | XCTAssertNotNil(navigationController.view) 31 | XCTAssertNotNil(viewController.view) 32 | 33 | if UIDevice.current.userInterfaceIdiom == .phone { 34 | continueAfterFailure = false 35 | XCTFail("Test does not work on an iPhone") 36 | } 37 | } 38 | 39 | override func tearDown() { 40 | super.tearDown() 41 | // Put teardown code here. This method is called after the invocation of each test method in the class. 42 | } 43 | 44 | func testFloating() { 45 | 46 | let mapPanel = viewController.mapPanelVC! 47 | 48 | XCTAssert(!mapPanel.isFloating) 49 | XCTAssert(!mapPanel.isPinned) 50 | XCTAssert(!mapPanel.isPresentedModally) 51 | XCTAssert(!mapPanel.isPresentedAsPopover) 52 | 53 | let exp = self.expectation(description: "floating") 54 | 55 | viewController.showMapPanelFromBarButton { 56 | 57 | XCTAssert(mapPanel.isPresentedAsPopover) 58 | 59 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 60 | 61 | XCTAssert(mapPanel.isFloating) 62 | 63 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 64 | 65 | XCTAssert(!mapPanel.isFloating) 66 | exp.fulfill() 67 | 68 | }) 69 | 70 | }) 71 | 72 | } 73 | 74 | waitForExpectations(timeout: 10.0) { (error) in 75 | if let error = error { 76 | XCTFail(error.localizedDescription) 77 | } 78 | } 79 | 80 | } 81 | 82 | func testExpose() { 83 | 84 | let mapPanel = viewController.mapPanelVC! 85 | let textPanel = viewController.textPanelVC! 86 | 87 | let exp = self.expectation(description: "expose") 88 | 89 | viewController.showMapPanelFromBarButton { 90 | 91 | XCTAssert(mapPanel.isPresentedAsPopover) 92 | 93 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 94 | 95 | self.viewController.showTextPanelFromBarButton { 96 | 97 | self.viewController.toggleFloatStatus(for: textPanel, completion: { 98 | 99 | XCTAssert(mapPanel.isFloating) 100 | XCTAssert(textPanel.isFloating) 101 | 102 | self.viewController.enterExpose() 103 | 104 | XCTAssert(mapPanel.isInExpose) 105 | XCTAssert(textPanel.isInExpose) 106 | 107 | self.viewController.exitExpose() 108 | 109 | XCTAssert(!mapPanel.isInExpose) 110 | XCTAssert(!textPanel.isInExpose) 111 | 112 | exp.fulfill() 113 | 114 | }) 115 | 116 | } 117 | 118 | }) 119 | 120 | } 121 | 122 | waitForExpectations(timeout: 10.0) { (error) in 123 | if let error = error { 124 | XCTFail(error.localizedDescription) 125 | } 126 | } 127 | 128 | } 129 | 130 | func testPinnedFloating() { 131 | 132 | let mapPanel = viewController.mapPanelVC! 133 | let textPanel = viewController.textPanelVC! 134 | 135 | let exp = self.expectation(description: "pinnedFloating") 136 | 137 | viewController.showMapPanelFromBarButton { 138 | 139 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 140 | 141 | self.viewController.showTextPanelFromBarButton { 142 | 143 | self.viewController.toggleFloatStatus(for: textPanel, completion: { 144 | 145 | self.viewController.didEndDrag(mapPanel, toEdgeOf: .right) 146 | 147 | XCTAssert(mapPanel.isPinned) 148 | XCTAssert(self.viewController.panelPinnedRight == mapPanel) 149 | 150 | self.viewController.didDragFree(mapPanel, from: mapPanel.view.frame.origin) 151 | XCTAssert(!mapPanel.isPinned) 152 | XCTAssert(self.viewController.panelPinnedRight == nil) 153 | 154 | exp.fulfill() 155 | 156 | }) 157 | 158 | } 159 | 160 | }) 161 | 162 | } 163 | 164 | waitForExpectations(timeout: 10.0) { (error) in 165 | if let error = error { 166 | XCTFail(error.localizedDescription) 167 | } 168 | } 169 | 170 | } 171 | 172 | func testPinned() { 173 | 174 | let mapPanel = viewController.mapPanelVC! 175 | 176 | let exp = self.expectation(description: "pinned") 177 | 178 | viewController.showMapPanelFromBarButton { 179 | 180 | XCTAssert(mapPanel.isPresentedAsPopover) 181 | 182 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 183 | 184 | self.viewController.didEndDrag(mapPanel, toEdgeOf: .right) 185 | 186 | XCTAssert(mapPanel.isPinned) 187 | XCTAssert(self.viewController.panelPinnedRight == mapPanel) 188 | 189 | self.viewController.didDragFree(mapPanel, from: mapPanel.view.frame.origin) 190 | XCTAssert(!mapPanel.isPinned) 191 | XCTAssert(self.viewController.panelPinnedRight == nil) 192 | 193 | exp.fulfill() 194 | 195 | }) 196 | 197 | } 198 | 199 | waitForExpectations(timeout: 10.0) { (error) in 200 | if let error = error { 201 | XCTFail(error.localizedDescription) 202 | } 203 | } 204 | 205 | } 206 | 207 | func testKeyboard() { 208 | 209 | let textPanel = viewController.textPanelVC! 210 | 211 | let exp = self.expectation(description: "keyboard") 212 | 213 | viewController.showTextPanelFromBarButton { 214 | 215 | XCTAssert(textPanel.isPresentedAsPopover) 216 | 217 | self.viewController.toggleFloatStatus(for: textPanel, completion: { 218 | 219 | let textView = self.viewController.textPanelContentVC.textView 220 | 221 | textView!.becomeFirstResponder() 222 | 223 | XCTAssert(textView!.isFirstResponder) 224 | 225 | textView!.resignFirstResponder() 226 | 227 | XCTAssert(!textView!.isFirstResponder) 228 | 229 | exp.fulfill() 230 | 231 | }) 232 | 233 | } 234 | 235 | waitForExpectations(timeout: 10.0) { (error) in 236 | if let error = error { 237 | XCTFail(error.localizedDescription) 238 | } 239 | } 240 | } 241 | 242 | func testOffOnScreen() { 243 | 244 | let mapPanel = viewController.mapPanelVC! 245 | 246 | let exp = self.expectation(description: "offOnScreen") 247 | 248 | viewController.showMapPanelFromBarButton { 249 | 250 | XCTAssert(mapPanel.isPresentedAsPopover) 251 | 252 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 253 | 254 | // Move off screen 255 | 256 | self.viewController.panelsPrepareMoveOffScreen() 257 | self.viewController.panelsMoveOffScreen() 258 | 259 | self.viewController.view.layoutIfNeeded() 260 | self.viewController.panelsCompleteMoveOffScreen() 261 | 262 | let vcFrame = self.viewController.view.bounds 263 | let mapPanelFrame = mapPanel.view.frame 264 | 265 | XCTAssert(!vcFrame.intersects(mapPanelFrame)) 266 | 267 | // Move on screen 268 | 269 | self.viewController.panelsPrepareMoveOnScreen() 270 | self.viewController.panelsMoveOnScreen() 271 | 272 | self.viewController.view.layoutIfNeeded() 273 | self.viewController.panelsCompleteMoveOnScreen() 274 | 275 | let mapPanelFrameOn = mapPanel.view.frame 276 | XCTAssert(vcFrame.intersects(mapPanelFrameOn)) 277 | 278 | exp.fulfill() 279 | 280 | }) 281 | 282 | } 283 | 284 | waitForExpectations(timeout: 10.0) { (error) in 285 | if let error = error { 286 | XCTFail(error.localizedDescription) 287 | } 288 | } 289 | 290 | } 291 | 292 | func testClosing() { 293 | 294 | let mapPanel = viewController.mapPanelVC! 295 | 296 | let exp = self.expectation(description: "closing") 297 | 298 | viewController.showMapPanelFromBarButton { 299 | 300 | XCTAssert(mapPanel.isPresentedAsPopover) 301 | 302 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 303 | 304 | XCTAssert(mapPanel.isFloating) 305 | 306 | self.viewController.close(mapPanel) 307 | 308 | XCTAssert(!mapPanel.isFloating) 309 | 310 | exp.fulfill() 311 | 312 | }) 313 | 314 | } 315 | 316 | waitForExpectations(timeout: 10.0) { (error) in 317 | if let error = error { 318 | XCTFail(error.localizedDescription) 319 | } 320 | } 321 | 322 | } 323 | 324 | func testClosingAllFloating() { 325 | 326 | let mapPanel = viewController.mapPanelVC! 327 | 328 | let exp = self.expectation(description: "closing") 329 | 330 | viewController.showMapPanelFromBarButton { 331 | 332 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 333 | 334 | XCTAssert(mapPanel.isFloating) 335 | 336 | self.viewController.closeAllFloatingPanels() 337 | 338 | XCTAssert(!mapPanel.isFloating) 339 | 340 | exp.fulfill() 341 | 342 | }) 343 | 344 | } 345 | 346 | waitForExpectations(timeout: 10.0) { (error) in 347 | if let error = error { 348 | XCTFail(error.localizedDescription) 349 | } 350 | } 351 | 352 | } 353 | 354 | func testClosingAllPinned() { 355 | 356 | let mapPanel = viewController.mapPanelVC! 357 | let textPanel = viewController.textPanelVC! 358 | 359 | let exp = self.expectation(description: "closing") 360 | 361 | viewController.showMapPanelFromBarButton { 362 | 363 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 364 | 365 | self.viewController.didEndDrag(mapPanel, toEdgeOf: .right) 366 | 367 | XCTAssert(mapPanel.isPinned) 368 | XCTAssert(self.viewController.panelPinnedRight == mapPanel) 369 | 370 | self.viewController.showTextPanelFromBarButton { 371 | 372 | self.viewController.toggleFloatStatus(for: textPanel, completion: { 373 | 374 | self.viewController.didEndDrag(textPanel, toEdgeOf: .left) 375 | 376 | XCTAssert(textPanel.isPinned) 377 | XCTAssert(self.viewController.panelPinnedLeft == textPanel) 378 | 379 | self.viewController.closeAllPinnedPanels() 380 | 381 | XCTAssert(!mapPanel.isPinned) 382 | XCTAssert(self.viewController.panelPinnedRight == nil) 383 | 384 | XCTAssert(!textPanel.isPinned) 385 | XCTAssert(self.viewController.panelPinnedLeft == nil) 386 | 387 | exp.fulfill() 388 | 389 | }) 390 | 391 | } 392 | 393 | }) 394 | 395 | } 396 | 397 | waitForExpectations(timeout: 10.0) { (error) in 398 | if let error = error { 399 | XCTFail(error.localizedDescription) 400 | } 401 | } 402 | 403 | } 404 | 405 | func testDragToPin() { 406 | 407 | let mapPanel = viewController.mapPanelVC! 408 | 409 | let exp = self.expectation(description: "dragToPin") 410 | 411 | viewController.showMapPanelFromBarButton { 412 | 413 | XCTAssert(mapPanel.isPresentedAsPopover) 414 | 415 | self.viewController.toggleFloatStatus(for: mapPanel, completion: { 416 | 417 | let from = CGPoint(x: mapPanel.view.center.x - 1, y: mapPanel.view.center.y + 200) 418 | let toX = self.viewController.view.bounds.width - mapPanel.contentViewController!.view.bounds.width/2 419 | let to = CGPoint(x: toX, y: mapPanel.view.center.y + 200) 420 | mapPanel.moveWithTouch(from: from, to: to) 421 | self.viewController.view.layoutIfNeeded() 422 | 423 | mapPanel.moveWithTouch(from: to, to: to) 424 | 425 | mapPanel.didEndDrag() 426 | 427 | XCTAssert(mapPanel.isPinned) 428 | XCTAssert(self.viewController.panelPinnedRight == mapPanel) 429 | 430 | mapPanel.moveWithTouch(from: to, to: from) 431 | self.viewController.view.layoutIfNeeded() 432 | mapPanel.moveWithTouch(from: to, to: from) 433 | mapPanel.didEndDrag() 434 | 435 | XCTAssert(!mapPanel.isPinned) 436 | XCTAssert(self.viewController.panelPinnedRight == nil) 437 | 438 | exp.fulfill() 439 | 440 | }) 441 | 442 | } 443 | 444 | waitForExpectations(timeout: 10.0) { (error) in 445 | if let error = error { 446 | XCTFail(error.localizedDescription) 447 | } 448 | } 449 | 450 | } 451 | 452 | } 453 | -------------------------------------------------------------------------------- /PanelKitTests/Panels/MapPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapPanelContentViewController.swift 3 | // PanelKitTests 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PanelKit 11 | import MapKit 12 | import UIKit 13 | 14 | class MapPanelContentViewController: UIViewController { 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | let mapView = MKMapView(frame: view.bounds) 20 | self.view.addSubview(mapView) 21 | 22 | self.title = "Map" 23 | 24 | } 25 | 26 | } 27 | 28 | extension MapPanelContentViewController: PanelContentDelegate { 29 | 30 | var preferredPanelContentSize: CGSize { 31 | return CGSize(width: 320, height: 500) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /PanelKitTests/Panels/TextPanelContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextPanelContentViewController.swift 3 | // PanelKitTests 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import PanelKit 12 | 13 | class TextPanelContentViewController: UIViewController, PanelContentDelegate { 14 | 15 | weak var textView: UITextView! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | let textView = UITextView(frame: view.bounds) 21 | self.view.addSubview(textView) 22 | self.textView = textView 23 | 24 | self.title = "TextView" 25 | 26 | } 27 | 28 | var shouldAdjustForKeyboard: Bool { 29 | return textView.isFirstResponder 30 | } 31 | 32 | var preferredPanelContentSize: CGSize { 33 | return CGSize(width: 320, height: 400) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /PanelKitTests/StateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateTests.swift 3 | // PanelKitTests 4 | // 5 | // Created by Louis D'hauwe on 16/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import PanelKit 12 | 13 | class StateTests: XCTestCase { 14 | 15 | var viewController: StateViewController! 16 | var navigationController: UINavigationController! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | viewController = StateViewController() 22 | 23 | navigationController = UINavigationController(rootViewController: viewController) 24 | navigationController.view.frame = CGRect(origin: .zero, size: CGSize(width: 1024, height: 768)) 25 | 26 | let window = UIWindow(frame: UIScreen.main.bounds) 27 | window.rootViewController = navigationController 28 | window.makeKeyAndVisible() 29 | 30 | XCTAssertNotNil(navigationController.view) 31 | XCTAssertNotNil(viewController.view) 32 | 33 | if UIDevice.current.userInterfaceIdiom == .phone { 34 | XCTFail("Test does not work on an iPhone") 35 | } 36 | } 37 | 38 | override func tearDown() { 39 | super.tearDown() 40 | // Put teardown code here. This method is called after the invocation of each test method in the class. 41 | } 42 | 43 | func testFloatPanel() { 44 | 45 | viewController.float(viewController.panel1VC, at: CGRect(x: 200, y: 200, width: 300, height: 300)) 46 | 47 | XCTAssert(viewController.panel1VC.isFloating) 48 | 49 | } 50 | 51 | func testPinMultiplePanelsRight() { 52 | 53 | viewController.pin(viewController.panel1VC, to: .right, atIndex: 0) 54 | viewController.pin(viewController.panel2VC, to: .right, atIndex: 0) 55 | 56 | XCTAssert(viewController.numberOfPanelsPinned(at: .right) == 2) 57 | XCTAssert(viewController.panel1VC.isPinned) 58 | XCTAssert(viewController.panel2VC.isPinned) 59 | 60 | } 61 | 62 | func testPinMultiplePanelsLeft() { 63 | 64 | viewController.pin(viewController.panel1VC, to: .left, atIndex: 0) 65 | viewController.pin(viewController.panel2VC, to: .left, atIndex: 0) 66 | 67 | XCTAssert(viewController.numberOfPanelsPinned(at: .left) == 2) 68 | XCTAssert(viewController.panel1VC.isPinned) 69 | XCTAssert(viewController.panel2VC.isPinned) 70 | 71 | } 72 | 73 | func testEncodeStates() { 74 | 75 | viewController.pin(viewController.panel1VC, to: .left, atIndex: 0) 76 | viewController.pin(viewController.panel2VC, to: .right, atIndex: 0) 77 | 78 | let states = viewController.panelStates 79 | 80 | guard let state1 = states[1] else { 81 | XCTFail("Expected state 1") 82 | return 83 | } 84 | 85 | guard let state2 = states[2] else { 86 | XCTFail("Expected state 2") 87 | return 88 | } 89 | 90 | XCTAssert(state1.pinnedMetadata?.side == .left) 91 | XCTAssert(state2.pinnedMetadata?.side == .right) 92 | 93 | } 94 | 95 | func testDecodeStates() { 96 | 97 | let json = """ 98 | { 99 | "2": { 100 | "floatingState": { 101 | "relativePosition": [0.4, 0.4], 102 | "zIndex": 0 103 | } 104 | }, 105 | "1": { 106 | "pinnedMetadata": { 107 | "side": 0, 108 | "index": 0, 109 | "date": 532555376.97106999 110 | } 111 | } 112 | } 113 | """ 114 | 115 | let decoder = JSONDecoder() 116 | let states = try! decoder.decode([Int: PanelState].self, from: json.data(using: .utf8)!) 117 | 118 | guard let state1 = states[1] else { 119 | XCTFail("Expected state 1") 120 | return 121 | } 122 | 123 | guard let state2 = states[2] else { 124 | XCTFail("Expected state 2") 125 | return 126 | } 127 | 128 | XCTAssert(state1.pinnedMetadata?.side == .left) 129 | XCTAssert(state2.floatingState?.zIndex == 0) 130 | 131 | viewController.restorePanelStates(states) 132 | 133 | XCTAssert(viewController.numberOfPanelsPinned(at: .left) == 1) 134 | XCTAssert(viewController.numberOfPanelsPinned(at: .right) == 0) 135 | XCTAssert(viewController.panel1VC.isPinned) 136 | XCTAssert(viewController.panel2VC.isFloating) 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /PanelKitTests/StateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateViewController.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 16/11/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import PanelKit 12 | 13 | class StateViewController: UIViewController { 14 | 15 | var panel1ContentVC: TestPanel1! 16 | var panel1VC: PanelViewController! 17 | 18 | var panel2ContentVC: TestPanel2! 19 | var panel2VC: PanelViewController! 20 | 21 | var contentWrapperView: UIView! 22 | var contentView: UIView! 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | contentWrapperView = UIView(frame: view.bounds) 28 | view.addSubview(contentWrapperView) 29 | 30 | contentView = UIView(frame: contentWrapperView.bounds) 31 | contentWrapperView.addSubview(contentView) 32 | 33 | panel1ContentVC = TestPanel1() 34 | panel1VC = PanelViewController(with: panel1ContentVC, in: self) 35 | 36 | panel2ContentVC = TestPanel2() 37 | panel2VC = PanelViewController(with: panel2ContentVC, in: self) 38 | 39 | self.navigationItem.title = "Test" 40 | 41 | } 42 | 43 | } 44 | 45 | extension StateViewController: PanelManager { 46 | 47 | var panelManagerLogLevel: LogLevel { 48 | return .full 49 | } 50 | 51 | var panelContentWrapperView: UIView { 52 | return contentWrapperView 53 | } 54 | 55 | var panelContentView: UIView { 56 | return contentView 57 | } 58 | 59 | var panels: [PanelViewController] { 60 | return [panel1VC, panel2VC] 61 | } 62 | 63 | func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int { 64 | return 2 65 | } 66 | 67 | } 68 | 69 | class TestPanel1: UIViewController { 70 | 71 | override func viewDidLoad() { 72 | super.viewDidLoad() 73 | 74 | self.view.backgroundColor = .red 75 | 76 | self.title = "Test Panel 1" 77 | 78 | } 79 | 80 | } 81 | 82 | extension TestPanel1: PanelContentDelegate { 83 | 84 | var preferredPanelContentSize: CGSize { 85 | return CGSize(width: 320, height: 500) 86 | } 87 | 88 | var minimumPanelContentSize: CGSize { 89 | return CGSize(width: 300, height: 400) 90 | } 91 | 92 | var maximumPanelContentSize: CGSize { 93 | return CGSize(width: 600, height: 600) 94 | } 95 | 96 | } 97 | 98 | extension TestPanel1: PanelStateCoder { 99 | 100 | var panelId: Int { 101 | return 1 102 | } 103 | 104 | } 105 | 106 | class TestPanel2: UIViewController { 107 | 108 | override func viewDidLoad() { 109 | super.viewDidLoad() 110 | 111 | self.view.backgroundColor = .green 112 | 113 | self.title = "Test Panel 2" 114 | 115 | } 116 | 117 | } 118 | 119 | extension TestPanel2: PanelContentDelegate { 120 | 121 | var preferredPanelContentSize: CGSize { 122 | return CGSize(width: 320, height: 500) 123 | } 124 | 125 | } 126 | 127 | extension TestPanel2: PanelStateCoder { 128 | 129 | var panelId: Int { 130 | return 2 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /PanelKitTests/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PanelKit 4 | // 5 | // Created by Louis D'hauwe on 09/03/2017. 6 | // Copyright © 2017 Silver Fox. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import PanelKit 12 | 13 | class ViewController: UIViewController, PanelManager { 14 | 15 | var mapPanelContentVC: MapPanelContentViewController! 16 | var mapPanelVC: PanelViewController! 17 | 18 | var textPanelContentVC: TextPanelContentViewController! 19 | var textPanelVC: PanelViewController! 20 | 21 | var contentWrapperView: UIView! 22 | var contentView: UIView! 23 | 24 | var mapPanelBarBtn: UIBarButtonItem! 25 | var textPanelBarBtn: UIBarButtonItem! 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | contentWrapperView = UIView(frame: view.bounds) 31 | view.addSubview(contentWrapperView) 32 | 33 | contentView = UIView(frame: contentWrapperView.bounds) 34 | contentWrapperView.addSubview(contentView) 35 | 36 | mapPanelContentVC = MapPanelContentViewController() 37 | 38 | mapPanelVC = PanelViewController(with: mapPanelContentVC, in: self) 39 | 40 | textPanelContentVC = TextPanelContentViewController() 41 | 42 | textPanelVC = PanelViewController(with: textPanelContentVC, in: self) 43 | 44 | enableTripleTapExposeActivation() 45 | 46 | mapPanelBarBtn = UIBarButtonItem(title: "Map", style: .done, target: self, action: nil) 47 | textPanelBarBtn = UIBarButtonItem(title: "Text", style: .done, target: self, action: nil) 48 | 49 | self.navigationItem.title = "Test" 50 | self.navigationItem.rightBarButtonItems = [mapPanelBarBtn, textPanelBarBtn] 51 | 52 | } 53 | 54 | // MARK: - Popover 55 | 56 | func showMapPanelFromBarButton(completion: @escaping (() -> Void)) { 57 | showPopover(mapPanelVC, from: mapPanelBarBtn, completion: completion) 58 | } 59 | 60 | func showTextPanelFromBarButton(completion: @escaping (() -> Void)) { 61 | showPopover(textPanelVC, from: textPanelBarBtn, completion: completion) 62 | } 63 | 64 | func showPopover(_ vc: UIViewController, from barButtonItem: UIBarButtonItem, completion: (() -> Void)? = nil) { 65 | 66 | vc.modalPresentationStyle = .popover 67 | vc.popoverPresentationController?.barButtonItem = barButtonItem 68 | 69 | present(vc, animated: false, completion: completion) 70 | 71 | } 72 | 73 | // MARK: - PanelManager 74 | 75 | let panelManagerLogLevel: LogLevel = .full 76 | 77 | var panelContentWrapperView: UIView { 78 | return contentWrapperView 79 | } 80 | 81 | var panelContentView: UIView { 82 | return contentView 83 | } 84 | 85 | var panels: [PanelViewController] { 86 | return [mapPanelVC, textPanelVC] 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PanelKit for iOS 3 |

4 | 5 |

6 | Build Status 7 | Codecov 8 |
9 | Swift 10 | PodVersion 11 | Carthage Compatible 12 | Platform: iOS 13 |
14 | Twitter 15 | Donate via PayPal 16 |

17 | 18 |

19 | PanelKit for iOS 20 |
21 | Applications using PanelKit can be seen in the showcase. 22 |

23 | 24 | 25 | ## About 26 | PanelKit is a UI framework that enables panels on iOS. A panel can be presented in the following ways: 27 | 28 | * Modally 29 | * As a popover 30 | * Floating (drag the panel around) 31 | * Pinned (either left or right) 32 | 33 | 34 | This framework does all the heavy lifting for dragging panels, pinning them and even moving/resizing them when a keyboard is shown/dismissed. 35 | 36 | 37 | ## Implementing 38 | A lot of effort has gone into making the API simple for a basic implementation, yet very customizable if needed. Since PanelKit is protocol based, you don't need to subclass anything in order to use it. There a two basic principles PanelKit entails: ```panels``` and a ```PanelManager```. 39 | 40 | ### Panels 41 | A panel is created using the ```PanelViewController``` initializer, which expects a ```UIViewController```, ```PanelContentDelegate``` and ```PanelManager```. 42 | 43 | #### PanelContentDelegate 44 | ```PanelContentDelegate ``` is a protocol that defines the appearance of a panel. Typically the ```PanelContentDelegate ``` protocol is implemented for each panel on its ```UIViewController```. 45 | 46 | 47 | Example: 48 | 49 | ```swift 50 | class MyPanelContentViewController: UIViewController, PanelContentDelegate { 51 | 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | self.title = "Panel title" 56 | } 57 | 58 | var preferredPanelContentSize: CGSize { 59 | return CGSize(width: 320, height: 500) 60 | } 61 | } 62 | ``` 63 | 64 | A panel is explicitly (without your action) shown in a ```UINavigationController```, but the top bar can be hidden or styled as with any ```UINavigationController```. 65 | 66 | 67 | ### PanelManager 68 | ```PanelManager``` is a protocol that in its most basic form expects the following: 69 | 70 | ```swift 71 | // The view in which the panels may be dragged around 72 | var panelContentWrapperView: UIView { 73 | return contentWrapperView 74 | } 75 | 76 | // The content view, which will be moved/resized when panels pin 77 | var panelContentView: UIView { 78 | return contentView 79 | } 80 | 81 | // An array of PanelViewController objects 82 | var panels: [PanelViewController] { 83 | return [] 84 | } 85 | ``` 86 | 87 | Typically the ```PanelManager``` protocol is implemented on a ```UIViewController```. 88 | 89 | ## Advanced features 90 | PanelKit has some advanced opt-in features: 91 | 92 | * [Multi-pinning](docs/MultiPinning.md) 93 | * [Panel resizing](docs/Resizing.md) 94 | * [State restoration](docs/States.md) 95 | * [Exposé](docs/Expose.md) 96 | 97 | ## Installation 98 | 99 | ### [CocoaPods](http://cocoapods.org) 100 | 101 | To install, add the following line to your ```Podfile```: 102 | 103 | ```ruby 104 | pod 'PanelKit', '~> 2.0' 105 | ``` 106 | 107 | ### [Carthage](https://github.com/Carthage/Carthage) 108 | To install, add the following line to your ```Cartfile```: 109 | 110 | ```ruby 111 | github "louisdh/panelkit" ~> 2.0 112 | ``` 113 | Run ```carthage update``` to build the framework and drag the built ```PanelKit.framework``` into your Xcode project. 114 | 115 | 116 | 117 | ## Requirements 118 | 119 | * iOS 10.0+ 120 | * Xcode 9.0+ 121 | 122 | ## Todo 123 | 124 | ### Long term: 125 | - [ ] Top/down pinning 126 | 127 | ## License 128 | 129 | This project is available under the MIT license. See the LICENSE file for more info. 130 | -------------------------------------------------------------------------------- /SHOWCASE.md: -------------------------------------------------------------------------------- 1 | # Showcase 2 | If you have an app that uses PanelKit, please [contact me](mailto:louisdhauwe@silverfox.be) or make a PR to add it here. 3 | 4 | ## [Pixure – Professional Pixel Art Studio](https://itunes.apple.com/app/pixure/id893400841?mt=8&at=1010lII4) 5 | ![Pixure](showcase-resources/pixure.gif) 6 | 7 | ## [SkipTimer](https://itunes.apple.com/app/skiptimer/id1308077196?mt=8&at=1010lII4) 8 | 9 | ## [Terminal](https://itunes.apple.com/app/terminal/id1323205755?mt=8&at=1010lII4) 10 | -------------------------------------------------------------------------------- /docs/Expose.md: -------------------------------------------------------------------------------- 1 | # Exposé 2 | An advanced feature of PanelKit is Exposé, which shows all floating and pinned panels in an overview, blurring the content behind it. 3 | 4 | ## How to implement 5 | One way to activate exposé is by calling `enableTripleTapExposeActivation()` on your `PanelManager`. Once enabled, you can tap twice with 3 fingers to toggle exposé. 6 | 7 | Exposé can also manually be activated by calling `toggleExpose()` on your `PanelManager`. 8 | 9 | ## Customization 10 | You can customize the blur effect of PanelKit's exposé by setting the `exposeOverlayBlurEffect` property on your `PanelManager`. -------------------------------------------------------------------------------- /docs/MultiPinning.md: -------------------------------------------------------------------------------- 1 | # Multi-pinning 2 | An advanced feature of PanelKit is the ability to have multiple panels pinned to the same side. 3 | 4 | ## How to implement 5 | To implement multi-pinning, a PanelManager should implement the `maximumNumberOfPanelsPinned(at side: PanelPinSide)` function and return a value greater than 1. By default, this API returns 1, which disabled the feature. 6 | 7 | ## Example implementation 8 | The following example will calculate the maximum number of pinned panels based on the available height: 9 | 10 | ```swift 11 | public func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int { 12 | return Int(floor(self.view.bounds.height / 320)) 13 | } 14 | ``` 15 | 16 | ## Pinned size 17 | When multiple panels are pinned to the same side, they each have the same height. 18 | 19 | The width is determined by the earliest panel pinned. For example: when a panel is pinned to the right side, the width of the pinned area is the panel's `preferredPanelPinnedWidth`. When a second panel is pinned to the right side, the width of the pinned area stays the same. When the first panel is unpinned, the width of the remaining pinned panel is updated to its `preferredPanelPinnedWidth`. -------------------------------------------------------------------------------- /docs/Resizing.md: -------------------------------------------------------------------------------- 1 | # Panel resizing 2 | An advanced feature of PanelKit is the ability to resize panels while they are floating. 3 | 4 | ## How to implement 5 | To implement panel resizing, the PanelContentDelegate of a panel should implement the `minimumPanelContentSize` or/and the `maximumPanelContentSize` API. By default, both of these return the panel's `preferredPanelContentSize`, disabling resizing. When resizing is enabled, a handle will appear in the bottom right corner. 6 | 7 | ## Example implementation 8 | The following implementation will enable resizing: 9 | 10 | ```swift 11 | extension MyPanelContentViewController: PanelContentDelegate { 12 | 13 | var preferredPanelContentSize: CGSize { 14 | return CGSize(width: 320, height: 240) 15 | } 16 | 17 | var minimumPanelContentSize: CGSize { 18 | return CGSize(width: 300, height: 200) 19 | } 20 | 21 | var maximumPanelContentSize: CGSize { 22 | return CGSize(width: 480, height: 640) 23 | } 24 | 25 | } 26 | ``` -------------------------------------------------------------------------------- /docs/States.md: -------------------------------------------------------------------------------- 1 | # State restoring 2 | An advanced feature of PanelKit is the ability to save and restore the state of panels. This allows you to save all the floating and pinned states at a particular moment in your app's life, store it (e.g. to disk), and restore to the exact same state at any moment. 3 | 4 | ## How to implement 5 | ### Panels 6 | Each panel that wants its state to be able to save and restore needs to implement the `PanelStateCoder` protocol. This procotol has one single requirement: 7 | 8 | ```swift 9 | var panelId: Int { get } 10 | ``` 11 | 12 | `panelId` is a unique id to identify a panel. Used when restoring the panel’s state. 13 | A panel’s id should be the same across app launches to successfully restore its state. 14 | 15 | ### PanelManager 16 | #### Saving 17 | The PanelManager protocol has the following API: 18 | 19 | ```swift 20 | var panelStates: [Int: PanelState] { get } 21 | ``` 22 | 23 | This returns a dictionary with the panel ids as keys and panel states as values. The `PanelState` struct conforms to the `Codable` protocol. 24 | 25 | #### Restoring 26 | 27 | Restoring can be done via the following API: 28 | 29 | ```swift 30 | func restorePanelStates(_ states: [Int: PanelState]) 31 | ``` 32 | 33 | ## Example implementation 34 | 35 | ```swift 36 | extension MyPanelManager { 37 | 38 | func savePanelStates() { 39 | 40 | let states = self.panelStates 41 | 42 | let encoder = JSONEncoder() 43 | 44 | guard let json = try? encoder.encode(states) else { 45 | return 46 | } 47 | 48 | UserDefaults.standard.set(json, forKey: "panelStates") 49 | 50 | } 51 | 52 | func restorePanelStatesFromDisk() { 53 | 54 | guard let jsonData = UserDefaults.standard.data(forKey: "panelStates") else { 55 | return 56 | } 57 | 58 | let decoder = JSONDecoder() 59 | guard let states = try? decoder.decode([Int: PanelState].self, from: jsonData) else { 60 | return 61 | } 62 | 63 | restorePanelStates(states) 64 | 65 | } 66 | 67 | } 68 | ``` -------------------------------------------------------------------------------- /readme-resources/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louisdh/panelkit/71d105aa793243f97009c560d8f0130339643f49/readme-resources/example.gif -------------------------------------------------------------------------------- /readme-resources/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louisdh/panelkit/71d105aa793243f97009c560d8f0130339643f49/readme-resources/hero.png -------------------------------------------------------------------------------- /showcase-resources/pixure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louisdh/panelkit/71d105aa793243f97009c560d8f0130339643f49/showcase-resources/pixure.gif --------------------------------------------------------------------------------