This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
//
// EKBackgroundView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final class EKBackgroundView: EKStyleView {
struct Style {
let background: EKAttributes.BackgroundStyle
let displayMode: EKAttributes.DisplayMode
}
// MARK: Props
private let visualEffectView: UIVisualEffectView
private let imageView: UIImageView
private let gradientView: GradientView
// MARK: Setup
init() {
imageView = UIImageView()
visualEffectView = UIVisualEffectView(effect: nil)
gradientView = GradientView()
super.init(frame: UIScreen.main.bounds)
addSubview(imageView)
imageView.contentMode = .scaleAspectFill
imageView.fillSuperview()
addSubview(visualEffectView)
visualEffectView.fillSuperview()
addSubview(gradientView)
gradientView.fillSuperview()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Background setter
var style: Style! {
didSet {
guard let style = style else {
return
}
var gradient: EKAttributes.BackgroundStyle.Gradient?
var backgroundEffect: UIBlurEffect?
var backgroundColor: UIColor = .clear
var backgroundImage: UIImage?
switch style.background {
case .color(color: let color):
backgroundColor = color.color(for: traitCollection,
mode: style.displayMode)
case .gradient(gradient: let value):
gradient = value
case .image(image: let image):
backgroundImage = image
case .visualEffect(style: let value):
backgroundEffect = value.blurEffect(for: traitCollection,
mode: style.displayMode)
case .clear:
break
}
gradientView.style = GradientView.Style(gradient: gradient,
displayMode: style.displayMode)
visualEffectView.effect = backgroundEffect
layer.backgroundColor = backgroundColor.cgColor
imageView.image = backgroundImage
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
guard let style = style else { return }
switch style.background {
case .color(color: let color):
layer.backgroundColor = color.color(for: traitCollection,
mode: style.displayMode).cgColor
case .visualEffect(style: let value):
visualEffectView.effect = value.blurEffect(for: traitCollection,
mode: style.displayMode)
default:
break
}
}
}

View File

@@ -0,0 +1,752 @@
//
// EKScrollView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
protocol EntryContentViewDelegate: AnyObject {
func changeToActive(withAttributes attributes: EKAttributes)
func changeToInactive(withAttributes attributes: EKAttributes, pushOut: Bool)
func didFinishDisplaying(entry: EKEntryView, keepWindowActive: Bool, dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?)
}
class EKContentView: UIView {
enum OutTranslation {
case exit
case pop
case swipeDown
case swipeUp
}
struct OutTranslationAnchor {
var messageOut: QLAttribute
var screenOut: QLAttribute
init(_ messageOut: QLAttribute, to screenOut: QLAttribute) {
self.messageOut = messageOut
self.screenOut = screenOut
}
}
// MARK: Props
// Entry delegate
private weak var entryDelegate: EntryContentViewDelegate!
// Constraints and Offsets
private var entranceOutConstraint: NSLayoutConstraint!
private var exitOutConstraint: NSLayoutConstraint!
private var swipeDownOutConstraint: NSLayoutConstraint!
private var swipeUpOutConstraint: NSLayoutConstraint!
private var popOutConstraint: NSLayoutConstraint!
private var inConstraint: NSLayoutConstraint!
private var resistanceConstraint: NSLayoutConstraint!
private var inKeyboardConstraint: NSLayoutConstraint!
private var inOffset: CGFloat = 0
private var totalTranslation: CGFloat = 0
private var verticalLimit: CGFloat = 0
private let swipeMinVelocity: CGFloat = 60
private var outDispatchWorkItem: DispatchWorkItem!
private var keyboardState = KeyboardState.hidden
// Dismissal handler
var dismissHandler: SwiftEntryKit.DismissCompletionHandler?
// Data source
private var attributes: EKAttributes {
return contentView.attributes
}
// Content
private var contentView: EKEntryView!
// MARK: Setup
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(withEntryDelegate entryDelegate: EntryContentViewDelegate) {
self.entryDelegate = entryDelegate
super.init(frame: .zero)
}
// Called from outer scope with a presentable view and attributes
func setup(with contentView: EKEntryView) {
self.contentView = contentView
// Execute willAppear lifecycle action if needed
contentView.attributes.lifecycleEvents.willAppear?()
// Setup attributes
setupAttributes()
// Setup initial position
setupInitialPosition()
// Setup width, height and maximum width
setupLayoutConstraints()
// Animate in
animateIn()
// Setup tap gesture
setupTapGestureRecognizer()
// Generate haptic feedback
generateHapticFeedback()
setupKeyboardChangeIfNeeded()
}
// Setup the scrollView initial position
private func setupInitialPosition() {
// Determine the layout entrance type according to the entry type
let messageInAnchor: NSLayoutConstraint.Attribute
inOffset = 0
var totalEntryHeight: CGFloat = 0
// Define a spacer to catch top / bottom offsets
var spacerView: UIView!
let safeAreaInsets = EKWindowProvider.safeAreaInsets
let overrideSafeArea = attributes.positionConstraints.safeArea.isOverridden
if !overrideSafeArea && safeAreaInsets.hasVerticalInsets && !attributes.position.isCenter {
spacerView = UIView()
addSubview(spacerView)
spacerView.set(.height, of: safeAreaInsets.top)
spacerView.layoutToSuperview(.width, .centerX)
totalEntryHeight += safeAreaInsets.top
}
switch attributes.position {
case .top:
messageInAnchor = .top
inOffset = overrideSafeArea ? 0 : safeAreaInsets.top
inOffset += attributes.positionConstraints.verticalOffset
spacerView?.layout(.bottom, to: .top, of: self)
case .bottom:
messageInAnchor = .bottom
inOffset = overrideSafeArea ? 0 : -safeAreaInsets.bottom
inOffset -= attributes.positionConstraints.verticalOffset
spacerView?.layout(.top, to: .bottom, of: self)
case .center:
messageInAnchor = .centerY
}
// Layout the content view inside the scroll view
addSubview(contentView)
contentView.layoutToSuperview(.left, .right, .top, .bottom)
contentView.layoutToSuperview(.width, .height)
inConstraint = layout(to: messageInAnchor, of: superview!, offset: inOffset, priority: .defaultLow)
// Set position constraints
setupOutConstraints(messageInAnchor: messageInAnchor)
totalTranslation = inOffset
switch attributes.position {
case .top:
verticalLimit = inOffset
case .bottom, .center:
verticalLimit = UIScreen.main.bounds.height + inOffset
}
// Setup keyboard constraints
switch attributes.positionConstraints.keyboardRelation {
case .bind(offset: let offset):
if let screenEdgeResistance = offset.screenEdgeResistance {
resistanceConstraint = layoutToSuperview(.top, relation: .greaterThanOrEqual, offset: screenEdgeResistance, priority: .defaultLow)
}
inKeyboardConstraint = layoutToSuperview(.bottom, priority: .defaultLow)
default:
break
}
}
private func setupOutConstraint(animation: EKAttributes.Animation?, messageInAnchor: QLAttribute, priority: QLPriority) -> NSLayoutConstraint {
let constraint: NSLayoutConstraint
if let translation = animation?.translate {
var anchor: OutTranslationAnchor
switch translation.anchorPosition {
case .top:
anchor = OutTranslationAnchor(.bottom, to: .top)
case .bottom:
anchor = OutTranslationAnchor(.top, to: .bottom)
case .automatic where attributes.position.isTop:
anchor = OutTranslationAnchor(.bottom, to: .top)
case .automatic: // attributes.position.isBottom:
anchor = OutTranslationAnchor(.top, to: .bottom)
}
constraint = layout(anchor.messageOut, to: anchor.screenOut, of: superview!, priority: priority)!
} else {
constraint = layout(to: messageInAnchor, of: superview!, offset: inOffset, priority: priority)!
}
return constraint
}
// Setup out constraints - taking into account the full picture and all the possible use-cases
private func setupOutConstraints(messageInAnchor: QLAttribute) {
// Setup entrance and exit out constraints
entranceOutConstraint = setupOutConstraint(animation: attributes.entranceAnimation, messageInAnchor: messageInAnchor, priority: .must)
exitOutConstraint = setupOutConstraint(animation: attributes.exitAnimation, messageInAnchor: messageInAnchor, priority: .defaultLow)
swipeDownOutConstraint = layout(.top, to: .bottom, of: superview!, priority: .defaultLow)!
swipeUpOutConstraint = layout(.bottom, to: .top, of: superview!, priority: .defaultLow)!
// Setup pop out constraint
var popAnimation: EKAttributes.Animation?
if case .animated(animation: let animation) = attributes.popBehavior {
popAnimation = animation
}
popOutConstraint = setupOutConstraint(animation: popAnimation, messageInAnchor: messageInAnchor, priority: .defaultLow)
}
private func setupSize() {
// Layout the scroll view horizontally inside the screen
switch attributes.positionConstraints.size.width {
case .offset(value: let offset):
layoutToSuperview(axis: .horizontally, offset: offset, priority: .must)
case .ratio(value: let ratio):
layoutToSuperview(.width, ratio: ratio, priority: .must)
case .constant(value: let constant):
set(.width, of: constant, priority: .must)
case .intrinsic:
break
}
// Layout the scroll view vertically inside the screen
switch attributes.positionConstraints.size.height {
case .offset(value: let offset):
layoutToSuperview(.height, offset: -offset * 2, priority: .must)
case .ratio(value: let ratio):
layoutToSuperview(.height, ratio: ratio, priority: .must)
case .constant(value: let constant):
set(.height, of: constant, priority: .must)
case .intrinsic:
break
}
}
private func setupMaxSize() {
// Layout the scroll view according to the maximum width (if given any)
switch attributes.positionConstraints.maxSize.width {
case .offset(value: let offset):
layout(to: .left, of: superview!, relation: .greaterThanOrEqual, offset: offset)
layout(to: .right, of: superview!, relation: .lessThanOrEqual, offset: -offset)
case .ratio(value: let ratio):
layoutToSuperview(.centerX)
layout(to: .width, of: superview!, relation: .lessThanOrEqual, ratio: ratio)
case .constant(value: let constant):
set(.width, of: constant, relation: .lessThanOrEqual)
break
case .intrinsic:
break
}
// Layout the scroll view according to the maximum width (if given any)
switch attributes.positionConstraints.maxSize.height {
case .offset(value: let offset):
layout(to: .height, of: superview!, relation: .lessThanOrEqual, offset: -offset * 2)
case .ratio(value: let ratio):
layout(to: .height, of: superview!, relation: .lessThanOrEqual, ratio: ratio)
case .constant(value: let constant):
set(.height, of: constant, relation: .lessThanOrEqual)
break
case .intrinsic:
break
}
}
// Setup layout constraints according to EKAttributes.PositionConstraints
private func setupLayoutConstraints() {
layoutToSuperview(.centerX)
setupSize()
setupMaxSize()
}
// Setup general attributes
private func setupAttributes() {
clipsToBounds = false
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(gr:)))
panGestureRecognizer.isEnabled = attributes.scroll.isEnabled
addGestureRecognizer(panGestureRecognizer)
}
// Setup tap gesture
private func setupTapGestureRecognizer() {
switch attributes.entryInteraction.defaultAction {
case .forward:
return
default:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureRecognized))
tapGestureRecognizer.numberOfTapsRequired = 1
tapGestureRecognizer.cancelsTouchesInView = false
addGestureRecognizer(tapGestureRecognizer)
}
}
// Generate a haptic feedback if needed
private func generateHapticFeedback() {
guard #available(iOS 10.0, *) else {
return
}
HapticFeedbackGenerator.notification(type: attributes.hapticFeedbackType)
}
// MARK: Animations
// Schedule out animation
private func scheduleAnimateOut(withDelay delay: TimeInterval? = nil) {
outDispatchWorkItem?.cancel()
outDispatchWorkItem = DispatchWorkItem { [weak self] in
self?.animateOut(pushOut: false)
}
let delay = attributes.entranceAnimation.totalDuration + (delay ?? attributes.displayDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: outDispatchWorkItem)
}
// Animate out
func animateOut(pushOut: Bool) {
// Execute willDisappear action if needed
contentView.attributes.lifecycleEvents.willDisappear?()
if attributes.positionConstraints.keyboardRelation.isBound {
endEditing(true)
}
outDispatchWorkItem?.cancel()
entryDelegate?.changeToInactive(withAttributes: attributes, pushOut: pushOut)
if case .animated(animation: let animation) = attributes.popBehavior, pushOut {
animateOut(with: animation, outTranslationType: .pop)
} else {
animateOut(with: attributes.exitAnimation, outTranslationType: .exit)
}
}
// Animate out
private func animateOut(with animation: EKAttributes.Animation, outTranslationType: OutTranslation) {
superview?.layoutIfNeeded()
if let translation = animation.translate {
performAnimation(out: true, with: translation) { [weak self] in
self?.translateOut(withType: outTranslationType)
}
}
if let fade = animation.fade {
performAnimation(out: true, with: fade, preAction: { self.alpha = fade.start }) {
self.alpha = fade.end
}
}
if let scale = animation.scale {
performAnimation(out: true, with: scale, preAction: { self.transform = CGAffineTransform(scaleX: scale.start, y: scale.start) }) {
self.transform = CGAffineTransform(scaleX: scale.end, y: scale.end)
}
}
if animation.containsAnimation {
DispatchQueue.main.asyncAfter(deadline: .now() + animation.maxDuration) {
self.removeFromSuperview(keepWindow: false)
}
} else {
translateOut(withType: outTranslationType)
removeFromSuperview(keepWindow: false)
}
}
// Animate in
private func animateIn() {
let animation = attributes.entranceAnimation
superview?.layoutIfNeeded()
if let translation = animation.translate {
performAnimation(out: false, with: translation, action: translateIn)
} else {
translateIn()
}
if let fade = animation.fade {
performAnimation(out: false, with: fade, preAction: { self.alpha = fade.start }) {
self.alpha = fade.end
}
}
if let scale = animation.scale {
performAnimation(out: false, with: scale, preAction: { self.transform = CGAffineTransform(scaleX: scale.start, y: scale.start) }) {
self.transform = CGAffineTransform(scaleX: scale.end, y: scale.end)
}
}
entryDelegate?.changeToActive(withAttributes: attributes)
// Execute didAppear action if needed
if animation.containsAnimation {
DispatchQueue.main.asyncAfter(deadline: .now() + animation.maxDuration) {
self.contentView.attributes.lifecycleEvents.didAppear?()
}
} else {
contentView.attributes.lifecycleEvents.didAppear?()
}
scheduleAnimateOut()
}
// Translate in
private func translateIn() {
entranceOutConstraint.priority = .defaultLow
exitOutConstraint.priority = .defaultLow
popOutConstraint.priority = .defaultLow
inConstraint.priority = .must
superview?.layoutIfNeeded()
}
// Translate out
private func translateOut(withType type: OutTranslation) {
inConstraint.priority = .defaultLow
entranceOutConstraint.priority = .defaultLow
switch type {
case .exit:
exitOutConstraint.priority = .must
case .pop:
popOutConstraint.priority = .must
case .swipeUp:
swipeUpOutConstraint.priority = .must
case .swipeDown:
swipeDownOutConstraint.priority = .must
}
superview?.layoutIfNeeded()
}
// Perform animation - translate / scale / fade
private func performAnimation(out: Bool, with animation: EKAnimation, preAction: @escaping () -> () = {}, action: @escaping () -> ()) {
let curve: UIView.AnimationOptions = out ? .curveEaseIn : .curveEaseOut
let options: UIView.AnimationOptions = [curve, .beginFromCurrentState]
preAction()
if let spring = animation.spring {
UIView.animate(withDuration: animation.duration, delay: animation.delay, usingSpringWithDamping: spring.damping, initialSpringVelocity: spring.initialVelocity, options: options, animations: {
action()
}, completion: nil)
} else {
UIView.animate(withDuration: animation.duration, delay: animation.delay, options: options, animations: {
action()
}, completion: nil)
}
}
// MARK: Remvoe entry
// Removes the view promptly - DOES NOT animate out
func removePromptly(keepWindow: Bool = true) {
outDispatchWorkItem?.cancel()
entryDelegate?.changeToInactive(withAttributes: attributes, pushOut: false)
contentView.content.attributes.lifecycleEvents.willDisappear?()
removeFromSuperview(keepWindow: keepWindow)
}
// Remove self from superview
func removeFromSuperview(keepWindow: Bool) {
guard superview != nil else {
return
}
// Execute didDisappear action if needed
let didDisappear = contentView.content.attributes.lifecycleEvents.didDisappear
// Remove the view from its superview and in a case of a view controller, from its parent controller.
super.removeFromSuperview()
contentView.content.viewController?.removeFromParent()
entryDelegate.didFinishDisplaying(entry: contentView, keepWindowActive: keepWindow, dismissCompletionHandler: dismissHandler)
// Lastly, perform the Dismiss Completion Handler as the entry is no longer displayed
didDisappear?()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// MARK: Keyboard Logic
extension EKContentView {
private enum KeyboardState {
case visible
case hidden
var isVisible: Bool {
return self == .visible
}
var isHidden: Bool {
return self == .hidden
}
}
private struct KeyboardAttributes {
let duration: TimeInterval
let curve: UIView.AnimationOptions
let begin: CGRect
let end: CGRect
init?(withRawValue rawValue: [AnyHashable: Any]?) {
guard let rawValue = rawValue else {
return nil
}
duration = rawValue[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
curve = .init(rawValue: rawValue[UIResponder.keyboardAnimationCurveUserInfoKey] as! UInt)
begin = (rawValue[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
end = (rawValue[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
}
var height: CGFloat {
return end.maxY - end.minY
}
}
private func setupKeyboardChangeIfNeeded() {
guard attributes.positionConstraints.keyboardRelation.isBound else {
return
}
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
private func animate(by userInfo: [AnyHashable: Any]?, entrance: Bool) {
// Guard that the entry is bound to the keyboard
guard case .bind(offset: let offset) = attributes.positionConstraints.keyboardRelation else {
return
}
// Convert the user info into keyboard attributes
guard let keyboardAtts = KeyboardAttributes(withRawValue: userInfo) else {
return
}
if entrance {
inKeyboardConstraint.constant = -(keyboardAtts.height + offset.bottom)
inKeyboardConstraint.priority = .must
resistanceConstraint?.priority = .must
inConstraint.priority = .defaultLow
} else {
inKeyboardConstraint.priority = .defaultLow
resistanceConstraint?.priority = .defaultLow
inConstraint.priority = .must
}
UIView.animate(withDuration: keyboardAtts.duration, delay: 0, options: keyboardAtts.curve, animations: {
self.superview?.layoutIfNeeded()
}, completion: nil)
}
@objc func keyboardWillShow(_ notification: Notification) {
guard containsFirstResponder else {
return
}
keyboardState = .visible
animate(by: notification.userInfo, entrance: true)
}
@objc func keyboardWillHide(_ notification: Notification) {
animate(by: notification.userInfo, entrance: false)
}
@objc func keyboardDidHide(_ notification: Notification) {
keyboardState = .hidden
}
@objc func keyboardWillChangeFrame(_ notification: Notification) {
guard containsFirstResponder else {
return
}
animate(by: notification.userInfo, entrance: true)
}
}
// MARK: Responds to user interactions (tap / pan / swipe / touches)
extension EKContentView {
// Tap gesture handler
@objc func tapGestureRecognized() {
switch attributes.entryInteraction.defaultAction {
case .delayExit(by: _) where attributes.displayDuration.isFinite:
scheduleAnimateOut()
case .dismissEntry:
animateOut(pushOut: false)
default:
break
}
attributes.entryInteraction.customTapActions.forEach { $0() }
}
// Pan gesture handler
@objc func panGestureRecognized(gr: UIPanGestureRecognizer) {
guard keyboardState.isHidden else {
return
}
// Delay the exit of the entry if needed
handleExitDelayIfNeeded(byPanState: gr.state)
let translation = gr.translation(in: superview!).y
if shouldStretch(with: translation) {
if attributes.scroll.isEdgeCrossingEnabled {
totalTranslation += translation
calculateLogarithmicOffset(forOffset: totalTranslation, currentTranslation: translation)
switch gr.state {
case .ended, .failed, .cancelled:
animateRubberBandPullback()
default:
break
}
}
} else {
switch gr.state {
case .ended, .failed, .cancelled:
let velocity = gr.velocity(in: superview!).y
swipeEnded(withVelocity: velocity)
case .changed:
inConstraint.constant += translation
default:
break
}
}
gr.setTranslation(.zero, in: superview!)
}
private func swipeEnded(withVelocity velocity: CGFloat) {
let distance = Swift.abs(inOffset - inConstraint.constant)
var duration = max(0.3, TimeInterval(distance / Swift.abs(velocity)))
duration = min(0.7, duration)
if attributes.scroll.isSwipeable && testSwipeVelocity(with: velocity) && testSwipeInConstraint() {
stretchOut(usingSwipe: velocity > 0 ? .swipeDown : .swipeUp, duration: duration)
} else {
animateRubberBandPullback()
}
}
private func stretchOut(usingSwipe type: OutTranslation, duration: TimeInterval) {
outDispatchWorkItem?.cancel()
entryDelegate?.changeToInactive(withAttributes: attributes, pushOut: false)
contentView.content.attributes.lifecycleEvents.willDisappear?()
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 4, options: [.allowUserInteraction, .beginFromCurrentState], animations: {
self.translateOut(withType: type)
}, completion: { finished in
self.removeFromSuperview(keepWindow: false)
})
}
private func calculateLogarithmicOffset(forOffset offset: CGFloat, currentTranslation: CGFloat) {
guard verticalLimit != 0 else { return }
if attributes.position.isTop {
inConstraint.constant = verticalLimit * (1 + log10(offset / verticalLimit))
} else {
let offset = Swift.abs(offset) + verticalLimit
let addition: CGFloat = abs(currentTranslation) < 2 ? 0 : 1
inConstraint.constant -= (addition + log10(offset / verticalLimit))
}
}
private func shouldStretch(with translation: CGFloat) -> Bool {
if attributes.position.isTop {
return translation > 0 && inConstraint.constant >= inOffset
} else {
return translation < 0 && inConstraint.constant <= inOffset
}
}
private func animateRubberBandPullback() {
totalTranslation = verticalLimit
let animation: EKAttributes.Scroll.PullbackAnimation
if case .enabled(swipeable: _, pullbackAnimation: let pullbackAnimation) = attributes.scroll {
animation = pullbackAnimation
} else {
animation = .easeOut
}
UIView.animate(withDuration: animation.duration, delay: 0, usingSpringWithDamping: animation.damping, initialSpringVelocity: animation.initialSpringVelocity, options: [.allowUserInteraction, .beginFromCurrentState], animations: {
self.inConstraint?.constant = self.inOffset
self.superview?.layoutIfNeeded()
}, completion: nil)
}
private func testSwipeInConstraint() -> Bool {
if attributes.position.isTop {
return inConstraint.constant < inOffset
} else {
return inConstraint.constant > inOffset
}
}
private func testSwipeVelocity(with velocity: CGFloat) -> Bool {
if attributes.position.isTop {
return velocity < -swipeMinVelocity
} else {
return velocity > swipeMinVelocity
}
}
private func handleExitDelayIfNeeded(byPanState state: UIGestureRecognizer.State) {
guard attributes.entryInteraction.isDelayExit && attributes.displayDuration.isFinite else {
return
}
switch state {
case .began:
outDispatchWorkItem?.cancel()
case .ended, .failed, .cancelled:
scheduleAnimateOut()
default:
break
}
}
// MARK: UIResponder
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if attributes.entryInteraction.isDelayExit && attributes.displayDuration.isFinite {
outDispatchWorkItem?.cancel()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if attributes.entryInteraction.isDelayExit && attributes.displayDuration.isFinite {
scheduleAnimateOut()
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesEnded(touches, with: event)
}
}

View File

@@ -0,0 +1,182 @@
//
// EKEntryView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/15/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
class EKEntryView: EKStyleView {
struct Content {
var viewController: UIViewController!
var view: UIView!
var attributes: EKAttributes
init(viewController: UIViewController, attributes: EKAttributes) {
self.viewController = viewController
self.view = viewController.view
self.attributes = attributes
}
init(view: UIView, attributes: EKAttributes) {
self.view = view
self.attributes = attributes
}
}
// MARK: Props
/** Background view */
private var backgroundView: EKBackgroundView!
/** The content - contains the view, view controller, attributes */
var content: Content
private lazy var contentView: UIView = {
return UIView()
}()
var attributes: EKAttributes {
return content.attributes
}
private lazy var contentContainerView: EKStyleView = {
let contentContainerView = EKStyleView()
self.addSubview(contentContainerView)
contentContainerView.layoutToSuperview(axis: .vertically)
contentContainerView.layoutToSuperview(axis: .horizontally)
contentContainerView.clipsToBounds = true
return contentContainerView
}()
// MARK: Setup
init(newEntry content: Content) {
self.content = content
super.init(frame: UIScreen.main.bounds)
setupContentView()
applyDropShadow()
applyBackgroundToContentView()
applyFrameStyle()
adjustInnerContentAppearanceIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
applyFrameStyle()
}
func transform(to view: UIView) {
let previousView = content.view
content.view = view
view.layoutIfNeeded()
let previousHeight = set(.height, of: frame.height, priority: .must)
let nextHeight = set(.height, of: view.frame.height, priority: .defaultLow)
SwiftEntryKit.layoutIfNeeded()
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.beginFromCurrentState, .layoutSubviews], animations: {
previousHeight.priority = .defaultLow
nextHeight.priority = .must
previousView!.alpha = 0
SwiftEntryKit.layoutIfNeeded()
}, completion: { (finished) in
view.alpha = 0
previousView!.removeFromSuperview()
self.removeConstraints([previousHeight, nextHeight])
self.setupContentView()
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.curveEaseOut], animations: {
view.alpha = 1
}, completion: nil)
})
}
private func setupContentView() {
contentView.addSubview(content.view)
content.view.layoutToSuperview(axis: .horizontally)
content.view.layoutToSuperview(axis: .vertically)
contentContainerView.addSubview(contentView)
contentView.fillSuperview()
contentView.layoutToSuperview(axis: .vertically)
contentView.layoutToSuperview(axis: .horizontally)
}
// Complementary logic for issue #117
private func adjustInnerContentAppearanceIfNeeded() {
guard let view = content.view as? EntryAppearanceDescriptor else {
return
}
view.bottomCornerRadius = attributes.roundCorners.cornerValues?.radius ?? 0
}
// Apply round corners
private func applyFrameStyle() {
backgroundView.applyFrameStyle(roundCorners: attributes.roundCorners, border: attributes.border)
}
// Apply drop shadow
private func applyDropShadow() {
switch attributes.shadow {
case .active(with: let value):
applyDropShadow(withOffset: value.offset,
opacity: value.opacity,
radius: value.radius,
color: value.color.color(for: traitCollection, mode: attributes.displayMode))
case .none:
removeDropShadow()
}
}
// Apply background
private func applyBackgroundToContentView() {
let attributes = content.attributes
let backgroundView = EKBackgroundView()
backgroundView.style = .init(background: attributes.entryBackground,
displayMode: attributes.displayMode)
switch attributes.positionConstraints.safeArea {
case .empty(fillSafeArea: let fillSafeArea) where fillSafeArea: // Safe area filled with color
insertSubview(backgroundView, at: 0)
backgroundView.layoutToSuperview(axis: .horizontally)
var topInset: CGFloat = 0
var bottomInset: CGFloat = 0
switch attributes.position {
case .top:
topInset = -EKWindowProvider.safeAreaInsets.top
case .bottom, .center:
bottomInset = EKWindowProvider.safeAreaInsets.bottom
}
backgroundView.layoutToSuperview(.top, offset: topInset)
backgroundView.layoutToSuperview(.bottom, offset: bottomInset)
default: // Float case or a Toast with unfilled safe area
contentView.insertSubview(backgroundView, at: 0)
backgroundView.fillSuperview()
}
self.backgroundView = backgroundView
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
applyDropShadow()
}
}

View File

@@ -0,0 +1,268 @@
//
// EntryViewController.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
protocol EntryPresenterDelegate: AnyObject {
var isResponsiveToTouches: Bool { set get }
func displayPendingEntryOrRollbackWindow(dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?)
}
class EKRootViewController: UIViewController {
// MARK: - Props
private unowned let delegate: EntryPresenterDelegate
private var lastAttributes: EKAttributes!
private let backgroundView = EKBackgroundView()
private lazy var wrapperView: EKWrapperView = {
return EKWrapperView()
}()
/*
Count the total amount of currently displaying entries,
meaning, total subviews less one - the backgorund of the entry
*/
fileprivate var displayingEntryCount: Int {
return view.subviews.count - 1
}
fileprivate var isDisplaying: Bool {
return lastEntry != nil
}
private var lastEntry: EKContentView? {
return view.subviews.last as? EKContentView
}
private var isResponsive = false {
didSet {
wrapperView.isAbleToReceiveTouches = isResponsive
delegate.isResponsiveToTouches = isResponsive
}
}
override var shouldAutorotate: Bool {
if lastAttributes == nil {
return true
}
return lastAttributes.positionConstraints.rotation.isEnabled
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
guard let lastAttributes = lastAttributes else {
return super.supportedInterfaceOrientations
}
switch lastAttributes.positionConstraints.rotation.supportedInterfaceOrientations {
case .standard:
return super.supportedInterfaceOrientations
case .all:
return .all
}
}
// Previous status bar style
private let previousStatusBar: EKAttributes.StatusBar
private var statusBar: EKAttributes.StatusBar? = nil {
didSet {
if let statusBar = statusBar, ![statusBar, oldValue].contains(.ignored) {
UIApplication.shared.set(statusBarStyle: statusBar)
}
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
if [previousStatusBar, statusBar].contains(.ignored) {
return super.preferredStatusBarStyle
}
return statusBar?.appearance.style ?? previousStatusBar.appearance.style
}
override var prefersStatusBarHidden: Bool {
if [previousStatusBar, statusBar].contains(.ignored) {
return super.prefersStatusBarHidden
}
return !(statusBar?.appearance.visible ?? previousStatusBar.appearance.visible)
}
// MARK: - Lifecycle
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(with delegate: EntryPresenterDelegate) {
self.delegate = delegate
previousStatusBar = .currentStatusBar
super.init(nibName: nil, bundle: nil)
}
override public func loadView() {
view = wrapperView
view.insertSubview(backgroundView, at: 0)
backgroundView.isUserInteractionEnabled = false
backgroundView.fillSuperview()
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
statusBar = previousStatusBar
}
// Set status bar
func setStatusBarStyle(for attributes: EKAttributes) {
statusBar = attributes.statusBar
}
// MARK: - Setup
func configure(entryView: EKEntryView) {
// In case the entry is a view controller, add the entry as child of root
if let viewController = entryView.content.viewController {
addChild(viewController)
}
// Extract the attributes struct
let attributes = entryView.attributes
// Assign attributes
let previousAttributes = lastAttributes
// Remove the last entry
removeLastEntry(lastAttributes: previousAttributes, keepWindow: true)
lastAttributes = attributes
let entryContentView = EKContentView(withEntryDelegate: self)
view.addSubview(entryContentView)
entryContentView.setup(with: entryView)
switch attributes.screenInteraction.defaultAction {
case .forward:
isResponsive = false
default:
isResponsive = true
}
if previousAttributes?.statusBar != attributes.statusBar {
setNeedsStatusBarAppearanceUpdate()
}
if shouldAutorotate {
UIViewController.attemptRotationToDeviceOrientation()
}
}
// Check priority precedence for a given entry
func canDisplay(attributes: EKAttributes) -> Bool {
guard let lastAttributes = lastAttributes else {
return true
}
return attributes.precedence.priority >= lastAttributes.precedence.priority
}
// Removes last entry - can keep the window 'ON' if necessary
private func removeLastEntry(lastAttributes: EKAttributes?, keepWindow: Bool) {
guard let attributes = lastAttributes else {
return
}
if attributes.popBehavior.isOverriden {
lastEntry?.removePromptly()
} else {
popLastEntry()
}
}
// Make last entry exit using exitAnimation - animatedly
func animateOutLastEntry(completionHandler: SwiftEntryKit.DismissCompletionHandler? = nil) {
lastEntry?.dismissHandler = completionHandler
lastEntry?.animateOut(pushOut: false)
}
// Pops last entry (using pop animation) - animatedly
func popLastEntry() {
lastEntry?.animateOut(pushOut: true)
}
}
// MARK: - UIResponder
extension EKRootViewController {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
switch lastAttributes.screenInteraction.defaultAction {
case .dismissEntry:
lastEntry?.animateOut(pushOut: false)
fallthrough
default:
lastAttributes.screenInteraction.customTapActions.forEach { $0() }
}
}
}
// MARK: - EntryScrollViewDelegate
extension EKRootViewController: EntryContentViewDelegate {
func didFinishDisplaying(entry: EKEntryView, keepWindowActive: Bool, dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?) {
guard !isDisplaying else {
return
}
guard !keepWindowActive else {
return
}
delegate.displayPendingEntryOrRollbackWindow(dismissCompletionHandler: dismissCompletionHandler)
}
func changeToInactive(withAttributes attributes: EKAttributes, pushOut: Bool) {
guard displayingEntryCount <= 1 else {
return
}
let clear = {
let style = EKBackgroundView.Style(background: .clear, displayMode: attributes.displayMode)
self.changeBackground(to: style, duration: attributes.exitAnimation.totalDuration)
}
guard pushOut else {
clear()
return
}
guard let lastBackroundStyle = lastAttributes?.screenBackground else {
clear()
return
}
if lastBackroundStyle != attributes.screenBackground {
clear()
}
}
func changeToActive(withAttributes attributes: EKAttributes) {
let style = EKBackgroundView.Style(background: attributes.screenBackground,
displayMode: attributes.displayMode)
changeBackground(to: style, duration: attributes.entranceAnimation.totalDuration)
}
private func changeBackground(to style: EKBackgroundView.Style, duration: TimeInterval) {
DispatchQueue.main.async {
UIView.animate(withDuration: duration, delay: 0, options: [], animations: {
self.backgroundView.style = style
}, completion: nil)
}
}
}

View File

@@ -0,0 +1,57 @@
//
// EKStyleView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/28/18.
//
import UIKit
class EKStyleView: UIView {
private lazy var borderLayer: CAShapeLayer = {
return CAShapeLayer()
}()
private var roundCorners: EKAttributes.RoundCorners!
private var border: EKAttributes.Border!
var appliedStyle = false
func applyFrameStyle(roundCorners: EKAttributes.RoundCorners, border: EKAttributes.Border) {
self.roundCorners = roundCorners
self.border = border
var cornerRadius: CGFloat = 0
var corners: UIRectCorner = []
(corners, cornerRadius) = roundCorners.cornerValues ?? ([], 0)
let size = CGSize(width: cornerRadius, height: cornerRadius)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: size)
if !corners.isEmpty && cornerRadius > 0 {
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}
if let borderValues = border.borderValues {
borderLayer.path = path.cgPath
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderValues.color.cgColor
borderLayer.lineWidth = borderValues.width
borderLayer.frame = bounds
layer.addSublayer(borderLayer)
}
appliedStyle = true
}
override func layoutSubviews() {
super.layoutSubviews()
guard let roundCorners = roundCorners, let border = border else {
return
}
applyFrameStyle(roundCorners: roundCorners, border: border)
}
}

View File

@@ -0,0 +1,50 @@
//
// EKWindow.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
class EKWindow: UIWindow {
var isAbleToReceiveTouches = false
init(with rootVC: UIViewController) {
if #available(iOS 13.0, *) {
// TODO: Patched to support SwiftUI out of the box but should require attendance
if let scene = UIApplication.shared.connectedScenes.filter({$0.activationState == .foregroundActive}).first as? UIWindowScene {
super.init(windowScene: scene)
} else {
super.init(frame: UIScreen.main.bounds)
}
} else {
super.init(frame: UIScreen.main.bounds)
}
backgroundColor = .clear
rootViewController = rootVC
accessibilityViewIsModal = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if isAbleToReceiveTouches {
return super.hitTest(point, with: event)
}
guard let rootVC = EKWindowProvider.shared.rootVC else {
return nil
}
if let view = rootVC.view.hitTest(point, with: event) {
return view
}
return nil
}
}

View File

@@ -0,0 +1,231 @@
//
// EKWindowProvider.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final class EKWindowProvider: EntryPresenterDelegate {
/** The artificial safe area insets */
static var safeAreaInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return EKWindowProvider.shared.entryWindow?.rootViewController?.view?.safeAreaInsets ?? UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets ?? .zero
} else {
let statusBarMaxY = UIApplication.shared.statusBarFrame.maxY
return UIEdgeInsets(top: statusBarMaxY, left: 0, bottom: 10, right: 0)
}
}
/** Single access point */
static let shared = EKWindowProvider()
/** Current entry window */
var entryWindow: EKWindow!
/** Returns the root view controller if it is instantiated */
var rootVC: EKRootViewController? {
return entryWindow?.rootViewController as? EKRootViewController
}
/** A window to go back to when the last entry has been dismissed */
private var rollbackWindow: SwiftEntryKit.RollbackWindow!
/** The main rollback window to be used internally in case `rollbackWindow`'s value is `.main` */
private weak var mainRollbackWindow: UIWindow?
/** Entry queueing heuristic */
private let entryQueue = EKAttributes.Precedence.QueueingHeuristic.value.heuristic
private weak var entryView: EKEntryView!
/** Cannot be instantiated, customized, inherited */
private init() {}
var isResponsiveToTouches: Bool {
set {
entryWindow.isAbleToReceiveTouches = newValue
}
get {
return entryWindow.isAbleToReceiveTouches
}
}
// MARK: - Setup and Teardown methods
// Prepare the window and the host view controller
private func prepare(for attributes: EKAttributes, presentInsideKeyWindow: Bool) -> EKRootViewController? {
let entryVC = setupWindowAndRootVC()
guard entryVC.canDisplay(attributes: attributes) || attributes.precedence.isEnqueue else {
return nil
}
entryVC.setStatusBarStyle(for: attributes)
entryWindow.windowLevel = attributes.windowLevel.value
if presentInsideKeyWindow {
entryWindow.makeKeyAndVisible()
} else {
entryWindow.isHidden = false
}
return entryVC
}
/** Boilerplate generic setup for entry-window and root-view-controller */
private func setupWindowAndRootVC() -> EKRootViewController {
let entryVC: EKRootViewController
if entryWindow == nil {
entryVC = EKRootViewController(with: self)
entryWindow = EKWindow(with: entryVC)
mainRollbackWindow = UIApplication.shared.keyWindow
} else {
entryVC = rootVC!
}
return entryVC
}
/**
Privately used to display an entry
*/
private func display(entryView: EKEntryView, using attributes: EKAttributes, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) {
switch entryView.attributes.precedence {
case .override(priority: _, dropEnqueuedEntries: let dropEnqueuedEntries):
if dropEnqueuedEntries {
entryQueue.removeAll()
}
show(entryView: entryView, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
case .enqueue where isCurrentlyDisplaying():
entryQueue.enqueue(entry: .init(view: entryView, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow))
case .enqueue:
show(entryView: entryView, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
}
}
// MARK: - Exposed Actions
func queueContains(entryNamed name: String? = nil) -> Bool {
if name == nil && !entryQueue.isEmpty {
return true
}
if let name = name {
return entryQueue.contains(entryNamed: name)
} else {
return false
}
}
/**
Returns *true* if the currently displayed entry has the given name.
In case *name* has the value of *nil*, the result is *true* if any entry is currently displayed.
*/
func isCurrentlyDisplaying(entryNamed name: String? = nil) -> Bool {
guard let entryView = entryView else {
return false
}
if let name = name { // Test for names equality
return entryView.content.attributes.name == name
} else { // Return true by default if the name is *nil*
return true
}
}
/** Transform current entry to view */
func transform(to view: UIView) {
entryView?.transform(to: view)
}
/** Display a view using attributes */
func display(view: UIView, using attributes: EKAttributes, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) {
let entryView = EKEntryView(newEntry: .init(view: view, attributes: attributes))
display(entryView: entryView, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
}
/** Display a view controller using attributes */
func display(viewController: UIViewController, using attributes: EKAttributes, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) {
let entryView = EKEntryView(newEntry: .init(viewController: viewController, attributes: attributes))
display(entryView: entryView, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
}
/** Clear all entries immediately and display to the rollback window */
func displayRollbackWindow() {
if #available(iOS 13.0, *) {
entryWindow.windowScene = nil
}
entryWindow = nil
entryView = nil
switch rollbackWindow! {
case .main:
if let mainRollbackWindow = mainRollbackWindow {
mainRollbackWindow.makeKeyAndVisible()
} else {
UIApplication.shared.keyWindow?.makeKeyAndVisible()
}
case .custom(window: let window):
window.makeKeyAndVisible()
}
}
/** Display a pending entry if there is any inside the queue */
func displayPendingEntryOrRollbackWindow(dismissCompletionHandler: SwiftEntryKit.DismissCompletionHandler?) {
if let next = entryQueue.dequeue() {
// Execute dismiss handler if needed before dequeuing (potentially) another entry
dismissCompletionHandler?()
// Show the next entry in queue
show(entryView: next.view, presentInsideKeyWindow: next.presentInsideKeyWindow, rollbackWindow: next.rollbackWindow)
} else {
// Display the rollback window
displayRollbackWindow()
// As a last step, invoke the dismissal method
dismissCompletionHandler?()
}
}
/** Dismiss entries according to a given descriptor */
func dismiss(_ descriptor: SwiftEntryKit.EntryDismissalDescriptor, with completion: SwiftEntryKit.DismissCompletionHandler? = nil) {
guard let rootVC = rootVC else {
return
}
switch descriptor {
case .displayed:
rootVC.animateOutLastEntry(completionHandler: completion)
case .specific(entryName: let name):
entryQueue.removeEntries(by: name)
if entryView?.attributes.name == name {
rootVC.animateOutLastEntry(completionHandler: completion)
}
case .prioritizedLowerOrEqualTo(priority: let priorityThreshold):
entryQueue.removeEntries(withPriorityLowerOrEqualTo: priorityThreshold)
if let currentPriority = entryView?.attributes.precedence.priority, currentPriority <= priorityThreshold {
rootVC.animateOutLastEntry(completionHandler: completion)
}
case .enqueued:
entryQueue.removeAll()
case .all:
entryQueue.removeAll()
rootVC.animateOutLastEntry(completionHandler: completion)
}
}
/** Layout the view-hierarchy rooted in the window */
func layoutIfNeeded() {
entryWindow?.layoutIfNeeded()
}
/** Privately used to prepare the root view controller and show the entry immediately */
private func show(entryView: EKEntryView, presentInsideKeyWindow: Bool, rollbackWindow: SwiftEntryKit.RollbackWindow) {
guard let entryVC = prepare(for: entryView.attributes, presentInsideKeyWindow: presentInsideKeyWindow) else {
return
}
entryVC.configure(entryView: entryView)
self.entryView = entryView
self.rollbackWindow = rollbackWindow
}
}

View File

@@ -0,0 +1,23 @@
//
// EKWrapperView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
class EKWrapperView: UIView {
var isAbleToReceiveTouches = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if isAbleToReceiveTouches {
return super.hitTest(point, with: event)
}
if let view = super.hitTest(point, with: event), view != self {
return view
}
return nil
}
}

View File

@@ -0,0 +1,98 @@
//
// EKEntryCacher.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 9/1/18.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import Foundation
struct CachedEntry {
let view: EKEntryView
let presentInsideKeyWindow: Bool
let rollbackWindow: SwiftEntryKit.RollbackWindow
}
protocol EntryCachingHeuristic: AnyObject {
var entries: [CachedEntry] { set get }
var isEmpty: Bool { get }
func dequeue() -> CachedEntry?
func enqueue(entry: CachedEntry)
func removeEntries(by name: String)
func removeEntries(withPriorityLowerOrEqualTo priority: EKAttributes.Precedence.Priority)
func remove(entry: CachedEntry)
func removeAll()
func contains(entryNamed name: String) -> Bool
}
extension EntryCachingHeuristic {
var isEmpty: Bool {
return entries.isEmpty
}
func contains(entryNamed name: String) -> Bool {
return entries.contains { $0.view.attributes.name == name }
}
func dequeue() -> CachedEntry? {
guard let first = entries.first else {
return nil
}
entries.removeFirst()
return first
}
func removeEntries(withPriorityLowerOrEqualTo priority: EKAttributes.Precedence.Priority) {
while let index = (entries.firstIndex { $0.view.attributes.precedence.priority <= priority }) {
entries.remove(at: index)
}
}
func removeEntries(by name: String) {
while let index = (entries.firstIndex { $0.view.attributes.name == name }) {
entries.remove(at: index)
}
}
func remove(entry: CachedEntry) {
guard let index = (entries.firstIndex { $0.view == entry.view }) else {
return
}
entries.remove(at: index)
}
func removeAll() {
entries.removeAll()
}
}
class EKEntryChronologicalQueue: EntryCachingHeuristic {
var entries: [CachedEntry] = []
func enqueue(entry: CachedEntry) {
entries.append(entry)
}
}
class EKEntryPriorityQueue: EntryCachingHeuristic {
var entries: [CachedEntry] = []
func enqueue(entry: CachedEntry) {
let entryPriority = entry.view.attributes.precedence.priority
let index = entries.firstIndex {
return entryPriority > $0.view.attributes.precedence.priority
}
if let index = index {
entries.insert(entry, at: index)
} else {
entries.append(entry)
}
}
}