Add video animation support, update examples SimpleVideoAnimation SimpleVideoTransitionSwitch
This commit is contained in:
+131
@@ -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
|
||||
}
|
||||
}
|
||||
+31
@@ -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()
|
||||
}
|
||||
}
|
||||
+65
@@ -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)
|
||||
}
|
||||
}
|
||||
+32
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -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>
|
||||
|
||||
|
||||
+195
@@ -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()
|
||||
}
|
||||
}
|
||||
+25
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -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
|
||||
}
|
||||
}
|
||||
+132
@@ -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()
|
||||
}
|
||||
}
|
||||
+342
@@ -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()
|
||||
}
|
||||
}
|
||||
+166
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -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)
|
||||
}
|
||||
}
|
||||
+54
@@ -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()
|
||||
}
|
||||
}
|
||||
+148
@@ -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)
|
||||
}
|
||||
}
|
||||
+133
@@ -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()
|
||||
}
|
||||
}
|
||||
+127
@@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
+86
@@ -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
|
||||
}
|
||||
}
|
||||
+186
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+68
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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
|
||||
}
|
||||
}
|
||||
+82
@@ -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)
|
||||
}
|
||||
}
|
||||
+52
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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)
|
||||
}
|
||||
}
|
||||
+371
@@ -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
|
||||
}
|
||||
}
|
||||
+136
@@ -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
|
||||
}
|
||||
}
|
||||
+58
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user