Commit ba8d8180 authored by Zach Knox's avatar Zach Knox
Browse files

Merge branch 'superfluousness' into 'dev-0.0'

Superfluousness

See merge request !18
parents f117a6cb fdf921a9
......@@ -64,3 +64,4 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
.DS_Store
......@@ -29,9 +29,9 @@ Requirements:
To get started, you'll need the following installed:
* [Git](http://git-scm.com/book/en/Getting-Started-Installing-Git)
* The latest **public** build of [Xcode](https://developer.apple.com/xcode/) (and a compatible Mac). *Currently Xcode 8* (You can get this from the [Mac App Store](https://itunes.apple.com/us/app/xcode/id497799835?mt=12) if you want easy updates)
* The latest **public** build of [Xcode](https://developer.apple.com/xcode/) (and a compatible Mac). *Currently Xcode 9* (You can get this from the [Mac App Store](https://itunes.apple.com/us/app/xcode/id497799835?mt=12) if you want easy updates)
* The latest **public** release of Swift. *Currently Swift 3.0* (bundled with Xcode)
* The latest **public** release of Swift. *Currently Swift 4.0* (bundled with Xcode)
* Cocoapods, for dependency management. You can install this by running `sudo gem install cocoapods` in your terminal. (Dependency management may change in the future)
......
# Uncomment this line to define a global platform for your project
# platform :ios, '8.0'
# Uncomment this line if you're using Swift
platform :ios, '11.0'
use_frameworks!
target 'WhatsOpen' do
pod 'RealmSwift'
pod 'ObjectMapper', '~> 2.2'
pod 'ObjectMapper', '~> 3.0'
#pod 'Segmentio', '~> 2.1'
pod 'DeckTransition', '~> 1.4.0'
end
......@@ -20,4 +24,7 @@ post_install do |installer|
config.build_settings['SWIFT_VERSION'] = '3.1'
end
end
end
require 'fileutils'
FileUtils.cp_r('Pods/Target Support Files/Pods-WhatsOpen/Pods-WhatsOpen-Acknowledgements.plist', 'WhatsOpen/Settings.bundle/Acknowledgements.plist', :remove_destination => true)
end
PODS:
- ObjectMapper (2.2.9)
- Realm (2.10.2):
- Realm/Headers (= 2.10.2)
- Realm/Headers (2.10.2)
- RealmSwift (2.10.2):
- Realm (= 2.10.2)
- DeckTransition (1.4.2)
- ObjectMapper (3.1.0)
- Realm (3.0.2):
- Realm/Headers (= 3.0.2)
- Realm/Headers (3.0.2)
- RealmSwift (3.0.2):
- Realm (= 3.0.2)
DEPENDENCIES:
- ObjectMapper (~> 2.2)
- DeckTransition (~> 1.4.0)
- ObjectMapper (~> 3.0)
- RealmSwift
SPEC CHECKSUMS:
ObjectMapper: 63cfe41bc6f8e7c8f44344c49901b8ae7de14c52
Realm: 0ef72b837fb67e9f4b098bac771ddd72c7fdbb69
RealmSwift: 07a9ae0505091eda6b2ee7c190c3786d6e90a7b0
DeckTransition: 56330226ddbefd2ddc9f57b8b56d37e0e93e6b91
ObjectMapper: 20505058f54e5c3ca69e1d6de9897d152a5369a6
Realm: 6f23fd1f178a09342eac21bfa7c2bf4312a7a180
RealmSwift: 695393add1b8f9d5fa75dd16e6355cf3935f71e2
PODFILE CHECKSUM: 2d5f357c11583d1910eee9ef03105571633f7ad2
PODFILE CHECKSUM: 00681e73744ee4b38807e182fb093bc825facfb8
COCOAPODS: 1.1.1
COCOAPODS: 1.3.1
Copyright (c) 2016 Harshil Shah <harshilshah1910@me.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
# DeckTransition
[![CI Status](http://img.shields.io/travis/HarshilShah/DeckTransition.svg)](https://travis-ci.org/HarshilShah/DeckTransition)
[![Version](https://img.shields.io/github/release/HarshilShah/DeckTransition.svg)](https://github.com/HarshilShah/DeckTransition/releases/latest)
![Package Managers](https://img.shields.io/badge/supports-CocoaPods%20%7C%20Carthage-orange.svg)
[![Documentation](https://cdn.rawgit.com/HarshilShah/DeckTransition/master/docs/badge.svg)](https://harshilshah.github.com/DeckTransition)
[![License](https://img.shields.io/badge/license-MIT-999999.svg)](https://github.com/HarshilShah/DeckTransition/blob/master/LICENSE)
[![Contact](https://img.shields.io/badge/contact-%40HarshilShah1910-3a8fc1.svg)](https://twitter.com/HarshilShah1910)
DeckTransition is an attempt to recreate the card-like transition found in the iOS 10 Apple Music and iMessage apps.
Hereʼs a GIF showing it in action.
![Demo](https://raw.githubusercontent.com/HarshilShah/DeckTransition/master/Resources/demo.gif)
## Requirements
- Swift 4
- iOS 9 or later
## Installation
### CocoaPods
To install DeckTransition using [CocoaPods](http://cocoapods.org), add the following line to your Podfile:
```
pod 'DeckTransition', '~> 1.0'
```
### Carthage
To install DeckTransition using [Carthage](https://github.com/Carthage/Carthage), add the following line to your Cartfile:
```
github "HarshilShah/DeckTransition" ~> 1.0
```
## Documentation
You can find [the docs here](https://harshilshah.github.io/DeckTransition "Documentation"). Documentation is generated with [Jazzy](https://github.com/realm/jazzy), and hosted on [GitHub Pages](https://pages.github.com).
## Usage
### Basics
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.
### Presentation
The transition can be called from code or using a storyboard.
To use via storyboards, just setup a custom segue (`kind` set to `custom`), and set the `class` to `DeckSegue`.
Hereʼs a snippet showing usage via code. Just replace `ModalViewController()` with your view controller's class and youʼre good to go.
```swift
let modal = ModalViewController()
let transitionDelegate = DeckTransitioningDelegate()
modal.transitioningDelegate = transitionDelegate
modal.modalPresentationStyle = .custom
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
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.)
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.
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.
#### 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
}
}
}
}
```
### Snapshots
For a variety of reasons, and especially because of iOS 11's safe area layout, DeckTransition uses a snapshot of your presenting view controller's view instead of using the view directly. This view is automatically updated whenever the frame is resized.
However, there can be some cases where you might want to update the snapshot view by yourself, and this can be achieved using the following one line snippet:
```swift
(presentationController as? DeckSnapshotUpdater)?.requestPresentedViewSnapshotUpdate()
```
All this does is request the presentation controller to update the snapshot.
You can also choose to update snapshot directly from the presenting view controller, as follows:
```swift
(presentedViewController?.presentationController as? DeckSnapshotUpdater)?.requestPresentedViewSnapshotUpdate()
```
It's worth noting that updating the snapshot is an expensive process and should only be used if necessary, for example if you are updating your entire app's theme.
## Apps Using DeckTransition
- [Petty](https://zachsim.one/projects/petty) by [Zach Simone](https://twitter.com/zachsimone)
- [Bitbook](https://bitbookapp.com) by [Sammy Gutierrez](https://sammygutierrez.com)
- [BookPlayer](https://github.com/GianniCarlo/Audiobook-Player) by [Gianni Carlo](https://twitter.com/GCarlo89)
Feel free to submit a PR if you’re using this library in your apps
## Author
Written by [Harshil Shah](https://twitter.com/HarshilShah1910)
## License
DeckTransition is available under the MIT license. See the LICENSE file for more info.
//
// Constants.swift
// DeckTransition
//
// Created by Harshil Shah on 04/08/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
struct Constants {
/// Default duration for present and dismiss animations when the user hasn't
/// specified one
static let defaultAnimationDuration: TimeInterval = 0.3
/// The corner radius applied to the presenting and presented view
/// controllers's views
static let cornerRadius: CGFloat = 8
/// The alpha value of the presented view controller's view
static let alphaForPresentingView: CGFloat = 0.8
/// As best as I can tell using my iPhone and a bunch of iOS UI templates I
/// came across online, 8 points is the distance between the top edges of
/// the presented and the presenting views
static let insetForPresentedView: CGFloat = 8
}
//
// DeckDismissingAnimationController.swift
// DeckTransition
//
// Created by Harshil Shah on 15/10/16.
// Copyright © 2016 Harshil Shah. All rights reserved.
//
import UIKit
final class DeckDismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
// MARK: - Private variables
private let duration: TimeInterval?
// MARK: - Initializers
init(duration: TimeInterval?) {
self.duration = duration
}
// MARK: - UIViewControllerAnimatedTransitioning
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
/// The presentedViewController throughout this library refers to the
/// card view controller which is presented in the Deck style, and so
/// for consistency, even through it's the view controller that we are
/// transitioning `.from` in the context of the dismissal animation and
/// should thus be the `presentingViewController`, it's referred to as
/// the `presentedViewController` here
guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
return
}
let containerView = transitionContext.containerView
let offscreenFrame = CGRect(x: 0, y: containerView.bounds.height, width: containerView.bounds.width, height: containerView.bounds.height)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: .curveEaseOut,
animations: {
presentedViewController.view.frame = offscreenFrame
}, completion: { finished in
transitionContext.completeTransition(finished)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration ?? Constants.defaultAnimationDuration
}
}
//
// DeckPresentingAnimationController.swift
// DeckTransition
//
// Created by Harshil Shah on 15/10/16.
// Copyright © 2016 Harshil Shah. All rights reserved.
//
import UIKit
final class DeckPresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
// MARK: - Private variables
private let duration: TimeInterval?
// MARK: - Initializers
init(duration: TimeInterval?) {
self.duration = duration
}
// MARK: - UIViewControllerAnimatedTransitioning
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
return
}
let containerView = transitionContext.containerView
containerView.addSubview(presentedViewController.view)
presentedViewController.view.frame = CGRect(x: 0, y: containerView.bounds.height, width: containerView.bounds.width, height: containerView.bounds.height)
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: .curveEaseOut,
animations: {
presentedViewController.view.frame = finalFrameForPresentedView
}, completion: { finished in
transitionContext.completeTransition(finished)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration ?? Constants.defaultAnimationDuration
}
}
//
// DeckSegue.swift
// DeckTransition
//
// Created by Harshil Shah on 15/10/16.
// Copyright © 2016 Harshil Shah. All rights reserved.
//
import UIKit
/// A segue to implement the Deck transition via Storyboards
///
/// To use this, set your segue's class to `DeckSegue`, and its `kind` to
/// `custom`
public final class DeckSegue: UIStoryboardSegue {
var transition: UIViewControllerTransitioningDelegate!
/// Performs the visual transition for the Deck segue.
public override func perform() {
transition = DeckTransitioningDelegate()
destination.transitioningDelegate = transition
destination.modalPresentationStyle = .custom
source.present(destination, animated: true, completion: nil)
}
}
//
// DeckTransitioningDelegate.swift
// DeckTransition
//
// Created by Harshil Shah on 15/10/16.
// Copyright © 2016 Harshil Shah. All rights reserved.
//
import UIKit
/// The DeckTransitioningDelegate class vends out the presentation and animation
/// controllers required to present a view controller with the Deck transition
/// style
///
/// The following snippet described the steps for presenting a given
/// `ModalViewController` with the `DeckTransitioningDelegate`
///
/// ```swift
/// let modal = ModalViewController()
/// let transitionDelegate = DeckTransitioningDelegate()
/// modal.transitioningDelegate = transitionDelegate
/// 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
// MARK: - Private variables
private let presentDuration: TimeInterval?
private let presentAnimation: (() -> ())?
private let presentCompletion: ((Bool) -> ())?
private let dismissDuration: TimeInterval?
private let dismissAnimation: (() -> ())?
private let dismissCompletion: ((Bool) -> ())?
// 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:
/// - presentDuration: The duration for the presentation animation
/// - presentAnimation: An animation block that will be performed
/// alongside the card presentation animation
/// - presentCompletion: A block that will be run after the card has been
/// presented
/// - dismissDuration: The duration for the dismissal animation
/// - dismissAnimation: An animation block that will be performed
/// alongside the card dismissal animation
/// - dismissCompletion: A block that will be run after the card has been
/// dismissed
@objc public init(presentDuration: NSNumber? = nil,
presentAnimation: (() -> ())? = nil,
presentCompletion: ((Bool) -> ())? = nil,
dismissDuration: NSNumber? = nil,
dismissAnimation: (() -> ())? = nil,
dismissCompletion: ((Bool) -> ())? = nil) {
self.presentDuration = presentDuration?.doubleValue
self.presentAnimation = presentAnimation
self.presentCompletion = presentCompletion
self.dismissDuration = dismissDuration?.doubleValue
self.dismissAnimation = dismissAnimation
self.dismissCompletion = dismissCompletion
}
// MARK: - UIViewControllerTransitioningDelegate
/// Returns an animation controller that animates the modal presentation
///
/// This is internal infrastructure handled entirely by UIKit and shouldn't
/// be called directly
///
/// - Parameters:
/// - presented: The modal view controller to be presented onscreen
/// - presenting: The view controller that will be presenting the modal
/// - source: The view controller whose `present` method is called
/// - Returns: An animation controller that animates the modal presentation
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DeckPresentingAnimationController(duration: presentDuration)
}
/// Returns an animation controller that animates the modal dismissal
///
/// This is internal infrastructure handled entirely by UIKit and shouldn't
/// be called directly
///
/// - Parameter dismissed: The modal view controller which will be dismissed
/// - Returns: An animation controller that animates the modal dismisall
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DeckDismissingAnimationController(duration: dismissDuration)
}
/// Returns a presentation controller that manages the modal presentation
///
/// This is internal infrastructure handled entirely by UIKit and shouldn't
/// be called directly
///
/// - Parameters:
/// - presented: The modal view controller
/// - presenting: The view controller which presented the modal
/// - source: The view controller whose `present` method was called to
/// present the modal
/// - Returns: A presentation controller that manages the modal presentation
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = DeckPresentationController(
presentedViewController: presented,
presenting: presenting,
presentAnimation: presentAnimation,
presentCompletion: presentCompletion,
dismissAnimation: dismissAnimation,
dismissCompletion: dismissCompletion)
presentationController.transitioningDelegate = self
return presentationController
}
// MARK: - DeckPresentationControllerDelegate methods
func isDismissGestureEnabled() -> Bool {
return isDismissEnabled
}
}
//
// ManualLayout.swift
// DeckTransition
//
// Created by Harshil Shah on 13/09/17.
// Copyright © 2017 Harshil Shah. All rights reserved.
//
import UIKit
/// A wrapper for a bunch of sizing methods
final class ManualLayout {
/// Just a convenience method to access the height of the status bar
class var statusBarHeight: CGFloat {
return UIApplication.shared.statusBarFrame.height
}
/// The top inset of the containerView within its window based on the status
/// bar. This exists entirely because the opaque blue location and green
/// in-call status bars on portrait non-X iPhones inset the containerView
/// by 20px
class var containerViewTopInset: CGFloat {
let statusBarHeight = ManualLayout.statusBarHeight
switch statusBarHeight {
case 0: return 0
case 20: return 0
case 40: return 20
case 44: return 0
default: return 0
}
}
/// The top inset of the presentingView within the containerView.
///
/// The values are as follows, for a given value of the status bar:
/// - Landscape on iPhone means the status bar is hidden, and since that
/// means an extremely wide aspect ratio, the inset is just 8 points
/// - On iPads, and with the single-height status bar on portrait, non-X
/// iPhones, the status bar is 20px, so the view is also inset by the same
/// - With the double-height status bar on portrait, non-X iPhones, the view
/// is inset by 20 points since the containerView itself is also inset by