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,14 @@
//
// QLCompatibility.swift
// Pods
//
// Created by Daniel Huri on 5/12/18.
//
import Foundation
import UIKit
public typealias QLAttribute = NSLayoutConstraint.Attribute
public typealias QLRelation = NSLayoutConstraint.Relation
public typealias QLView = UIView
public typealias QLPriority = UILayoutPriority

View File

@@ -0,0 +1,107 @@
//
// QLUtils.swift
// QuickLayout
//
// Created by Daniel Huri on 11/21/17.
//
import Foundation
import UIKit
/**
Typealias for dictionary that contains multiple constraints
*/
public typealias QLMultipleConstraints = [QLAttribute: NSLayoutConstraint]
/**
Extends layout priority to other readable types
*/
public extension QLPriority {
static let must = QLPriority(rawValue: 999)
static let zero = QLPriority(rawValue: 0)
}
/**
Represents pair of attributes
*/
public struct QLAttributePair {
public let first: QLAttribute
public let second: QLAttribute
}
/**
Represents size constraints
*/
public struct QLSizeConstraints {
public let width: NSLayoutConstraint
public let height: NSLayoutConstraint
}
/**
Represents center constraints
*/
public struct QLCenterConstraints {
public let x: NSLayoutConstraint
public let y: NSLayoutConstraint
}
/**
Represents axis constraints (might be .top and .bottom, .left and .right, .leading and .trailing)
*/
public struct QLAxisConstraints {
public let first: NSLayoutConstraint
public let second: NSLayoutConstraint
}
/**
Represents center and size constraints
*/
public struct QLFillConstraints {
public let center: QLCenterConstraints
public let size: QLSizeConstraints
}
/**
Represents pair of priorities
*/
public struct QLPriorityPair {
public let horizontal: QLPriority
public let vertical: QLPriority
public static var required: QLPriorityPair {
return QLPriorityPair(.required, .required)
}
public static var must: QLPriorityPair {
return QLPriorityPair(.must, .must)
}
public init(_ horizontal: QLPriority, _ vertical: QLPriority) {
self.horizontal = horizontal
self.vertical = vertical
}
}
/**
Represents axis description
*/
public enum QLAxis {
case horizontally
case vertically
public var attributes: QLAttributePair {
let first: QLAttribute
let second: QLAttribute
switch self {
case .horizontally:
first = .left
second = .right
case .vertically:
first = .top
second = .bottom
}
return QLAttributePair(first: first, second: second)
}
}

View File

@@ -0,0 +1,111 @@
//
// QLView+QLContentWrap.swift
// QuickLayout
//
// Created by Daniel Huri on 11/21/17.
//
import Foundation
import UIKit
// MARK: Content Compression Resistance & Content Hugging Priority
public extension QLView {
/**
Force hugging and compression resistance for the given axes, using variadic parameter.
- parameter axes: The axes
*/
func forceContentWrap(_ axes: QLAxis...) {
if axes.contains(.vertically) {
verticalHuggingPriority = .required
verticalCompressionResistancePriority = .required
}
if axes.contains(.horizontally) {
horizontalHuggingPriority = .required
horizontalCompressionResistancePriority = .required
}
}
/**
Force hugging and compression resistance vertically and horizontally.
*/
func forceContentWrap() {
contentHuggingPriority = .required
contentCompressionResistancePriority = .required
}
/**
Vertical hugging priority
*/
var verticalHuggingPriority: QLPriority {
set {
setContentHuggingPriority(newValue, for: .vertical)
}
get {
return contentHuggingPriority(for: .vertical)
}
}
/**
Horizontal hugging priority
*/
var horizontalHuggingPriority: QLPriority {
set {
setContentHuggingPriority(newValue, for: .horizontal)
}
get {
return contentHuggingPriority(for: .horizontal)
}
}
/**
Content hugging priority (Vertical & Horizontal)
*/
var contentHuggingPriority: QLPriorityPair {
set {
horizontalHuggingPriority = newValue.horizontal
verticalHuggingPriority = newValue.vertical
}
get {
return QLPriorityPair(horizontalHuggingPriority, verticalHuggingPriority)
}
}
/**
Vertical content compression resistance priority
*/
var verticalCompressionResistancePriority: QLPriority {
set {
setContentCompressionResistancePriority(newValue, for: .vertical)
}
get {
return contentCompressionResistancePriority(for: .vertical)
}
}
/**
Horizontal content compression resistance priority
*/
var horizontalCompressionResistancePriority: QLPriority {
set {
setContentCompressionResistancePriority(newValue, for: .horizontal)
}
get {
return contentCompressionResistancePriority(for: .horizontal)
}
}
/**
Content compression resistance priority (Vertical & Horizontal)
*/
var contentCompressionResistancePriority: QLPriorityPair {
set {
horizontalCompressionResistancePriority = newValue.horizontal
verticalCompressionResistancePriority = newValue.vertical
}
get {
return QLPriorityPair(horizontalCompressionResistancePriority, verticalCompressionResistancePriority)
}
}
}

View File

@@ -0,0 +1,267 @@
//
// QLView+QuickLayout.swift
// QuickLayout
//
// Created by Daniel Huri on 11/19/17.
//
import Foundation
import UIKit
public extension QLView {
/**
Set constant value of an edge.
Should be used with *width* or *height*
- parameter edge: Edge type.
- parameter value: Edge size.
- parameter relation: Relation to the given constant value (default is *.equal*).
- parameter ratio: Ratio of the cconstant constraint to actual given value (default is *1*)
- parameter priority: Constraint's priority (default is *.required*).
- returns: The applied constraint (discardable).
*/
@discardableResult
func set(_ edge: QLAttribute, of value: CGFloat, relation: QLRelation = .equal,
ratio: CGFloat = 1.0, priority: QLPriority = .required) -> NSLayoutConstraint {
if translatesAutoresizingMaskIntoConstraints {
translatesAutoresizingMaskIntoConstraints = false
}
let constraint = NSLayoutConstraint(item: self, attribute: edge, relatedBy: relation, toItem: nil, attribute: .notAnAttribute, multiplier: ratio, constant: value)
constraint.priority = priority
addConstraint(constraint)
return constraint
}
/**
Set constant value for multiple edges simultaniously, using variadic parameter.
Should be used with *width* or *height*
- parameter edges: Edge types.
- parameter value: Edges size.
- parameter priority: Constraint's priority (default is *.required*).
- returns: The applied constraints in QLMultipleConstraints - see definition (discardable).
*/
@discardableResult
func set(_ edges: QLAttribute..., of value: CGFloat, relation: QLRelation = .equal,
ratio: CGFloat = 1.0, priority: QLPriority = .required) -> QLMultipleConstraints {
return set(edges, to: value, relation: relation, ratio: ratio, priority: priority)
}
/** **PRIVATELY USED** AS A REPLACEMENT for the variadic version for the method*/
@discardableResult
func set(_ edges: [QLAttribute], to value: CGFloat, relation: QLRelation = .equal,
ratio: CGFloat = 1.0, priority: QLPriority = .required) -> QLMultipleConstraints {
var constraints: QLMultipleConstraints = [:]
let uniqueEdges = Set(edges)
for edge in uniqueEdges {
let constraint = set(edge, of: value, priority: priority)
constraints[edge] = constraint
}
return constraints
}
/**
Layout edge to another view's edge.
- You can optionally define relation, ratio, constant and priority (each gets a default value)
- For example - Can be used to align self *left* edge to the *right* of another view.
- *self* and *view* must be directly connected (siblings / child-parent) in the view hierarchy.
- *superview* must not be *nil*.
- parameter edge: The edge of the first view. If not sent or *nil* - The function automatically assumes *edge* to be *otherEdge*
- parameter otherEdge: The edge of the second view.
- parameter view: The second view that self must be aligned with.
- parameter relation: The relation of the first edge to the second edge (default is .equal)
- parameter ratio: The ratio of the edge in relative to the superview edge (default is 1).
- parameter offset: Additional offset which is applied to the constraint (default is 0).
- parameter priority: Constraint's priority (default is *.required*).
- returns: The instance of the constraint that was applied (discardable). nil if method failed to apply the constraint.
*/
@discardableResult
func layout(_ edge: QLAttribute? = nil, to otherEdge: QLAttribute, of view: QLView,
relation: QLRelation = .equal, ratio: CGFloat = 1.0, offset: CGFloat = 0,
priority: QLPriority = .required) -> NSLayoutConstraint? {
guard isValidForQuickLayout else {
print("\(String(describing: self)) Error in func: \(#function)")
return nil
}
let constraint = NSLayoutConstraint(item: self, attribute: edge ?? otherEdge, relatedBy: relation, toItem: view, attribute: otherEdge, multiplier: ratio, constant: offset)
constraint.priority = priority
superview!.addConstraint(constraint)
return constraint
}
/**
Layout multiple edges of the view to the corresonding edges of another given view.
- You can optionally define relation, ratio, constant and priority (each gets a default value)
- For example - Can be used to align self *left* and *right* edges the same edge of another given view.
- *self* and *view* must be directly connected (siblings / child-parent) in the view hierarchy.
- *superview* must not be *nil*.
- parameter edges: The view edges
- parameter view: Another view that self must be aligned with.
- parameter relation: The relation of the edges. Can be applied to *.width* or *height* for example. (default is *.equal*).
- parameter ratio: The ratio of the edges to the other view edges (default is 1).
- parameter offset: Additional offset which is applied to each of the constraints (default is 0).
- parameter priority: Constraints' priority (default is *.required*).
- returns: The instance of the constraint that was applied (discardable). *nil* if the method failed to apply the constraint.
*/
@discardableResult
func layout(_ edges: QLAttribute..., to view: QLView, relation: QLRelation = .equal,
ratio: CGFloat = 1.0, offset: CGFloat = 0,
priority: QLPriority = .required) -> QLMultipleConstraints {
var constraints: QLMultipleConstraints = [:]
guard isValidForQuickLayout else {
print("\(String(describing: self)) Error in func: \(#function)")
return constraints
}
let uniqueEdges = Set(edges)
for edge in uniqueEdges {
let constraint = NSLayoutConstraint(item: self, attribute: edge, relatedBy: relation, toItem: view, attribute: edge, multiplier: ratio, constant: offset)
constraint.priority = priority
superview!.addConstraint(constraint)
constraints[edge] = constraint
}
return constraints
}
/**
Layout edge to the same edge of superview.
- Example of usage: *view.layoutToSuperview(.top)* makes *view* cling to the *top* of it's *superview*.
- You can optionally define ratio, constant and priority (each gets a default value)
- *superview* must not be *nil*.
- parameter edge: The edge (.width, .height, .left, .right, .leading, .trailing, etc...)
- parameter relation: The relation of the edge to the superview's corresponding edge (default is *.equal*)
- parameter ratio: The ratio of the edge in relative to the superview edge (default is 1).
- parameter offset: Additional offset from that can be applied to the constraint (default is 0).
- parameter priority: Constraint's priority (default is *.required*).
- returns: The instance of the constraint that was applied (discardable). Nil if method failed to apply constraint.
*/
@discardableResult
func layoutToSuperview(_ edge: QLAttribute, relation: QLRelation = .equal,
ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> NSLayoutConstraint? {
guard isValidForQuickLayout else {
print("\(String(describing: self)) Error in func: \(#function)")
return nil
}
let constraint = NSLayoutConstraint(item: self, attribute: edge, relatedBy: relation, toItem: superview, attribute: edge, multiplier: ratio, constant: offset)
constraint.priority = priority
superview!.addConstraint(constraint)
return constraint
}
/**
Layout multiple edges to the same edges as superview, using variadic parameter.
Example for edges value:
- You can optionally define ratio, constant and priority (each gets a default value)
- *superview* must not be *nil*.
- parameter edges: The edges (.width, .height, .left, .right, .leading, .trailing, etc...)
- parameter relation: The relation of the edges to the superview's corresponding edges (default is *.equal*)
- parameter ratio: The ratio of the edges in relative to the superview edge (default is 1).
- parameter offset: Additional offset from that can be applied to the constraints (default is 0).
- parameter priority: Constraints' priority (default is *.required*).
- returns: The instance of QLMultipleConstraints - see type definition (discardable).
*/
@discardableResult
func layoutToSuperview(_ edges: QLAttribute..., relation: QLRelation = .equal,
ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> QLMultipleConstraints {
var constraints: QLMultipleConstraints = [:]
guard !edges.isEmpty && isValidForQuickLayout else {
return constraints
}
let uniqueEdges = Set(edges)
for edge in uniqueEdges {
let constraint = NSLayoutConstraint(item: self, attribute: edge, relatedBy: relation, toItem: superview, attribute: edge, multiplier: ratio, constant: offset)
constraint.priority = priority
superview!.addConstraint(constraint)
constraints[edge] = constraint
}
return constraints
}
/**
Layout to one of the superview's axes.
- You can optionally define ratio, constant and priority (each gets a default value)
- *superview* must not be *nil*.
- parameter axis: The axis to which the view must be stretched (horizontally or vertically)
- parameter offset: Represents an additional edge offset from that can be applied to the constraints (default is 0)
- parameter priority: Represents constraint's priority (default is *.required*)
- returns: The instance of the constraint that was applied (discardable).
*/
@discardableResult
func layoutToSuperview(axis: QLAxis, offset: CGFloat = 0,
priority: QLPriority = .required) -> QLAxisConstraints? {
let attributes = axis.attributes
guard let first = layoutToSuperview(attributes.first, offset: offset, priority: priority) else {
return nil
}
guard let second = layoutToSuperview(attributes.second, offset: -offset, priority: priority) else {
return nil
}
return QLAxisConstraints(first: first, second: second)
}
/**
Size to superview with a given ratio and constant
- *superview* must not be *nil*.
- parameter ratio: The ratio of view to the size of superview.
- parameter offset: Represents an additional edge offset from that can be applied to the size (default is 0)
- parameter priority: Represents constraint's priority (default is *.required*)
- returns: The instance of QLSizeConstraints - see definition (discardable).
*/
@discardableResult
func sizeToSuperview(withRatio ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> QLSizeConstraints? {
let size = layoutToSuperview(.width, .height, ratio: ratio, offset: offset, priority: priority)
guard !size.isEmpty else {
return nil
}
return QLSizeConstraints(width: size[.width]!, height: size[.height]!)
}
/**
Center in superview with an optional offset
- *superview* must not be *nil*.
- parameter offset: Represents an additional offset from the center (default is 0)
- parameter priority: Represents constraint's priority (default is *.required*)
- returns: The instance of QLCenterConstraints - see definition (discardable).
*/
@discardableResult
func centerInSuperview(offset: CGFloat = 0, priority: QLPriority = .required) -> QLCenterConstraints? {
let center = layoutToSuperview(.centerX, .centerY, offset: offset)
guard !center.isEmpty else {
return nil
}
return QLCenterConstraints(x: center[.centerX]!, y: center[.centerY]!)
}
/**
Fill superview totally (center and size to superview)
- *superview* must not be *nil*.
- parameter ratio: Ratio to the superview's size (default is 1)
- parameter offset: Offset from center (default is 0)
- parameter priority: Represents constraint's priority (default is *.required*)
- returns: The instance of QLFillConstraints - see definition (discardable).
*/
@discardableResult
func fillSuperview(withSizeRatio ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> QLFillConstraints? {
guard let center = centerInSuperview(priority: priority) else {
return nil
}
guard let size = sizeToSuperview(withRatio: ratio, offset: offset, priority: priority) else {
return nil
}
return QLFillConstraints(center: center, size: size)
}
/** **PRIVATELY USED** to test for validation*/
var isValidForQuickLayout: Bool {
guard superview != nil else {
print("\(String(describing: self)):\(#function) - superview is unexpectedly nullified")
return false
}
if translatesAutoresizingMaskIntoConstraints {
translatesAutoresizingMaskIntoConstraints = false
}
return true
}
}

View File

@@ -0,0 +1,214 @@
//
// QLViewArray+QuickLayout.swift
// QuickLayout
//
// Created by Daniel Huri on 11/20/17.
//
import Foundation
import UIKit
// MARK: Multiple Views in Array
public extension Array where Element: QLView {
/**
All elements in the collection recieve constant value for the given edge.
- parameter edge: Should be used with *.width* or *.height*.
- parameter value: The size of the edge.
- parameter priority: Constraint's priority (default is *.required*).
- returns: The instance of the constraint that was applied (discardable).
*/
@discardableResult
func set(_ edge: QLAttribute, of value: CGFloat,
priority: QLPriority = .required) -> [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
for view in self {
let constraint = view.set(edge, of: value)
constraints.append(constraint)
}
return constraints
}
/**
All elements in the collection recieve constant value for the given edges, using variadic parameter.
- parameter edges: Should be used with *.width* or *.height*.
- parameter value: The size of the edge.
- parameter priority: Constraint's priority (default is *.required*).
- returns: The instance of the constraint that was applied (discardable).
*/
@discardableResult
func set(_ edges: QLAttribute..., of value: CGFloat,
priority: QLPriority = .required) -> [QLMultipleConstraints] {
var constraintsArray: [QLMultipleConstraints] = []
for view in self {
let constraints = view.set(edges, to: value, priority: priority)
constraintsArray.append(constraints)
}
return constraintsArray
}
/**
Spread elements consecutively according to the given axis.
- parameter axis: The axis: *.vertically*, *horizontally*
- parameter stretchEdgesToSuperview: Decides whether the first and last items in the array must be clipped to their parent edges.
- parameter priority: Constraint's priority (default is *.required*).
- returns: Array of constraints that were applied (discardable)
*/
@discardableResult
func spread(_ axis: QLAxis, stretchEdgesToSuperview: Bool = false, offset: CGFloat = 0,
priority: QLPriority = .required) -> [NSLayoutConstraint] {
guard isValidForQuickLayout else {
return []
}
let attributes = axis.attributes
var constraints: [NSLayoutConstraint] = []
if stretchEdgesToSuperview {
let constraint = first!.layoutToSuperview(attributes.first, offset: offset)!
constraints.append(constraint)
}
for (index, view) in enumerated() {
guard index > 0 else {
continue
}
let previousView = self[index - 1]
let constraint = view.layout(attributes.first, to: attributes.second, of: previousView, offset: offset, priority: priority)!
constraints.append(constraint)
}
if stretchEdgesToSuperview {
let constraint = last!.layoutToSuperview(attributes.second, offset: -offset)!
constraints.append(constraint)
}
return constraints
}
/**
Layout elements to superview's axis
- parameter axis: The axis: *.vertically*, *horizontally*
- parameter offset: Additional side offset that must be applied (identical spacing from each side)
- parameter priority: Constraint's priority (default is *.required*).
- returns: Array of QLAxisConstraints - see definition (discardable)
*/
@discardableResult
func layoutToSuperview(axis: QLAxis, offset: CGFloat = 0,
priority: QLPriority = .required) -> [QLAxisConstraints] {
let attributes = axis.attributes
let firstConstraints = layoutToSuperview(attributes.first, offset: offset, priority: priority)
guard !firstConstraints.isEmpty else {
return []
}
let secondConstraints = layoutToSuperview(attributes.second, offset: -offset, priority: priority)
guard !secondConstraints.isEmpty else {
return []
}
var constraints: [QLAxisConstraints] = []
for (first, second) in zip(firstConstraints, secondConstraints) {
constraints.append(QLAxisConstraints(first: first, second: second))
}
return constraints
}
/**
Layout elements' edges to superview's edge (The same edge - top to top, bottom to bottom, etc...)
- parameter edge: The edge of the view / superview
- parameter ratio: The ratio of the edge in relation to the superview's (default is 1).
- parameter offset: Additional offset from that must be applied to the constraint (default is 0).
- parameter priority: Constraint's priority (default is *.required*).
- returns: Array of applied constraints - see definition (discardable)
*/
@discardableResult
func layoutToSuperview(_ edge: QLAttribute, ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> [NSLayoutConstraint] {
guard isValidForQuickLayout else {
return []
}
return layout(to: edge, of: first!.superview!, ratio: ratio, offset: offset, priority: priority)
}
/**
Layout elements' edges to to anchorView edge
- parameter firstEdge: The edge of the elements in the array
- parameter anchorEdge: The edge of the anchor view
- parameter anchorView: The anchor view
- parameter ratio: The ratio of the edge in relative to the superview edge (default is 1).
- parameter offset: Additional offset from that can be applied to the constraints (default is 0).
- parameter priority: Constraints' priority (default is *.required*).
- returns: Array of applied constraints - see definition (discardable)
*/
@discardableResult
func layout(_ firstEdge: QLAttribute? = nil, to anchorEdge: QLAttribute,
of anchorView: QLView, ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> [NSLayoutConstraint] {
guard isValidForQuickLayout else {
return []
}
let edge: QLAttribute
if let firstEdge = firstEdge {
edge = firstEdge
} else {
edge = anchorEdge
}
var result: [NSLayoutConstraint] = []
for view in self {
let constraint = view.layout(edge, to: anchorEdge, of: anchorView, ratio: ratio, offset: offset, priority: priority)!
result.append(constraint)
}
return result
}
/**
Layout elements' multiple edges to to anchorView's same edges (top to top, bottom to bottom, etc...)
- parameter edges: The edges of the view - variadic parameter
- parameter anchorView: The anchor view
- parameter ratio: The ratio of the edge in relative to the superview edge (default is 1).
- parameter offset: Additional offset from that can be applied to the constraints (default is 0).
- parameter priority: Constraints' priority (default is *.required*).
- returns: Array of applied constraints, each element is of type QLMultipleConstraints - see definition (discardable)
*/
@discardableResult
func layout(_ edges: QLAttribute..., to anchorView: QLView,
ratio: CGFloat = 1, offset: CGFloat = 0,
priority: QLPriority = .required) -> [QLMultipleConstraints] {
guard !edges.isEmpty && isValidForQuickLayout else {
return []
}
// Avoid duplicities
let uniqueEdges = Set(edges)
var result: [QLMultipleConstraints] = []
for view in self {
var multipleConstraints: QLMultipleConstraints = [:]
for edge in uniqueEdges {
let constraint = view.layout(to: edge, of: anchorView, ratio: ratio, offset: offset, priority: priority)!
multipleConstraints[edge] = constraint
}
result.append(multipleConstraints)
}
return result
}
/** **PRIVATELY USED** to test for validation*/
var isValidForQuickLayout: Bool {
guard !isEmpty else {
print("\(String(describing: self)) Error in func: \(#function), Views collection is empty!")
return false
}
for view in self {
guard view.isValidForQuickLayout else {
print("\(String(describing: self)) Error in func: \(#function)")
return false
}
}
return true
}
}

View File

@@ -0,0 +1,18 @@
//
// UIApplication+EKAppearance.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/25/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
extension UIApplication {
func set(statusBarStyle: EKAttributes.StatusBar) {
let appearance = statusBarStyle.appearance
UIApplication.shared.isStatusBarHidden = !appearance.visible
UIApplication.shared.statusBarStyle = appearance.style
}
}

View File

@@ -0,0 +1,28 @@
//
// UIColor+Utils.swift
// SwiftEntryKit
//
// Created by Daniel on 21/07/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
import UIKit
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
assert(red >= 0 && red <= 255, "Invalid red component")
assert(green >= 0 && green <= 255, "Invalid green component")
assert(blue >= 0 && blue <= 255, "Invalid blue component")
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
}
convenience init(rgb: Int) {
self.init(
red: (rgb >> 16) & 0xFF,
green: (rgb >> 8) & 0xFF,
blue: rgb & 0xFF
)
}
}

View File

@@ -0,0 +1,14 @@
//
// UIEdgeInsets.swift
// FBSnapshotTestCase
//
// Created by Daniel Huri on 4/21/18.
//
import UIKit
extension UIEdgeInsets {
var hasVerticalInsets: Bool {
return top > 0 || bottom > 0
}
}

View File

@@ -0,0 +1,15 @@
//
// UIView+FrameStyle.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/24/18.
//
import Foundation
import UIKit
extension UIRectCorner {
static let top: UIRectCorner = [.topLeft, .topRight]
static let bottom: UIRectCorner = [.bottomLeft, .bottomRight]
static let none: UIRectCorner = []
}

View File

@@ -0,0 +1,46 @@
//
// UIView+Shadow.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/25/18.
//
import UIKit
extension UIView {
func applyDropShadow(withOffset offset: CGSize,
opacity: Float,
radius: CGFloat,
color: UIColor) {
layer.applyDropShadow(withOffset: offset,
opacity: opacity,
radius: radius,
color: color)
}
func removeDropShadow() {
layer.removeDropShadow()
}
}
extension CALayer {
func applyDropShadow(withOffset offset: CGSize,
opacity: Float,
radius: CGFloat,
color: UIColor) {
shadowOffset = offset
shadowOpacity = opacity
shadowRadius = radius
shadowColor = color.cgColor
shouldRasterize = true
rasterizationScale = UIScreen.main.scale
}
func removeDropShadow() {
shadowOffset = .zero
shadowOpacity = 0
shadowRadius = 0
shadowColor = UIColor.clear.cgColor
shouldRasterize = false
}
}

View File

@@ -0,0 +1,149 @@
//
// UILabel+Message.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 04/14/2018.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
extension UILabel {
var style: EKProperty.LabelStyle {
set {
font = newValue.font
textColor = newValue.color(for: traitCollection)
textAlignment = newValue.alignment
numberOfLines = newValue.numberOfLines
}
get {
return EKProperty.LabelStyle(font: font,
color: EKColor(textColor),
alignment: textAlignment,
numberOfLines: numberOfLines)
}
}
var content: EKProperty.LabelContent {
set {
text = newValue.text
accessibilityIdentifier = newValue.accessibilityIdentifier
style = newValue.style
}
get {
return EKProperty.LabelContent(text: text ?? "", style: style)
}
}
}
extension UIButton {
var buttonContent: EKProperty.ButtonContent {
set {
setTitle(newValue.label.text, for: .normal)
setTitleColor(newValue.label.style.color(for: traitCollection), for: .normal)
titleLabel?.font = newValue.label.style.font
accessibilityIdentifier = newValue.accessibilityIdentifier
backgroundColor = newValue.backgroundColor.color(
for: traitCollection,
mode: newValue.displayMode
)
}
get {
fatalError("buttonContent doesn't have a getter")
}
}
}
extension UIImageView {
var imageContent: EKProperty.ImageContent {
set {
stopAnimating()
if newValue.images.count == 1 {
image = newValue.images.first
} else {
animationImages = newValue.images.map {
$0.withRenderingMode(.alwaysTemplate)
}
animationDuration = newValue.imageSequenceAnimationDuration
}
contentMode = newValue.contentMode
tintColor = newValue.tint?.color(for: traitCollection,
mode: newValue.displayMode)
accessibilityIdentifier = newValue.accessibilityIdentifier
if let size = newValue.size {
set(.width, of: size.width)
set(.height, of: size.height)
} else {
forceContentWrap()
}
if newValue.makesRound {
clipsToBounds = true
if let size = newValue.size {
layer.cornerRadius = min(size.width, size.height) * 0.5
} else {
layoutIfNeeded()
layer.cornerRadius = min(bounds.width, bounds.height) * 0.5
}
}
startAnimating()
if case .animate(duration: let duration,
options: let options,
transform: let transform) = newValue.animation {
let options: UIView.AnimationOptions = [.repeat, .autoreverse, options]
// A hack that forces the animation to run on the main thread,
// on one of the next run loops
DispatchQueue.main.async {
UIView.animate(withDuration: duration,
delay: 0,
options: options,
animations: {
self.transform = transform
}, completion: nil)
}
}
}
get {
fatalError("imageContent doesn't have a getter")
}
}
}
extension UITextField {
var placeholder: EKProperty.LabelContent {
set {
attributedPlaceholder = NSAttributedString(
string: newValue.text,
attributes: [
.font: newValue.style.font,
.foregroundColor: newValue.style.color(for: traitCollection)
]
)
}
get {
fatalError("placeholder doesn't have a getter")
}
}
var textFieldContent: EKProperty.TextFieldContent {
set {
placeholder = newValue.placeholder
keyboardType = newValue.keyboardType
textColor = newValue.textStyle.color(for: traitCollection)
font = newValue.textStyle.font
textAlignment = newValue.textStyle.alignment
isSecureTextEntry = newValue.isSecure
text = newValue.textContent
tintColor = newValue.tintColor(for: traitCollection)
accessibilityIdentifier = newValue.accessibilityIdentifier
}
get {
fatalError("textFieldContent doesn't have a getter")
}
}
}

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)
}
}
}

View File

@@ -0,0 +1,90 @@
//
// EKAlertMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/5/18.
//
import UIKit
final public class EKAlertMessageView: EKSimpleMessageView, EntryAppearanceDescriptor {
// MARK: Props
var buttonBarView: EKButtonBarView!
private var buttonsBarCompressedConstraint: NSLayoutConstraint!
private var buttonsBarExpandedConstraint: NSLayoutConstraint!
// MARK: EntryAppearenceDescriptor
var bottomCornerRadius: CGFloat = 0 {
didSet {
buttonBarView.bottomCornerRadius = bottomCornerRadius
}
}
// MARK: Setup
public init(with message: EKAlertMessage) {
super.init(with: message.simpleMessage)
setupButtonBarView(with: message.buttonBarContent)
layoutContent(with: message)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupButtonBarView(with content: EKProperty.ButtonBarContent) {
buttonBarView = EKButtonBarView(with: content)
buttonBarView.clipsToBounds = true
addSubview(buttonBarView)
}
func layoutContent(with message: EKAlertMessage) {
switch message.imagePosition {
case .top:
messageContentView.verticalMargins = 16
messageContentView.horizontalMargins = 16
messageContentView.labelsOffset = 5
if let thumbImageView = thumbImageView {
thumbImageView.layoutToSuperview(.top, offset: 20)
thumbImageView.layoutToSuperview(.centerX)
messageContentView.layout(.top, to: .bottom, of: thumbImageView)
} else {
messageContentView.layoutToSuperview(.top)
}
messageContentView.layoutToSuperview(axis: .horizontally)
buttonBarView.layout(.top, to: .bottom, of: messageContentView)
case .left:
messageContentView.verticalMargins = 0
messageContentView.horizontalMargins = 0
messageContentView.labelsOffset = 5
if let thumbImageView = thumbImageView {
thumbImageView.layoutToSuperview(.top, .left, offset: 16)
messageContentView.layout(.left, to: .right, of: thumbImageView, offset: 12)
messageContentView.layout(to: .top, of: thumbImageView, offset: 2)
} else {
messageContentView.layoutToSuperview(.left, .top, offset: 16)
}
messageContentView.layoutToSuperview(.right, offset: -16)
buttonBarView.layout(.top, to: .bottom, of: messageContentView, offset: 10)
}
buttonBarView.layoutToSuperview(axis: .horizontally)
buttonBarView.layoutToSuperview(.bottom)
buttonBarView.alpha = 0
if !message.buttonBarContent.content.isEmpty {
if message.buttonBarContent.expandAnimatedly {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.buttonBarView.expand()
}
} else {
buttonBarView.expand()
}
}
}
}

View File

@@ -0,0 +1,130 @@
//
// EKFormMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/15/18.
//
import UIKit
final public class EKFormMessageView: UIView {
private let scrollViewVerticalOffset: CGFloat = 20
// MARK: Props
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
private let textFieldsContent: [EKProperty.TextFieldContent]
private var textFieldViews: [EKTextField] = []
private var buttonBarView: EKButtonBarView!
private let titleContent: EKProperty.LabelContent
// MARK: Setup
public init(with title: EKProperty.LabelContent,
textFieldsContent: [EKProperty.TextFieldContent],
buttonContent: EKProperty.ButtonContent) {
self.titleContent = title
self.textFieldsContent = textFieldsContent
super.init(frame: UIScreen.main.bounds)
setupScrollView()
setupTitleLabel()
setupTextFields(with: textFieldsContent)
setupButton(with: buttonContent)
setupTapGestureRecognizer()
scrollView.layoutIfNeeded()
set(.height,
of: scrollView.contentSize.height + scrollViewVerticalOffset * 2,
priority: .defaultHigh)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupTextFields(with textFieldsContent: [EKProperty.TextFieldContent]) {
var textFieldIndex = 0
textFieldViews = textFieldsContent.map { content -> EKTextField in
let textField = EKTextField(with: content)
scrollView.addSubview(textField)
textField.tag = textFieldIndex
textFieldIndex += 1
return textField
}
textFieldViews.first!.layout(.top, to: .bottom, of: titleLabel, offset: 20)
textFieldViews.spread(.vertically, offset: 5)
textFieldViews.layoutToSuperview(axis: .horizontally)
}
// Setup tap gesture
private func setupTapGestureRecognizer() {
let tapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(tapGestureRecognized)
)
tapGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(tapGestureRecognizer)
}
private func setupScrollView() {
addSubview(scrollView)
scrollView.layoutToSuperview(axis: .horizontally, offset: 20)
scrollView.layoutToSuperview(axis: .vertically, offset: scrollViewVerticalOffset)
scrollView.layoutToSuperview(.width, .height, offset: -scrollViewVerticalOffset * 2)
}
private func setupTitleLabel() {
scrollView.addSubview(titleLabel)
titleLabel.layoutToSuperview(.top, .width)
titleLabel.layoutToSuperview(axis: .horizontally)
titleLabel.forceContentWrap(.vertically)
titleLabel.content = titleContent
}
private func setupButton(with buttonContent: EKProperty.ButtonContent) {
var buttonContent = buttonContent
let action = buttonContent.action
buttonContent.action = { [weak self] in
self?.extractTextFieldsContent()
action?()
}
let buttonsBarContent = EKProperty.ButtonBarContent(
with: buttonContent,
separatorColor: .clear,
expandAnimatedly: true
)
buttonBarView = EKButtonBarView(with: buttonsBarContent)
buttonBarView.clipsToBounds = true
scrollView.addSubview(buttonBarView)
buttonBarView.expand()
buttonBarView.layout(.top, to: .bottom, of: textFieldViews.last!, offset: 20)
buttonBarView.layoutToSuperview(.centerX)
buttonBarView.layoutToSuperview(.width, offset: -40)
buttonBarView.layoutToSuperview(.bottom)
buttonBarView.layer.cornerRadius = 5
}
private func extractTextFieldsContent() {
for (content, textField) in zip(textFieldsContent, textFieldViews) {
content.contentWrapper.text = textField.text
}
}
/** Makes a specific text field the first responder */
public func becomeFirstResponder(with textFieldIndex: Int) {
textFieldViews[textFieldIndex].makeFirstResponder()
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
titleLabel.textColor = titleContent.style.color(for: traitCollection)
}
// MARK: User Intractions
// Tap Gesture
@objc func tapGestureRecognized() {
endEditing(true)
}
}

View File

@@ -0,0 +1,119 @@
//
// EKMessageContentView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public class EKMessageContentView: UIView {
// MARK: Properties
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private var horizontalConstraints: QLAxisConstraints!
private var topConstraint: NSLayoutConstraint!
private var bottomConstraint: NSLayoutConstraint!
private var labelsOffsetConstraint: NSLayoutConstraint!
public var titleContent: EKProperty.LabelContent! {
didSet {
titleLabel.content = titleContent
}
}
public var subtitleContent: EKProperty.LabelContent! {
didSet {
subtitleLabel.content = subtitleContent
}
}
public var titleAttributes: EKProperty.LabelStyle! {
didSet {
titleLabel.style = titleAttributes
}
}
public var subtitleAttributes: EKProperty.LabelStyle! {
didSet {
subtitleLabel.style = subtitleAttributes
}
}
public var title: String! {
didSet {
titleLabel.text = title
}
}
public var subtitle: String! {
didSet {
subtitleLabel.text = subtitle
}
}
public var verticalMargins: CGFloat = 20 {
didSet {
topConstraint.constant = verticalMargins
bottomConstraint.constant = -verticalMargins
layoutIfNeeded()
}
}
public var horizontalMargins: CGFloat = 20 {
didSet {
horizontalConstraints.first.constant = horizontalMargins
horizontalConstraints.second.constant = -horizontalMargins
layoutIfNeeded()
}
}
public var labelsOffset: CGFloat = 8 {
didSet {
labelsOffsetConstraint.constant = labelsOffset
layoutIfNeeded()
}
}
// MARK: Setup
public init() {
super.init(frame: UIScreen.main.bounds)
clipsToBounds = true
setupTitleLabel()
setupSubtitleLabel()
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupTitleLabel() {
addSubview(titleLabel)
topConstraint = titleLabel.layoutToSuperview(.top, offset: verticalMargins)
horizontalConstraints = titleLabel.layoutToSuperview(axis: .horizontally, offset: horizontalMargins)
titleLabel.forceContentWrap(.vertically)
}
private func setupSubtitleLabel() {
addSubview(subtitleLabel)
labelsOffsetConstraint = subtitleLabel.layout(.top, to: .bottom, of: titleLabel, offset: labelsOffset)
subtitleLabel.layout(to: .left, of: titleLabel)
subtitleLabel.layout(to: .right, of: titleLabel)
bottomConstraint = subtitleLabel.layoutToSuperview(.bottom, offset: -verticalMargins, priority: .must)
subtitleLabel.forceContentWrap(.vertically)
}
private func setupInterfaceStyle() {
titleLabel.textColor = titleContent?.style.color(for: traitCollection)
subtitleLabel.textColor = subtitleContent?.style.color(for: traitCollection)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupInterfaceStyle()
}
}

View File

@@ -0,0 +1,71 @@
//
// EKNotificationMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final public class EKNotificationMessageView: EKSimpleMessageView {
// MARK: Props
private var auxLabel: UILabel!
private var auxiliaryContent: EKProperty.LabelContent!
private let message: EKNotificationMessage
// MARK: Setup
public init(with message: EKNotificationMessage) {
self.message = message
super.init(with: message.simpleMessage)
setupAuxLabel(with: message.auxiliary)
layoutContent(with: message.insets)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupAuxLabel(with content: EKProperty.LabelContent?) {
auxiliaryContent = content
guard let content = content else {
return
}
auxLabel = UILabel()
auxLabel.content = content
addSubview(auxLabel)
}
private func layoutContent(with insets: EKNotificationMessage.Insets) {
messageContentView.verticalMargins = 0
messageContentView.horizontalMargins = 0
messageContentView.labelsOffset = insets.titleToDescription
if let thumbImageView = thumbImageView {
thumbImageView.layoutToSuperview(.left, offset: insets.contentInsets.left)
thumbImageView.layoutToSuperview(.top, offset: insets.contentInsets.top)
messageContentView.layout(.left, to: .right, of: thumbImageView, offset: 12)
messageContentView.layout(to: .top, of: thumbImageView, offset: 4)
} else {
messageContentView.layoutToSuperview(.left, offset: insets.contentInsets.left)
messageContentView.layoutToSuperview(.top, offset: insets.contentInsets.top)
}
if let auxLabel = auxLabel {
auxLabel.layoutToSuperview(.right, offset: -insets.contentInsets.right)
auxLabel.layoutToSuperview(.top, offset: insets.contentInsets.top + 2)
auxLabel.forceContentWrap()
messageContentView.layout(.right, to: .left, of: auxLabel)
} else {
messageContentView.layoutToSuperview(.right, offset: -insets.contentInsets.right)
}
messageContentView.layoutToSuperview(.bottom, offset: -insets.contentInsets.bottom)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
auxLabel?.textColor = auxiliaryContent?.style.color(for: traitCollection)
}
}

View File

@@ -0,0 +1,106 @@
//
// EKPopUpMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final public class EKPopUpMessageView: UIView {
// MARK: - Properties
private var imageView: UIImageView!
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let actionButton = UIButton()
private let message: EKPopUpMessage
// MARK: - Setup
public init(with message: EKPopUpMessage) {
self.message = message
super.init(frame: UIScreen.main.bounds)
setupImageView()
setupTitleLabel()
setupDescriptionLabel()
setupActionButton()
setupInterfaceStyle()
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupImageView() {
guard let themeImage = message.themeImage else {
return
}
imageView = UIImageView()
addSubview(imageView)
imageView.layoutToSuperview(.centerX)
switch themeImage.position {
case .centerToTop(offset: let value):
imageView.layout(.centerY, to: .top, of: self, offset: value)
case .topToTop(offset: let value):
imageView.layoutToSuperview(.top, offset: value)
}
imageView.imageContent = themeImage.image
}
private func setupTitleLabel() {
addSubview(titleLabel)
titleLabel.content = message.title
titleLabel.layoutToSuperview(axis: .horizontally, offset: 30)
if let imageView = imageView {
titleLabel.layout(.top, to: .bottom, of: imageView, offset: 20)
} else {
titleLabel.layoutToSuperview(.top, offset: 20)
}
titleLabel.forceContentWrap(.vertically)
}
private func setupDescriptionLabel() {
addSubview(descriptionLabel)
descriptionLabel.content = message.description
descriptionLabel.layoutToSuperview(axis: .horizontally, offset: 30)
descriptionLabel.layout(.top, to: .bottom, of: titleLabel, offset: 16)
descriptionLabel.forceContentWrap(.vertically)
}
private func setupActionButton() {
addSubview(actionButton)
let height: CGFloat = 45
actionButton.set(.height, of: height)
actionButton.layout(.top, to: .bottom, of: descriptionLabel, offset: 30)
actionButton.layoutToSuperview(.bottom, offset: -30)
actionButton.layoutToSuperview(.centerX)
let buttonAttributes = message.button
actionButton.buttonContent = buttonAttributes
actionButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 30)
actionButton.layer.cornerRadius = height * 0.5
actionButton.addTarget(self, action: #selector(actionButtonPressed), for: .touchUpInside)
}
private func setupInterfaceStyle() {
titleLabel.textColor = message.title.style.color(for: traitCollection)
imageView?.tintColor = message.themeImage?.image.tintColor(for: traitCollection)
let tapColor = message.button.highlighedLabelColor(for: traitCollection)
actionButton.setTitleColor(tapColor, for: .highlighted)
actionButton.setTitleColor(tapColor, for: .selected)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupInterfaceStyle()
}
// MARK: - User Interaction
@objc func actionButtonPressed() {
message.action()
}
}

View File

@@ -0,0 +1,110 @@
//
// EKRatingMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final public class EKRatingMessageView: UIView, EntryAppearanceDescriptor {
// MARK: Properties
private var message: EKRatingMessage
// MARK: EntryAppearenceDescriptor
var bottomCornerRadius: CGFloat = 0 {
didSet {
buttonBarView.bottomCornerRadius = bottomCornerRadius
}
}
private var selectedIndex: Int! {
didSet {
message.selectedIndex = selectedIndex
let item = message.ratingItems[selectedIndex]
set(title: item.title,
description: item.description)
}
}
private let messageContentView = EKMessageContentView()
private let symbolsView = EKRatingSymbolsContainerView()
private var buttonBarView: EKButtonBarView!
public init(with message: EKRatingMessage) {
self.message = message
super.init(frame: UIScreen.main.bounds)
setupMessageContentView()
setupSymbolsView()
setupButtonBarView()
set(title: message.initialTitle,
description: message.initialDescription)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func set(title: EKProperty.LabelContent,
description: EKProperty.LabelContent) {
messageContentView.titleContent = title
messageContentView.subtitleContent = description
UIView.animate(withDuration: 0.4,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0,
options: [.transitionCrossDissolve],
animations: {
SwiftEntryKit.layoutIfNeeded()
}, completion: nil)
}
private func setupMessageContentView() {
addSubview(messageContentView)
messageContentView.verticalMargins = 20
messageContentView.horizontalMargins = 30
messageContentView.layoutToSuperview(axis: .horizontally,
priority: .must)
messageContentView.layoutToSuperview(.top, offset: 10)
}
private func setupSymbolsView() {
addSubview(symbolsView)
symbolsView.setup(with: message) { [unowned self] (index: Int) in
self.message.selectedIndex = index
self.message.selection?(index)
self.selectedIndex = index
self.animateIn()
}
symbolsView.layoutToSuperview(.centerX)
symbolsView.layout(.top,
to: .bottom,
of: messageContentView,
offset: 10,
priority: .must)
}
private func setupButtonBarView() {
buttonBarView = EKButtonBarView(with: message.buttonBarContent)
buttonBarView.clipsToBounds = true
buttonBarView.alpha = 0
addSubview(buttonBarView)
buttonBarView.layout(.top,
to: .bottom,
of: symbolsView,
offset: 30)
buttonBarView.layoutToSuperview(.bottom)
buttonBarView.layoutToSuperview(axis: .horizontally)
}
// MARK: - Internal Animation
private func animateIn() {
layoutIfNeeded()
buttonBarView.expand()
}
}

View File

@@ -0,0 +1,58 @@
//
// EKMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/5/18.
//
import UIKit
public class EKSimpleMessageView: UIView {
// MARK: Props
var thumbImageView: UIImageView!
let messageContentView = EKMessageContentView()
private let message: EKSimpleMessage
// MARK: Setup
init(with message: EKSimpleMessage) {
self.message = message
super.init(frame: UIScreen.main.bounds)
setupThumbImageView(with: message.image)
setupMessageContentView(with: message.title,
description: message.description)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupThumbImageView(with content: EKProperty.ImageContent?) {
guard let content = content else {
return
}
thumbImageView = UIImageView()
addSubview(thumbImageView)
thumbImageView.imageContent = content
}
private func setupMessageContentView(with title: EKProperty.LabelContent,
description: EKProperty.LabelContent) {
messageContentView.titleContent = title
messageContentView.subtitleContent = description
addSubview(messageContentView)
}
private func setupInterfaceStyle() {
if let image = message.image {
thumbImageView?.tintColor = image.tint?.color(
for: traitCollection,
mode: image.displayMode
)
}
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupInterfaceStyle()
}
}

View File

@@ -0,0 +1,191 @@
//
// ButtonsBarView.swift
// SwiftEntryKit_Example
//
// Created by Daniel Huri on 4/28/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
/**
Dynamic button bar view
Buttons are set according to the received content.
1-2 buttons spread horizontally
3 or more buttons spread vertically
*/
final public class EKButtonBarView: UIView {
// MARK: - Properties
private var buttonViews: [EKButtonView] = []
private var separatorViews: [UIView] = []
private let buttonBarContent: EKProperty.ButtonBarContent
private let spreadAxis: QLAxis
private let oppositeAxis: QLAxis
private let relativeEdge: NSLayoutConstraint.Attribute
var bottomCornerRadius: CGFloat = 0 {
didSet {
adjustRoundCornersIfNecessary()
}
}
private lazy var buttonEdgeRatio: CGFloat = {
return 1.0 / CGFloat(self.buttonBarContent.content.count)
}()
private(set) lazy var intrinsicHeight: CGFloat = {
var height: CGFloat = 0
switch buttonBarContent.content.count {
case 0:
height += 1
case 1...buttonBarContent.horizontalDistributionThreshold:
height += buttonBarContent.buttonHeight
default:
for _ in 1...buttonBarContent.content.count {
height += buttonBarContent.buttonHeight
}
}
return height
}()
private var compressedConstraint: NSLayoutConstraint!
private lazy var expandedConstraint: NSLayoutConstraint = {
return set(.height, of: intrinsicHeight, priority: .defaultLow)
}()
// MARK: Setup
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(with buttonBarContent: EKProperty.ButtonBarContent) {
self.buttonBarContent = buttonBarContent
if buttonBarContent.content.count <= buttonBarContent.horizontalDistributionThreshold {
spreadAxis = .horizontally
oppositeAxis = .vertically
relativeEdge = .width
} else {
spreadAxis = .vertically
oppositeAxis = .horizontally
relativeEdge = .height
}
super.init(frame: .zero)
setupButtonBarContent()
setupSeparatorViews()
compressedConstraint = set(.height, of: 1, priority: .must)
}
public override func layoutSubviews() {
super.layoutSubviews()
adjustRoundCornersIfNecessary()
}
private func setupButtonBarContent() {
for content in buttonBarContent.content {
let buttonView = EKButtonView(content: content)
addSubview(buttonView)
buttonViews.append(buttonView)
}
layoutButtons()
}
private func layoutButtons() {
guard !buttonViews.isEmpty else {
return
}
let suffix = Array(buttonViews.dropFirst())
if !suffix.isEmpty {
suffix.layout(.height, to: buttonViews.first!)
}
buttonViews.layoutToSuperview(axis: oppositeAxis)
buttonViews.spread(spreadAxis, stretchEdgesToSuperview: true)
buttonViews.layout(relativeEdge, to: self, ratio: buttonEdgeRatio, priority: .must)
}
private func setupTopSeperatorView() {
let topSeparatorView = UIView()
addSubview(topSeparatorView)
topSeparatorView.set(.height, of: 1)
topSeparatorView.layoutToSuperview(.left, .right, .top)
separatorViews.append(topSeparatorView)
}
private func setupSeperatorView(after view: UIView) {
let midSepView = UIView()
addSubview(midSepView)
let sepAttribute: NSLayoutConstraint.Attribute
let buttonAttribute: NSLayoutConstraint.Attribute
switch oppositeAxis {
case .vertically:
sepAttribute = .centerX
buttonAttribute = .right
case .horizontally:
sepAttribute = .centerY
buttonAttribute = .bottom
}
midSepView.layout(sepAttribute, to: buttonAttribute, of: view)
midSepView.set(relativeEdge, of: 1)
midSepView.layoutToSuperview(axis: oppositeAxis)
separatorViews.append(midSepView)
}
private func setupSeparatorViews() {
setupTopSeperatorView()
for button in buttonViews.dropLast() {
setupSeperatorView(after: button)
}
setupInterfaceStyle()
}
// Amination
public func expand() {
let expansion = {
self.compressedConstraint.priority = .defaultLow
self.expandedConstraint.priority = .must
/* NOTE: Calling layoutIfNeeded for the whole view hierarchy.
Sometimes it's easier to just use frames instead of AutoLayout for
hierarch complexity considerations. Here the animation influences almost the
entire view hierarchy. */
SwiftEntryKit.layoutIfNeeded()
}
alpha = 1
if buttonBarContent.expandAnimatedly {
let damping: CGFloat = buttonBarContent.content.count <= 2 ? 0.4 : 0.8
SwiftEntryKit.layoutIfNeeded()
UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: damping, initialSpringVelocity: 0, options: [.beginFromCurrentState, .allowUserInteraction, .layoutSubviews, .allowAnimatedContent], animations: {
expansion()
}, completion: nil)
} else {
expansion()
}
}
public func compress() {
compressedConstraint.priority = .must
expandedConstraint.priority = .defaultLow
}
private func adjustRoundCornersIfNecessary() {
let size = CGSize(width: bottomCornerRadius, height: bottomCornerRadius)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .bottom, cornerRadii: size)
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}
private func setupInterfaceStyle() {
for view in separatorViews {
view.backgroundColor = buttonBarContent.separatorColor(for: traitCollection)
}
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupInterfaceStyle()
}
}

View File

@@ -0,0 +1,97 @@
//
// EKButtonView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 12/8/18.
// Copyright © 2018 CocoaPods. All rights reserved.
//
import UIKit
final class EKButtonView: UIView {
// MARK: - Properties
private let button = UIButton()
private let titleLabel = UILabel()
private let content: EKProperty.ButtonContent
// MARK: - Setup
init(content: EKProperty.ButtonContent) {
self.content = content
super.init(frame: .zero)
setupTitleLabel()
setupButton()
setupAcceessibility()
setupInterfaceStyle()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupAcceessibility() {
isAccessibilityElement = false
button.isAccessibilityElement = true
button.accessibilityIdentifier = content.accessibilityIdentifier
button.accessibilityLabel = content.label.text
}
private func setupButton() {
addSubview(button)
button.fillSuperview()
button.addTarget(self, action: #selector(buttonTouchUp),
for: [.touchUpInside, .touchUpOutside, .touchCancel])
button.addTarget(self, action: #selector(buttonTouchDown),
for: .touchDown)
button.addTarget(self, action: #selector(buttonTouchUpInside),
for: .touchUpInside)
}
private func setupTitleLabel() {
titleLabel.numberOfLines = content.label.style.numberOfLines
titleLabel.font = content.label.style.font
titleLabel.text = content.label.text
titleLabel.textAlignment = .center
titleLabel.lineBreakMode = .byWordWrapping
addSubview(titleLabel)
titleLabel.layoutToSuperview(axis: .horizontally,
offset: content.contentEdgeInset)
titleLabel.layoutToSuperview(axis: .vertically,
offset: content.contentEdgeInset)
}
private func setBackground(by content: EKProperty.ButtonContent,
isHighlighted: Bool) {
if isHighlighted {
backgroundColor = content.highlightedBackgroundColor(for: traitCollection)
} else {
backgroundColor = content.backgroundColor(for: traitCollection)
}
}
private func setupInterfaceStyle() {
backgroundColor = content.backgroundColor(for: traitCollection)
titleLabel.textColor = content.label.style.color(for: traitCollection)
}
// MARK: - Selectors
@objc func buttonTouchUpInside() {
content.action?()
}
@objc func buttonTouchDown() {
setBackground(by: content, isHighlighted: true)
}
@objc func buttonTouchUp() {
setBackground(by: content, isHighlighted: false)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupInterfaceStyle()
}
}

View File

@@ -0,0 +1,69 @@
//
// EKRatingSymbolView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final public class EKRatingSymbolView: UIView {
private let button = UIButton()
private let imageView = UIImageView()
private let unselectedImage: EKProperty.ImageContent
private let selectedImage: EKProperty.ImageContent
var selection: EKRatingMessage.Selection
public var isSelected: Bool {
set {
imageView.imageContent = newValue ? selectedImage : unselectedImage
}
get {
return imageView.image == selectedImage.images.first
}
}
public init(unselectedImage: EKProperty.ImageContent, selectedImage: EKProperty.ImageContent, selection: @escaping EKRatingMessage.Selection) {
self.unselectedImage = unselectedImage
self.selectedImage = selectedImage
self.selection = selection
super.init(frame: .zero)
setupImageView()
setupButton()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupButton() {
addSubview(button)
button.fillSuperview()
button.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
button.addTarget(self, action: #selector(touchDown), for: [.touchDown])
button.addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
}
private func setupImageView() {
addSubview(imageView)
imageView.imageContent = unselectedImage
imageView.centerInSuperview()
imageView.sizeToSuperview(withRatio: 0.7)
}
@objc func touchUpInside() {
selection(tag)
}
@objc func touchDown() {
transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
}
@objc func touchUp() {
transform = CGAffineTransform(scaleX: 1, y: 1)
}
}

View File

@@ -0,0 +1,62 @@
//
// EKRatingSymbolsContainerView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
final public class EKRatingSymbolsContainerView: UIView {
private var message: EKRatingMessage!
private var symbolsArray: [EKRatingSymbolView] = []
public func setup(with message: EKRatingMessage,
externalSelection: @escaping EKRatingMessage.Selection) {
self.message = message
let internalSelection = { [unowned self] (index: Int) in
self.select(index: index)
externalSelection(index)
}
for (index, item) in message.ratingItems.enumerated() {
let itemView = EKRatingSymbolView(unselectedImage: item.unselectedImage,
selectedImage: item.selectedImage,
selection: internalSelection)
itemView.tag = index
addSubview(itemView)
itemView.set(.height, of: item.size.height)
itemView.set(.width, of: item.size.width)
symbolsArray.append(itemView)
}
symbolsArray.layoutToSuperview(axis: .vertically, priority: .must)
symbolsArray.spread(.horizontally, stretchEdgesToSuperview: true)
select(index: message.selectedIndex)
}
private func select(index: Int? = nil) {
var delay: TimeInterval = 0
for (i, view) in symbolsArray.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if let index = index, i <= index {
view.isSelected = true
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
} else if view.isSelected || index == nil {
view.isSelected = false
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
}
UIView.animate(withDuration: 0.6,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0,
options: [.allowUserInteraction], animations: {
view.transform = .identity
}, completion: nil)
}
delay += 0.05
}
}
}

View File

@@ -0,0 +1,88 @@
//
// EKTextField.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/16/18.
//
import Foundation
import UIKit
final public class EKTextField: UIView {
// MARK: - Properties
static let totalHeight: CGFloat = 45
private let content: EKProperty.TextFieldContent
private let imageView = UIImageView()
private let textField = UITextField()
private let separatorView = UIView()
public var text: String {
set {
textField.text = newValue
}
get {
return textField.text ?? ""
}
}
// MARK: - Setup
public init(with content: EKProperty.TextFieldContent) {
self.content = content
super.init(frame: UIScreen.main.bounds)
setupImageView()
setupTextField()
setupSeparatorView()
textField.accessibilityIdentifier = content.accessibilityIdentifier
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupImageView() {
addSubview(imageView)
imageView.contentMode = .center
imageView.set(.width, .height, of: EKTextField.totalHeight)
imageView.layoutToSuperview(.leading)
imageView.image = content.leadingImage
imageView.tintColor = content.tintColor(for: traitCollection)
}
private func setupTextField() {
addSubview(textField)
textField.textFieldContent = content
textField.delegate = content.delegate
textField.set(.height, of: EKTextField.totalHeight)
textField.layout(.leading, to: .trailing, of: imageView)
textField.layoutToSuperview(.top, .trailing)
imageView.layout(to: .centerY, of: textField)
}
private func setupSeparatorView() {
addSubview(separatorView)
separatorView.layout(.top, to: .bottom, of: textField)
separatorView.set(.height, of: 1)
separatorView.layoutToSuperview(.bottom)
separatorView.layoutToSuperview(axis: .horizontally, offset: 10)
separatorView.backgroundColor = content.bottomBorderColor.color(
for: traitCollection,
mode: content.displayMode
)
}
public func makeFirstResponder() {
textField.becomeFirstResponder()
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
separatorView.backgroundColor = content.bottomBorderColor(for: traitCollection)
imageView.tintColor = content.tintColor(for: traitCollection)
textField.textColor = content.textStyle.color(for: traitCollection)
textField.placeholder = content.placeholder
}
}

View File

@@ -0,0 +1,18 @@
//
// EntryAppearanceDescriptor.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 1/5/19.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import UIKit
/**
An anti-pattern for SwiftEntryKit views to know more about their appearence,
if necessary, since views don't have access to EKAttributes.
This is a solution to bug #117 (round buttons in alert)
*/
protocol EntryAppearanceDescriptor: AnyObject {
var bottomCornerRadius: CGFloat { get set }
}

View File

@@ -0,0 +1,35 @@
//
// EKAccessoryNoteMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/4/18.
//
import UIKit
public class EKAccessoryNoteMessageView: UIView {
// MARK: Props
private let contentView = UIView()
private var noteMessageView: EKNoteMessageView!
var accessoryView: UIView!
func setup(with content: EKProperty.LabelContent) {
clipsToBounds = true
addSubview(contentView)
contentView.layoutToSuperview(.centerX, .top, .bottom)
contentView.layoutToSuperview(.left, relation: .greaterThanOrEqual, offset: 16)
contentView.layoutToSuperview(.right, relation: .lessThanOrEqual, offset: -16)
noteMessageView = EKNoteMessageView(with: content)
noteMessageView.horizontalOffset = 8
noteMessageView.verticalOffset = 7
contentView.addSubview(noteMessageView)
noteMessageView.layoutToSuperview(.top, .bottom, .trailing)
contentView.addSubview(accessoryView)
accessoryView.layoutToSuperview(.leading, .centerY)
accessoryView.layout(.trailing, to: .leading, of: noteMessageView)
}
}

View File

@@ -0,0 +1,28 @@
//
// EKImageNoteMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/4/18.
//
import UIKit
public class EKImageNoteMessageView: EKAccessoryNoteMessageView {
// MARK: Setup
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(with content: EKProperty.LabelContent, imageContent: EKProperty.ImageContent) {
super.init(frame: UIScreen.main.bounds)
setup(with: content, imageContent: imageContent)
}
private func setup(with content: EKProperty.LabelContent, imageContent: EKProperty.ImageContent) {
let imageView = UIImageView()
imageView.imageContent = imageContent
accessoryView = imageView
super.setup(with: content)
}
}

View File

@@ -0,0 +1,52 @@
//
// EKNoteMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public class EKNoteMessageView: UIView {
// MARK: Props
private let label = UILabel()
private var horizontalConstrainsts: QLAxisConstraints!
private var verticalConstrainsts: QLAxisConstraints!
public var horizontalOffset: CGFloat = 10 {
didSet {
horizontalConstrainsts.first.constant = horizontalOffset
horizontalConstrainsts.second.constant = -horizontalOffset
layoutIfNeeded()
}
}
public var verticalOffset: CGFloat = 5 {
didSet {
verticalConstrainsts.first.constant = verticalOffset
verticalConstrainsts.second.constant = -verticalOffset
layoutIfNeeded()
}
}
// MARK: Setup
public init(with content: EKProperty.LabelContent) {
super.init(frame: UIScreen.main.bounds)
setup(with: content)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(with content: EKProperty.LabelContent) {
clipsToBounds = true
addSubview(label)
label.content = content
horizontalConstrainsts = label.layoutToSuperview(axis: .horizontally, offset: horizontalOffset, priority: .must)
verticalConstrainsts = label.layoutToSuperview(axis: .vertically, offset: verticalOffset, priority: .must)
}
}

View File

@@ -0,0 +1,45 @@
//
// EKProcessingNoteMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public class EKProcessingNoteMessageView: EKAccessoryNoteMessageView {
// MARK: Props
private var activityIndicatorView: UIActivityIndicatorView!
private var noteMessageView: EKNoteMessageView!
/** Activity indication can be turned off / on */
public var isProcessing: Bool = true {
didSet {
if isProcessing {
activityIndicatorView.startAnimating()
} else {
activityIndicatorView.stopAnimating()
}
}
}
// MARK: Setup
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(with content: EKProperty.LabelContent, activityIndicator: UIActivityIndicatorView.Style) {
super.init(frame: UIScreen.main.bounds)
setup(with: content, activityIndicator: activityIndicator)
}
private func setup(with content: EKProperty.LabelContent, activityIndicator: UIActivityIndicatorView.Style, setProcessing: Bool = true) {
activityIndicatorView = UIActivityIndicatorView()
activityIndicatorView.style = activityIndicator
isProcessing = setProcessing
accessoryView = activityIndicatorView
super.setup(with: content)
}
}

View File

@@ -0,0 +1,46 @@
//
// EKXStatusBarMessageView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public class EKXStatusBarMessageView: UIView {
// MARK: Props
private let leadingLabel = UILabel()
private let trailingLabel = UILabel()
// MARK: Setup
public init(leading: EKProperty.LabelContent, trailing: EKProperty.LabelContent) {
super.init(frame: UIScreen.main.bounds)
setup(leading: leading, trailing: trailing)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(leading: EKProperty.LabelContent, trailing: EKProperty.LabelContent) {
clipsToBounds = true
set(.height, of: UIApplication.shared.statusBarFrame.maxY)
addSubview(leadingLabel)
leadingLabel.content = leading
leadingLabel.layoutToSuperview(axis: .vertically)
leadingLabel.layoutToSuperview(.leading)
leadingLabel.layoutToSuperview(.width, ratio: 0.26)
addSubview(trailingLabel)
trailingLabel.content = trailing
trailingLabel.layoutToSuperview(axis: .vertically)
trailingLabel.layoutToSuperview(.trailing)
trailingLabel.layoutToSuperview(.width, ratio: 0.26)
}
}

View File

@@ -0,0 +1,32 @@
//
// EKAlertMessage.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
public struct EKAlertMessage {
public enum ImagePosition {
case top
case left
}
/** The position of the image inside the alert */
public let imagePosition: ImagePosition
/** Image, Title, Description */
public let simpleMessage: EKSimpleMessage
/** Contents of button bar */
public let buttonBarContent: EKProperty.ButtonBarContent
public init(simpleMessage: EKSimpleMessage,
imagePosition: ImagePosition = .top,
buttonBarContent: EKProperty.ButtonBarContent) {
self.simpleMessage = simpleMessage
self.imagePosition = imagePosition
self.buttonBarContent = buttonBarContent
}
}

View File

@@ -0,0 +1,110 @@
//
// EKColor.swift
// SwiftEntryKit
//
// Created by Daniel on 21/07/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import UIKit
/** A color representation attribute as per user interface style */
public struct EKColor: Equatable {
// MARK: - Properties
public private(set) var dark: UIColor
public private(set) var light: UIColor
// MARK: - Setup
public init(light: UIColor, dark: UIColor) {
self.light = light
self.dark = dark
}
public init(_ unified: UIColor) {
self.light = unified
self.dark = unified
}
public init(rgb: Int) {
dark = UIColor(rgb: rgb)
light = UIColor(rgb: rgb)
}
public init(red: Int, green: Int, blue: Int) {
assert(red >= 0 && red <= 255, "Invalid red component")
assert(green >= 0 && green <= 255, "Invalid green component")
assert(blue >= 0 && blue <= 255, "Invalid blue component")
let color = UIColor(red: CGFloat(red) / 255.0,
green: CGFloat(green) / 255.0,
blue: CGFloat(blue) / 255.0,
alpha: 1.0)
light = color
dark = color
}
/** Computes the proper UIColor */
public func color(for traits: UITraitCollection,
mode: EKAttributes.DisplayMode) -> UIColor {
switch mode {
case .inferred:
if #available(iOS 13, *) {
switch traits.userInterfaceStyle {
case .light, .unspecified:
return light
case .dark:
return dark
@unknown default:
return light
}
} else {
return light
}
case .light:
return light
case .dark:
return dark
}
}
}
public extension EKColor {
/// Returns the inverse of `self` (light and dark swapped)
var inverted: EKColor {
return EKColor(light: dark, dark: light)
}
/** Returns an `EKColor` with the specified alpha component */
func with(alpha: CGFloat) -> EKColor {
return EKColor(light: light.withAlphaComponent(alpha),
dark: dark.withAlphaComponent(alpha))
}
/** White color for all user interface styles */
static var white: EKColor {
return EKColor(.white)
}
/** Black color for all user interface styles */
static var black: EKColor {
return EKColor(.black)
}
/** Clear color for all user interface styles */
static var clear: EKColor {
return EKColor(.clear)
}
/** Color that represents standard background. White for light mode, black for dark mode */
static var standardBackground: EKColor {
return EKColor(light: .white, dark: .black)
}
/** Color that represents standard content. black for light mode, white for dark mode */
static var standardContent: EKColor {
return EKColor(light: .black, dark: .white)
}
}

View File

@@ -0,0 +1,41 @@
//
// EKNotificationMessage.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public struct EKNotificationMessage {
/** Insets of the content of the message */
public struct Insets {
/** The insets of the content of the message, from the top, bottom, left, right */
public var contentInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
/** The distance between the title and the description */
public var titleToDescription: CGFloat = 5
public static var `default` = Insets()
}
/** Image, Title, Description */
public let simpleMessage: EKSimpleMessage
/** Optional auxiliary label descriptor (For instance, it be used to display time of message) */
public let auxiliary: EKProperty.LabelContent?
/** Defines the vertical and horizontal margins */
public let insets: Insets
public init(simpleMessage: EKSimpleMessage,
auxiliary: EKProperty.LabelContent? = nil,
insets: Insets = .default) {
self.simpleMessage = simpleMessage
self.auxiliary = auxiliary
self.insets = insets
}
}

View File

@@ -0,0 +1,60 @@
//
// EKPopUpMessage.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public struct EKPopUpMessage {
/** Code block that is executed as the user taps the popup button */
public typealias EKPopUpMessageAction = () -> ()
/** Popup theme image */
public struct ThemeImage {
/** Position of the theme image */
public enum Position {
case topToTop(offset: CGFloat)
case centerToTop(offset: CGFloat)
}
/** The content of the image */
public var image: EKProperty.ImageContent
/** The psotion of the image */
public var position: Position
/** Initializer */
public init(image: EKProperty.ImageContent,
position: Position = .topToTop(offset: 40)) {
self.image = image
self.position = position
}
}
public var themeImage: ThemeImage?
public var title: EKProperty.LabelContent
public var description: EKProperty.LabelContent
public var button: EKProperty.ButtonContent
public var action: EKPopUpMessageAction
var containsImage: Bool {
return themeImage != nil
}
public init(themeImage: ThemeImage? = nil,
title: EKProperty.LabelContent,
description: EKProperty.LabelContent,
button: EKProperty.ButtonContent,
action: @escaping EKPopUpMessageAction) {
self.themeImage = themeImage
self.title = title
self.description = description
self.button = button
self.action = action
}
}

View File

@@ -0,0 +1,399 @@
//
// EKProperty.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public struct EKProperty {
/** Button content descriptor */
public struct ButtonContent {
public typealias Action = () -> ()
/** Button title label content descriptor */
public var label: LabelContent
/** Button background color */
public var backgroundColor: EKColor
public var highlightedBackgroundColor: EKColor
/** Content edge inset */
public var contentEdgeInset: CGFloat
/** The display mode of the button */
public var displayMode: EKAttributes.DisplayMode
/** Accessibility identifier that identifies the button */
public var accessibilityIdentifier: String?
/** Action */
public var action: Action?
public init(label: LabelContent,
backgroundColor: EKColor,
highlightedBackgroundColor: EKColor,
contentEdgeInset: CGFloat = 5,
displayMode: EKAttributes.DisplayMode = .inferred,
accessibilityIdentifier: String? = nil,
action: @escaping Action = {}) {
self.label = label
self.backgroundColor = backgroundColor
self.highlightedBackgroundColor = highlightedBackgroundColor
self.contentEdgeInset = contentEdgeInset
self.displayMode = displayMode
self.accessibilityIdentifier = accessibilityIdentifier
self.action = action
}
public func backgroundColor(for traitCollection: UITraitCollection) -> UIColor {
return backgroundColor.color(for: traitCollection, mode: displayMode)
}
public func highlightedBackgroundColor(for traitCollection: UITraitCollection) -> UIColor {
return highlightedBackgroundColor.color(for: traitCollection, mode: displayMode)
}
public func highlighedLabelColor(for traitCollection: UITraitCollection) -> UIColor {
return label.style.color.with(alpha: 0.8).color(
for: traitCollection,
mode: label.style.displayMode
)
}
}
/** Label content descriptor */
public struct LabelContent {
/** The text */
public var text: String
/** The label's style */
public var style: LabelStyle
/** The label's accessibility ideentifier */
public var accessibilityIdentifier: String?
public init(text: String,
style: LabelStyle,
accessibilityIdentifier: String? = nil) {
self.text = text
self.style = style
self.accessibilityIdentifier = accessibilityIdentifier
}
}
/** Label style descriptor */
public struct LabelStyle {
/** Font of the text */
public var font: UIFont
/** Color of the text */
public var color: EKColor
/** Text Alignment */
public var alignment: NSTextAlignment
/** Number of lines */
public var numberOfLines: Int
/** Display mode for the label */
public var displayMode: EKAttributes.DisplayMode
public init(font: UIFont,
color: EKColor,
alignment: NSTextAlignment = .left,
displayMode: EKAttributes.DisplayMode = .inferred,
numberOfLines: Int = 0) {
self.font = font
self.color = color
self.alignment = alignment
self.displayMode = displayMode
self.numberOfLines = numberOfLines
}
public func color(for traitCollection: UITraitCollection) -> UIColor {
return color.color(for: traitCollection, mode: displayMode)
}
}
/** Image View style descriptor */
public struct ImageContent {
/** Repeated-reversed animation throughout the presentation of an image */
public enum TransformAnimation {
case animate(duration: TimeInterval, options: UIView.AnimationOptions, transform: CGAffineTransform)
case none
}
/** Tint color for the image/s */
public var tint: EKColor?
/** The images */
public var images: [UIImage]
/** Image sequence duration, if any */
public var imageSequenceAnimationDuration: TimeInterval
/** Image View size - can be forced.
If nil, then the image view hugs content and resists compression */
public var size: CGSize?
/** Content mode */
public var contentMode: UIView.ContentMode
/** Should the image be rounded */
public var makesRound: Bool
/** Repeated-Reversed animation */
public var animation: TransformAnimation
/** The display mode of the image */
public var displayMode: EKAttributes.DisplayMode
/** Image accessibility identifier */
public var accessibilityIdentifier: String?
public init(imageName: String,
animation: TransformAnimation = .none,
displayMode: EKAttributes.DisplayMode = .inferred,
size: CGSize? = nil,
contentMode: UIView.ContentMode = .scaleToFill,
tint: EKColor? = nil,
makesRound: Bool = false,
accessibilityIdentifier: String? = nil) {
let image = UIImage(named: imageName)!
self.init(image: image,
displayMode: displayMode,
size: size,
tint: tint,
contentMode: contentMode,
makesRound: makesRound,
accessibilityIdentifier: accessibilityIdentifier)
}
public init(image: UIImage,
animation: TransformAnimation = .none,
displayMode: EKAttributes.DisplayMode = .inferred,
size: CGSize? = nil,
tint: EKColor? = nil,
contentMode: UIView.ContentMode = .scaleToFill,
makesRound: Bool = false,
accessibilityIdentifier: String? = nil) {
self.images = [image]
self.size = size
self.tint = tint
self.displayMode = displayMode
self.contentMode = contentMode
self.makesRound = makesRound
self.animation = animation
self.imageSequenceAnimationDuration = 0
self.accessibilityIdentifier = accessibilityIdentifier
}
public init(images: [UIImage],
imageSequenceAnimationDuration: TimeInterval = 1,
displayMode: EKAttributes.DisplayMode = .inferred,
animation: TransformAnimation = .none,
size: CGSize? = nil,
tint: EKColor? = nil,
contentMode: UIView.ContentMode = .scaleToFill,
makesRound: Bool = false,
accessibilityIdentifier: String? = nil) {
self.images = images
self.size = size
self.displayMode = displayMode
self.tint = tint
self.contentMode = contentMode
self.makesRound = makesRound
self.animation = animation
self.imageSequenceAnimationDuration = imageSequenceAnimationDuration
self.accessibilityIdentifier = accessibilityIdentifier
}
public init(imagesNames: [String],
imageSequenceAnimationDuration: TimeInterval = 1,
displayMode: EKAttributes.DisplayMode = .inferred,
animation: TransformAnimation = .none,
size: CGSize? = nil,
tint: EKColor? = nil,
contentMode: UIView.ContentMode = .scaleToFill,
makesRound: Bool = false,
accessibilityIdentifier: String? = nil) {
let images = imagesNames.map { return UIImage(named: $0)! }
self.init(images: images,
imageSequenceAnimationDuration: imageSequenceAnimationDuration,
displayMode: displayMode,
animation: animation,
size: size,
tint: tint,
contentMode: contentMode,
makesRound: makesRound,
accessibilityIdentifier: accessibilityIdentifier)
}
/** Quick thumbail property generator */
public static func thumb(with image: UIImage,
edgeSize: CGFloat) -> ImageContent {
return ImageContent(images: [image],
size: CGSize(width: edgeSize, height: edgeSize),
contentMode: .scaleAspectFill,
makesRound: true)
}
/** Quick thumbail property generator */
public static func thumb(with imageName: String,
edgeSize: CGFloat) -> ImageContent {
return ImageContent(imagesNames: [imageName],
size: CGSize(width: edgeSize, height: edgeSize),
contentMode: .scaleAspectFill,
makesRound: true)
}
public func tintColor(for traitCollection: UITraitCollection) -> UIColor? {
return tint?.color(for: traitCollection, mode: displayMode)
}
}
/** Text field content **/
public struct TextFieldContent {
// NOTE: Intentionally a reference type
class ContentWrapper {
var text = ""
}
public weak var delegate: UITextFieldDelegate?
public var keyboardType: UIKeyboardType
public var isSecure: Bool
public var leadingImage: UIImage!
public var placeholder: LabelContent
public var textStyle: LabelStyle
public var tintColor: EKColor!
public var displayMode: EKAttributes.DisplayMode
public var bottomBorderColor: EKColor
public var accessibilityIdentifier: String?
let contentWrapper = ContentWrapper()
public var textContent: String {
set {
contentWrapper.text = newValue
}
get {
return contentWrapper.text
}
}
public init(delegate: UITextFieldDelegate? = nil,
keyboardType: UIKeyboardType = .default,
placeholder: LabelContent,
tintColor: EKColor? = nil,
displayMode: EKAttributes.DisplayMode = .inferred,
textStyle: LabelStyle,
isSecure: Bool = false,
leadingImage: UIImage? = nil,
bottomBorderColor: EKColor = .clear,
accessibilityIdentifier: String? = nil) {
self.delegate = delegate
self.keyboardType = keyboardType
self.placeholder = placeholder
self.textStyle = textStyle
self.tintColor = tintColor
self.displayMode = displayMode
self.isSecure = isSecure
self.leadingImage = leadingImage
self.bottomBorderColor = bottomBorderColor
self.accessibilityIdentifier = accessibilityIdentifier
}
public func tintColor(for traitCollection: UITraitCollection) -> UIColor? {
return tintColor?.color(for: traitCollection, mode: displayMode)
}
public func bottomBorderColor(for traitCollection: UITraitCollection) -> UIColor? {
return bottomBorderColor.color(for: traitCollection, mode: displayMode)
}
}
/** Button bar content */
public struct ButtonBarContent {
/** Button content array */
public var content: [ButtonContent] = []
/** The color of the separator */
public var separatorColor: EKColor
/** Upper threshold for the number of buttons (*ButtonContent*) for horizontal distribution. Must be a positive value */
public var horizontalDistributionThreshold: Int
/** Determines whether the buttons expands animately */
public var expandAnimatedly: Bool
/** The height of each button. All are equally distributed in their axis */
public var buttonHeight: CGFloat
/** The display mode of the button bar */
public var displayMode: EKAttributes.DisplayMode
public init(with buttonContents: ButtonContent...,
separatorColor: EKColor,
horizontalDistributionThreshold: Int = 2,
buttonHeight: CGFloat = 50,
displayMode: EKAttributes.DisplayMode = .inferred,
expandAnimatedly: Bool) {
self.init(with: buttonContents,
separatorColor: separatorColor,
horizontalDistributionThreshold: horizontalDistributionThreshold,
buttonHeight: buttonHeight,
displayMode: displayMode,
expandAnimatedly: expandAnimatedly)
}
public init(with buttonContents: [ButtonContent],
separatorColor: EKColor,
horizontalDistributionThreshold: Int = 2,
buttonHeight: CGFloat = 50,
displayMode: EKAttributes.DisplayMode = .inferred,
expandAnimatedly: Bool) {
guard horizontalDistributionThreshold > 0 else {
fatalError("horizontalDistributionThreshold Must have a positive value!")
}
self.separatorColor = separatorColor
self.horizontalDistributionThreshold = horizontalDistributionThreshold
self.buttonHeight = buttonHeight
self.displayMode = displayMode
self.expandAnimatedly = expandAnimatedly
content.append(contentsOf: buttonContents)
}
public func separatorColor(for traitCollection: UITraitCollection) -> UIColor {
return separatorColor.color(for: traitCollection, mode: displayMode)
}
}
/** Rating item content */
public struct EKRatingItemContent {
public var title: EKProperty.LabelContent
public var description: EKProperty.LabelContent
public var unselectedImage: EKProperty.ImageContent
public var selectedImage: EKProperty.ImageContent
public var size: CGSize
public init(title: EKProperty.LabelContent,
description: EKProperty.LabelContent,
unselectedImage: EKProperty.ImageContent,
selectedImage: EKProperty.ImageContent,
size: CGSize = CGSize(width: 50, height: 50)) {
self.title = title
self.description = description
self.unselectedImage = unselectedImage
self.selectedImage = selectedImage
self.size = size
}
}
}

View File

@@ -0,0 +1,60 @@
//
// EKRatingMessage.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
public struct EKRatingMessage {
// NOTE: Intentionally a reference type
class SelectedIndex {
var selectedIndex: Int!
}
/** Selection */
public typealias Selection = (Int) -> Void
/** Initial title */
public var initialTitle: EKProperty.LabelContent
/** Initial description */
public var initialDescription: EKProperty.LabelContent
/** Rating items */
public var ratingItems: [EKProperty.EKRatingItemContent]
/** Button bar content appears after selection */
public var buttonBarContent: EKProperty.ButtonBarContent
/** Selection event - Each time the user interacts a rating star */
public var selection: Selection!
let selectedIndexRef = SelectedIndex()
/** Selected index (if there is one) */
public var selectedIndex: Int? {
get {
return selectedIndexRef.selectedIndex
}
set {
selectedIndexRef.selectedIndex = newValue
}
}
/** Initializer */
public init(initialTitle: EKProperty.LabelContent,
initialDescription: EKProperty.LabelContent,
ratingItems: [EKProperty.EKRatingItemContent],
buttonBarContent: EKProperty.ButtonBarContent,
selection: Selection? = nil) {
self.initialTitle = initialTitle
self.initialDescription = initialDescription
self.ratingItems = ratingItems
self.buttonBarContent = buttonBarContent
self.selection = selection
}
}

View File

@@ -0,0 +1,29 @@
//
// EKSimpleMessage.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/1/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
public struct EKSimpleMessage {
/** The image view descriptor */
public let image: EKProperty.ImageContent?
/** The title label descriptor */
public let title: EKProperty.LabelContent
/** The description label descriptor */
public let description: EKProperty.LabelContent
public init(image: EKProperty.ImageContent? = nil,
title: EKProperty.LabelContent,
description: EKProperty.LabelContent) {
self.image = image
self.title = title
self.description = description
}
}

View File

@@ -0,0 +1,171 @@
//
// EKAttributes+Animation.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
// A protocol that describes an animation
protocol EKAnimation {
var delay: TimeInterval { get set }
var duration: TimeInterval { get set }
var spring: EKAttributes.Animation.Spring? { get set }
}
// A protocol that describes a range animation
protocol EKRangeAnimation: EKAnimation {
var start: CGFloat { get set }
var end: CGFloat { get set }
}
public extension EKAttributes {
/** Describes an animation that can be performed on the entry */
struct Animation: Equatable {
/** Describes properties for a spring animation that can be performed on the entry */
public struct Spring: Equatable {
/** The dampic of the spring animation */
public var damping: CGFloat
/** The initial velocity of the spring animation */
public var initialVelocity: CGFloat
/** Initializer */
public init(damping: CGFloat, initialVelocity: CGFloat) {
self.damping = damping
self.initialVelocity = initialVelocity
}
}
/** Describes an animation with range */
public struct RangeAnimation: EKRangeAnimation, Equatable {
/** The duration of the range animation */
public var duration: TimeInterval
/** The delay of the range animation */
public var delay: TimeInterval
/** The start value of the range animation (e.g. alpha, scale) */
public var start: CGFloat
/** The end value of the range animation (e.g. alpha, scale) */
public var end: CGFloat
/** The spring of the animation */
public var spring: Spring?
/** Initializer */
public init(from start: CGFloat, to end: CGFloat, duration: TimeInterval, delay: TimeInterval = 0, spring: Spring? = nil) {
self.start = start
self.end = end
self.delay = delay
self.duration = duration
self.spring = spring
}
}
/** Describes translation animation */
public struct Translate: EKAnimation, Equatable {
/** Describes the anchor position */
public enum AnchorPosition: Equatable {
/** Top position - the entry shows from top or exits towards the top */
case top
/** Bottom position - the entry shows from bottom or exits towards the bottom */
case bottom
/** Automatic position - the entry shows and exits according to EKAttributes.Position value. If the position of the entry is top, bottom, the entry's translation anchor is top, bottom - respectively.*/
case automatic
}
/** Animation duration */
public var duration: TimeInterval
/** Animation delay */
public var delay: TimeInterval
/** To where OR from the entry is animated */
public var anchorPosition: AnchorPosition
/** Optional translation spring */
public var spring: Spring?
/** Initializer */
public init(duration: TimeInterval, anchorPosition: AnchorPosition = .automatic, delay: TimeInterval = 0, spring: Spring? = nil) {
self.anchorPosition = anchorPosition
self.duration = duration
self.delay = delay
self.spring = spring
}
}
/** Translation animation prop */
public var translate: Translate?
/** Scale animation prop */
public var scale: RangeAnimation?
/** Fade animation prop */
public var fade: RangeAnimation?
/** Does the animation contains translation */
public var containsTranslation: Bool {
return translate != nil
}
/** Does the animation contains scale */
public var containsScale: Bool {
return scale != nil
}
/** Does the animation contains fade */
public var containsFade: Bool {
return fade != nil
}
/** Does the animation contains any animation whatsoever */
public var containsAnimation: Bool {
return containsTranslation || containsScale || containsFade
}
/** Returns the maximum delay amongst all animations */
public var maxDelay: TimeInterval {
return max(translate?.delay ?? 0, max(scale?.delay ?? 0, fade?.delay ?? 0))
}
/** Returns the maximum duration amongst all animations */
public var maxDuration: TimeInterval {
return max(translate?.duration ?? 0, max(scale?.duration ?? 0, fade?.duration ?? 0))
}
/** Returns the maximum (duration+delay) amongst all animations */
public var totalDuration: TimeInterval {
return maxDelay + maxDuration
}
/** Returns the maximum (duration+delay) amongst all animations */
public static var translation: Animation {
return Animation(translate: .init(duration: 0.3))
}
/** No animation at all */
public static var none: Animation {
return Animation()
}
/** Initializer */
public init(translate: Translate? = nil, scale: RangeAnimation? = nil, fade: RangeAnimation? = nil) {
self.translate = translate
self.scale = scale
self.fade = fade
}
}
}

View File

@@ -0,0 +1,138 @@
//
// EKAttributes+BackgroundStyle.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public extension EKAttributes {
/** The background style property */
enum BackgroundStyle: Equatable {
/** Blur style for light and dark modes */
public struct BlurStyle: Equatable {
public static var extra: BlurStyle {
return BlurStyle(light: .extraLight, dark: .dark)
}
public static var standard: BlurStyle {
return BlurStyle(light: .light, dark: .dark)
}
@available(iOS 10.0, *)
public static var prominent: BlurStyle {
return BlurStyle(light: .prominent, dark: .prominent)
}
public static var dark: BlurStyle {
return BlurStyle(light: .dark, dark: .dark)
}
let light: UIBlurEffect.Style
let dark: UIBlurEffect.Style
public init(style: UIBlurEffect.Style) {
self.light = style
self.dark = style
}
public init(light: UIBlurEffect.Style, dark: UIBlurEffect.Style) {
self.light = light
self.dark = dark
}
/** Computes a proper `UIBlurEffect.Style` instance */
public func blurStyle(for traits: UITraitCollection,
mode: EKAttributes.DisplayMode) -> UIBlurEffect.Style {
switch mode {
case .inferred:
if #available(iOS 13, *) {
switch traits.userInterfaceStyle {
case .light, .unspecified:
return light
case .dark:
return dark
@unknown default:
return light
}
} else {
return light
}
case .light:
return light
case .dark:
return dark
}
}
public func blurEffect(for traits: UITraitCollection,
mode: EKAttributes.DisplayMode) -> UIBlurEffect {
return UIBlurEffect(style: blurStyle(for: traits, mode: mode))
}
}
/** Gradient background style */
public struct Gradient {
public var colors: [EKColor]
public var startPoint: CGPoint
public var endPoint: CGPoint
public init(colors: [EKColor],
startPoint: CGPoint,
endPoint: CGPoint) {
self.colors = colors
self.startPoint = startPoint
self.endPoint = endPoint
}
}
/** Visual Effect (Blurred) background style */
case visualEffect(style: BlurStyle)
/** Color background style */
case color(color: EKColor)
/** Gradient background style */
case gradient(gradient: Gradient)
/** Image background style */
case image(image: UIImage)
/** Clear background style */
case clear
/** == operator overload */
public static func == (lhs: EKAttributes.BackgroundStyle,
rhs: EKAttributes.BackgroundStyle) -> Bool {
switch (lhs, rhs) {
case (visualEffect(style: let leftStyle),
visualEffect(style: let rightStyle)):
return leftStyle == rightStyle
case (color(color: let leftColor),
color(color: let rightColor)):
return leftColor == rightColor
case (image(image: let leftImage),
image(image: let rightImage)):
return leftImage == rightImage
case (gradient(gradient: let leftGradient),
gradient(gradient: let rightGradient)):
for (leftColor, rightColor) in zip(leftGradient.colors, rightGradient.colors) {
guard leftColor == rightColor else {
return false
}
}
return leftGradient.startPoint == rightGradient.startPoint &&
leftGradient.endPoint == rightGradient.endPoint
case (clear, clear):
return true
default:
return false
}
}
}
}

View File

@@ -0,0 +1,25 @@
//
// EKAttributes+DisplayMode.swift
// SwiftEntryKit
//
// Created by Daniel on 26/07/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
public extension EKAttributes {
/** Display mode for the entry */
enum DisplayMode {
/** The display mode is inferred from the current user interface style */
case inferred
/** The display mode is light */
case light
/** The display mode is dark */
case dark
}
}

View File

@@ -0,0 +1,12 @@
//
// EKAttributes+Duration.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/4/18.
//
import Foundation
public extension EKAttributes {
typealias DisplayDuration = TimeInterval
}

View File

@@ -0,0 +1,79 @@
//
// EKAttributes+FrameStyle.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/28/18.
//
import Foundation
import CoreGraphics
import UIKit
public extension EKAttributes {
/** Corner radius of the entry - Specifies the corners */
enum RoundCorners {
/** *None* of the corners will be round */
case none
/** *All* of the corners will be round */
case all(radius: CGFloat)
/** Only the *top* left and right corners will be round */
case top(radius: CGFloat)
/** Only the *bottom* left and right corners will be round */
case bottom(radius: CGFloat)
var hasRoundCorners: Bool {
switch self {
case .none:
return false
default:
return true
}
}
var cornerValues: (value: UIRectCorner, radius: CGFloat)? {
switch self {
case .all(radius: let radius):
return (value: .allCorners, radius: radius)
case .top(radius: let radius):
return (value: .top, radius: radius)
case .bottom(radius: let radius):
return (value: .bottom, radius: radius)
case .none:
return nil
}
}
}
/** The border around the entry */
enum Border {
/** No border */
case none
/** Border wirh color and width */
case value(color: UIColor, width: CGFloat)
var hasBorder: Bool {
switch self {
case .none:
return false
default:
return true
}
}
var borderValues: (color: UIColor, width: CGFloat)? {
switch self {
case .value(color: let color, width: let width):
return(color: color, width: width)
case .none:
return nil
}
}
}
}

View File

@@ -0,0 +1,38 @@
//
// EKAttributes+HapticFeedback.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/1/18.
//
import UIKit
public extension EKAttributes {
/** Notification haptic feedback type. Adds an additional sensuous layer. Read more at UINotificationFeedbackType. Available from iOS 10, but you are not required to check the iOS version before using it. It's automatically handled by the kit.
*/
enum NotificationHapticFeedback {
case success
case warning
case error
case none
@available(iOS 10.0, *)
var value: UINotificationFeedbackGenerator.FeedbackType? {
switch self {
case .success:
return .success
case .warning:
return .warning
case .error:
return .error
case .none:
return nil
}
}
var isValid: Bool {
return self != .none
}
}
}

View File

@@ -0,0 +1,41 @@
//
// EKAttributes+LifecycleActions.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 6/16/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
public extension EKAttributes {
/** Contains optionally injected events that take place during the entry lifecycle */
struct LifecycleEvents {
public typealias Event = () -> Void
/** Executed before the entry appears - before the animation starts.
Might not get called in case another entry with a higher display priority is displayed.
*/
public var willAppear: Event?
/** Executed after the animation ends.
Might not get called in case another entry with a higher display priority is displayed.
*/
public var didAppear: Event?
/** Executed before the entry disappears (Before the animation starts) */
public var willDisappear: Event?
/** Executed after the entry disappears (After the animation ends) */
public var didDisappear: Event?
public init(willAppear: Event? = nil, didAppear: Event? = nil, willDisappear: Event? = nil, didDisappear: Event? = nil) {
self.willAppear = willAppear
self.didAppear = didAppear
self.willDisappear = willDisappear
self.didDisappear = didDisappear
}
}
}

View File

@@ -0,0 +1,51 @@
//
// EKAttributes+PopBehavior.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/26/18.
//
import Foundation
public extension EKAttributes {
/** Describes the entry behavior when a new entry shows (with equal or higher display-priority) */
enum PopBehavior {
/** The entry disappears promptly (Does not animates out) when a new one shows */
case overridden
/** Animate the entry out - The entry rolls out when a new one shows */
case animated(animation: Animation)
public var isOverriden: Bool {
switch self {
case .overridden:
return true
case .animated:
return false
}
}
var animation: Animation? {
switch self {
case .animated(animation: let animation):
return animation
case .overridden:
return nil
}
}
func validate() {
#if DEBUG
guard let animation = animation else { return }
guard animation == .none else { return }
print("""
SwiftEntryKit warning: cannot associate value `EKAttributes.Animation()`
with `EKAttributes.PopBehavior.animated`. This may result in undefined behavior.
Please use `PopBehavior.overridden` instead.
""")
#endif
}
}
}

View File

@@ -0,0 +1,37 @@
//
// EKAttributes+Position.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
public extension EKAttributes {
/** The position of the entry. */
enum Position {
/** The entry appears at the top of the screen. */
case top
/** The entry appears at the bottom of the screen. */
case bottom
/** The entry appears at the center of the screen. */
case center
public var isTop: Bool {
return self == .top
}
public var isCenter: Bool {
return self == .center
}
public var isBottom: Bool {
return self == .bottom
}
}
}

View File

@@ -0,0 +1,196 @@
//
// EKAttributes+Frame.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public extension EKAttributes {
/** Describes the frame of the entry. It's limitations, width and offset from the anchor (top / bottom of the screen) */
struct PositionConstraints {
/** Describes safe area relation */
public enum SafeArea {
/** Entry overrides safe area */
case overridden
/** The entry shows outs. But can optionally be colored */
case empty(fillSafeArea: Bool)
public var isOverridden: Bool {
switch self {
case .overridden:
return true
default:
return false
}
}
}
/** Describes an edge constraint of the entry */
public enum Edge {
/** Ratio constraint to screen edge */
case ratio(value: CGFloat)
/** Offset from each edge of the screen */
case offset(value: CGFloat)
/** Constant edge length */
case constant(value: CGFloat)
/** Unspecified edge length */
case intrinsic
/** Edge totally filled */
public static var fill: Edge {
return .offset(value: 0)
}
}
/** Describes the size of the entry */
public struct Size {
/** Describes a width constraint */
public var width: Edge
/** Describes a height constraint */
public var height: Edge
/** Initializer */
public init(width: Edge, height: Edge) {
self.width = width
self.height = height
}
/** The content's size. Entry's content view must have tight constraints */
public static var intrinsic: Size {
return Size(width: .intrinsic, height: .intrinsic)
}
/** The content's size. Entry's content view must have tight constraints */
public static var sizeToWidth: Size {
return Size(width: .offset(value: 0), height: .intrinsic)
}
/** Screen size, without horizontal or vertical offset */
public static var screen: Size {
return Size(width: .fill, height: .fill)
}
}
/** The relation to the keyboard's top and the screen's top while it is opened */
public enum KeyboardRelation {
/** Describes the offset when the keyboard is opened */
public struct Offset {
/** Describes top keyboard offset to the entry's bottom */
public var bottom: CGFloat
/** Describes top screen offset to the entry's top, useful to prevent the entry from exceeding the screen top bounds */
public var screenEdgeResistance: CGFloat?
public init(bottom: CGFloat = 0, screenEdgeResistance: CGFloat? = nil) {
self.bottom = bottom
self.screenEdgeResistance = screenEdgeResistance
}
/** None offset */
public static var none: Offset {
return Offset()
}
}
/** Bind the entry's bottom to the keyboard's top with an offset.
Additionally, the top edge of the screen can have a resistance offset which the entry isn't able to cross.
The resistance is mostly used when the device orientation changes and the entry's frame crosses the screen bounds.
Current isn't supported with center entry position.*/
case bind(offset: Offset)
/** Entry is unbound to the keyboard. It's location doesn't change. */
case unbind
/** Returns true if the entry is bound to the keyboard */
public var isBound: Bool {
switch self {
case .bind(offset: _):
return true
case .unbind:
return false
}
}
}
/** Rotation related position constraints */
public struct Rotation {
/** Attributes of supported interface orientations */
public enum SupportedInterfaceOrientation {
/** Uses standard supported interface orientation (target specification in general settings) */
case standard
/** Supports all orinetations */
case all
}
/** Autorotate the entry along with the device orientation */
public var isEnabled = true
/** The screen autorotates with accordance to this option */
public var supportedInterfaceOrientations = SupportedInterfaceOrientation.standard
public init() {}
}
/** The rotation attributes of the entry */
public var rotation = Rotation()
/** The entry can be bound to keyboard in case of appearance */
public var keyboardRelation = KeyboardRelation.unbind
/** The size of the entry */
public var size: Size
/** The maximum size of the entry */
public var maxSize: Size
/** The vertical offset from the top or bottom anchor */
public var verticalOffset: CGFloat
/** Can be used to display the content outside the safe area margins such as on the notch of the iPhone X or the status bar itself. */
public var safeArea = SafeArea.empty(fillSafeArea: false)
public var hasVerticalOffset: Bool {
return verticalOffset > 0
}
/** Returns a floating entry (float-like) */
public static var float: PositionConstraints {
return PositionConstraints(verticalOffset: 10, size: .init(width: .offset(value: 20), height: .intrinsic))
}
/** A full width entry (toast-like) */
public static var fullWidth: PositionConstraints {
return PositionConstraints(verticalOffset: 0, size: .sizeToWidth)
}
/** A full screen entry - fills the entire screen, modal-like */
public static var fullScreen: PositionConstraints {
return PositionConstraints(verticalOffset: 0, size: .screen)
}
/** Initialize with default parameters */
public init(verticalOffset: CGFloat = 0, size: Size = .sizeToWidth, maxSize: Size = .intrinsic) {
self.verticalOffset = verticalOffset
self.size = size
self.maxSize = maxSize
}
}
}

View File

@@ -0,0 +1,142 @@
//
// EKAttributes+Precedence.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/29/18.
//
import Foundation
fileprivate extension Int {
var isValidDisplayPriority: Bool {
return self >= EKAttributes.Precedence.Priority.minRawValue && self <= EKAttributes.Precedence.Priority.maxRawValue
}
}
public extension EKAttributes {
/**
Describes the manner on which the entry is pushed and displayed.
See the various values of more explanation.
*/
enum Precedence {
/**
The display priority of the entry - Determines whether is can be overriden by other entries.
Must be in range [0...1000]
*/
public struct Priority: Hashable, Equatable, RawRepresentable, Comparable {
public var rawValue: Int
public var hashValue: Int {
return rawValue
}
public init(_ rawValue: Int) {
assert(rawValue.isValidDisplayPriority, "Display Priority must be in range [\(Priority.minRawValue)...\(Priority.maxRawValue)]")
self.rawValue = rawValue
}
public init(rawValue: Int) {
assert(rawValue.isValidDisplayPriority, "Display Priority must be in range [\(Priority.minRawValue)...\(Priority.maxRawValue)]")
self.rawValue = rawValue
}
public static func == (lhs: Priority, rhs: Priority) -> Bool {
return lhs.rawValue == rhs.rawValue
}
public static func < (lhs: Priority, rhs: Priority) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
/**
Describes the queueing heoristic of entries.
*/
public enum QueueingHeuristic {
/** Determines the heuristic which the entry-queue is based on */
public static var value = QueueingHeuristic.priority
/** Chronological - FIFO */
case chronological
/** Ordered by priority */
case priority
/** Returns the caching heuristics mechanism that determines the priority in queue */
var heuristic: EntryCachingHeuristic {
switch self {
case .chronological:
return EKEntryChronologicalQueue()
case .priority:
return EKEntryPriorityQueue()
}
}
}
/**
Describes an *overriding* behavior for a new entry.
- In case no previous entry is currently presented, display the new entry.
- In case there is an entry that is currently presented - override it using the new entry. Also optionally drop all previously enqueued entries.
*/
case override(priority: Priority, dropEnqueuedEntries: Bool)
/**
Describes a FIFO behavior for an entry presentation.
- In case no previous entry is currently presented, display the new entry.
- In case there is an entry that is currently presented - enqueue the new entry, an present it just after the previous one is dismissed.
*/
case enqueue(priority: Priority)
var isEnqueue: Bool {
switch self {
case .enqueue:
return true
default:
return false
}
}
/** Setter / Getter for the display priority */
public var priority: Priority {
set {
switch self {
case .enqueue(priority: _):
self = .enqueue(priority: newValue)
case .override(priority: _, dropEnqueuedEntries: let dropEnqueuedEntries):
self = .override(priority: newValue, dropEnqueuedEntries: dropEnqueuedEntries)
}
}
get {
switch self {
case .enqueue(priority: let priority):
return priority
case .override(priority: let priority, dropEnqueuedEntries: _):
return priority
}
}
}
}
}
/** High priority entries can be overriden by other equal or higher priority entries only.
Entries are ignored as a higher priority entry is being displayed.
High priority entry overrides any other entry including another equal priority one.
You can you on of the values (.max, high, normal, low, min) and also set your own values. */
public extension EKAttributes.Precedence.Priority {
static let maxRawValue = 1000
static let highRawValue = 750
static let normalRawValue = 500
static let lowRawValue = 250
static let minRawValue = 0
/** Max - the highest possible priority of an entry. Can override only entries with *max* priority */
static let max = EKAttributes.Precedence.Priority(rawValue: maxRawValue)
static let high = EKAttributes.Precedence.Priority(rawValue: highRawValue)
static let normal = EKAttributes.Precedence.Priority(rawValue: normalRawValue)
static let low = EKAttributes.Precedence.Priority(rawValue: lowRawValue)
static let min = EKAttributes.Precedence.Priority(rawValue: minRawValue)
}

View File

@@ -0,0 +1,97 @@
//
// EKAttributes+Presets.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/23/18.
//
import Foundation
public extension EKAttributes {
/** Default attributes - Can be mutated according to the hosting application theme */
static var `default` = EKAttributes()
/** Toast preset - The frame fills margins and safe area is filled with background view */
static var toast: EKAttributes {
var attributes = EKAttributes()
attributes.positionConstraints = .fullWidth
attributes.positionConstraints.safeArea = .empty(fillSafeArea: true)
attributes.windowLevel = .statusBar
attributes.scroll = .edgeCrossingDisabled(swipeable: true)
attributes.popBehavior = .animated(animation: .translation)
return attributes
}
/** Float preset - The frame is margined and the safe area is left cleared */
static var float: EKAttributes {
var attributes = EKAttributes()
attributes.positionConstraints = .float
attributes.roundCorners = .all(radius: 10)
attributes.positionConstraints.safeArea = .empty(fillSafeArea: false)
attributes.windowLevel = .statusBar
return attributes
}
/** Preset for top float entry */
static var topFloat: EKAttributes {
var attributes = float
attributes.position = .top
return attributes
}
/** Preset for a bottom float entry */
static var bottomFloat: EKAttributes {
var attributes = float
attributes.position = .bottom
return attributes
}
/** Preset for a center float entry */
static var centerFloat: EKAttributes {
var attributes = float
attributes.position = .center
return attributes
}
/** Preset for a bottom toast entry */
static var bottomToast: EKAttributes {
var attributes = toast
attributes.position = .bottom
return attributes
}
/** Preset for a top toast entry */
static var topToast: EKAttributes {
var attributes = toast
attributes.position = .top
return attributes
}
/** Preset for a top note entry */
static var topNote: EKAttributes {
var attributes = topToast
attributes.scroll = .disabled
attributes.windowLevel = .normal
attributes.entryInteraction = .absorbTouches
return attributes
}
/** Preset for a bottom note entry */
static var bottomNote: EKAttributes {
var attributes = bottomToast
attributes.scroll = .disabled
attributes.windowLevel = .normal
attributes.entryInteraction = .absorbTouches
return attributes
}
/** Preset for a status bar entry - appears on top of the status bar */
static var statusBar: EKAttributes {
var attributes = topToast
attributes.windowLevel = .statusBar
attributes.entryInteraction = .absorbTouches
attributes.positionConstraints.safeArea = .overridden
return attributes
}
}

View File

@@ -0,0 +1,75 @@
//
// EKAttributes+Scroll.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/30/18.
//
import Foundation
import CoreGraphics
public extension EKAttributes {
/** Describes the event of scroll user interaction */
enum Scroll {
/** Describes the event when the user leaves the entry after rubber-banding it - How the entry behaves */
public struct PullbackAnimation {
public var duration: TimeInterval
public var damping: CGFloat
public var initialSpringVelocity: CGFloat
public init(duration: TimeInterval, damping: CGFloat, initialSpringVelocity: CGFloat) {
self.duration = duration
self.damping = damping
self.initialSpringVelocity = initialSpringVelocity
}
/** The entry is jolted when it's pulled back into the original position */
public static var jolt: PullbackAnimation {
return PullbackAnimation(duration: 0.5, damping: 0.3, initialSpringVelocity: 10)
}
/** The view eases out when it's pulled back into the original position */
public static var easeOut: PullbackAnimation {
return PullbackAnimation(duration: 0.3, damping: 1, initialSpringVelocity: 10)
}
}
/** The scroll ability is totally disabled */
case disabled
/** The scroll in the opposite direction to the edge is disabled */
case edgeCrossingDisabled(swipeable: Bool)
/** The scroll abiliby is enabled */
case enabled(swipeable: Bool, pullbackAnimation: PullbackAnimation)
var isEnabled: Bool {
switch self {
case .disabled:
return false
default:
return true
}
}
var isSwipeable: Bool {
switch self {
case .edgeCrossingDisabled(swipeable: let swipeable), .enabled(swipeable: let swipeable, pullbackAnimation: _):
return swipeable
default:
return false
}
}
var isEdgeCrossingEnabled: Bool {
switch self {
case .edgeCrossingDisabled:
return false
default:
return true
}
}
}
}

View File

@@ -0,0 +1,43 @@
//
// EKAttributes+Shadow.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
import UIKit
public extension EKAttributes {
/** The shadow around the entry */
enum Shadow {
/** No shadow */
case none
/** Shadow with value */
case active(with: Value)
/** The shadow properties */
public struct Value {
public let radius: CGFloat
public let opacity: Float
public let color: EKColor
public let offset: CGSize
public init(color: EKColor = .black,
opacity: Float,
radius: CGFloat,
offset: CGSize = .zero) {
self.color = color
self.radius = radius
self.offset = offset
self.opacity = opacity
}
}
}
}

View File

@@ -0,0 +1,91 @@
//
// EKAttributes+StatusBar.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/25/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public extension EKAttributes {
/** Status bar appearance */
enum StatusBar {
/** The appearance of the status bar */
public typealias Appearance = (visible: Bool, style: UIStatusBarStyle)
/** Ignored. Status bar is ignored by entries with this apperance value*/
case ignored
/** Hidden. Doesn't apply to iPhone X */
case hidden
/** Visible with explicit dark style */
case dark
/** Visible with explicit light style */
case light
/** Keep previous state of status bar.
In case there is an already displayed entry, keep its status bar appearance.
In case the app is already displaying a status bar, keep its appearance */
case inferred
/** Returns the status bar appearance.
Note: See *Appearance* */
public var appearance: Appearance {
switch self {
case .dark:
if #available(iOS 13, *) {
return (true, .darkContent)
} else {
return (true, .default)
}
case .light:
return (true, .lightContent)
case .inferred:
return StatusBar.currentAppearance
case .hidden:
return (false, StatusBar.currentStyle)
case .ignored:
fatalError("There is no defined appearance for an ignored status bar")
}
}
/** Returns the status bar according to a given appearance */
public static func statusBar(by appearance: Appearance) -> StatusBar {
guard appearance.visible else {
return .hidden
}
switch appearance.style {
case .lightContent:
return .light
default:
return .dark
}
}
/** Returns the current appearance */
public static var currentAppearance: Appearance {
return (StatusBar.isCurrentVisible, StatusBar.currentStyle)
}
/** Returns the current status bar */
public static var currentStatusBar: StatusBar {
return statusBar(by: currentAppearance)
}
// Accessors
// TODO: Use `statusBarManager` of the window scene on iOS 13
private static var currentStyle: UIStatusBarStyle {
return UIApplication.shared.statusBarStyle
}
// TODO: Use `statusBarManager` of the window scene on iOS 13
private static var isCurrentVisible: Bool {
return !UIApplication.shared.isStatusBarHidden
}
}
}

View File

@@ -0,0 +1,84 @@
//
// EKAttributes+UserInteraction.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
public extension EKAttributes {
/** Describes the user interaction events that are triggered as the user taps the entry / screen */
struct UserInteraction {
/** Code that is executed when the user taps the entry / screen */
public typealias Action = () -> ()
/** The default event that happens as the user interacts */
public enum Default {
/** Absorbs touches. The entry / screen does nothing (Swallows the touch) */
case absorbTouches
/** Touches delay the exit of the entry */
case delayExit(by: TimeInterval)
/** Taps dismiss the entry immediately */
case dismissEntry
/** Touches are forwarded to the lower window (In most cases it would be the application main window that will handle it */
case forward
}
var isResponsive: Bool {
switch defaultAction {
case .forward:
return false
default:
return true
}
}
var isDelayExit: Bool {
switch defaultAction {
case .delayExit:
return true
default:
return false
}
}
/** A default action that is executed when the entry or the screen are interacted by the user */
public var defaultAction: Default
/** Additional actions that can be customized by the user */
public var customTapActions: [Action]
public init(defaultAction: Default = .absorbTouches, customTapActions: [Action] = []) {
self.defaultAction = defaultAction
self.customTapActions = customTapActions
}
/** Dismiss action */
public static var dismiss: UserInteraction {
return UserInteraction(defaultAction: .dismissEntry)
}
/** Forward action */
public static var forward: UserInteraction {
return UserInteraction(defaultAction: .forward)
}
/** Absorb touches action */
public static var absorbTouches: UserInteraction {
return UserInteraction(defaultAction: .absorbTouches)
}
/** Delay exit action */
public static func delayExit(by delay: TimeInterval) -> UserInteraction {
return UserInteraction(defaultAction: .delayExit(by: delay))
}
}
}

View File

@@ -0,0 +1,30 @@
//
// EKAttributes+Validations.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/18/18.
//
import Foundation
extension EKAttributes {
private static var minDisplayDuration: DisplayDuration {
return 0
}
var validateDisplayDuration: Bool {
guard displayDuration >= EKAttributes.minDisplayDuration else {
return false
}
return true
}
var validateWindowLevel: Bool {
return windowLevel.value >= .normal
}
var isValid: Bool {
return validateDisplayDuration && validateWindowLevel
}
}

View File

@@ -0,0 +1,42 @@
//
// EKAttributes+WindowLevel.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/21/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
public extension EKAttributes {
/** Describes the window level in which the entry would be displayed */
enum WindowLevel {
/** Above the alerts */
case alerts
/** Above the status bar */
case statusBar
/** Above the application window */
case normal
/** Custom level */
case custom(level: UIWindow.Level)
/** Returns the raw value - the window level itself */
public var value: UIWindow.Level {
switch self {
case .alerts:
return .alert
case .statusBar:
return .statusBar
case .normal:
return .normal
case .custom(level: let level):
return level
}
}
}
}

View File

@@ -0,0 +1,99 @@
//
// EKAttributes.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/19/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import Foundation
import UIKit
public struct EKAttributes {
// MARK: Identification
/**
A settable **optional** name that matches the entry-attributes.
- Nameless entries cannot be inquired using *SwiftEntryKit.isCurrentlyDisplaying(entryNamed: _) -> Bool*
*/
public var name: String?
// MARK: Display Attributes
/** Entry presentation window level */
public var windowLevel = WindowLevel.statusBar
/** The position of the entry inside the screen */
public var position = Position.top
/** The display manner of the entry. */
public var precedence = Precedence.override(priority: .normal, dropEnqueuedEntries: false)
/** Describes how long the entry is displayed before it is dismissed */
public var displayDuration: DisplayDuration = 2 // Use .infinity for infinite duration
/** The frame attributes of the entry */
public var positionConstraints = PositionConstraints()
// MARK: User Interaction Attributes
/** Describes what happens when the user interacts the screen,
forwards the touch to the application window by default */
public var screenInteraction = UserInteraction.forward
/** Describes what happens when the user interacts the entry,
dismisses the content by default */
public var entryInteraction = UserInteraction.dismiss
/** Describes the scrolling behaviour of the entry.
The entry can be swiped out and in with an ability to spring back with a jolt */
public var scroll = Scroll.enabled(swipeable: true, pullbackAnimation: .jolt)
/** Generate haptic feedback once the entry is displayed */
public var hapticFeedbackType = NotificationHapticFeedback.none
/** Describes the actions that take place when the entry appears or is being dismissed */
public var lifecycleEvents = LifecycleEvents()
// MARK: Theme & Style Attributes
/** The display mode of the entry */
public var displayMode = DisplayMode.inferred
/** Describes the entry's background appearance while it shows */
public var entryBackground = BackgroundStyle.clear
/** Describes the background appearance while the entry shows */
public var screenBackground = BackgroundStyle.clear
/** The shadow around the entry */
public var shadow = Shadow.none
/** The corner attributes */
public var roundCorners = RoundCorners.none
/** The border around the entry */
public var border = Border.none
/** Preferred status bar style while the entry shows */
public var statusBar = StatusBar.inferred
// MARK: Animation Attributes
/** Describes how the entry animates in */
public var entranceAnimation = Animation.translation
/** Describes how the entry animates out */
public var exitAnimation = Animation.translation
/** Describes the previous entry behaviour when a new entry with higher display-priority shows */
public var popBehavior = PopBehavior.animated(animation: .translation) {
didSet {
popBehavior.validate()
}
}
/** Init with default attributes */
public init() {}
}

View File

@@ -0,0 +1,170 @@
//
// SwiftEntryKit.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/29/18.
//
import UIKit
/**
A stateless, threadsafe (unless described otherwise) entry point that contains the display and the dismissal logic of entries.
*/
public final class SwiftEntryKit {
/** Describes the a single or multiple entries for possible dismissal states */
public enum EntryDismissalDescriptor {
/** Describes specific entry / entries with name */
case specific(entryName: String)
/** Describes a group of entries with lower or equal display priority */
case prioritizedLowerOrEqualTo(priority: EKAttributes.Precedence.Priority)
/** Describes all the entries that are currently in the queue and pending presentation */
case enqueued
/** Describes all the entries */
case all
/** Describes the currently displayed entry */
case displayed
}
/** The window to rollback to after dismissal */
public enum RollbackWindow {
/** The main window */
case main
/** A given custom window */
case custom(window: UIWindow)
}
/** Completion handler for the dismissal method */
public typealias DismissCompletionHandler = () -> Void
/** Cannot be instantiated, customized, inherited. */
private init() {}
/**
Returns the window that displays the entry.
**Warning**: the returned `UIWindow` instance is `nil` in case
no entry is currently displayed.
This can be used
*/
public class var window: UIWindow? {
return EKWindowProvider.shared.entryWindow
}
/**
Returns true if **any** entry is currently displayed.
- Not thread safe - should be called from the main queue only in order to receive a reliable result.
- Convenience computed variable. Using it is the same as invoking **isCurrentlyDisplaying() -> Bool** (witohut the name of the entry).
*/
public class var isCurrentlyDisplaying: Bool {
return isCurrentlyDisplaying()
}
/**
Returns true if an entry with a given name is currently displayed.
- Not thread safe - should be called from the main queue only in order to receive a reliable result.
- If invoked with *name* = *nil* or without the parameter value, it will return *true* if **any** entry is currently displayed.
- Returns a *false* value for currently enqueued entries.
- parameter name: The name of the entry. Its default value is *nil*.
*/
public class func isCurrentlyDisplaying(entryNamed name: String? = nil) -> Bool {
return EKWindowProvider.shared.isCurrentlyDisplaying(entryNamed: name)
}
/**
Returns true if **any** entry is currently enqueued and waiting to be displayed.
- Not thread safe - should be called from the main queue only in order to receive a reliable result.
- Convenience computed variable. Using it is the same as invoking **~queueContains() -> Bool** (witohut the name of the entry)
*/
public class var isQueueEmpty: Bool {
return !queueContains()
}
/**
Returns true if an entry with a given name is currently enqueued and waiting to be displayed.
- Not thread safe - should be called from the main queue only in order to receive a reliable result.
- If invoked with *name* = *nil* or without the parameter value, it will return *true* if **any** entry is currently displayed, meaning, the queue is not currently empty.
- parameter name: The name of the entry. Its default value is *nil*.
*/
public class func queueContains(entryNamed name: String? = nil) -> Bool {
return EKWindowProvider.shared.queueContains(entryNamed: name)
}
/**
Displays a given entry view using an attributes struct.
- A thread-safe method - Can be invokes from any thread
- A class method - Should be called on the class
- parameter view: Custom view that is to be displayed
- parameter attributes: Display properties
- parameter presentInsideKeyWindow: Indicates whether the entry window should become the key window.
- parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* which is the app main window
*/
public class func display(entry view: UIView, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) {
DispatchQueue.main.async {
EKWindowProvider.shared.display(view: view, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
}
}
/**
Displays a given entry view controller using an attributes struct.
- A thread-safe method - Can be invokes from any thread
- A class method - Should be called on the class
- parameter view: Custom view that is to be displayed
- parameter attributes: Display properties
- parameter presentInsideKeyWindow: Indicates whether the entry window should become the key window.
- parameter rollbackWindow: After the entry has been dismissed, SwiftEntryKit rolls back to the given window. By default it is *.main* - which is the app main window
*/
public class func display(entry viewController: UIViewController, using attributes: EKAttributes, presentInsideKeyWindow: Bool = false, rollbackWindow: RollbackWindow = .main) {
DispatchQueue.main.async {
EKWindowProvider.shared.display(viewController: viewController, using: attributes, presentInsideKeyWindow: presentInsideKeyWindow, rollbackWindow: rollbackWindow)
}
}
/**
ALPHA FEATURE: Transform the previous entry to the current one using the previous attributes struct.
- A thread-safe method - Can be invoked from any thread.
- A class method - Should be called on the class.
- This feature hasn't been fully tested. Use with caution.
- parameter view: Custom view that is to be displayed instead of the currently displayed entry
*/
public class func transform(to view: UIView) {
DispatchQueue.main.async {
EKWindowProvider.shared.transform(to: view)
}
}
/**
Dismisses the currently presented entry and removes the presented window instance after the exit animation is concluded.
- A thread-safe method - Can be invoked from any thread.
- A class method - Should be called on the class.
- parameter descriptor: A descriptor for the entries that are to be dismissed. The default value is *.displayed*.
- parameter completion: A completion handler that is to be called right after the entry is dismissed (After the animation is concluded).
*/
public class func dismiss(_ descriptor: EntryDismissalDescriptor = .displayed, with completion: DismissCompletionHandler? = nil) {
DispatchQueue.main.async {
EKWindowProvider.shared.dismiss(descriptor, with: completion)
}
}
/**
Layout the view hierarchy that is rooted in the window.
- In case you use complex animations, you can call it to refresh the AutoLayout mechanism on the entire view hierarchy.
- A thread-safe method - Can be invoked from any thread.
- A class method - Should be called on the class.
*/
public class func layoutIfNeeded() {
if Thread.isMainThread {
EKWindowProvider.shared.layoutIfNeeded()
} else {
DispatchQueue.main.async {
EKWindowProvider.shared.layoutIfNeeded()
}
}
}
}

View File

@@ -0,0 +1,63 @@
//
// GradientView.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
class GradientView: UIView {
struct Style {
let gradient: EKAttributes.BackgroundStyle.Gradient
let displayMode: EKAttributes.DisplayMode
init?(gradient: EKAttributes.BackgroundStyle.Gradient?,
displayMode: EKAttributes.DisplayMode) {
guard let gradient = gradient else {
return nil
}
self.gradient = gradient
self.displayMode = displayMode
}
}
private let gradientLayer = CAGradientLayer()
var style: Style? {
didSet {
setupColor()
}
}
init() {
super.init(frame: .zero)
layer.addSublayer(gradientLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
private func setupColor() {
guard let style = style else {
return
}
gradientLayer.colors = style.gradient.colors.map {
$0.color(for: traitCollection, mode: style.displayMode).cgColor
}
gradientLayer.startPoint = style.gradient.startPoint
gradientLayer.endPoint = style.gradient.endPoint
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setupColor()
}
}

View File

@@ -0,0 +1,20 @@
//
// HapticFeedbackGenerator.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 4/20/18.
// Copyright (c) 2018 huri000@gmail.com. All rights reserved.
//
import UIKit
struct HapticFeedbackGenerator {
@available(iOS 10.0, *)
static func notification(type: EKAttributes.NotificationHapticFeedback) {
guard let value = type.value else {
return
}
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(value)
}
}

View File

@@ -0,0 +1,22 @@
//
// UIView+Responder.swift
// SwiftEntryKit
//
// Created by Daniel Huri on 5/17/18.
//
import UIKit
extension UIView {
var containsFirstResponder: Bool {
var contains = false
for subview in subviews.reversed() where !contains {
if subview.isFirstResponder {
contains = true
} else {
contains = subview.containsFirstResponder
}
}
return contains
}
}