Add video animation support, update examples SimpleVideoAnimation SimpleVideoTransitionSwitch

This commit is contained in:
RenZhu Macro
2020-07-22 16:02:13 +08:00
parent 3929205637
commit bd7ee080bd
144 changed files with 10316 additions and 70 deletions
@@ -0,0 +1,131 @@
//
// BlurLayer.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/14.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
import UIKit
private extension CGRect {
func rectangle(_ s: CGSize) -> CGRect {
let x = origin.x / s.width
let y = origin.y / s.height
let width = size.width / s.width
let height = size.height / s.height
return CGRect(x: x, y: y, width: width, height: height)
}
}
class BlurLayer: CALayer {
private static let blurRadiusKey = "blurRadius"
private static let blurLayoutKey = "blurLayout"
@NSManaged var blurRadius: CGFloat
@NSManaged private var blurLayout: CGFloat
private var fromBlurRadius: CGFloat?
var presentationRadius: CGFloat {
if let radius = fromBlurRadius {
if let layer = presentation() {
return layer.blurRadius
} else {
return radius
}
} else {
return blurRadius
}
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == blurRadiusKey || key == blurLayoutKey {
return true
}
return super.needsDisplay(forKey: key)
}
open override func action(forKey event: String) -> CAAction? {
if event == BlurLayer.blurRadiusKey {
fromBlurRadius = nil
if let action = super.action(forKey: "opacity") as? CABasicAnimation {
fromBlurRadius = (presentation() ?? self).blurRadius
action.keyPath = event
action.fromValue = fromBlurRadius
return action
}
}
if event == BlurLayer.blurLayoutKey, let action = super.action(forKey: "opacity") as? CABasicAnimation {
action.keyPath = event
action.fromValue = 0
action.toValue = 1
return action
}
return super.action(forKey: event)
}
}
extension BlurLayer {
func draw(_ image: UIImage, fixes isFixes: Bool, baseLayer: CALayer?) {
contents = image.cgImage
contentsScale = image.scale
if isFixes, let blurLayer = presentation() {
contentsRect = blurLayer.convert(blurLayer.bounds, to: baseLayer).rectangle(image.size)
}
}
func refresh() {
fromBlurRadius = nil
}
func animate() {
UIView.performWithoutAnimation {
blurLayout = 0
}
blurLayout = 1
}
func render(in context: CGContext, for layer: CALayer) {
let layers = hideOverlappingLayers(layer.sublayers)
layer.render(in: context)
layers.forEach {
$0.isHidden = false
}
}
private func hideOverlappingLayers(_ layers: [CALayer]?) -> [CALayer] {
var hiddenLayers: [CALayer] = []
guard let layers = layers else {
return hiddenLayers
}
for layer in layers.reversed() {
if isHang(to: layer) {
return hiddenLayers + hideOverlappingLayers(layer.sublayers)
}
if layer.isHidden == false {
layer.isHidden = true
hiddenLayers.append(layer)
}
if layer == self {
return hiddenLayers
}
}
return hiddenLayers
}
private func isHang(to target: CALayer) -> Bool {
var layer = superlayer
while layer != nil {
if layer == target {
return true
}
layer = layer?.superlayer
}
return false
}
}
@@ -0,0 +1,31 @@
//
// CGContext+CGImage.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/17.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
extension CGContext {
static func imageContext(with quality: CaptureQuality, rect: CGRect, opaque: Bool) -> CGContext? {
UIGraphicsBeginImageContextWithOptions(rect.size, opaque, quality.imageScale)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.translateBy(x: -rect.origin.x, y: -rect.origin.y)
context.interpolationQuality = quality.interpolationQuality
return context
}
func makeImage(with blendColor: UIColor?, blendMode: CGBlendMode, size: CGSize) -> CGImage? {
if let color = blendColor {
setFillColor(color.cgColor)
setBlendMode(blendMode)
fill(CGRect(origin: .zero, size: size))
}
return makeImage()
}
}
@@ -0,0 +1,65 @@
//
// CGImage+Accelerate.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/17.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
import Accelerate
extension CGImage {
var area: Int {
return width * height
}
private var size: CGSize {
return CGSize(width: width, height: height)
}
private var bytes: Int {
return bytesPerRow * height
}
private func imageBuffer(from data: UnsafeMutableRawPointer!) -> vImage_Buffer {
return vImage_Buffer(data: data, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
}
func blurred(with boxSize: UInt32, iterations: Int, blendColor: UIColor?, blendMode: CGBlendMode) -> CGImage? {
guard let providerData = dataProvider?.data else {
return nil
}
let inData = malloc(bytes)
var inBuffer = imageBuffer(from: inData)
let outData = malloc(bytes)
var outBuffer = imageBuffer(from: outData)
let tempSize = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, boxSize, boxSize, nil, vImage_Flags(kvImageEdgeExtend + kvImageGetTempBufferSize))
let tempData = malloc(tempSize)
defer {
free(inData)
free(outData)
free(tempData)
}
let source = CFDataGetBytePtr(providerData)
memcpy(inBuffer.data, source, bytes)
for _ in 0..<iterations {
vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, tempData, 0, 0, boxSize, boxSize, nil, vImage_Flags(kvImageEdgeExtend))
let temp = inBuffer.data
inBuffer.data = outBuffer.data
outBuffer.data = temp
}
let context = colorSpace.flatMap {
CGContext(data: inBuffer.data, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: $0, bitmapInfo: bitmapInfo.rawValue)
}
return context?.makeImage(with: blendColor, blendMode: blendMode, size: size)
}
}
@@ -0,0 +1,32 @@
//
// CaptureQuality.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/17.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
public enum CaptureQuality {
case `default`
case low
case medium
case high
var imageScale: CGFloat {
switch self {
case .default, .high:
return 0
case .low, .medium:
return 1
}
}
var interpolationQuality: CGInterpolationQuality {
switch self {
case .default, .low:
return .none
case .medium, .high:
return .default
}
}
}
@@ -0,0 +1,19 @@
//
// DynamicBlurView.h
// DynamicBlurView
//
// Created by Kyohei Ito on 2015/04/08.
// Copyright (c) 2015年 kyohei_ito. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for DynamicBlurView.
FOUNDATION_EXPORT double DynamicBlurViewVersionNumber;
//! Project version string for DynamicBlurView.
FOUNDATION_EXPORT const unsigned char DynamicBlurViewVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <DynamicBlurView/PublicHeader.h>
@@ -0,0 +1,195 @@
//
// DynamicBlurView.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2015/04/08.
// Copyright (c) 2015年 kyohei_ito. All rights reserved.
//
import UIKit
open class DynamicBlurView: UIView {
open override class var layerClass : AnyClass {
return BlurLayer.self
}
private var staticImage: UIImage?
private var displayLink: CADisplayLink?
private var blurLayer: BlurLayer {
return layer as! BlurLayer
}
private let mainQueue = DispatchQueue.main
private let globalQueue: DispatchQueue = {
if #available (iOS 8.0, *) {
return .global(qos: .userInteractive)
} else {
return .global(priority: .high)
}
}()
private var renderingTarget: UIView? {
if isDeepRendering {
return window
} else {
return superview
}
}
/// When true, it captures displays image and blur it asynchronously. Try to set true if needs more performance.
/// Asynchronous drawing is possibly crash when needs to process on main thread that drawing with animation for example.
open var drawsAsynchronously: Bool = false
/// Radius of blur.
open var blurRadius: CGFloat {
set { blurLayer.blurRadius = newValue }
get { return blurLayer.blurRadius }
}
/// Default is none.
open var trackingMode: TrackingMode = .none {
didSet {
if trackingMode != oldValue {
linkForDisplay()
}
}
}
/// Blend color.
open var blendColor: UIColor?
/// Blend mode.
open var blendMode: CGBlendMode = .plusLighter
/// Default is 3.
open var iterations: Int = 3
/// If the view want to render beyond the layer, should be true.
open var isDeepRendering: Bool = false
/// When none of tracking mode, it can change the radius of blur with the ratio. Should set from 0 to 1.
open var blurRatio: CGFloat = 1 {
didSet {
if let image = staticImage, oldValue != blurRatio {
draw(image, blurRadius: blurRadius, fixes: false, baseLayer: renderingTarget?.layer)
}
}
}
/// Quality of captured image.
open var quality: CaptureQuality = .medium
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = false
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = false
}
open override func didMoveToWindow() {
super.didMoveToWindow()
if let view = renderingTarget, window != nil && trackingMode == .none {
staticImage = snapshotImage(for: view.layer, conversion: !isDeepRendering)
}
}
open override func didMoveToSuperview() {
super.didMoveToSuperview()
if superview == nil {
displayLink?.invalidate()
displayLink = nil
} else {
linkForDisplay()
}
}
private func async(on queue: DispatchQueue, actions: @escaping () -> Void) {
if drawsAsynchronously {
queue.async(execute: actions)
} else {
actions()
}
}
private func sync(on queue: DispatchQueue, actions: () -> Void) {
if drawsAsynchronously {
queue.sync(execute: actions)
} else {
actions()
}
}
private func draw(_ image: UIImage, blurRadius radius: CGFloat, fixes isFixes: Bool, baseLayer: CALayer?) {
async(on: globalQueue) { [weak self] in
if let me = self, let blurredImage = image.blurred(radius: radius, iterations: me.iterations, ratio: me.blurRatio, blendColor: me.blendColor, blendMode: me.blendMode) {
me.sync(on: me.mainQueue) {
me.blurLayer.draw(blurredImage, fixes: isFixes, baseLayer: baseLayer)
}
}
}
}
private func blurLayerRect(to layer: CALayer, conversion: Bool) -> CGRect {
if conversion {
let presentationLayer = blurLayer.presentation() ?? blurLayer
return presentationLayer.convert(presentationLayer.bounds, to: layer)
} else {
return layer.bounds
}
}
private func snapshotImage(for layer: CALayer, conversion: Bool) -> UIImage? {
let rect = blurLayerRect(to: layer, conversion: conversion)
guard let context = CGContext.imageContext(with: quality, rect: rect, opaque: isOpaque) else {
return nil
}
blurLayer.render(in: context, for: layer)
defer {
UIGraphicsEndImageContext()
}
return UIGraphicsGetImageFromCurrentImageContext()
}
}
extension DynamicBlurView {
open override func display(_ layer: CALayer) {
let blurRadius = blurLayer.presentationRadius
let isFixes = isDeepRendering && staticImage != nil
if let view = renderingTarget, let image = staticImage ?? snapshotImage(for: view.layer, conversion: !isFixes) {
draw(image, blurRadius: blurRadius, fixes: isFixes, baseLayer: view.layer)
}
}
}
extension DynamicBlurView {
private func linkForDisplay() {
displayLink?.invalidate()
displayLink = UIScreen.main.displayLink(withTarget: self, selector: #selector(DynamicBlurView.displayDidRefresh(_:)))
displayLink?.add(to: .main, forMode: RunLoop.Mode(rawValue: trackingMode.description))
}
@objc private func displayDidRefresh(_ displayLink: CADisplayLink) {
display(layer)
}
}
extension DynamicBlurView {
/// Remove cache of blur image then get it again.
open func refresh() {
blurLayer.refresh()
staticImage = nil
blurRatio = 1
display(layer)
}
/// Remove cache of blur image.
open func remove() {
blurLayer.refresh()
staticImage = nil
blurRatio = 1
layer.contents = nil
}
/// Should use when needs to change layout with animation when is set none of tracking mode.
public func animate() {
blurLayer.animate()
}
}
@@ -0,0 +1,25 @@
//
// TrackingMode.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/17.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
public enum TrackingMode: CustomStringConvertible {
case tracking
case common
case none
public var description: String {
switch self {
case .tracking:
return RunLoop.Mode.tracking.rawValue
case .common:
return RunLoop.Mode.common.rawValue
case .none:
return ""
}
}
}
@@ -0,0 +1,28 @@
//
// UIImage+Blur.swift
// DynamicBlurView
//
// Created by Kyohei Ito on 2017/08/11.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
public extension UIImage {
func blurred(radius: CGFloat, iterations: Int, ratio: CGFloat, blendColor color: UIColor?, blendMode mode: CGBlendMode) -> UIImage? {
guard let cgImage = cgImage else {
return nil
}
if cgImage.area <= 0 || radius <= 0 {
return self
}
var boxSize = UInt32(radius * scale * ratio)
if boxSize % 2 == 0 {
boxSize += 1
}
return cgImage.blurred(with: boxSize, iterations: iterations, blendColor: color, blendMode: mode).map {
UIImage(cgImage: $0, scale: scale, orientation: imageOrientation)
}
}
}
@@ -0,0 +1,86 @@
//
// PopupDialogInteractiveTransition.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
// Handles interactive transition triggered via pan gesture recognizer on dialog
final internal class InteractiveTransition: UIPercentDrivenInteractiveTransition {
// If the interactive transition was started
var hasStarted = false
// If the interactive transition
var shouldFinish = false
// The view controller containing the views
// with attached gesture recognizers
weak var viewController: UIViewController?
@objc func handlePan(_ sender: UIPanGestureRecognizer) {
guard let vc = viewController else { return }
guard let progress = calculateProgress(sender: sender) else { return }
switch sender.state {
case .began:
hasStarted = true
vc.dismiss(animated: true, completion: nil)
case .changed:
shouldFinish = progress > 0.3
update(progress)
case .cancelled:
hasStarted = false
cancel()
case .ended:
hasStarted = false
completionSpeed = 0.55
shouldFinish ? finish() : cancel()
default:
break
}
}
}
internal extension InteractiveTransition {
/*!
Translates the pan gesture recognizer position to the progress percentage
- parameter sender: A UIPanGestureRecognizer
- returns: Progress
*/
func calculateProgress(sender: UIPanGestureRecognizer) -> CGFloat? {
guard let vc = viewController else { return nil }
// http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/
let translation = sender.translation(in: vc.view)
let verticalMovement = translation.y / vc.view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
return progress
}
}
@@ -0,0 +1,132 @@
//
// PopupDialog+Keyboard.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// This extension is designed to handle dialog positioning
/// if a keyboard is displayed while the popup is on top
internal extension PopupDialog {
// MARK: - Keyboard & orientation observers
/*! Add obserservers for UIKeyboard notifications */
func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged),
name: UIDevice.orientationDidChangeNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillChangeFrame),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}
/*! Remove observers */
func removeObservers() {
NotificationCenter.default.removeObserver(self,
name: UIDevice.orientationDidChangeNotification,
object: nil)
NotificationCenter.default.removeObserver(self,
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.removeObserver(self,
name: UIResponder.keyboardWillHideNotification,
object: nil)
NotificationCenter.default.removeObserver(self,
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}
// MARK: - Actions
/*!
Keyboard will show notification listener
- parameter notification: NSNotification
*/
@objc fileprivate func keyboardWillShow(_ notification: Notification) {
guard isTopAndVisible else { return }
keyboardShown = true
centerPopup()
}
/*!
Keyboard will hide notification listener
- parameter notification: NSNotification
*/
@objc fileprivate func keyboardWillHide(_ notification: Notification) {
guard isTopAndVisible else { return }
keyboardShown = false
centerPopup()
}
/*!
Keyboard will change frame notification listener
- parameter notification: NSNotification
*/
@objc fileprivate func keyboardWillChangeFrame(_ notification: Notification) {
guard let keyboardRect = (notification as NSNotification).userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return
}
keyboardHeight = keyboardRect.cgRectValue.height
}
/*!
Listen to orientation changes
- parameter notification: NSNotification
*/
@objc fileprivate func orientationChanged(_ notification: Notification) {
if keyboardShown { centerPopup() }
}
fileprivate func centerPopup() {
// Make sure keyboard should reposition on keayboard notifications
guard keyboardShiftsView else { return }
// Make sure a valid keyboard height is available
guard let keyboardHeight = keyboardHeight else { return }
// Calculate new center of shadow background
let popupCenter = keyboardShown ? keyboardHeight / -2 : 0
// Reposition and animate
popupContainerView.centerYConstraint?.constant = popupCenter
popupContainerView.pv_layoutIfNeededAnimated()
}
}
@@ -0,0 +1,342 @@
//
// PopupDialog.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// Creates a Popup dialog similar to UIAlertController
final public class PopupDialog: UIViewController {
// MARK: Private / Internal
/// First init flag
fileprivate var initialized = false
/// StatusBar display related
fileprivate let hideStatusBar: Bool
fileprivate var statusBarShouldBeHidden: Bool = false
/// Width for iPad displays
fileprivate let preferredWidth: CGFloat
/// The completion handler
fileprivate var completion: (() -> Void)?
/// The custom transition presentation manager
fileprivate var presentationManager: PresentationManager!
/// Interactor class for pan gesture dismissal
fileprivate lazy var interactor = InteractiveTransition()
/// Returns the controllers view
internal var popupContainerView: PopupDialogContainerView {
return view as! PopupDialogContainerView // swiftlint:disable:this force_cast
}
/// The set of buttons
fileprivate var buttons = [PopupDialogButton]()
/// Whether keyboard has shifted view
internal var keyboardShown = false
/// Keyboard height
internal var keyboardHeight: CGFloat?
// MARK: Public
/// The content view of the popup dialog
public var viewController: UIViewController
/// Whether or not to shift view for keyboard display
public var keyboardShiftsView = true
// MARK: - Initializers
/*!
Creates a standard popup dialog with title, message and image field
- parameter title: The dialog title
- parameter message: The dialog message
- parameter image: The dialog image
- parameter buttonAlignment: The dialog button alignment
- parameter transitionStyle: The dialog transition style
- parameter preferredWidth: The preferred width for iPad screens
- parameter tapGestureDismissal: Indicates if dialog can be dismissed via tap gesture
- parameter panGestureDismissal: Indicates if dialog can be dismissed via pan gesture
- parameter hideStatusBar: Whether to hide the status bar on PopupDialog presentation
- parameter completion: Completion block invoked when dialog was dismissed
- returns: Popup dialog default style
*/
@objc public convenience init(
title: String?,
message: String?,
image: UIImage? = nil,
buttonAlignment: NSLayoutConstraint.Axis = .vertical,
transitionStyle: PopupDialogTransitionStyle = .bounceUp,
preferredWidth: CGFloat = 340,
tapGestureDismissal: Bool = true,
panGestureDismissal: Bool = true,
hideStatusBar: Bool = false,
completion: (() -> Void)? = nil) {
// Create and configure the standard popup dialog view
let viewController = PopupDialogDefaultViewController()
viewController.titleText = title
viewController.messageText = message
viewController.image = image
// Call designated initializer
self.init(viewController: viewController,
buttonAlignment: buttonAlignment,
transitionStyle: transitionStyle,
preferredWidth: preferredWidth,
tapGestureDismissal: tapGestureDismissal,
panGestureDismissal: panGestureDismissal,
hideStatusBar: hideStatusBar,
completion: completion)
}
/*!
Creates a popup dialog containing a custom view
- parameter viewController: A custom view controller to be displayed
- parameter buttonAlignment: The dialog button alignment
- parameter transitionStyle: The dialog transition style
- parameter preferredWidth: The preferred width for iPad screens
- parameter tapGestureDismissal: Indicates if dialog can be dismissed via tap gesture
- parameter panGestureDismissal: Indicates if dialog can be dismissed via pan gesture
- parameter hideStatusBar: Whether to hide the status bar on PopupDialog presentation
- parameter completion: Completion block invoked when dialog was dismissed
- returns: Popup dialog with a custom view controller
*/
@objc public init(
viewController: UIViewController,
buttonAlignment: NSLayoutConstraint.Axis = .vertical,
transitionStyle: PopupDialogTransitionStyle = .bounceUp,
preferredWidth: CGFloat = 340,
tapGestureDismissal: Bool = true,
panGestureDismissal: Bool = true,
hideStatusBar: Bool = false,
completion: (() -> Void)? = nil) {
self.viewController = viewController
self.preferredWidth = preferredWidth
self.hideStatusBar = hideStatusBar
self.completion = completion
super.init(nibName: nil, bundle: nil)
// Init the presentation manager
presentationManager = PresentationManager(transitionStyle: transitionStyle, interactor: interactor)
// Assign the interactor view controller
interactor.viewController = self
// Define presentation styles
transitioningDelegate = presentationManager
modalPresentationStyle = .custom
// StatusBar setup
modalPresentationCapturesStatusBarAppearance = true
// Add our custom view to the container
addChild(viewController)
popupContainerView.stackView.insertArrangedSubview(viewController.view, at: 0)
popupContainerView.buttonStackView.axis = buttonAlignment
viewController.didMove(toParent: self)
// Allow for dialog dismissal on background tap
if tapGestureDismissal {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapRecognizer.cancelsTouchesInView = false
popupContainerView.addGestureRecognizer(tapRecognizer)
}
// Allow for dialog dismissal on dialog pan gesture
if panGestureDismissal {
let panRecognizer = UIPanGestureRecognizer(target: interactor, action: #selector(InteractiveTransition.handlePan))
panRecognizer.cancelsTouchesInView = false
popupContainerView.stackView.addGestureRecognizer(panRecognizer)
}
}
// Init with coder not implemented
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View life cycle
/// Replaces controller view with popup view
public override func loadView() {
view = PopupDialogContainerView(frame: UIScreen.main.bounds, preferredWidth: preferredWidth)
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addObservers()
guard !initialized else { return }
appendButtons()
initialized = true
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
statusBarShouldBeHidden = hideStatusBar
UIView.animate(withDuration: 0.15) {
self.setNeedsStatusBarAppearanceUpdate()
}
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
removeObservers()
}
deinit {
completion?()
completion = nil
}
// MARK: - Dismissal related
@objc fileprivate func handleTap(_ sender: UITapGestureRecognizer) {
// Make sure it's not a tap on the dialog but the background
let point = sender.location(in: popupContainerView.stackView)
guard !popupContainerView.stackView.point(inside: point, with: nil) else { return }
dismiss()
}
/*!
Dismisses the popup dialog
*/
@objc public func dismiss(_ completion: (() -> Void)? = nil) {
self.dismiss(animated: true) {
completion?()
}
}
// MARK: - Button related
/*!
Appends the buttons added to the popup dialog
to the placeholder stack view
*/
fileprivate func appendButtons() {
// Add action to buttons
let stackView = popupContainerView.stackView
let buttonStackView = popupContainerView.buttonStackView
if buttons.isEmpty {
stackView.removeArrangedSubview(popupContainerView.buttonStackView)
}
for (index, button) in buttons.enumerated() {
button.needsLeftSeparator = buttonStackView.axis == .horizontal && index > 0
buttonStackView.addArrangedSubview(button)
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
}
/*!
Adds a single PopupDialogButton to the Popup dialog
- parameter button: A PopupDialogButton instance
*/
@objc public func addButton(_ button: PopupDialogButton) {
buttons.append(button)
}
/*!
Adds an array of PopupDialogButtons to the Popup dialog
- parameter buttons: A list of PopupDialogButton instances
*/
@objc public func addButtons(_ buttons: [PopupDialogButton]) {
self.buttons += buttons
}
/// Calls the action closure of the button instance tapped
@objc fileprivate func buttonTapped(_ button: PopupDialogButton) {
if button.dismissOnTap {
dismiss({ button.buttonAction?() })
} else {
button.buttonAction?()
}
}
/*!
Simulates a button tap for the given index
Makes testing a breeze
- parameter index: The index of the button to tap
*/
public func tapButtonWithIndex(_ index: Int) {
let button = buttons[index]
button.buttonAction?()
}
// MARK: - StatusBar display related
public override var prefersStatusBarHidden: Bool {
return statusBarShouldBeHidden
}
public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
}
// MARK: - View proxy values
extension PopupDialog {
/// The button alignment of the alert dialog
@objc public var buttonAlignment: NSLayoutConstraint.Axis {
get {
return popupContainerView.buttonStackView.axis
}
set {
popupContainerView.buttonStackView .axis = newValue
popupContainerView.pv_layoutIfNeededAnimated()
}
}
/// The transition style
@objc public var transitionStyle: PopupDialogTransitionStyle {
get { return presentationManager.transitionStyle }
set { presentationManager.transitionStyle = newValue }
}
}
// MARK: - Shake
extension PopupDialog {
/// Performs a shake animation on the dialog
@objc public func shake() {
popupContainerView.pv_shake()
}
}
@@ -0,0 +1,166 @@
//
// PopupDialogButton.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// Represents the default button for the popup dialog
open class PopupDialogButton: UIButton {
public typealias PopupDialogButtonAction = () -> Void
// MARK: Public
/// The font and size of the button title
@objc open dynamic var titleFont: UIFont? {
get { return titleLabel?.font }
set { titleLabel?.font = newValue }
}
/// The height of the button
@objc open dynamic var buttonHeight: Int
/// The title color of the button
@objc open dynamic var titleColor: UIColor? {
get { return self.titleColor(for: UIControl.State()) }
set { setTitleColor(newValue, for: UIControl.State()) }
}
/// The background color of the button
@objc open dynamic var buttonColor: UIColor? {
get { return backgroundColor }
set { backgroundColor = newValue }
}
/// The separator color of this button
@objc open dynamic var separatorColor: UIColor? {
get { return separator.backgroundColor }
set {
separator.backgroundColor = newValue
leftSeparator.backgroundColor = newValue
}
}
/// Default appearance of the button
open var defaultTitleFont = UIFont.systemFont(ofSize: 14)
open var defaultTitleColor = UIColor(red: 0.25, green: 0.53, blue: 0.91, alpha: 1)
open var defaultButtonColor = UIColor.clear
open var defaultSeparatorColor = UIColor(white: 0.9, alpha: 1)
/// Whether button should dismiss popup when tapped
@objc open var dismissOnTap = true
/// The action called when the button is tapped
open fileprivate(set) var buttonAction: PopupDialogButtonAction?
// MARK: Private
fileprivate lazy var separator: UIView = {
let line = UIView(frame: .zero)
line.translatesAutoresizingMaskIntoConstraints = false
return line
}()
fileprivate lazy var leftSeparator: UIView = {
let line = UIView(frame: .zero)
line.translatesAutoresizingMaskIntoConstraints = false
line.alpha = 0
return line
}()
// MARK: Internal
internal var needsLeftSeparator: Bool = false {
didSet {
leftSeparator.alpha = needsLeftSeparator ? 1.0 : 0.0
}
}
// MARK: Initializers
/*!
Creates a button that can be added to the popup dialog
- parameter title: The button title
- parameter dismisssOnTap: Whether a tap automatically dismisses the dialog
- parameter action: The action closure
- returns: PopupDialogButton
*/
@objc public init(title: String, height: Int = 45, dismissOnTap: Bool = true, action: PopupDialogButtonAction?) {
// Assign the button height
buttonHeight = height
// Assign the button action
buttonAction = action
super.init(frame: .zero)
// Set the button title
setTitle(title, for: UIControl.State())
self.dismissOnTap = dismissOnTap
// Setup the views
setupView()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: View setup
open func setupView() {
// Default appearance
setTitleColor(defaultTitleColor, for: UIControl.State())
titleLabel?.font = defaultTitleFont
backgroundColor = defaultButtonColor
separator.backgroundColor = defaultSeparatorColor
leftSeparator.backgroundColor = defaultSeparatorColor
// Add and layout views
addSubview(separator)
addSubview(leftSeparator)
let views = ["separator": separator, "leftSeparator": leftSeparator, "button": self]
let metrics = ["buttonHeight": buttonHeight]
var constraints = [NSLayoutConstraint]()
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:[button(buttonHeight)]", options: [], metrics: metrics, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[separator]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[separator(1)]", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[leftSeparator(1)]", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[leftSeparator]|", options: [], metrics: nil, views: views)
NSLayoutConstraint.activate(constraints)
}
open override var isHighlighted: Bool {
didSet {
isHighlighted ? pv_fade(.out, 0.5) : pv_fade(.in, 1.0)
}
}
}
@@ -0,0 +1,197 @@
//
// PopupDialogContainerView.swift
// Pods
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// The main view of the popup dialog
final public class PopupDialogContainerView: UIView {
// MARK: - Appearance
/// The background color of the popup dialog
override public dynamic var backgroundColor: UIColor? {
get { return container.backgroundColor }
set { container.backgroundColor = newValue }
}
/// The corner radius of the popup view
@objc public dynamic var cornerRadius: Float {
get { return Float(shadowContainer.layer.cornerRadius) }
set {
let radius = CGFloat(newValue)
shadowContainer.layer.cornerRadius = radius
container.layer.cornerRadius = radius
}
}
// MARK: Shadow related
/// Enable / disable shadow rendering of the container
@objc public dynamic var shadowEnabled: Bool {
get { return shadowContainer.layer.shadowRadius > 0 }
set { shadowContainer.layer.shadowRadius = newValue ? shadowRadius : 0 }
}
/// Color of the container shadow
@objc public dynamic var shadowColor: UIColor? {
get {
guard let color = shadowContainer.layer.shadowColor else {
return nil
}
return UIColor(cgColor: color)
}
set { shadowContainer.layer.shadowColor = newValue?.cgColor }
}
/// Radius of the container shadow
@objc public dynamic var shadowRadius: CGFloat {
get { return shadowContainer.layer.shadowRadius }
set { shadowContainer.layer.shadowRadius = newValue }
}
/// Opacity of the the container shadow
@objc public dynamic var shadowOpacity: Float {
get { return shadowContainer.layer.shadowOpacity }
set { shadowContainer.layer.shadowOpacity = newValue }
}
/// Offset of the the container shadow
@objc public dynamic var shadowOffset: CGSize {
get { return shadowContainer.layer.shadowOffset }
set { shadowContainer.layer.shadowOffset = newValue }
}
/// Path of the the container shadow
@objc public dynamic var shadowPath: CGPath? {
get { return shadowContainer.layer.shadowPath}
set { shadowContainer.layer.shadowPath = newValue }
}
// MARK: - Views
/// The shadow container is the basic view of the PopupDialog
/// As it does not clip subviews, a shadow can be applied to it
internal lazy var shadowContainer: UIView = {
let shadowContainer = UIView(frame: .zero)
shadowContainer.translatesAutoresizingMaskIntoConstraints = false
shadowContainer.backgroundColor = UIColor.clear
shadowContainer.layer.shadowColor = UIColor.black.cgColor
shadowContainer.layer.shadowRadius = 5
shadowContainer.layer.shadowOpacity = 0.4
shadowContainer.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowContainer.layer.cornerRadius = 4
return shadowContainer
}()
/// The container view is a child of shadowContainer and contains
/// all other views. It clips to bounds so cornerRadius can be set
internal lazy var container: UIView = {
let container = UIView(frame: .zero)
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = UIColor.white
container.clipsToBounds = true
container.layer.cornerRadius = 4
return container
}()
// The container stack view for buttons
internal lazy var buttonStackView: UIStackView = {
let buttonStackView = UIStackView()
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
buttonStackView.distribution = .fillEqually
buttonStackView.spacing = 0
return buttonStackView
}()
// The main stack view, containing all relevant views
internal lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.buttonStackView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 0
return stackView
}()
// The preferred width for iPads
fileprivate let preferredWidth: CGFloat
// MARK: - Constraints
/// The center constraint of the shadow container
internal var centerYConstraint: NSLayoutConstraint?
// MARK: - Initializers
internal init(frame: CGRect, preferredWidth: CGFloat) {
self.preferredWidth = preferredWidth
super.init(frame: frame)
setupViews()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View setup
internal func setupViews() {
// Add views
addSubview(shadowContainer)
shadowContainer.addSubview(container)
container.addSubview(stackView)
// Layout views
let views = ["shadowContainer": shadowContainer, "container": container, "stackView": stackView]
var constraints = [NSLayoutConstraint]()
// Shadow container constraints
if UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad {
let metrics = ["preferredWidth": preferredWidth]
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=40)-[shadowContainer(==preferredWidth@900)]-(>=40)-|", options: [], metrics: metrics, views: views)
} else {
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-(>=10,==20@900)-[shadowContainer(<=340,>=300)]-(>=10,==20@900)-|", options: [], metrics: nil, views: views)
}
constraints += [NSLayoutConstraint(item: shadowContainer, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)]
centerYConstraint = NSLayoutConstraint(item: shadowContainer, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0)
if let centerYConstraint = centerYConstraint {
constraints.append(centerYConstraint)
}
// Container constraints
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[container]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[container]|", options: [], metrics: nil, views: views)
// Main stack view constraints
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[stackView]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: [], metrics: nil, views: views)
// Activate constraints
NSLayoutConstraint.activate(constraints)
}
}
@@ -0,0 +1,54 @@
//
// PopupDialogDefaultButtons.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
// MARK: Default button
/// Represents the default button for the popup dialog
public final class DefaultButton: PopupDialogButton {}
// MARK: Cancel button
/// Represents a cancel button for the popup dialog
public final class CancelButton: PopupDialogButton {
override public func setupView() {
defaultTitleColor = UIColor.lightGray
super.setupView()
}
}
// MARK: destructive button
/// Represents a destructive button for the popup dialog
public final class DestructiveButton: PopupDialogButton {
override public func setupView() {
defaultTitleColor = UIColor.red
super.setupView()
}
}
@@ -0,0 +1,148 @@
//
// PopupDialogView.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// The main view of the popup dialog
final public class PopupDialogDefaultView: UIView {
// MARK: - Appearance
/// The font and size of the title label
@objc public dynamic var titleFont: UIFont {
get { return titleLabel.font }
set { titleLabel.font = newValue }
}
/// The color of the title label
@objc public dynamic var titleColor: UIColor? {
get { return titleLabel.textColor }
set { titleLabel.textColor = newValue }
}
/// The text alignment of the title label
@objc public dynamic var titleTextAlignment: NSTextAlignment {
get { return titleLabel.textAlignment }
set { titleLabel.textAlignment = newValue }
}
/// The font and size of the body label
@objc public dynamic var messageFont: UIFont {
get { return messageLabel.font }
set { messageLabel.font = newValue }
}
/// The color of the message label
@objc public dynamic var messageColor: UIColor? {
get { return messageLabel.textColor }
set { messageLabel.textColor = newValue}
}
/// The text alignment of the message label
@objc public dynamic var messageTextAlignment: NSTextAlignment {
get { return messageLabel.textAlignment }
set { messageLabel.textAlignment = newValue }
}
// MARK: - Views
/// The view that will contain the image, if set
internal lazy var imageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
/// The title label of the dialog
internal lazy var titleLabel: UILabel = {
let titleLabel = UILabel(frame: .zero)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.numberOfLines = 0
titleLabel.textAlignment = .center
titleLabel.textColor = UIColor(white: 0.4, alpha: 1)
titleLabel.font = .boldSystemFont(ofSize: 14)
return titleLabel
}()
/// The message label of the dialog
internal lazy var messageLabel: UILabel = {
let messageLabel = UILabel(frame: .zero)
messageLabel.translatesAutoresizingMaskIntoConstraints = false
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.textColor = UIColor(white: 0.6, alpha: 1)
messageLabel.font = .systemFont(ofSize: 14)
return messageLabel
}()
/// The height constraint of the image view, 0 by default
internal var imageHeightConstraint: NSLayoutConstraint?
// MARK: - Initializers
internal override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View setup
internal func setupViews() {
// Self setup
translatesAutoresizingMaskIntoConstraints = false
// Add views
addSubview(imageView)
addSubview(titleLabel)
addSubview(messageLabel)
// Layout views
let views = ["imageView": imageView, "titleLabel": titleLabel, "messageLabel": messageLabel] as [String: Any]
var constraints = [NSLayoutConstraint]()
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[imageView]|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-(==20@900)-[titleLabel]-(==20@900)-|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-(==20@900)-[messageLabel]-(==20@900)-|", options: [], metrics: nil, views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[imageView]-(==30@900)-[titleLabel]-(==8@900)-[messageLabel]-(==30@900)-|", options: [], metrics: nil, views: views)
// ImageView height constraint
imageHeightConstraint = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: imageView, attribute: .height, multiplier: 0, constant: 0)
if let imageHeightConstraint = imageHeightConstraint {
constraints.append(imageHeightConstraint)
}
// Activate constraints
NSLayoutConstraint.activate(constraints)
}
}
@@ -0,0 +1,133 @@
//
// PopupDialogDefaultViewController.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import UIKit
final public class PopupDialogDefaultViewController: UIViewController {
public var standardView: PopupDialogDefaultView {
return view as! PopupDialogDefaultView // swiftlint:disable:this force_cast
}
override public func loadView() {
super.loadView()
view = PopupDialogDefaultView(frame: .zero)
}
}
public extension PopupDialogDefaultViewController {
// MARK: - Setter / Getter
// MARK: Content
/// The dialog image
var image: UIImage? {
get { return standardView.imageView.image }
set {
standardView.imageView.image = newValue
standardView.imageHeightConstraint?.constant = standardView.imageView.pv_heightForImageView()
}
}
/// The title text of the dialog
var titleText: String? {
get { return standardView.titleLabel.text }
set {
standardView.titleLabel.text = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The message text of the dialog
var messageText: String? {
get { return standardView.messageLabel.text }
set {
standardView.messageLabel.text = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
// MARK: Appearance
/// The font and size of the title label
@objc dynamic var titleFont: UIFont {
get { return standardView.titleFont }
set {
standardView.titleFont = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The color of the title label
@objc dynamic var titleColor: UIColor? {
get { return standardView.titleLabel.textColor }
set {
standardView.titleColor = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The text alignment of the title label
@objc dynamic var titleTextAlignment: NSTextAlignment {
get { return standardView.titleTextAlignment }
set {
standardView.titleTextAlignment = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The font and size of the body label
@objc dynamic var messageFont: UIFont {
get { return standardView.messageFont}
set {
standardView.messageFont = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The color of the message label
@objc dynamic var messageColor: UIColor? {
get { return standardView.messageColor }
set {
standardView.messageColor = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
/// The text alignment of the message label
@objc dynamic var messageTextAlignment: NSTextAlignment {
get { return standardView.messageTextAlignment }
set {
standardView.messageTextAlignment = newValue
standardView.pv_layoutIfNeededAnimated()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
standardView.imageHeightConstraint?.constant = standardView.imageView.pv_heightForImageView()
}
}
@@ -0,0 +1,127 @@
//
// PopupDialogOverlayView.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
/// The (blurred) overlay view below the popup dialog
final public class PopupDialogOverlayView: UIView {
// MARK: - Appearance
/// Turns the blur of the overlay view on or off
@objc public dynamic var blurEnabled: Bool {
get { return !blurView.isHidden }
set { blurView.isHidden = !newValue }
}
/// The blur radius of the overlay view
@objc public dynamic var blurRadius: CGFloat {
get { return blurView.blurRadius }
set { blurView.blurRadius = newValue }
}
/// Whether the blur view should allow for
/// live rendering of the background
@objc public dynamic var liveBlurEnabled: Bool {
get { return blurView.trackingMode == .common }
set {
if newValue {
blurView.trackingMode = .common
} else {
blurView.trackingMode = .none
}
}
}
/// The background color of the overlay view
@objc public dynamic var color: UIColor? {
get { return overlay.backgroundColor }
set { overlay.backgroundColor = newValue }
}
/// The opacity of the overlay view
@objc public dynamic var opacity: CGFloat {
get { return overlay.alpha }
set { overlay.alpha = newValue }
}
// MARK: - Views
internal lazy var blurView: DynamicBlurView = {
let blurView = DynamicBlurView(frame: .zero)
blurView.blurRadius = 8
blurView.trackingMode = .none
blurView.isDeepRendering = true
blurView.tintColor = .clear
blurView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
return blurView
}()
internal lazy var overlay: UIView = {
let overlay = UIView(frame: .zero)
overlay.backgroundColor = .black
overlay.alpha = 0.7
overlay.autoresizingMask = [.flexibleHeight, .flexibleWidth]
return overlay
}()
// MARK: - Inititalizers
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View setup
fileprivate func setupView() {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
backgroundColor = .clear
alpha = 0
addSubview(blurView)
addSubview(overlay)
}
}
// MARK: - Deprecated
extension PopupDialogOverlayView {
/// Whether the blur view should allow for
/// dynamic rendering of the background
@available(*, deprecated, message: "liveBlur has been deprecated and will be removed with future versions of PopupDialog. Please use isLiveBlurEnabled instead.")
@objc public dynamic var liveBlur: Bool {
get { return liveBlurEnabled }
set { liveBlurEnabled = newValue }
}
}
@@ -0,0 +1,61 @@
//
// PopupDialogPresentationController.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
final internal class PresentationController: UIPresentationController {
private lazy var overlay: PopupDialogOverlayView = {
return PopupDialogOverlayView(frame: .zero)
}()
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
overlay.frame = containerView.bounds
containerView.insertSubview(overlay, at: 0)
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
self?.overlay.alpha = 1.0
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
self?.overlay.alpha = 0.0
}, completion: nil)
}
override func containerViewWillLayoutSubviews() {
guard let presentedView = presentedView else { return }
presentedView.frame = frameOfPresentedViewInContainerView
overlay.blurView.refresh()
}
}
@@ -0,0 +1,86 @@
//
// PopupDialogPresentationManager.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
final internal class PresentationManager: NSObject, UIViewControllerTransitioningDelegate {
var transitionStyle: PopupDialogTransitionStyle
var interactor: InteractiveTransition
init(transitionStyle: PopupDialogTransitionStyle, interactor: InteractiveTransition) {
self.transitionStyle = transitionStyle
self.interactor = interactor
super.init()
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = PresentationController(presentedViewController: presented, presenting: source)
return presentationController
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
var transition: TransitionAnimator
switch transitionStyle {
case .bounceUp:
transition = BounceUpTransition(direction: .in)
case .bounceDown:
transition = BounceDownTransition(direction: .in)
case .zoomIn:
transition = ZoomTransition(direction: .in)
case .fadeIn:
transition = FadeTransition(direction: .in)
}
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if interactor.hasStarted || interactor.shouldFinish {
return DismissInteractiveTransition()
}
var transition: TransitionAnimator
switch transitionStyle {
case .bounceUp:
transition = BounceUpTransition(direction: .out)
case .bounceDown:
transition = BounceDownTransition(direction: .out)
case .zoomIn:
transition = ZoomTransition(direction: .out)
case .fadeIn:
transition = FadeTransition(direction: .out)
}
return transition
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
}
@@ -0,0 +1,186 @@
//
// PopupDialogTransitionAnimations.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/*!
Presentation transition styles for the popup dialog
- BounceUp: Dialog bounces in from bottom and is dismissed to bottom
- BounceDown: Dialog bounces in from top and is dismissed to top
- ZoomIn: Dialog zooms in and is dismissed by zooming out
- FadeIn: Dialog fades in and is dismissed by fading out
*/
@objc public enum PopupDialogTransitionStyle: Int {
case bounceUp
case bounceDown
case zoomIn
case fadeIn
}
/// Dialog bounces in from bottom and is dismissed to bottom
final internal class BounceUpTransition: TransitionAnimator {
init(direction: AnimationDirection) {
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction)
}
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
super.animateTransition(using: transitionContext)
switch direction {
case .in:
to.view.bounds.origin = CGPoint(x: 0, y: -from.view.bounds.size.height)
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in
guard let self = self else { return }
self.to.view.bounds = self.from.view.bounds
}, completion: { _ in
transitionContext.completeTransition(true)
})
case .out:
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in
guard let self = self else { return }
self.from.view.bounds.origin = CGPoint(x: 0, y: -self.from.view.bounds.size.height)
self.from.view.alpha = 0.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
/// Dialog bounces in from top and is dismissed to top
final internal class BounceDownTransition: TransitionAnimator {
init(direction: AnimationDirection) {
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction)
}
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
super.animateTransition(using: transitionContext)
switch direction {
case .in:
to.view.bounds.origin = CGPoint(x: 0, y: from.view.bounds.size.height)
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in
guard let self = self else { return }
self.to.view.bounds = self.from.view.bounds
}, completion: { _ in
transitionContext.completeTransition(true)
})
case .out:
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in
guard let self = self else { return }
self.from.view.bounds.origin = CGPoint(x: 0, y: self.from.view.bounds.size.height)
self.from.view.alpha = 0.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
/// Dialog zooms in and is dismissed by zooming out
final internal class ZoomTransition: TransitionAnimator {
init(direction: AnimationDirection) {
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction)
}
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
super.animateTransition(using: transitionContext)
switch direction {
case .in:
to.view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in
guard let self = self else { return }
self.to.view.transform = CGAffineTransform(scaleX: 1, y: 1)
}, completion: { _ in
transitionContext.completeTransition(true)
})
case .out:
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in
guard let self = self else { return }
self.from.view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
self.from.view.alpha = 0.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
/// Dialog fades in and is dismissed by fading out
final internal class FadeTransition: TransitionAnimator {
init(direction: AnimationDirection) {
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction)
}
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
super.animateTransition(using: transitionContext)
switch direction {
case .in:
to.view.alpha = 0
UIView.animate(withDuration: 0.6, delay: 0.0, options: [.curveEaseOut],
animations: { [weak self] in
guard let self = self else { return }
self.to.view.alpha = 1
}, completion: { _ in
transitionContext.completeTransition(true)
})
case .out:
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in
guard let self = self else { return }
self.from.view.alpha = 0.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
/// Used for the always drop out animation with pan gesture dismissal
final internal class DismissInteractiveTransition: TransitionAnimator {
init() {
super.init(inDuration: 0.22, outDuration: 0.32, direction: .out)
}
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
super.animateTransition(using: transitionContext)
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.beginFromCurrentState], animations: { [weak self] in
guard let self = self else { return }
self.from.view.bounds.origin = CGPoint(x: 0, y: -self.from.view.bounds.size.height)
self.from.view.alpha = 0.0
}, completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
@@ -0,0 +1,68 @@
//
// PopupDialogTransitionAnimator.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/// Base class for custom transition animations
internal class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var to: UIViewController!
var from: UIViewController!
let inDuration: TimeInterval
let outDuration: TimeInterval
let direction: AnimationDirection
init(inDuration: TimeInterval, outDuration: TimeInterval, direction: AnimationDirection) {
self.inDuration = inDuration
self.outDuration = outDuration
self.direction = direction
super.init()
}
internal func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return direction == .in ? inDuration : outDuration
}
internal func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch direction {
case .in:
guard let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else { return }
self.to = to
self.from = from
let container = transitionContext.containerView
container.addSubview(to.view)
case .out:
guard let to = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let from = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else { return }
self.to = to
self.from = from
}
}
}
@@ -0,0 +1,44 @@
//
// UIImageView+Calculations.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
internal extension UIImageView {
/*!
Calculates the height of the the UIImageView has to
have so the image is displayed correctly
- returns: Height to set on the imageView
*/
func pv_heightForImageView() -> CGFloat {
guard let image = image, image.size.height > 0 else {
return 0.0
}
let width = bounds.size.width
let ratio = image.size.height / image.size.width
return width * ratio
}
}
@@ -0,0 +1,82 @@
//
// UIView+Animations.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
/*!
The intended direction of the animation
- in: Animate in
- out: Animate out
*/
internal enum AnimationDirection {
case `in` // swiftlint:disable:this identifier_name
case out
}
internal extension UIView {
/// The key for the fade animation
var fadeKey: String { return "FadeAnimation" }
var shakeKey: String { return "ShakeAnimation" }
func pv_fade(_ direction: AnimationDirection, _ value: Float, duration: CFTimeInterval = 0.08) {
layer.removeAnimation(forKey: fadeKey)
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = duration
animation.fromValue = layer.presentation()?.opacity
layer.opacity = value
animation.fillMode = CAMediaTimingFillMode.forwards
layer.add(animation, forKey: fadeKey)
}
func pv_layoutIfNeededAnimated(duration: CFTimeInterval = 0.08) {
UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(), animations: {
self.layoutIfNeeded()
}, completion: nil)
}
// As found at https://gist.github.com/mourad-brahim/cf0bfe9bec5f33a6ea66#file-uiview-animations-swift-L9
// Slightly modified
func pv_shake() {
layer.removeAnimation(forKey: shakeKey)
let vals: [Double] = [-2, 2, -2, 2, 0]
let translation = CAKeyframeAnimation(keyPath: "transform.translation.x")
translation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
translation.values = vals
let rotation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
rotation.values = vals.map { (degrees: Double) in
let radians: Double = (Double.pi * degrees) / 180.0
return radians
}
let shakeGroup: CAAnimationGroup = CAAnimationGroup()
shakeGroup.animations = [translation, rotation]
shakeGroup.duration = 0.3
self.layer.add(shakeGroup, forKey: shakeKey)
}
}
@@ -0,0 +1,52 @@
//
// UIViewController+Visibility.swift
//
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
// Author - Martin Wildfeuer (http://www.mwfire.de)
//
// 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.
//
import Foundation
import UIKit
// http://stackoverflow.com/questions/2777438/how-to-tell-if-uiviewcontrollers-view-is-visible
internal extension UIViewController {
var isTopAndVisible: Bool {
return isVisible && isTopViewController
}
var isVisible: Bool {
if isViewLoaded {
return view.window != nil
}
return false
}
var isTopViewController: Bool {
if self.navigationController != nil {
return self.navigationController?.visibleViewController === self
} else if self.tabBarController != nil {
return self.tabBarController?.selectedViewController == self && self.presentedViewController == nil
} else {
return self.presentedViewController == nil && self.isVisible
}
}
}
@@ -0,0 +1,41 @@
//
// ScrollRects.swift
// TableViewDragger
//
// Created by Kyohei Ito on 2017/12/08.
// Copyright © 2017年 kyohei_ito. All rights reserved.
//
import Foundation
import CoreImage
struct ScrollRects {
private let maxDistance: CGFloat = 10
private let size: CGSize
private let scrollRange: CGFloat
let topRect: CGRect
let bottomRect: CGRect
init(size: CGSize) {
self.size = size
scrollRange = size.height / 2.5
let scrollSize = CGSize(width: size.width, height: scrollRange)
topRect = CGRect(origin: .zero, size: scrollSize)
bottomRect = CGRect(origin: CGPoint(x: 0, y: size.height - scrollRange), size: scrollSize)
}
func distance(at point: CGPoint) -> CGFloat {
let ratio: CGFloat
if topRect.contains(point) {
ratio = -(scrollRange - point.y)
} else if bottomRect.contains(point) {
ratio = point.y - (size.height - scrollRange)
} else {
ratio = 0
}
return max(min(ratio / 30, maxDistance), -maxDistance)
}
}
@@ -0,0 +1,371 @@
//
// TableViewDragger.swift
// TableViewDragger
//
// Created by Kyohei Ito on 2015/09/24.
// Copyright © 2015年 kyohei_ito. All rights reserved.
//
import UIKit
@objc public protocol TableViewDraggerDelegate: class {
/// If allow movement of cell, please return `true`. require a call to `moveRowAt:toIndexPath:` of UITableView and rearranged of data.
func dragger(_ dragger: TableViewDragger, moveDraggingAt indexPath: IndexPath, newIndexPath: IndexPath) -> Bool
/// If allow dragging of cell, prease return `true`.
@objc optional func dragger(_ dragger: TableViewDragger, shouldDragAt indexPath: IndexPath) -> Bool
@objc optional func dragger(_ dragger: TableViewDragger, willBeginDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, didBeginDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, willEndDraggingAt indexPath: IndexPath)
@objc optional func dragger(_ dragger: TableViewDragger, didEndDraggingAt indexPath: IndexPath)
}
@objc public protocol TableViewDraggerDataSource: class {
/// Return any cell if want to change the cell in drag.
@objc optional func dragger(_ dragger: TableViewDragger, cellForRowAt indexPath: IndexPath) -> UIView?
/// Return the indexPath if want to change the indexPath to start drag.
@objc optional func dragger(_ dragger: TableViewDragger, indexPathForDragAt indexPath: IndexPath) -> IndexPath
}
open class TableViewDragger: NSObject {
let longPressGesture = UILongPressGestureRecognizer()
let panGesture = UIPanGestureRecognizer()
var draggingCell: TableViewDraggerCell?
var displayLink: CADisplayLink?
var targetClipsToBounds = true
weak var targetTableView: UITableView?
private var draggingDirection: UIScrollView.DraggingDirection?
/// It will be `true` if want to hide the original cell.
open var isHiddenOriginCell: Bool = true
/// Zoom scale of cell in drag.
open var zoomScaleForCell: CGFloat = 1
/// Alpha of cell in drag.
open var alphaForCell: CGFloat = 1
/// Opacity of cell shadow in drag.
open var opacityForShadowOfCell: Float = 0.4
/// Velocity of auto scroll in drag.
open var scrollVelocity: CGFloat = 1
open weak var delegate: TableViewDraggerDelegate?
open weak var dataSource: TableViewDraggerDataSource?
//
open var availableHorizontalScroll : Bool = true
open var tableView: UITableView? {
return targetTableView
}
/// `UITableView` want to drag.
public init(tableView: UITableView, _ minimumPressDuration: CFTimeInterval = 0.5) {
super.init()
self.targetTableView = tableView
tableView.addGestureRecognizer(longPressGesture)
tableView.addGestureRecognizer(panGesture)
longPressGesture.addTarget(self, action: #selector(TableViewDragger.longPressGestureAction(_:)))
longPressGesture.delegate = self
longPressGesture.allowableMovement = 5.0
longPressGesture.minimumPressDuration = minimumPressDuration
panGesture.addTarget(self, action: #selector(TableViewDragger.panGestureAction(_:)))
panGesture.delegate = self
panGesture.maximumNumberOfTouches = 1
}
deinit {
targetTableView?.removeGestureRecognizer(longPressGesture)
targetTableView?.removeGestureRecognizer(panGesture)
}
func targetIndexPath(_ tableView: UITableView, draggingCell: TableViewDraggerCell) -> IndexPath {
let location = draggingCell.location
let offsetY = (draggingCell.viewHeight / 2) + 2
let offsetX = tableView.center.x
let topPoint = CGPoint(x: offsetX, y: location.y - offsetY)
let bottomPoint = CGPoint(x: offsetX, y: location.y + offsetY)
let point = draggingDirection == .up ? topPoint : bottomPoint
if let targetIndexPath = tableView.indexPathForRow(at: point) {
if tableView.cellForRow(at: targetIndexPath) == nil {
return draggingCell.dropIndexPath
}
let targetRect = tableView.rectForRow(at: targetIndexPath)
let targetCenterY = targetRect.origin.y + (targetRect.height / 2)
guard let direction = draggingDirection else {
return draggingCell.dropIndexPath
}
switch direction {
case .up:
if (targetCenterY > point.y && draggingCell.dropIndexPath > targetIndexPath) {
return targetIndexPath
}
case .down:
if (targetCenterY < point.y && draggingCell.dropIndexPath < targetIndexPath) {
return targetIndexPath
}
}
} else {
let section = (0..<tableView.numberOfSections).filter { section -> Bool in
tableView.rect(forSection: section).contains(point)
}.first
if let section = section, tableView.numberOfRows(inSection: section) == 0 {
return IndexPath(row: 0, section: section)
}
}
return draggingCell.dropIndexPath
}
func dragCell(_ tableView: UITableView, draggingCell: TableViewDraggerCell) {
let indexPath = targetIndexPath(tableView, draggingCell: draggingCell)
if draggingCell.dropIndexPath.compare(indexPath) == .orderedSame {
return
}
if let cell = tableView.cellForRow(at: draggingCell.dropIndexPath) {
cell.isHidden = isHiddenOriginCell
}
if delegate?.dragger(self, moveDraggingAt: draggingCell.dropIndexPath, newIndexPath: indexPath) == true {
draggingCell.dropIndexPath = indexPath
}
}
func copiedCell(at indexPath: IndexPath) -> UIView? {
if let view = dataSource?.dragger?(self, cellForRowAt: indexPath) {
return view
}
if let cell = targetTableView?.cellForRow(at: indexPath) {
if let view = cell.snapshotView(afterScreenUpdates: false) {
return view
} else if let view = cell.captured() {
view.frame = cell.bounds
return view
}
}
return nil
}
func draggedCell(_ tableView: UITableView, indexPath: IndexPath) -> TableViewDraggerCell? {
guard let copiedCell = copiedCell(at: indexPath) else {
return nil
}
let cellRect = tableView.rectForRow(at: indexPath)
copiedCell.frame.size = cellRect.size
if let height = tableView.delegate?.tableView?(tableView, heightForRowAt: indexPath) {
copiedCell.frame.size.height = height
}
let cell = TableViewDraggerCell(cell: copiedCell)
cell.dragScale = zoomScaleForCell
cell.dragAlpha = alphaForCell
cell.dragShadowOpacity = opacityForShadowOfCell
cell.dropIndexPath = indexPath
return cell
}
}
// MARK: - Dragging Cell
extension TableViewDragger {
private func draggingDidBegin(_ gesture: UIGestureRecognizer, indexPath: IndexPath) {
displayLink?.invalidate()
displayLink = UIScreen.main.displayLink(withTarget: self, selector: #selector(TableViewDragger.displayDidRefresh(_:)))
displayLink?.add(to: .main, forMode: .default)
displayLink?.isPaused = true
let dragIndexPath = dataSource?.dragger?(self, indexPathForDragAt: indexPath) ?? indexPath
delegate?.dragger?(self, willBeginDraggingAt: dragIndexPath)
if let tableView = targetTableView {
let actualCell = tableView.cellForRow(at: dragIndexPath)
if let draggedCell = draggedCell(tableView, indexPath: dragIndexPath) {
let point = gesture.location(in: actualCell)
if availableHorizontalScroll == true{
draggedCell.offset = point
draggedCell.transformToPoint(point)
draggedCell.location = gesture.location(in: tableView)
} else {
draggedCell.offset = CGPoint(x: (draggedCell.frame.size.width)/2, y: point.y)
draggedCell.transformToPoint(CGPoint(x: (draggedCell.frame.size.width)/2, y: point.y))
draggedCell.location = CGPoint(x: (draggedCell.frame.size.width)/2, y: gesture.location(in: tableView).y)
}
tableView.addSubview(draggedCell)
draggingCell = draggedCell
}
actualCell?.isHidden = isHiddenOriginCell
targetClipsToBounds = tableView.clipsToBounds
tableView.clipsToBounds = false
}
delegate?.dragger?(self, didBeginDraggingAt: indexPath)
}
private func draggingDidChange(_ gesture: UIGestureRecognizer, direction: UIScrollView.DraggingDirection?) {
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
if availableHorizontalScroll == true{
draggingCell.location = gesture.location(in: tableView)
} else {
draggingCell.location = CGPoint(x: (draggingCell.frame.size.width)/2, y: gesture.location(in: tableView).y)
}
if let adjustedDirection = tableView.draggingDirection(at: draggingCell.adjustedCenter(on: tableView)) {
displayLink?.isPaused = false
draggingDirection = adjustedDirection
} else {
draggingDirection = direction
}
dragCell(tableView, draggingCell: draggingCell)
}
private func draggingDidEnd(_ gesture: UIGestureRecognizer) {
displayLink?.invalidate()
displayLink = nil
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
delegate?.dragger?(self, willEndDraggingAt: draggingCell.dropIndexPath)
let targetRect = tableView.rectForRow(at: draggingCell.dropIndexPath)
let center = CGPoint(x: targetRect.width / 2, y: targetRect.origin.y + (targetRect.height / 2))
draggingCell.drop(center) {
self.delegate?.dragger?(self, didEndDraggingAt: draggingCell.dropIndexPath)
if let cell = tableView.cellForRow(at: draggingCell.dropIndexPath) {
cell.isHidden = false
}
tableView.clipsToBounds = self.targetClipsToBounds
self.draggingCell = nil
}
}
}
// MARK: - Action Methods
private extension TableViewDragger {
@objc func displayDidRefresh(_ displayLink: CADisplayLink) {
guard let tableView = targetTableView, let draggingCell = draggingCell else {
return
}
let center = draggingCell.adjustedCenter(on: tableView)
if let direction = tableView.draggingDirection(at: center) {
draggingDirection = direction
} else {
displayLink.isPaused = true
}
tableView.contentOffset = tableView.preferredContentOffset(at: center, velocity: scrollVelocity)
dragCell(tableView, draggingCell: draggingCell)
if availableHorizontalScroll == true{
draggingCell.location = panGesture.location(in: tableView)
} else {
draggingCell.location = CGPoint(x: draggingCell.frame.size.width/2, y: panGesture.location(in: tableView).y)
}
}
@objc func longPressGestureAction(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
targetTableView?.isScrollEnabled = false
let point = gesture.location(in: targetTableView)
if let path = targetTableView?.indexPathForRow(at: point) {
draggingDidBegin(gesture, indexPath: path)
}
case .ended, .cancelled:
draggingDidEnd(gesture)
targetTableView?.isScrollEnabled = true
case .changed, .failed, .possible:
break
@unknown default:
return
}
}
@objc func panGestureAction(_ gesture: UIPanGestureRecognizer) {
guard targetTableView?.isScrollEnabled == false && gesture.state == .changed else {
return
}
let offsetY = gesture.translation(in: targetTableView).y
if offsetY < 0 {
draggingDidChange(gesture, direction: .up)
} else if offsetY > 0 {
draggingDidChange(gesture, direction: .down)
} else {
draggingDidChange(gesture, direction: nil)
}
gesture.setTranslation(.zero, in: targetTableView)
}
}
// MARK: - UIGestureRecognizerDelegate Methods
extension TableViewDragger: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if gestureRecognizer == longPressGesture {
let point = touch.location(in: targetTableView)
if let indexPath = targetTableView?.indexPathForRow(at: point) {
if let ret = delegate?.dragger?(self, shouldDragAt: indexPath) {
return ret
}
} else {
return false
}
}
return true
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == panGesture || otherGestureRecognizer == panGesture || gestureRecognizer == longPressGesture || otherGestureRecognizer == longPressGesture
}
}
extension UIView {
fileprivate func captured() -> UIView? {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) else {
return nil
}
return try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [UIView.self], from: data) as? UIView
}
}
@@ -0,0 +1,136 @@
//
// TableViewDraggerCell.swift
// TableViewDragger
//
// Created by Kyohei Ito on 2015/09/24.
// Copyright © 2015年 kyohei_ito. All rights reserved.
//
import UIKit
class TableViewDraggerCell: UIScrollView {
private let zoomingView: UIView!
var dragAlpha: CGFloat = 1
var dragScale: CGFloat = 1
var dragShadowOpacity: Float = 0.4
var dropIndexPath: IndexPath = IndexPath(index: 0)
var offset: CGPoint = CGPoint.zero {
didSet {
offset.x -= (bounds.width / 2)
offset.y -= (bounds.height / 2)
center = adjustCenter(location)
}
}
var location: CGPoint = CGPoint.zero {
didSet {
center = adjustCenter(location)
}
}
var viewHeight: CGFloat {
return zoomingView.bounds.height * zoomScale
}
private func adjustCenter(_ center: CGPoint) -> CGPoint {
var center = center
center.x -= offset.x
center.y -= offset.y
return center
}
required init?(coder aDecoder: NSCoder) {
zoomingView = UIView(frame: .zero)
super.init(coder: aDecoder)
}
init(cell: UIView) {
zoomingView = UIView(frame: cell.bounds)
zoomingView.addSubview(cell)
super.init(frame: cell.bounds)
delegate = self
clipsToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0
layer.shadowRadius = 5
layer.shadowOffset = .zero
addSubview(zoomingView)
}
func transformToPoint(_ point: CGPoint) {
if dragScale > 1 {
maximumZoomScale = dragScale
} else {
minimumZoomScale = dragScale
}
var center = zoomingView.center
center.x -= (center.x * dragScale) - point.x
center.y -= (center.y * dragScale) - point.y
UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseInOut, animations: {
self.zoomingView.center = center
self.zoomScale = self.dragScale
self.alpha = self.dragAlpha
}, completion: nil)
CATransaction.begin()
let anim = CABasicAnimation(keyPath: "shadowOpacity")
anim.fromValue = 0
anim.toValue = dragShadowOpacity
anim.duration = 0.1
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards
layer.add(anim, forKey: "cellDragAnimation")
CATransaction.commit()
}
func adjustedCenter(on scrollView: UIScrollView) -> CGPoint {
var center = location
center.y -= scrollView.contentOffset.y
center.x = scrollView.center.x
return center
}
func drop(_ center: CGPoint, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: 0.25) {
self.zoomingView.adjustCenterAtRect(self.zoomingView.frame)
self.center = center
self.zoomScale = 1.0
self.alpha = 1.0
}
CATransaction.begin()
let anim = CABasicAnimation(keyPath: "shadowOpacity")
anim.fromValue = dragShadowOpacity
anim.toValue = 0
anim.duration = 0.15
anim.beginTime = CACurrentMediaTime() + 0.15
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards
CATransaction.setCompletionBlock {
self.removeFromSuperview()
completion?()
}
layer.add(anim, forKey: "cellDropAnimation")
CATransaction.commit()
}
}
extension TableViewDraggerCell: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomingView
}
}
private extension UIView {
func adjustCenterAtRect(_ rect: CGRect) {
let center = CGPoint(x: rect.size.width / 2, y: rect.size.height / 2)
self.center = center
}
}
@@ -0,0 +1,58 @@
//
// UIScrollViewExtension.swift
// Pods
//
// Created by Kyohei Ito on 2015/09/29.
//
//
import UIKit
extension UIScrollView {
enum DraggingDirection {
case up
case down
}
func preferredContentOffset(at point: CGPoint, velocity: CGFloat) -> CGPoint {
let distance = ScrollRects(size: bounds.size).distance(at: point) / velocity
var offset = contentOffset
offset.y += distance
let topOffset = -contentInset.top
let bottomOffset = contentInset.bottom
let height = floor(contentSize.height) - bounds.size.height
if offset.y > height + bottomOffset {
offset.y = height + bottomOffset
} else if offset.y < topOffset {
offset.y = topOffset
}
return offset
}
func draggingDirection(at point: @autoclosure () -> CGPoint) -> DraggingDirection? {
let contentHeight = floor(contentSize.height)
if bounds.size.height >= contentHeight {
return nil
}
let rects = ScrollRects(size: bounds.size)
let point = point()
if rects.topRect.contains(point) {
let topOffset = -contentInset.top
if contentOffset.y > topOffset {
return .up
}
} else if rects.bottomRect.contains(point) {
let bottomOffset = contentHeight + contentInset.bottom - bounds.size.height
if contentOffset.y < bottomOffset {
return .down
}
}
return nil
}
}