Merge pull request 'feat: detents api' (#1) from feature/detents_api into master
Reviewed-on: TouchInstinct/TIPanModal#1
This commit is contained in:
commit
871fc46020
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -30,6 +30,7 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
*/
|
||||
public enum PresentationState {
|
||||
case shortForm
|
||||
case mediumForm
|
||||
case longForm
|
||||
}
|
||||
|
||||
|
|
@ -79,11 +80,17 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
*/
|
||||
private var shortFormYPosition: CGFloat = 0
|
||||
|
||||
private var mediumFormYPosition: CGFloat = 0
|
||||
|
||||
/**
|
||||
The y value for the long form presentation state
|
||||
*/
|
||||
private var longFormYPosition: CGFloat = 0
|
||||
|
||||
private var allowsDragToDismiss: Bool {
|
||||
presentable?.onDragToDismiss != nil
|
||||
}
|
||||
|
||||
/**
|
||||
Determine anchored Y postion based on the `anchorModalToLongForm` flag
|
||||
*/
|
||||
|
|
@ -106,16 +113,18 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
*/
|
||||
private lazy var backgroundView: DimmedView = {
|
||||
let view: DimmedView
|
||||
let type = presentable?.dimmedViewType ?? .opaque
|
||||
|
||||
if let color = presentable?.panModalBackgroundColor {
|
||||
view = DimmedView(dimColor: color)
|
||||
view = DimmedView(presentingController: presentingViewController, dimColor: color, appearanceType: type)
|
||||
} else {
|
||||
view = DimmedView()
|
||||
}
|
||||
view.didTap = { [weak self] _ in
|
||||
if self?.presentable?.allowsTapToDismiss == true {
|
||||
self?.presentedViewController.dismiss(animated: true)
|
||||
view = DimmedView(presentingController: presentingViewController, appearanceType: type)
|
||||
}
|
||||
|
||||
view.didTap = { [weak self] in
|
||||
self?.presentable?.onTapToDismiss?()
|
||||
}
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
|
|
@ -129,16 +138,6 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
return PanContainerView(presentedView: presentedViewController.view, frame: frame)
|
||||
}()
|
||||
|
||||
/**
|
||||
Drag Indicator View
|
||||
*/
|
||||
private lazy var dragIndicatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = presentable?.dragIndicatorBackgroundColor
|
||||
view.layer.cornerRadius = Constants.dragIndicatorSize.height / 2.0
|
||||
return view
|
||||
}()
|
||||
|
||||
/**
|
||||
Override presented view to return the pan container wrapper
|
||||
*/
|
||||
|
|
@ -180,6 +179,7 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
layoutBackgroundView(in: containerView)
|
||||
layoutPresentedView(in: containerView)
|
||||
configureScrollViewInsets()
|
||||
configureShadowIfNeeded()
|
||||
|
||||
guard let coordinator = presentedViewController.transitionCoordinator else {
|
||||
backgroundView.dimState = .max
|
||||
|
|
@ -187,7 +187,9 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
}
|
||||
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.backgroundView.dimState = .max
|
||||
if let yPos = self?.shortFormYPosition {
|
||||
self?.adjust(toYPosition: yPos)
|
||||
}
|
||||
self?.presentedViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
|
@ -211,7 +213,6 @@ open class PanModalPresentationController: UIPresentationController {
|
|||
so hiding it on view dismiss means avoiding visual bugs
|
||||
*/
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.dragIndicatorView.alpha = 0.0
|
||||
self?.backgroundView.dimState = .off
|
||||
self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
|
|
@ -262,6 +263,10 @@ public extension PanModalPresentationController {
|
|||
switch state {
|
||||
case .shortForm:
|
||||
snap(toYPosition: shortFormYPosition)
|
||||
|
||||
case .mediumForm:
|
||||
snap(toYPosition: mediumFormYPosition)
|
||||
|
||||
case .longForm:
|
||||
snap(toYPosition: longFormYPosition)
|
||||
}
|
||||
|
|
@ -347,10 +352,6 @@ private extension PanModalPresentationController {
|
|||
containerView.addSubview(presentedView)
|
||||
containerView.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
if presentable.showDragIndicator {
|
||||
addDragIndicatorView(to: presentedView)
|
||||
}
|
||||
|
||||
if presentable.shouldRoundTopCorners {
|
||||
addRoundedCorners(to: presentedView)
|
||||
}
|
||||
|
|
@ -371,7 +372,7 @@ private extension PanModalPresentationController {
|
|||
let panFrame = panContainerView.frame
|
||||
panContainerView.frame.size = frame.size
|
||||
|
||||
if ![shortFormYPosition, longFormYPosition].contains(panFrame.origin.y) {
|
||||
if ![shortFormYPosition, mediumFormYPosition, longFormYPosition].contains(panFrame.origin.y) {
|
||||
// if the container is already in the correct position, no need to adjust positioning
|
||||
// (rotations & size changes cause positioning to be out of sync)
|
||||
let yPosition = panFrame.origin.y - panFrame.height + frame.height
|
||||
|
|
@ -404,19 +405,6 @@ private extension PanModalPresentationController {
|
|||
backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
|
||||
}
|
||||
|
||||
/**
|
||||
Adds the drag indicator view to the view hierarchy
|
||||
& configures its layout constraints.
|
||||
*/
|
||||
func addDragIndicatorView(to view: UIView) {
|
||||
view.addSubview(dragIndicatorView)
|
||||
dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
dragIndicatorView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: -Constants.indicatorYOffset).isActive = true
|
||||
dragIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
|
||||
dragIndicatorView.widthAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.width).isActive = true
|
||||
dragIndicatorView.heightAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.height).isActive = true
|
||||
}
|
||||
|
||||
/**
|
||||
Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
|
|
@ -426,6 +414,7 @@ private extension PanModalPresentationController {
|
|||
else { return }
|
||||
|
||||
shortFormYPosition = layoutPresentable.shortFormYPos
|
||||
mediumFormYPosition = layoutPresentable.mediumFormYPos
|
||||
longFormYPosition = layoutPresentable.longFormYPos
|
||||
anchorModalToLongForm = layoutPresentable.anchorModalToLongForm
|
||||
extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling
|
||||
|
|
@ -465,6 +454,13 @@ private extension PanModalPresentationController {
|
|||
}
|
||||
}
|
||||
|
||||
func configureShadowIfNeeded() {
|
||||
let type = presentable?.dimmedViewType ?? .opaque
|
||||
|
||||
if case let .transparentWithShadow(shadow) = type {
|
||||
containerView?.configure(shadow: shadow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pan Gesture Event Handler
|
||||
|
|
@ -518,12 +514,16 @@ private extension PanModalPresentationController {
|
|||
if velocity.y < 0 {
|
||||
transition(to: .longForm)
|
||||
|
||||
} else if nearest(to: presentedView.frame.minY, inValues: [mediumFormYPosition, containerView.bounds.height]) == mediumFormYPosition
|
||||
&& presentedView.frame.minY < mediumFormYPosition {
|
||||
transition(to: .mediumForm)
|
||||
|
||||
} else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition
|
||||
&& presentedView.frame.minY < shortFormYPosition) || presentable?.allowsDragToDismiss == false {
|
||||
&& presentedView.frame.minY < shortFormYPosition) || allowsDragToDismiss == false {
|
||||
transition(to: .shortForm)
|
||||
|
||||
} else {
|
||||
presentedViewController.dismiss(animated: true)
|
||||
presentable?.onDragToDismiss?()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
@ -532,16 +532,19 @@ private extension PanModalPresentationController {
|
|||
The `containerView.bounds.height` is used to determine
|
||||
how close the presented view is to the bottom of the screen
|
||||
*/
|
||||
let position = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, longFormYPosition])
|
||||
let position = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, mediumFormYPosition, longFormYPosition])
|
||||
|
||||
if position == longFormYPosition {
|
||||
transition(to: .longForm)
|
||||
|
||||
} else if position == shortFormYPosition || presentable?.allowsDragToDismiss == false {
|
||||
} else if position == mediumFormYPosition {
|
||||
transition(to: .mediumForm)
|
||||
|
||||
} else if position == shortFormYPosition || allowsDragToDismiss == false {
|
||||
transition(to: .shortForm)
|
||||
|
||||
} else {
|
||||
presentedViewController.dismiss(animated: true)
|
||||
presentable?.onDragToDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -654,18 +657,9 @@ private extension PanModalPresentationController {
|
|||
func adjust(toYPosition yPos: CGFloat) {
|
||||
presentedView.frame.origin.y = max(yPos, anchoredYPosition)
|
||||
|
||||
guard presentedView.frame.origin.y > shortFormYPosition else {
|
||||
backgroundView.dimState = .max
|
||||
return
|
||||
}
|
||||
let maxHeight = UIScreen.main.bounds.height - longFormYPosition
|
||||
|
||||
let yDisplacementFromShortForm = presentedView.frame.origin.y - shortFormYPosition
|
||||
|
||||
/**
|
||||
Once presentedView is translated below shortForm, calculate yPos relative to bottom of screen
|
||||
and apply percentage to backgroundView alpha
|
||||
*/
|
||||
backgroundView.dimState = .percent(1.0 - (yDisplacementFromShortForm / presentedView.frame.height))
|
||||
backgroundView.dimState = .percent(1 - presentedView.frame.origin.y / maxHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -847,12 +841,6 @@ private extension PanModalPresentationController {
|
|||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: radius, height: radius))
|
||||
|
||||
// Draw around the drag indicator view, if displayed
|
||||
if presentable?.showDragIndicator == true {
|
||||
let indicatorLeftEdgeXPos = view.bounds.width/2.0 - Constants.dragIndicatorSize.width/2.0
|
||||
drawAroundDragIndicator(currentPath: path, indicatorLeftEdgeXPos: indicatorLeftEdgeXPos)
|
||||
}
|
||||
|
||||
// Set path as a mask to display optional drag indicator view & rounded corners
|
||||
let mask = CAShapeLayer()
|
||||
mask.path = path.cgPath
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// Copyright (c) 2022 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ModalViewPresentationDetent: Hashable {
|
||||
|
||||
// MARK: - Default Values
|
||||
|
||||
public static var headerOnly: ModalViewPresentationDetent {
|
||||
ModalViewPresentationDetent(height: CGFloat(Int.min))
|
||||
}
|
||||
|
||||
public static func height(_ height: CGFloat) -> ModalViewPresentationDetent {
|
||||
ModalViewPresentationDetent(height: height)
|
||||
}
|
||||
|
||||
public static var maxHeight: ModalViewPresentationDetent {
|
||||
ModalViewPresentationDetent(height: CGFloat(Int.max))
|
||||
}
|
||||
|
||||
// MARK: - Public Properties
|
||||
|
||||
public var height: CGFloat
|
||||
|
||||
// MARK: - Internal Methods
|
||||
|
||||
func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight {
|
||||
if self == .headerOnly {
|
||||
return .contentHeight(headerHeight)
|
||||
}
|
||||
|
||||
if self == .maxHeight {
|
||||
return .maxHeight
|
||||
}
|
||||
|
||||
return .contentHeight(height)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Comparable
|
||||
|
||||
extension ModalViewPresentationDetent: Comparable {
|
||||
public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool {
|
||||
lhs.height < rhs.height
|
||||
}
|
||||
}
|
||||
|
|
@ -13,14 +13,30 @@ import UIKit
|
|||
*/
|
||||
public extension PanModalPresentable where Self: UIViewController {
|
||||
|
||||
var onTapToDismiss: (() -> Void)? {
|
||||
{ [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
var onDragToDismiss: (() -> Void)? {
|
||||
{ [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
var topOffset: CGFloat {
|
||||
return topLayoutOffset + 21.0
|
||||
topLayoutOffset
|
||||
}
|
||||
|
||||
var shortFormHeight: PanModalHeight {
|
||||
return longFormHeight
|
||||
}
|
||||
|
||||
var mediumFormHeight: PanModalHeight {
|
||||
longFormHeight
|
||||
}
|
||||
|
||||
var longFormHeight: PanModalHeight {
|
||||
|
||||
guard let scrollView = panScrollable
|
||||
|
|
@ -31,6 +47,14 @@ public extension PanModalPresentable where Self: UIViewController {
|
|||
return .contentHeight(scrollView.contentSize.height)
|
||||
}
|
||||
|
||||
var dimmedViewType: DimmedView.AppearanceType {
|
||||
.opaque
|
||||
}
|
||||
|
||||
var presentationDetents: [ModalViewPresentationDetent] {
|
||||
[]
|
||||
}
|
||||
|
||||
var cornerRadius: CGFloat {
|
||||
return 8.0
|
||||
}
|
||||
|
|
@ -51,10 +75,6 @@ public extension PanModalPresentable where Self: UIViewController {
|
|||
return UIColor.black.withAlphaComponent(0.7)
|
||||
}
|
||||
|
||||
var dragIndicatorBackgroundColor: UIColor {
|
||||
return UIColor.lightGray
|
||||
}
|
||||
|
||||
var scrollIndicatorInsets: UIEdgeInsets {
|
||||
let top = shouldRoundTopCorners ? cornerRadius : 0
|
||||
return UIEdgeInsets(top: CGFloat(top), left: 0, bottom: bottomLayoutOffset, right: 0)
|
||||
|
|
@ -93,10 +113,6 @@ public extension PanModalPresentable where Self: UIViewController {
|
|||
return isPanModalPresented
|
||||
}
|
||||
|
||||
var showDragIndicator: Bool {
|
||||
return shouldRoundTopCorners
|
||||
}
|
||||
|
||||
func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ extension PanModalPresentable where Self: UIViewController {
|
|||
return max(shortFormYPos, longFormYPos)
|
||||
}
|
||||
|
||||
var mediumFormYPos: CGFloat {
|
||||
let mediumFormYPos = topMargin(from: mediumFormHeight)
|
||||
|
||||
return max(mediumFormYPos, longFormYPos)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the long form Y position
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public protocol PanModalPresentable: AnyObject {
|
|||
*/
|
||||
var shortFormHeight: PanModalHeight { get }
|
||||
|
||||
var mediumFormHeight: PanModalHeight { get }
|
||||
|
||||
/**
|
||||
The height of the pan modal container view
|
||||
when in the longForm presentation state.
|
||||
|
|
@ -55,6 +57,9 @@ public protocol PanModalPresentable: AnyObject {
|
|||
*/
|
||||
var longFormHeight: PanModalHeight { get }
|
||||
|
||||
var presentationDetents: [ModalViewPresentationDetent] { get }
|
||||
|
||||
var dimmedViewType: DimmedView.AppearanceType { get }
|
||||
/**
|
||||
The corner radius used when `shouldRoundTopCorners` is enabled.
|
||||
|
||||
|
|
@ -95,13 +100,6 @@ public protocol PanModalPresentable: AnyObject {
|
|||
*/
|
||||
var panModalBackgroundColor: UIColor { get }
|
||||
|
||||
/**
|
||||
The drag indicator view color.
|
||||
|
||||
Default value is light gray.
|
||||
*/
|
||||
var dragIndicatorBackgroundColor: UIColor { get }
|
||||
|
||||
/**
|
||||
We configure the panScrollable's scrollIndicatorInsets interally so override this value
|
||||
to set custom insets.
|
||||
|
|
@ -134,6 +132,8 @@ public protocol PanModalPresentable: AnyObject {
|
|||
Default value is true.
|
||||
*/
|
||||
var allowsDragToDismiss: Bool { get }
|
||||
var onTapToDismiss: (() -> Void)? { get }
|
||||
var onDragToDismiss: (() -> Void)? { get }
|
||||
|
||||
/**
|
||||
A flag to determine if dismissal should be initiated when tapping on the dimmed background view.
|
||||
|
|
@ -165,13 +165,6 @@ public protocol PanModalPresentable: AnyObject {
|
|||
*/
|
||||
var shouldRoundTopCorners: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if a drag indicator should be shown
|
||||
above the pan modal container view.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var showDragIndicator: Bool { get }
|
||||
|
||||
/**
|
||||
Asks the delegate if the pan modal should respond to the pan modal gesture recognizer.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ import UIKit
|
|||
*/
|
||||
public class DimmedView: UIView {
|
||||
|
||||
public enum AppearanceType {
|
||||
|
||||
case transparent
|
||||
case transparentWithShadow(ShadowConfigurator)
|
||||
case opaque
|
||||
|
||||
var isTransparent: Bool {
|
||||
switch self {
|
||||
case .transparent, .transparentWithShadow:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Represents the possible states of the dimmed view.
|
||||
max, off or a percentage of dimAlpha.
|
||||
|
|
@ -30,6 +47,11 @@ public class DimmedView: UIView {
|
|||
*/
|
||||
var dimState: DimState = .off {
|
||||
didSet {
|
||||
guard !appearanceType.isTransparent else {
|
||||
alpha = .zero
|
||||
return
|
||||
}
|
||||
|
||||
switch dimState {
|
||||
case .max:
|
||||
alpha = 1.0
|
||||
|
|
@ -41,36 +63,61 @@ public class DimmedView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
weak var presentingController: UIViewController?
|
||||
var appearanceType: AppearanceType
|
||||
|
||||
/**
|
||||
The closure to be executed when a tap occurs
|
||||
*/
|
||||
var didTap: ((_ recognizer: UIGestureRecognizer) -> Void)?
|
||||
|
||||
/**
|
||||
Tap gesture recognizer
|
||||
*/
|
||||
private lazy var tapGesture: UIGestureRecognizer = {
|
||||
return UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
}()
|
||||
var didTap: (() -> Void)?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init(dimColor: UIColor = UIColor.black.withAlphaComponent(0.7)) {
|
||||
init(presentingController: UIViewController? = nil,
|
||||
dimColor: UIColor = UIColor.black.withAlphaComponent(0.7),
|
||||
appearanceType: AppearanceType) {
|
||||
|
||||
self.presentingController = presentingController
|
||||
self.appearanceType = appearanceType
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
alpha = 0.0
|
||||
backgroundColor = dimColor
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// MARK: - Overrides
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if appearanceType.isTransparent {
|
||||
let subviews = presentingController?.view.subviews.reversed() ?? []
|
||||
|
||||
for subview in subviews {
|
||||
if let hittedView = subview.hitTest(point, with: event) {
|
||||
return hittedView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
!appearanceType.isTransparent
|
||||
}
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
@objc private func didTapView() {
|
||||
didTap?(tapGesture)
|
||||
didTap?()
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import class UIKit.UIColor
|
||||
import CoreGraphics
|
||||
|
||||
public protocol ShadowConfigurator {
|
||||
var radius: CGFloat { get set }
|
||||
var offset: CGSize { get set }
|
||||
var color: UIColor { get set }
|
||||
var opacity: Float { get set }
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIView {
|
||||
func configure(shadow: ShadowConfigurator) {
|
||||
layer.shadowOpacity = shadow.opacity
|
||||
layer.shadowOffset = shadow.offset
|
||||
layer.shadowColor = shadow.color.cgColor
|
||||
layer.shadowRadius = shadow.radius
|
||||
clipsToBounds = false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue