Unverified Commit dda271ce authored by Zach Knox's avatar Zach Knox
Browse files

Merge branch 'dev-1.0.1' into 42-reviews

parents 8f969dbb fe834a65
......@@ -12,7 +12,7 @@ def ios_pods
#pod 'Segmentio', '~> 2.1'
pod 'DeckTransition', '~> 1.4.0'
pod 'DeckTransition', '~> 2.0'
pod 'Fabric'
pod 'Crashlytics'
......@@ -43,4 +43,4 @@ post_install do |installer|
require 'fileutils'
FileUtils.cp_r('Pods/Target Support Files/Pods-WhatsOpen/Pods-WhatsOpen-Acknowledgements.plist', 'WhatsOpen/Settings.bundle/Acknowledgements.plist', :remove_destination => true)
end
\ No newline at end of file
end
PODS:
- Crashlytics (3.9.3):
- Fabric (~> 1.7.2)
- DeckTransition (1.4.2)
- Fabric (1.7.2)
- Crashlytics (3.10.1):
- Fabric (~> 1.7.5)
- DeckTransition (2.0.0)
- Fabric (1.7.6)
- ObjectMapper (3.1.0)
- ObjectMapper+Realm (0.5):
- ObjectMapper
- RealmSwift
- Realm (3.0.2):
- Realm/Headers (= 3.0.2)
- Realm/Headers (3.0.2)
- RealmSwift (3.0.2):
- Realm (= 3.0.2)
- Realm (3.3.1):
- Realm/Headers (= 3.3.1)
- Realm/Headers (3.3.1)
- RealmSwift (3.3.1):
- Realm (= 3.3.1)
DEPENDENCIES:
- Crashlytics
- DeckTransition (~> 1.4.0)
- DeckTransition (~> 2.0)
- Fabric
- ObjectMapper (~> 3.0)
- ObjectMapper+Realm
- RealmSwift
SPEC CHECKSUMS:
Crashlytics: dbb07d01876c171c5ccbdf7826410380189e452c
DeckTransition: 56330226ddbefd2ddc9f57b8b56d37e0e93e6b91
Fabric: 9cd6a848efcf1b8b07497e0b6a2e7d336353ba15
Crashlytics: aee1a064cbbf99b32efa3f056a5f458d846bc8ff
DeckTransition: 2300694f94fbeca2c103f365dc9dc09ffae14a4f
Fabric: f8d42c893bb187326a7968b62abe55c36a987a46
ObjectMapper: 20505058f54e5c3ca69e1d6de9897d152a5369a6
ObjectMapper+Realm: 3188789fb77c189c7a83cb625333a607eb61a08f
Realm: 6f23fd1f178a09342eac21bfa7c2bf4312a7a180
RealmSwift: 695393add1b8f9d5fa75dd16e6355cf3935f71e2
Realm: 63ebf4903b446b0bdf2131244eb1e1549925c724
RealmSwift: f28fe8507bdfb8f1c924962401b83e7b9d7abb5c
PODFILE CHECKSUM: c947fc1ca20537a2e287e824029a7f9af0d9e588
PODFILE CHECKSUM: 1225e83da50847e17730900da3b05d9d69748392
COCOAPODS: 1.4.0
......@@ -25,7 +25,7 @@ Hereʼs a GIF showing it in action.
To install DeckTransition using [CocoaPods](http://cocoapods.org), add the following line to your Podfile:
```
pod 'DeckTransition', '~> 1.0'
pod 'DeckTransition', '~> 2.0'
```
### Carthage
......@@ -33,7 +33,7 @@ pod 'DeckTransition', '~> 1.0'
To install DeckTransition using [Carthage](https://github.com/Carthage/Carthage), add the following line to your Cartfile:
```
github "HarshilShah/DeckTransition" ~> 1.0
github "HarshilShah/DeckTransition" ~> 2.0
```
## Documentation
......@@ -46,7 +46,7 @@ You can find [the docs here](https://harshilshah.github.io/DeckTransition "Docum
Set `modalPresentationCapturesStatusBarAppearance` to `true` in your modal view controller, and override the `preferredStatusBarStyle` variable to return `.lightContent`.
The background color for the presentation can be changed by changing the `backgroundColor` property of the `window`. This is `.black` by default.
Additionally, the `UIScrollView` instances which should be tracked for the swipe-to-dismiss gesture should have their `backgroundColor` set to `.clear`.
### Presentation
......@@ -66,51 +66,15 @@ present(modal, animated: true, completion: nil)
### Dismissal
This is the part where it gets a bit tricky. If youʼve got a fixed-sized i.e. non-scrolling modal, feel free to just skip the rest of this section. Swipe-to-dismiss will work perfectly for you
By default, DeckTransition has a swipe-to-dismiss gesture which is automatically enabled when your modalʼs main `UIScrollView` is scrolled to the top.
For modals which have a vertically scrolling layout, the dismissal gesture should be fired only when the view is scrolled to the top. To achieve this behaviour, you need to modify the `isDismissEnabled` property of the `DeckTransitioningDelegate`. (You can also set `isDismissEnabled` to false if you want to disable the swipe-to-dismiss UI.)
You can opt-out of this behaviour by passing in `false` for the `isSwipeToDismissEnabled` parameter while initialising your `DeckTransitioningDelegate`.
The one issue with doing this in response to the scrollviewʼs `contentOffset` is momentum scrolling. When the user pans from top the bottom, once the top of the scrollview is reached (`contentOffset.y` is 0), the dismiss gesture should take over and the scrollview should stop scrolling, not showing the usual iOS bounce effect. The dismiss gesture, however, only responds to pans and not swipes, so should you swipe and not pan, the scrollview will scroll to the top and abruptly stop (as the `contentOffset.y` is 0) without the usual iOS bounce effect.
### `UIScrollView` detection
I've found a temporary workaround for this, the code for this can be found below. Itʼs a bit messy right now, but is the only workaround Iʼve found for this issue (so far). It has one caveat, in that it fails utterly miserably when using with a scrollview whose `backgroundColor` isnʼt `.clear`.
Iʼll update this project if/when I find a better solution.
DeckTransition has an internal heuristic to determine which `UIScrollView` should be tracked for the swipe-to-dismiss gesture. In general, this should be sufficient for and cover most use cases.
#### Dismissal code for scrolling modals
First up, make your modal view controller conform to `UIScrollViewDelegate` (or `UITableViewDelegate`/`UITextFieldDelegate`, as the case may be), and assign self as the scrollview's `delegate`.
Next, add this method to your modal view controller, swapping in your scrollviewʼs variable for `textView`.
```swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isEqual(textView) else {
return
}
if let delegate = transitioningDelegate as? DeckTransitioningDelegate {
if scrollView.contentOffset.y > 0 {
// Normal behaviour if the `scrollView` isn't scrolled to the top
scrollView.bounces = true
delegate.isDismissEnabled = false
} else {
if scrollView.isDecelerating {
// If the `scrollView` is scrolled to the top but is decelerating
// that means a swipe has been performed. The view and
// scrollviewʼs subviews are both translated in response to this.
view.transform = CGAffineTransform(translationX: 0, y: -scrollView.contentOffset.y)
scrollView.subviews.forEach {
$0.transform = CGAffineTransform(translationX: 0, y: scrollView.contentOffset.y)
}
} else {
// If the user has panned to the top, the scrollview doesnʼt bounce and
// the dismiss gesture is enabled.
scrollView.bounces = false
delegate.isDismissEnabled = true
}
}
}
}
```
However there are some edge cases, and should you run into one, these can we worked around by making your modal view controller conform to the `DeckTransitionViewControllerProtocol` protocol. More information about this can be found in the documentation page about [UIScrollView detection](https://harshilshah.github.io/DeckTransition/uiscrollview-detection.html).
### Snapshots
......
......@@ -8,40 +8,25 @@
import UIKit
/// Delegate that communicates to the `DeckPresentationController` whether the
/// dismiss by pan gesture is enabled
protocol DeckPresentationControllerDelegate {
func isDismissGestureEnabled() -> Bool
}
/// A protocol to communicate to the transition that an update of the snapshot
/// view is required. This is adopted only by the presentation controller of
/// any view controller presented using DeckTransition
public protocol DeckSnapshotUpdater {
/// For various reasons (performance, the way iOS handles safe area,
/// layout issues, etc.) this transition uses a snapshot view of your
/// `presentingViewController` and not the live view itself.
///
/// In some cases this snapshot might become outdated before the dismissal,
/// and for those cases you can request to have the snapshot updated. While
/// the transition only shows a small portion of the presenting view, in
/// some cases that might become inconsistent enough to demand an update.
///
/// This is an expensive process and should only be used if necessary, for
/// example if you are updating your entire app's theme.
func requestPresentedViewSnapshotUpdate()
}
final class DeckPresentationController: UIPresentationController, UIGestureRecognizerDelegate, DeckSnapshotUpdater {
// MARK: - Internal variables
var transitioningDelegate: DeckPresentationControllerDelegate?
/// The presentation controller holds a strong reference to the
/// transitioning delegate because `UIViewController.transitioningDelegate`
/// is a weak property, and thus the `DeckTransitioningDelegate` would be
/// unallocated right after the presentation animation.
///
/// Since the transitioningDelegate only vends the presentation controller
/// object and does not hold a reference to it, there is no issue of a
/// circular dependency here.
var transitioningDelegate: DeckTransitioningDelegate?
// MARK: - Private variables
private var isSwipeToDismissGestureEnabled = true
private var pan: UIPanGestureRecognizer?
private var scrollViewUpdater: ScrollViewUpdater?
private let backgroundView = UIView()
private let roundedViewForPresentingView = RoundedView()
......@@ -66,6 +51,7 @@ final class DeckPresentationController: UIPresentationController, UIGestureRecog
convenience init(presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
isSwipeToDismissGestureEnabled: Bool,
presentAnimation: (() -> ())? = nil,
presentCompletion: ((Bool) ->())? = nil,
dismissAnimation: (() -> ())? = nil,
......@@ -73,6 +59,7 @@ final class DeckPresentationController: UIPresentationController, UIGestureRecog
self.init(presentedViewController: presentedViewController,
presenting: presentingViewController)
self.isSwipeToDismissGestureEnabled = isSwipeToDismissGestureEnabled
self.presentAnimation = presentAnimation
self.presentCompletion = presentCompletion
self.dismissAnimation = dismissAnimation
......@@ -272,11 +259,13 @@ final class DeckPresentationController: UIPresentationController, UIGestureRecog
roundedViewForPresentingView.bottomAnchor.constraint(equalTo: snapshotViewContainer.bottomAnchor)
])
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
pan!.delegate = self
pan!.maximumNumberOfTouches = 1
pan!.cancelsTouchesInView = false
presentedViewController.view.addGestureRecognizer(pan!)
if isSwipeToDismissGestureEnabled {
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
pan!.delegate = self
pan!.maximumNumberOfTouches = 1
pan!.cancelsTouchesInView = false
presentedViewController.view.addGestureRecognizer(pan!)
}
presentCompletion?(completed)
}
......@@ -559,26 +548,36 @@ final class DeckPresentationController: UIPresentationController, UIGestureRecog
// MARK: - Gesture handling
private func isSwipeToDismissAllowed() -> Bool {
guard let updater = scrollViewUpdater else {
return isSwipeToDismissGestureEnabled
}
return updater.isDismissEnabled
}
@objc private func handlePan(gestureRecognizer: UIPanGestureRecognizer) {
guard gestureRecognizer.isEqual(pan) else {
guard gestureRecognizer.isEqual(pan), isSwipeToDismissGestureEnabled else {
return
}
switch gestureRecognizer.state {
case .began:
let detector = ScrollViewDetector(withViewController: presentedViewController)
if let scrollView = detector.scrollView {
scrollViewUpdater = ScrollViewUpdater(
withRootView: presentedViewController.view,
scrollView: scrollView)
}
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: containerView)
case .changed:
if let view = presentedView {
/// The dismiss gesture needs to be enabled for the pan gesture
/// to do anything.
if transitioningDelegate?.isDismissGestureEnabled() ?? false {
let translation = gestureRecognizer.translation(in: view)
updatePresentedViewForTranslation(inVerticalDirection: translation.y)
} else {
gestureRecognizer.setTranslation(.zero, in: view)
}
if isSwipeToDismissAllowed() {
let translation = gestureRecognizer.translation(in: presentedView)
updatePresentedViewForTranslation(inVerticalDirection: translation.y)
} else {
gestureRecognizer.setTranslation(.zero, in: presentedView)
}
case .ended:
......@@ -645,15 +644,3 @@ final class DeckPresentationController: UIPresentationController, UIGestureRecog
}
}
fileprivate extension UIViewController {
/// A Boolean value indicating whether the view controller is presented
/// using Deck.
var isPresentedWithDeck: Bool {
return transitioningDelegate is DeckTransitioningDelegate
&& modalPresentationStyle == .custom
&& presentingViewController != nil
}
}
......@@ -14,7 +14,7 @@ import UIKit
/// `custom`
public final class DeckSegue: UIStoryboardSegue {
var transition: UIViewControllerTransitioningDelegate!
var transition: UIViewControllerTransitioningDelegate?
/// Performs the visual transition for the Deck segue.
public override func perform() {
......
//
// DeckSnapshotUpdater.swift
// DeckTransition
//
// Created by Harshil Shah on 06/12/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
/// A protocol to communicate to the transition that an update of the snapshot
/// view is required. This is adopted only by the presentation controller of
/// any view controller presented using DeckTransition
public protocol DeckSnapshotUpdater {
/// For various reasons (performance, the way iOS handles safe area,
/// layout issues, etc.) this transition uses a snapshot view of your
/// `presentingViewController` and not the live view itself.
///
/// In some cases this snapshot might become outdated before the dismissal,
/// and for those cases you can request to have the snapshot updated. While
/// the transition only shows a small portion of the presenting view, in
/// some cases that might become inconsistent enough to demand an update.
///
/// This is an expensive process and should only be used if necessary, for
/// example if you are updating your entire app's theme.
func requestPresentedViewSnapshotUpdate()
}
//
// DeckTransitionViewControllerProtocol.swift
// DeckTransition
//
// Created by Harshil Shah on 06/12/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
import UIKit
/// A set of methods that vend objects used to customize a DeckTransition
/// presentation's swipe-to-dismiss behaviour.
///
/// The transition has an internal heuristic to determine which `UIScrollView`
/// should be tracked for the swipe-to-dismiss gesture. However that has some
/// edge cases, which can we worked around by making your modal view controller
/// and view controllers presented by or contained within it conform to this
/// protocol.
@objc public protocol DeckTransitionViewControllerProtocol: class {
/// The child view controller which contains the scroll view that should
/// be tracked for the swipe-to-dismiss gesture.
///
/// The default heuristic for searching the `UIScrollView` to track
/// traverses only the first level of subviews of the presented view
/// controller. As a result of this, subviews of any child view controller
/// are not inspected.
///
/// A container view controller presented using DeckTransition can
/// implement this variable and return the child view controller which
/// contains the scroll view to be tracked.
///
/// If this variable is not implemented or is `nil`, then the container view
/// controller's own view is searched.
///
/// If this variable is implemented and is not `nil`, the container view
/// controller's own subviews and the value returned in the
/// `scrollViewForDeck` variable are both ignored, and the search continues
/// within the child view controller returned here.
@objc optional var childViewControllerForDeck: UIViewController? { get }
/// The scroll view that should be tracked for Deck's swipe-to-dismiss
/// gesture.
///
/// The default heuristic for searching the `UIScrollView` to track only
/// traverses only the first level of subviews of the presented view
/// controller, returning the lowermost scroll view found.
///
/// This is a similar heuristic to that used in `UINavigationController`
/// (which to the best of my knowledge, is even more limited and checks only
/// one view, the lowermost subview of the main view), however it can miss
/// out on the intended scroll view for more complex view hierarchies.
/// For those cases, you can implement this variable and return the
/// `UIScrollView` instance which should be tracked.
///
/// - Note: The value returned in this variable is ignored if the
/// `childViewControllerForDeck` variable is also implemented.
@objc optional var scrollViewForDeck: UIScrollView { get }
}
......@@ -22,19 +22,11 @@ import UIKit
/// modal.modalPresentationStyle = .custom
/// present(modal, animated: true, completion: nil)
/// ```
public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate, DeckPresentationControllerDelegate {
// MARK: - Public variables
/// A variable indicating whether or not the presenting view controller
/// can currently be dismissed using a pan gestures from top to bottom.
///
/// When set to `true`, this allows the presented modal view to be dismissed
/// using a pan gesture. The default value of this property is `true`
public var isDismissEnabled = true
public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
// MARK: - Private variables
private let isSwipeToDismissEnabled: Bool
private let presentDuration: TimeInterval?
private let presentAnimation: (() -> ())?
private let presentCompletion: ((Bool) -> ())?
......@@ -45,10 +37,13 @@ public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransiti
// MARK: - Initializers
/// Returns a transitioning delegate to perform a Deck transition. All
/// parameters are optional. Leaving the duration parameters empty gives you
/// animations with the default durations (0.3s for both)
/// parameters are optional. Swipe-to-dimiss is enabled by default. Leaving
/// the duration parameters empty gives you animations with the default
/// durations (0.3s for both)
///
/// - Parameters:
/// - isSwipeToDismissEnabled: Whether the modal view controller should
/// be dismissed with a swipe gesture from top to bottom
/// - presentDuration: The duration for the presentation animation
/// - presentAnimation: An animation block that will be performed
/// alongside the card presentation animation
......@@ -59,12 +54,14 @@ public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransiti
/// alongside the card dismissal animation
/// - dismissCompletion: A block that will be run after the card has been
/// dismissed
@objc public init(presentDuration: NSNumber? = nil,
@objc public init(isSwipeToDismissEnabled: Bool = true,
presentDuration: NSNumber? = nil,
presentAnimation: (() -> ())? = nil,
presentCompletion: ((Bool) -> ())? = nil,
dismissDuration: NSNumber? = nil,
dismissAnimation: (() -> ())? = nil,
dismissCompletion: ((Bool) -> ())? = nil) {
self.isSwipeToDismissEnabled = isSwipeToDismissEnabled
self.presentDuration = presentDuration?.doubleValue
self.presentAnimation = presentAnimation
self.presentCompletion = presentCompletion
......@@ -115,6 +112,7 @@ public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransiti
let presentationController = DeckPresentationController(
presentedViewController: presented,
presenting: presenting,
isSwipeToDismissGestureEnabled: isSwipeToDismissEnabled,
presentAnimation: presentAnimation,
presentCompletion: presentCompletion,
dismissAnimation: dismissAnimation,
......@@ -123,10 +121,4 @@ public final class DeckTransitioningDelegate: NSObject, UIViewControllerTransiti
return presentationController
}
// MARK: - DeckPresentationControllerDelegate methods
func isDismissGestureEnabled() -> Bool {
return isDismissEnabled
}
}
//
// UIViewController+DeckTransitionViewControllerProtocol.swift
// DeckTransition
//
// Created by Harshil Shah on 06/12/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
import UIKit
extension UITabBarController: DeckTransitionViewControllerProtocol {
/// The view controller representing the selected tab is assumed to contain
/// the `UIScrollView` to be tracked
public var childViewControllerForDeck: UIViewController? {
return self.selectedViewController
}
}
extension UINavigationController: DeckTransitionViewControllerProtocol {
/// The view controller at the top of the navigation stack is assumed to
/// contain the `UIScrollView` to be tracked
public var childViewControllerForDeck: UIViewController? {
return self.topViewController
}
}
//
// UIViewController+IsPresentedWithDeck.swift
// DeckTransition
//
// Created by Harshil Shah on 06/12/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
import UIKit
extension UIViewController {
/// A Boolean value indicating whether the view controller is presented
/// using Deck.
var isPresentedWithDeck: Bool {
return transitioningDelegate is DeckTransitioningDelegate
&& modalPresentationStyle == .custom
&& presentingViewController != nil
}
}
//
// ScrollViewDetector.swift
// DeckTransition
//
// Created by Harshil Shah on 06/12/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
import UIKit
/// An encapsulation for the logic used to parse a view controller hierarchy
/// and detect the scroll view to be tracked for the swipe-to-dismiss gesture
final class ScrollViewDetector {
// MARK: - Public variables
weak var scrollView: UIScrollView?
// MARK: - Initializers
init(withViewController viewController: UIViewController) {
let topViewController = getVisibleViewController(fromViewController: viewController)
self.scrollView = getScrollView(fromViewController: topViewController)
}
// MARK: - Private methods
/// Returns the view controller whose `view` should be searched for the
/// `UIScrollView` to track.
///
/// - Parameter viewController: The view controller from which the search
/// should begin.
/// - Returns: The view controller whose `view` must be searched.
private func getVisibleViewController(fromViewController viewController: UIViewController) -> UIViewController {
guard let deckViewController = viewController as? DeckTransitionViewControllerProtocol,
let childViewController = deckViewController.childViewControllerForDeck as? UIViewController
else {
return viewController
}
return getVisibleViewController(fromViewController: childViewController)
}
/// Returns the `UIScrollView` which should be tracked.
///
/// - Parameter viewController: The view controller whose view hierarchy
/// must be searched.
/// - Returns: The scrollView specified in the
/// `DeckTransitionViewControllerProtocol` implementation if one exists,
/// failing which the lowermost `UIScrollView` in the view's top level
/// subviews, or nil if one isn't found.
private func getScrollView(fromViewController viewController: UIViewController) -> UIScrollView? {
if let deckViewController = viewController as? DeckTransitionViewControllerProtocol,
let scrollView = deckViewController.scrollViewForDeck {
return scrollView
}
for subview in viewController.view.subviews {
if let scrollView = subview as? UIScrollView {
return scrollView