Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions Example/Source/Control/NavigationBaseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,20 @@ extension NavigationBaseViewController {
])
/* TODO: Support iOS 10 */
/* FIXME: Fix subview's constraints using fluid transition on iOS 10 */
if #available(iOS 11.0, *) { return }
if #available(iOS 11.0, *) {
switch self.model! {
case .navigationFluidModal, .transitionFluidModal:
break
default:
return
}
}
self.subviewTopConstraint.deactivate()
self.subviewBottomConstraint.deactivate()
self.subviewLeadingConstraint.deactivate()
self.subviewTrailingConstraint.deactivate()
self.closeButtonTopConstraint.deactivate()
self.closeButtonTrailingConstraint.deactivate()
switch self.model! {
case .navigationFluidModal, .transitionFluidModal:
self.subviewTopConstraint = subview.topAnchor.constraint(equalTo: self.view.topAnchor).activate()
Expand All @@ -77,12 +90,6 @@ extension NavigationBaseViewController {
self.subviewTrailingConstraint = subview.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).activate()
self.closeButtonTopConstraint = self.closeButton?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 16).activate()
self.closeButtonTrailingConstraint = self.closeButton?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16).activate()
subview.removeConstraints([self.subviewTopConstraint,
self.subviewBottomConstraint,
self.subviewLeadingConstraint,
self.subviewTrailingConstraint])
self.closeButton?.removeConstraints([self.closeButtonTopConstraint,
self.closeButtonTrailingConstraint])
default:
subview.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).activate()
subview.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).activate()
Expand Down
2 changes: 1 addition & 1 deletion Example/Source/Control/NavigationChildViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ extension NavigationChildViewController: FluidTransitionDestinationActionDelegat
])
guard transitionStyle.isFluid else { return }
switch state {
case .begin: self.navigationController?.setNavigationBarHidden(true, animated: true)
case .begin: break
case .update: break
case .cancel: break
case .end: break
Expand Down
8 changes: 8 additions & 0 deletions Example/Source/Control/NavigationScrollViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class NavigationScrollViewController: NavigationBaseViewController, Fluidable {
self.headerView.rightAnchor.constraint(equalTo: self.imageContainerView.rightAnchor).activate()
self.headerView.heightAnchor.constraint(equalToConstant: self.headerView.estimatedHeight).activate()
self.imageView.image = UIImage(row: self.modelIndex, size: .medium)
if #available(iOS 11.0, *) {
switch self.model! {
case .navigationFluidModal, .transitionFluidModal:
self.scrollView.contentInsetAdjustmentBehavior = .never
default:
break
}
}
self.scrollView.contentInset.bottom = 40
self.configureConstraints(for: self.scrollView)
}
Expand Down
7 changes: 4 additions & 3 deletions Example/Source/Control/RootViewController+Fluidable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ extension RootViewController: FluidTransitionSourceActionDelegate {
let navBar: UINavigationBar = self.navigationController?.navigationBar else { return }
self.selectedCell?.isHidden = true
/* NOTE: If the navigation bar overlaps the cell, hide the navigation bar to avoid that the dismissal animation jumps */
let cellFrame: CGRect = cell.convert(cell.frame, to: navBar)
if navBar.frame.intersects(cellFrame) { self.navigationController?.setNavigationBarHidden(true, animated: true) }
let referenceView: UIView? = navBar.superview ?? self.view
let cellFrame: CGRect = cell.convert(cell.bounds, to: referenceView)
let navBarFrame: CGRect = navBar.convert(navBar.bounds, to: referenceView)
if navBarFrame.intersects(cellFrame) { self.navigationController?.setNavigationBarHidden(true, animated: true) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 128 characters (line_length)

case .cancel:
self.selectedCell?.isHidden = false
case .update, .end:
Expand Down Expand Up @@ -234,4 +236,3 @@ extension RootViewController: FluidTransitionSourceActionDelegate {
state: FluidProgressState, progress: CGFloat, info: FluidGestureInfo) {
}
}

9 changes: 8 additions & 1 deletion Example/Source/Control/TransitionBaseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ extension TransitionBaseViewController {
])
/* TODO: Support iOS 10 */
/* FIXME: Fix subview's constraints using fluid transition on iOS 10 */
if #available(iOS 11.0, *) { return }
if #available(iOS 11.0, *) {
switch self.model! {
case .navigationFluidModal, .transitionFluidModal:
break
default:
return
}
}
self.subviewTopConstraint.deactivate()
self.subviewBottomConstraint.deactivate()
self.subviewLeadingConstraint.deactivate()
Expand Down
8 changes: 8 additions & 0 deletions Example/Source/Control/TransitionScrollViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ class TransitionScrollViewController: TransitionBaseViewController, Fluidable {
self.headerView.rightAnchor.constraint(equalTo: self.imageContainerView.rightAnchor).activate()
self.headerView.heightAnchor.constraint(equalToConstant: self.headerView.estimatedHeight).activate()
self.imageView.image = UIImage(row: self.modelIndex, size: .medium)
if #available(iOS 11.0, *) {
switch self.model! {
case .navigationFluidModal, .transitionFluidModal:
self.scrollView.contentInsetAdjustmentBehavior = .never
default:
break
}
}
self.scrollView.contentInset.bottom = 40
self.configureConstraints(for: self.scrollView)
}
Expand Down
2 changes: 1 addition & 1 deletion Example/Source/Model/RootModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ extension RootModel {
var backgroundStyle: FluidBackgroundStyle {
switch self {
case .navigationFluidModal, .transitionFluidModal:
return .blur(radius: 12, color: .clear, alpha: 1.0)
return .blur(radius: 8, color: .clear, alpha: 1.0)
case .navigationFluidFullScreen, .transitionFluidFullScreen:
return .dim(color: UIColor.black.withAlphaComponent(0.5))
case .navigationDrawerTop, .navigationDrawerBottom, .navigationDrawerLeft, .navigationDrawerRight,
Expand Down
206 changes: 145 additions & 61 deletions Sources/View/FluidBackgroundView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2019 Gumob. All rights reserved.
//

import CoreImage
import Foundation
import UIKit

Expand All @@ -25,24 +26,56 @@ extension FluidBackgroundCompatible where Self: UIView {
}
}

internal class FluidBlurredBackgroundView: BlurView, FluidBackgroundCompatible {
internal class FluidBlurredBackgroundView: UIView, FluidBackgroundCompatible {
private static let ciContext = CIContext(options: nil)
private static let blurQueue = DispatchQueue(label: "Fluidable.FluidBlurredBackgroundView.blur", qos: .userInitiated)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 121 characters (line_length)


private let blurView = UIImageView()
private let tintView = UIView()
private var snapshotSize: CGSize = .zero
private var snapshotGeneration: Int = 0
private var isSnapshotUpdatePending: Bool = false

var baseBlurRadius: CGFloat = 0

/** A percentage of visibility. */
@objc internal dynamic var visibility: CGFloat = -1 {
didSet {
guard self.visibility != oldValue else { return }
self.blurRadius = self.baseBlurRadius * self.visibility.clamped(0, 1)
self.applyVisibility()
}
}

/** Tint color. The default value is nil. */
@objc dynamic internal var colorTint: UIColor? {
didSet { self.tintView.backgroundColor = self.colorTint }
}

/** Tint color alpha. The default value is 0.0. */
@objc dynamic open var colorTintAlpha: CGFloat = 0 {
didSet { self.tintView.alpha = self.colorTintAlpha * self.visibility.clamped(0, 1) }
}

/** Blur radius. The default value is 0.0. */
@objc dynamic open var blurRadius: CGFloat = 0

/** Scale factor. */
@objc dynamic open var scale: CGFloat = 1

required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

init(radius: CGFloat, color: UIColor, alpha: CGFloat) {
super.init(effect: nil)
super.init(frame: .zero)
self.clipsToBounds = true
self.blurView.alpha = 0
self.blurView.contentMode = .scaleToFill
self.addSubview(self.blurView)
self.tintView.alpha = 0
self.addSubview(self.tintView)
self.baseBlurRadius = radius
self.blurRadius = 0
self.colorTint = color
self.tintView.backgroundColor = color
self.colorTintAlpha = alpha
self.visibility = 0
self.isUserInteractionEnabled = false
Expand All @@ -51,10 +84,119 @@ internal class FluidBlurredBackgroundView: BlurView, FluidBackgroundCompatible {

deinit { Logger()?.log("🥶🧹🧹🧹️", []) }

override func didMoveToWindow() {
super.didMoveToWindow()
self.applyVisibility()
}

override func layoutSubviews() {
super.layoutSubviews()
self.blurView.frame = self.bounds
self.tintView.frame = self.bounds
if self.snapshotSize != self.bounds.size {
self.invalidateSnapshot()
self.applyVisibility()
}
}

override func updateConstraints() {
self.fitToSuperview()
super.updateConstraints()
}

private func applyVisibility() {
let clampedVisibility = self.visibility.clamped(0, 1)
self.blurRadius = self.baseBlurRadius * clampedVisibility
if clampedVisibility > 0, self.blurView.image == nil {
self.updateSnapshot()
}
self.blurView.alpha = clampedVisibility
self.tintView.alpha = self.colorTintAlpha * clampedVisibility
}

private func updateSnapshot() {
guard !self.isSnapshotUpdatePending else { return }
guard self.bounds.width > 0, self.bounds.height > 0 else { return }
guard let snapshot = self.makeBackgroundSnapshot() else { return }

self.snapshotGeneration += 1
let generation = self.snapshotGeneration
let snapshotSize = self.bounds.size
let blurRadius = self.baseBlurRadius
self.isSnapshotUpdatePending = true

Self.blurQueue.async { [weak self] in
let blurredImage = Self.makeBlurredImage(from: snapshot, radius: blurRadius)
DispatchQueue.main.async {
guard let self = self else { return }
guard self.snapshotGeneration == generation else { return }
self.isSnapshotUpdatePending = false
guard self.bounds.size == snapshotSize else { return }
self.blurView.image = blurredImage
self.snapshotSize = snapshotSize
}
}
}

private func invalidateSnapshot() {
self.snapshotGeneration += 1
self.isSnapshotUpdatePending = false
self.blurView.image = nil
self.snapshotSize = .zero
}

private func makeBackgroundSnapshot() -> UIImage? {
guard let superview = self.superview,
let backgroundIndex = superview.subviews.firstIndex(of: self) else { return nil }

let previousSubviews = superview.subviews[..<backgroundIndex]
let hasBackgroundContent = previousSubviews.contains { view in
return !view.isHidden && view.alpha > 0 && view.bounds.width > 1 && view.bounds.height > 1
}
if hasBackgroundContent {
return self.renderSnapshot(of: superview, hiding: Array(superview.subviews[backgroundIndex...]))
}
if let window = self.window, window !== superview {
return self.renderSnapshot(of: window, hiding: [superview])
}
return self.renderSnapshot(of: superview, hiding: Array(superview.subviews[backgroundIndex...]))
}

private func renderSnapshot(of view: UIView, hiding hiddenSubviews: [UIView]) -> UIImage {
let origin = self.convert(CGPoint.zero, to: view)
let hiddenStates = hiddenSubviews.map { $0.isHidden }
hiddenSubviews.forEach { $0.isHidden = true }
defer {
zip(hiddenSubviews, hiddenStates).forEach { view, isHidden in
view.isHidden = isHidden
}
}

let format = UIGraphicsImageRendererFormat()
format.scale = UIScreen.main.scale
format.opaque = false
let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
return renderer.image { context in
context.cgContext.translateBy(x: -origin.x, y: -origin.y)
view.layer.render(in: context.cgContext)
}
}

private static func makeBlurredImage(from image: UIImage, radius: CGFloat) -> UIImage {
guard radius > 0,
let inputImage = CIImage(image: image) else { return image }

let clampedImage = inputImage.clampedToExtent()
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(clampedImage, forKey: kCIInputImageKey)
filter?.setValue(radius * image.scale, forKey: kCIInputRadiusKey)

guard let outputImage = filter?.outputImage?.cropped(to: inputImage.extent),
let cgImage = ciContext.createCGImage(outputImage, from: inputImage.extent) else {
return image
}
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
}

internal class FluidDimmedBackgroundView: UIView, FluidBackgroundCompatible {
Expand Down Expand Up @@ -83,61 +225,3 @@ internal class FluidDimmedBackgroundView: UIView, FluidBackgroundCompatible {
super.updateConstraints()
}
}

/**
https://github.com/efremidze/VisualEffectView/blob/master/Sources/VisualEffectView.swift
*/
internal class BlurView: UIVisualEffectView {
/** Returns the instance of UIBlurEffect. */
private let blurEffect: UIBlurEffect = (NSClassFromString("_UICustomBlurEffect") as! UIBlurEffect.Type).init()

/** Tint color. The default value is nil. */
@objc dynamic internal var colorTint: UIColor? {
get { return _value(forKey: "colorTint") as? UIColor }
set { _setValue(newValue, forKey: "colorTint") }
}

/** Tint color alpha. The default value is 0.0. */
@objc dynamic open var colorTintAlpha: CGFloat {
get { return _value(forKey: "colorTintAlpha") as! CGFloat }
set { _setValue(newValue, forKey: "colorTintAlpha") }
}

/** Blur radius. The default value is 0.0. */
@objc dynamic open var blurRadius: CGFloat {
get { return _value(forKey: "blurRadius") as! CGFloat }
set { _setValue(newValue, forKey: "blurRadius") }
}

/** Scale factor. The scale factor determines how content in the view is mapped from the logical coordinate space (measured in points) to the device coordinate space (measured in pixels). The default value is 1.0. */
@objc dynamic open var scale: CGFloat {
get { return _value(forKey: "scale") as! CGFloat }
set { _setValue(newValue, forKey: "scale") }
}

/** Initialization */
internal override init(effect: UIVisualEffect?) {
super.init(effect: effect)
configure()
}

required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}

private func configure() {
self.scale = 1
}

/** Returns the value for the key on the blurEffect. */
private func _value(forKey key: String) -> Any? {
return blurEffect.value(forKeyPath: key)
}

/** Sets the value for the key on the blurEffect. */
private func _setValue(_ value: Any?, forKey key: String) {
blurEffect.setValue(value, forKeyPath: key)
self.effect = blurEffect
}
}
Loading