From 2a54de5652e26645dabee3c4682504df73dbf21d Mon Sep 17 00:00:00 2001 From: Dominic Go <18517029+dominicstop@users.noreply.github.com> Date: Mon, 19 Jun 2023 23:57:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AB=20Update:=20Exp=20-=20`AdaptiveMod?= =?UTF-8?q?al`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Update experiment/test - `swift-programmatic-modal/AdaptiveModal`. --- .../AdaptiveModal/AdaptiveModalConfig.swift | 3 + .../AdaptiveModalInterpolationPoint.swift | 6 +- .../AdaptiveModal/AdaptiveModalManager.swift | 231 ++++++++++++------ .../AdaptiveModalSnapPoint.swift | 49 +++- .../Test/AdaptiveModalConfigTestPresets.swift | 29 ++- ...eModalPresentationTestViewController.swift | 50 +++- 6 files changed, 287 insertions(+), 81 deletions(-) diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift index 6ec44e6e..f8986c35 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift @@ -40,6 +40,9 @@ struct AdaptiveModalConfig { // let entranceConfig: AdaptiveModalEntranceConfig; // let snapSwipeVelocityThreshold: CGFloat = 0; + // MARK: - Computed Properties + // --------------------------- + var snapPoints: [AdaptiveModalSnapPointConfig] { .Element.deriveSnapPoints( undershootSnapPoint: self.undershootSnapPoint, diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift index 88974e04..5a7964a5 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift @@ -339,12 +339,14 @@ extension AdaptiveModalInterpolationPoint { static func compute( usingModalConfig modalConfig: AdaptiveModalConfig, + snapPoints: [AdaptiveModalSnapPointConfig]? = nil, layoutValueContext context: RNILayoutValueContext ) -> [Self] { - + + let snapPoints = snapPoints ?? modalConfig.snapPoints; var items: [AdaptiveModalInterpolationPoint] = []; - for (index, snapConfig) in modalConfig.snapPoints.enumerated() { + for (index, snapConfig) in snapPoints.enumerated() { items.append( AdaptiveModalInterpolationPoint( usingModalConfig: modalConfig, diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift index a2bb08be..8e52bee0 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift @@ -74,12 +74,31 @@ class AdaptiveModalManager: NSObject { return .default; }; + // MARK: - Properties - Config Interpolation Points + // ------------------------------------------------- + + /// The computed frames of the modal based on the snap points + private(set) var configInterpolationSteps: [AdaptiveModalInterpolationPoint]!; + + var currentConfigInterpolationStep: AdaptiveModalInterpolationPoint { + self.interpolationSteps[self.currentInterpolationIndex]; + }; + + private var configInterpolationRangeInput: [CGFloat]! { + self.interpolationSteps.map { $0.percent }; + }; + + // MARK: - Properties - Override Interpolation Points + // --------------------------------------------------- + + private var isOverridingSnapPoints = false; + + private var overrideSnapPoints: [AdaptiveModalSnapPointConfig]?; + private var overrideInterpolationPoints: [AdaptiveModalInterpolationPoint]?; + // MARK: - Properties - Interpolation Points // ------------------------------------------ - /// The computed frames of the modal based on the snap points - private(set) var interpolationSteps: [AdaptiveModalInterpolationPoint]!; - var prevInterpolationIndex = 0; var nextInterpolationIndex: Int?; @@ -89,14 +108,16 @@ class AdaptiveModalManager: NSObject { } }; + var interpolationSteps: [AdaptiveModalInterpolationPoint]! { + self.configInterpolationSteps + }; + var currentInterpolationStep: AdaptiveModalInterpolationPoint { self.interpolationSteps[self.currentInterpolationIndex]; }; private var interpolationRangeInput: [CGFloat]! { - self.interpolationSteps.map { - $0.percent - }; + self.interpolationSteps.map { $0.percent }; }; private var interpolationRangeMaxInput: CGFloat? { @@ -104,6 +125,7 @@ class AdaptiveModalManager: NSObject { return targetView.frame[keyPath: self.modalConfig.maxInputRangeKeyForRect]; }; + // MARK: - Properties - Animation-Related // --------------------------------------- @@ -1054,15 +1076,84 @@ class AdaptiveModalManager: NSObject { self.didTriggerSetup = false; }; + + private func cleanupSnapPointOverride(){ + self.isOverridingSnapPoints = false; + self.overrideSnapPoints = nil; + self.overrideInterpolationPoints = nil; + }; private func cleanup() { self.clearGestureValues(); self.clearAnimators(); self.cleanupViews(); + self.cleanupSnapPointOverride(); self.currentInterpolationIndex = 0; }; + // MARK: - Functions - Helpers/Utilities + // ------------------------------------- + + private func adjustInterpolationIndex(for nextIndex: Int) -> Int { + if nextIndex == 0 { + return self.shouldSnapToUnderShootSnapPoint + ? nextIndex + : 1; + }; + + let lastIndex = self.interpolationSteps.count - 1; + + if nextIndex == lastIndex { + return self.shouldSnapToOvershootSnapPoint + ? nextIndex + : lastIndex - 1; + }; + + return nextIndex; + }; + + private func applyGestureOffsets( + forGesturePoint gesturePoint: CGPoint + ) -> CGPoint { + + guard let computedGestureOffset = self.computedGestureOffset + else { return gesturePoint }; + + switch self.modalConfig.snapDirection { + case .bottomToTop, .rightToLeft: return CGPoint( + x: gesturePoint.x - computedGestureOffset.x, + y: gesturePoint.y - computedGestureOffset.y + ); + + case .topToBottom, .leftToRight: return CGPoint( + x: gesturePoint.x + computedGestureOffset.x, + y: gesturePoint.y + computedGestureOffset.y + ); + }; + }; + + func debug(prefix: String? = ""){ + print( + "\n - AdaptiveModalManager.debug - \(prefix ?? "N/A")" + + "\n - modalView: \(self.modalView?.debugDescription ?? "N/A")" + + "\n - modalView frame: \(self.modalView?.frame.debugDescription ?? "N/A")" + + "\n - modalView superview: \(self.modalView?.superview.debugDescription ?? "N/A")" + + "\n - targetView: \(self.targetView?.debugDescription ?? "N/A")" + + "\n - targetView frame: \(self.targetView?.frame.debugDescription ?? "N/A")" + + "\n - targetView superview: \(self.targetView?.superview.debugDescription ?? "N/A")" + + "\n - modalViewController: \(self.modalViewController?.debugDescription ?? "N/A")" + + "\n - targetViewController: \(self.targetViewController?.debugDescription ?? "N/A")" + + "\n - currentInterpolationIndex: \(self.currentInterpolationIndex)" + + "\n - modalView gestureRecognizers: \(self.modalView?.gestureRecognizers.debugDescription ?? "N/A")" + + "\n - interpolationSteps.computedRect: \(self.interpolationSteps.map({ $0.computedRect }))" + + "\n - interpolationSteps.percent: \(self.interpolationSteps.map({ $0.percent }))" + + "\n - interpolationSteps.backgroundVisualEffectIntensity: \(self.interpolationSteps.map({ $0.backgroundVisualEffectIntensity }))" + + "\n - interpolationSteps.backgroundVisualEffect: \(self.interpolationSteps.map({ $0.backgroundVisualEffect }))" + + "\n" + ); + }; + // MARK: - Functions // ----------------- @@ -1071,7 +1162,7 @@ class AdaptiveModalManager: NSObject { ) { let context = context ?? self.layoutValueContext; - self.interpolationSteps = .Element.compute( + self.configInterpolationSteps = .Element.compute( usingModalConfig: self.modalConfig, layoutValueContext: context ); @@ -1099,17 +1190,13 @@ class AdaptiveModalManager: NSObject { let inputRect = self.modalFrame!; let inputCoord = coord ?? - inputRect.origin[keyPath: self.modalConfig.inputValueKeyForPoint]; - - let inputCoordAdj = inputCoord < 0 - ? min(inputCoord, 0) - : inputCoord; + inputRect[keyPath: self.modalConfig.inputValueKeyForRect]; let delta = self.interpolationSteps.map { let coord = $0.computedRect[keyPath: self.modalConfig.inputValueKeyForRect]; - return abs(inputCoordAdj - coord); + return abs(inputCoord - coord); }; let deltaSorted = delta.enumerated().sorted { @@ -1168,24 +1255,6 @@ class AdaptiveModalManager: NSObject { ); }; - private func adjustInterpolationIndex(for nextIndex: Int) -> Int { - if nextIndex == 0 { - return self.shouldSnapToUnderShootSnapPoint - ? nextIndex - : 1; - }; - - let lastIndex = self.interpolationSteps.count - 1; - - if nextIndex == lastIndex { - return self.shouldSnapToOvershootSnapPoint - ? nextIndex - : lastIndex - 1; - }; - - return nextIndex; - }; - private func animateModal( to interpolationPoint: AdaptiveModalInterpolationPoint, completion: ((UIViewAnimatingPosition) -> Void)? = nil @@ -1239,26 +1308,6 @@ class AdaptiveModalManager: NSObject { self.startDisplayLink(); }; - private func applyGestureOffsets( - forGesturePoint gesturePoint: CGPoint - ) -> CGPoint { - - guard let computedGestureOffset = self.computedGestureOffset - else { return gesturePoint }; - - switch self.modalConfig.snapDirection { - case .bottomToTop, .rightToLeft: return CGPoint( - x: gesturePoint.x - computedGestureOffset.x, - y: gesturePoint.y - computedGestureOffset.y - ); - - case .topToBottom, .leftToRight: return CGPoint( - x: gesturePoint.x + computedGestureOffset.x, - y: gesturePoint.y + computedGestureOffset.y - ); - }; - }; - @objc private func onDragPanGesture(_ sender: UIPanGestureRecognizer) { let gesturePoint = sender.location(in: self.targetView); self.gesturePoint = gesturePoint; @@ -1297,27 +1346,6 @@ class AdaptiveModalManager: NSObject { }; }; - func debug(prefix: String? = ""){ - print( - "\n - AdaptiveModalManager.debug - \(prefix ?? "N/A")" - + "\n - modalView: \(self.modalView?.debugDescription ?? "N/A")" - + "\n - modalView frame: \(self.modalView?.frame.debugDescription ?? "N/A")" - + "\n - modalView superview: \(self.modalView?.superview.debugDescription ?? "N/A")" - + "\n - targetView: \(self.targetView?.debugDescription ?? "N/A")" - + "\n - targetView frame: \(self.targetView?.frame.debugDescription ?? "N/A")" - + "\n - targetView superview: \(self.targetView?.superview.debugDescription ?? "N/A")" - + "\n - modalViewController: \(self.modalViewController?.debugDescription ?? "N/A")" - + "\n - targetViewController: \(self.targetViewController?.debugDescription ?? "N/A")" - + "\n - currentInterpolationIndex: \(self.currentInterpolationIndex)" - + "\n - modalView gestureRecognizers: \(self.modalView?.gestureRecognizers.debugDescription ?? "N/A")" - + "\n - interpolationSteps.computedRect: \(self.interpolationSteps.map({ $0.computedRect }))" - + "\n - interpolationSteps.percent: \(self.interpolationSteps.map({ $0.percent }))" - + "\n - interpolationSteps.backgroundVisualEffectIntensity: \(self.interpolationSteps.map({ $0.backgroundVisualEffectIntensity }))" - + "\n - interpolationSteps.backgroundVisualEffect: \(self.interpolationSteps.map({ $0.backgroundVisualEffect }))" - + "\n" - ); - }; - // MARK: - Functions - DisplayLink-Related // --------------------------------------- @@ -1671,4 +1699,63 @@ class AdaptiveModalManager: NSObject { completion: completion ); }; + + public func snapTo( + snapPointConfig: AdaptiveModalSnapPointConfig, + overshootSnapPointPreset: AdaptiveModalSnapPointPreset? = nil, + fallbackSnapPointKey: AdaptiveModalSnapPointConfig.SnapPointKey? = nil, + animated: Bool = true, + completion: (() -> Void)? = nil + ) { + var snapPoints = [ + self.currentSnapPointConfig, + snapPointConfig + ]; + + let interpolationPoint = AdaptiveModalInterpolationPoint( + usingModalConfig: self.modalConfig, + snapPointIndex: self.currentInterpolationIndex + 1, + layoutValueContext: self.layoutValueContext, + snapPointConfig: snapPointConfig, + prevInterpolationPoint: self.currentInterpolationStep + ); + + var interpolationPoints = [ + self.currentInterpolationStep, + interpolationPoint + ]; + + if let overshootSnapPointPreset = overshootSnapPointPreset { + let overshootSnapPointConfig = AdaptiveModalSnapPointConfig( + key: .overshootPoint, + fromSnapPointPreset: overshootSnapPointPreset, + fromBaseLayoutConfig: snapPointConfig.snapPoint + ); + + let overshootInterpolationPoint = AdaptiveModalInterpolationPoint( + usingModalConfig: self.modalConfig, + snapPointIndex: self.currentInterpolationIndex + 2, + layoutValueContext: self.layoutValueContext, + snapPointConfig: overshootSnapPointConfig, + prevInterpolationPoint: interpolationPoints.last! + ); + + snapPoints.append(overshootSnapPointConfig); + interpolationPoints.append(overshootInterpolationPoint); + }; + + self.isOverridingSnapPoints = true; + self.overrideSnapPoints = snapPoints; + self.overrideInterpolationPoints = interpolationPoints; + + print( + "\n - interpolationPoints.percent:", interpolationPoints.map({$0.percent}), + "\n - interpolationPoints.snapPointIndex:", interpolationPoints.map({$0.snapPointIndex}), + "\n - interpolationPoints.computedRect:", interpolationPoints.map({$0.computedRect}) + ); + + self.animateModal(to: interpolationPoint) { _ in + completion?(); + }; + }; }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalSnapPoint.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalSnapPoint.swift index aeb59b55..61434361 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalSnapPoint.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalSnapPoint.swift @@ -8,18 +8,39 @@ import UIKit struct AdaptiveModalSnapPointConfig { + + // MARK: Types + // ----------- + + enum SnapPointKey: Equatable { + case undershootPoint, overshootPoint, unspecified; + case string(_ stringKey: String); + case index(_ indexKey: Int); + }; + + // MARK: Properties + // ---------------- + + let key: SnapPointKey; + let snapPoint: RNILayout; let animationKeyframe: AdaptiveModalAnimationConfig?; + // MARK: Init + // ---------- + init( + key: SnapPointKey = .unspecified, snapPoint: RNILayout, animationKeyframe: AdaptiveModalAnimationConfig? = nil ) { - self.snapPoint = snapPoint - self.animationKeyframe = animationKeyframe + self.key = key; + self.snapPoint = snapPoint; + self.animationKeyframe = animationKeyframe; }; init( + key: SnapPointKey = .unspecified, fromSnapPointPreset snapPointPreset: AdaptiveModalSnapPointPreset, fromBaseLayoutConfig baseLayoutConfig: RNILayout ) { @@ -29,11 +50,29 @@ struct AdaptiveModalSnapPointConfig { fromBaseLayoutConfig: baseLayoutConfig ); + self.key = key; self.snapPoint = snapPointLayout; self.animationKeyframe = snapPointPreset.animationKeyframe; }; + + init( + fromBase base: Self, + newKey: SnapPointKey, + newSnapPoint: RNILayout? = nil, + newAnimationKeyframe: AdaptiveModalAnimationConfig? = nil + ){ + self.snapPoint = newSnapPoint ?? base.snapPoint; + self.animationKeyframe = newAnimationKeyframe ?? base.animationKeyframe; + + self.key = base.key == .unspecified + ? newKey + : base.key; + }; }; +// MARK: Helpers +// ------------- + extension AdaptiveModalSnapPointConfig { static func deriveSnapPoints( @@ -46,6 +85,7 @@ extension AdaptiveModalSnapPointConfig { if let snapPointFirst = inBetweenSnapPoints.first { let initialSnapPointConfig = AdaptiveModalSnapPointConfig( + key: .undershootPoint, fromSnapPointPreset: undershootSnapPoint, fromBaseLayoutConfig: snapPointFirst.snapPoint ); @@ -53,10 +93,13 @@ extension AdaptiveModalSnapPointConfig { items.append(initialSnapPointConfig); }; - items += inBetweenSnapPoints; + items += inBetweenSnapPoints.map { + .init(fromBase: $0, newKey: .index(items.count)); + }; if let snapPointLast = inBetweenSnapPoints.last { let overshootSnapPointConfig = AdaptiveModalSnapPointConfig( + key: .overshootPoint, fromSnapPointPreset: overshootSnapPoint, fromBaseLayoutConfig: snapPointLast.snapPoint ); diff --git a/experiments/swift-programmatic-modal/Test/AdaptiveModalConfigTestPresets.swift b/experiments/swift-programmatic-modal/Test/AdaptiveModalConfigTestPresets.swift index c9a5a879..9867d2f3 100644 --- a/experiments/swift-programmatic-modal/Test/AdaptiveModalConfigTestPresets.swift +++ b/experiments/swift-programmatic-modal/Test/AdaptiveModalConfigTestPresets.swift @@ -951,14 +951,39 @@ enum AdaptiveModalConfigTestPresets: CaseIterable { modalShadowOffset: .init(width: 0, height: -2), modalShadowOpacity: 0.2, modalShadowRadius: 7, - modalCornerRadius: 25, + modalCornerRadius: 0, modalMaskedCorners: [ .layerMinXMinYCorner, .layerMaxXMinYCorner ], modalBackgroundOpacity: 0.9, modalBackgroundVisualEffect: UIBlurEffect(style: .systemUltraThinMaterial), - modalBackgroundVisualEffectIntensity: 1 + modalBackgroundVisualEffectIntensity: 1, + backgroundVisualEffect: UIBlurEffect(style: .regular), + backgroundVisualEffectIntensity: 0 + ) + ), + + // Snap Point 2 + AdaptiveModalSnapPointConfig( + snapPoint: RNILayout( + horizontalAlignment: .center, + verticalAlignment: .bottom, + width: .stretch, + height: .percent(percentValue: 0.75) + ), + animationKeyframe: AdaptiveModalAnimationConfig( + modalShadowOffset: .init(width: 0, height: -2), + modalShadowOpacity: 0.2, + modalShadowRadius: 7, + modalCornerRadius: 15, + modalMaskedCorners: [ + .layerMinXMinYCorner, + .layerMaxXMinYCorner + ], + modalBackgroundOpacity: 0.85, + modalBackgroundVisualEffectIntensity: 0.25, + backgroundVisualEffectIntensity: 0.75 ) ) ], diff --git a/experiments/swift-programmatic-modal/Test/AdaptiveModalPresentationTestViewController.swift b/experiments/swift-programmatic-modal/Test/AdaptiveModalPresentationTestViewController.swift index 0fa014e7..b1b12aad 100644 --- a/experiments/swift-programmatic-modal/Test/AdaptiveModalPresentationTestViewController.swift +++ b/experiments/swift-programmatic-modal/Test/AdaptiveModalPresentationTestViewController.swift @@ -10,6 +10,9 @@ import UIKit fileprivate class TestModalViewController: UIViewController, AdaptiveModalEventNotifiable { weak var modalManager: AdaptiveModalManager?; + + var showDismissButton = false; + var showCustomSnapPointButton = true; lazy var floatingViewLabel: UILabel = { let label = UILabel(); @@ -24,7 +27,7 @@ fileprivate class TestModalViewController: UIViewController, AdaptiveModalEventN override func viewDidLoad() { self.view.backgroundColor = .white; - let presentButton: UIButton = { + let dismissButton: UIButton = { let button = UIButton(); button.setTitle("Dismiss Modal", for: .normal); @@ -39,6 +42,21 @@ fileprivate class TestModalViewController: UIViewController, AdaptiveModalEventN return button; }(); + let customSnapPointButton: UIButton = { + let button = UIButton(); + + button.setTitle("Custom Snap Point", for: .normal); + button.configuration = .filled(); + + button.addTarget( + self, + action: #selector(self.onPressButtonCustomSnapPoint(_:)), + for: .touchUpInside + ); + + return button; + }(); + let stackView: UIStackView = { let stack = UIStackView(); @@ -48,7 +66,14 @@ fileprivate class TestModalViewController: UIViewController, AdaptiveModalEventN stack.spacing = 10; stack.addArrangedSubview(self.floatingViewLabel); - stack.addArrangedSubview(presentButton); + + if self.showDismissButton { + stack.addArrangedSubview(dismissButton); + }; + + if self.showCustomSnapPointButton { + stack.addArrangedSubview(customSnapPointButton); + }; return stack; }(); @@ -66,6 +91,26 @@ fileprivate class TestModalViewController: UIViewController, AdaptiveModalEventN self.dismiss(animated: true); }; + @objc func onPressButtonCustomSnapPoint(_ sender: UIButton){ + let snapPoint = AdaptiveModalSnapPointConfig( + key: .string("custom"), + snapPoint: .init( + horizontalAlignment: .center, + verticalAlignment: .center, + width: .stretch, + height: .stretch, + marginLeft: .constant(15), + marginRight: .constant(15), + marginBottom: .constant(100) + ) + ); + + //self.dismiss(animated: true); + self.modalManager?.snapTo( + snapPointConfig: snapPoint + ); + }; + func notifyOnModalWillSnap( prevSnapPointIndex: Int?, nextSnapPointIndex: Int, @@ -99,6 +144,7 @@ class AdaptiveModalPresentationTestViewController : UIViewController { .demo05, .demo06, .demo07, + .demo08, ]; var currentModalConfigPresetCounter = 0;