,
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 | 
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
--------------------------------------------------------------------------------