diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalClampingConfig.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalClampingConfig.swift index 9b3e91ec..66437fa7 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalClampingConfig.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalClampingConfig.swift @@ -22,6 +22,21 @@ struct AdaptiveModalClampingConfig { let shouldClampModalInitY: Bool; let shouldClampModalLastY: Bool; + let shouldClampModalInitRotation: Bool; + let shouldClampModalLastRotation: Bool; + + let shouldClampModalInitScaleX: Bool; + let shouldClampModalLastScaleX: Bool; + + let shouldClampModalInitScaleY: Bool; + let shouldClampModalLastScaleY: Bool; + + let shouldClampModalInitTranslateX: Bool; + let shouldClampModalLastTranslateX: Bool; + + let shouldClampModalInitTranslateY: Bool; + let shouldClampModalLastTranslateY: Bool; + init( shouldClampModalInitHeight: Bool = false, shouldClampModalLastHeight: Bool = false, @@ -30,15 +45,43 @@ struct AdaptiveModalClampingConfig { shouldClampModalInitX: Bool = false, shouldClampModalLastX: Bool = false, shouldClampModalInitY: Bool = false, - shouldClampModalLastY: Bool = false + shouldClampModalLastY: Bool = false, + shouldClampModalInitRotation: Bool = true, + shouldClampModalLastRotation: Bool = true, + shouldClampModalInitScaleX: Bool = true, + shouldClampModalLastScaleX: Bool = true, + shouldClampModalInitScaleY: Bool = true, + shouldClampModalLastScaleY: Bool = true, + shouldClampModalInitTranslateX: Bool = true, + shouldClampModalLastTranslateX: Bool = true, + shouldClampModalInitTranslateY: Bool = true, + shouldClampModalLastTranslateY: Bool = true ) { self.shouldClampModalInitHeight = shouldClampModalInitHeight; self.shouldClampModalLastHeight = shouldClampModalLastHeight; + self.shouldClampModalInitWidth = shouldClampModalInitWidth; self.shouldClampModalLastWidth = shouldClampModalLastWidth; + self.shouldClampModalInitX = shouldClampModalInitX; self.shouldClampModalLastX = shouldClampModalLastX; + self.shouldClampModalInitY = shouldClampModalInitY; self.shouldClampModalLastY = shouldClampModalLastY; + + self.shouldClampModalInitRotation = shouldClampModalInitRotation; + self.shouldClampModalLastRotation = shouldClampModalLastRotation; + + self.shouldClampModalInitScaleX = shouldClampModalInitScaleX; + self.shouldClampModalLastScaleX = shouldClampModalLastScaleX; + + self.shouldClampModalInitScaleY = shouldClampModalInitScaleY; + self.shouldClampModalLastScaleY = shouldClampModalLastScaleY; + + self.shouldClampModalInitTranslateX = shouldClampModalInitTranslateX; + self.shouldClampModalLastTranslateX = shouldClampModalLastTranslateX; + + self.shouldClampModalInitTranslateY = shouldClampModalInitTranslateY; + self.shouldClampModalLastTranslateY = shouldClampModalLastTranslateY; } }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift index 3caff1d8..6d3787b4 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalInterpolationPoint.swift @@ -9,19 +9,32 @@ import UIKit struct AdaptiveModalInterpolationPoint: Equatable { + private static let DefaultMaskedCorners: CACornerMask = [ + .layerMaxXMinYCorner, + .layerMinXMinYCorner, + .layerMaxXMaxYCorner, + .layerMinXMaxYCorner, + ]; + + // MARK: - Properties + // ------------------ + let percent: CGFloat; let snapPointIndex: Int; /// The computed frames of the modal based on the snap points let computedRect: CGRect; - //let modalRotation: CGFloat; + // MARK: - Properties - Keyframes + // ------------------------------ + + let modalRotation: CGFloat; - //let modalScaleX: CGFloat; - //let modalScaleY: CGFloat; + let modalScaleX: CGFloat; + let modalScaleY: CGFloat; - //let modalTranslateX: CGFloat; - //let modalTranslateY: CGFloat; + let modalTranslateX: CGFloat; + let modalTranslateY: CGFloat; //let modalBackgroundColor: UIColor; let modalBackgroundOpacity: CGFloat; @@ -37,6 +50,30 @@ struct AdaptiveModalInterpolationPoint: Equatable { let backgroundVisualEffect: UIVisualEffect?; let backgroundVisualEffectIntensity: CGFloat; + + // MARK: - Computed Properties + // --------------------------- + + var transform: CGAffineTransform { + var transform: CGAffineTransform = .identity; + + transform = transform.rotated(by: self.modalRotation); + + transform = transform.scaledBy( + x: self.modalScaleX, + y: self.modalScaleY + ); + + transform = transform.translatedBy( + x: self.modalTranslateX, + y: self.modalTranslateY + ); + + return transform; + }; + + // MARK: - Init + // ------------ init( usingModalConfig modalConfig: AdaptiveModalConfig, @@ -81,13 +118,33 @@ struct AdaptiveModalInterpolationPoint: Equatable { let keyframeCurrent = snapPointConfig.animationKeyframe; + self.modalRotation = keyframeCurrent?.modalRotation + ?? keyframePrev?.modalRotation + ?? 0; + + self.modalScaleX = keyframeCurrent?.modalScaleX + ?? keyframePrev?.modalScaleX + ?? 1; + + self.modalScaleY = keyframeCurrent?.modalScaleY + ?? keyframePrev?.modalScaleY + ?? 1; + + self.modalTranslateX = keyframeCurrent?.modalTranslateX + ?? keyframePrev?.modalTranslateX + ?? 0; + + self.modalTranslateY = keyframeCurrent?.modalTranslateY + ?? keyframePrev?.modalTranslateY + ?? 0; + self.modalBackgroundOpacity = keyframeCurrent?.modalBackgroundOpacity ?? keyframePrev?.modalBackgroundOpacity ?? 1; self.modalCornerRadius = keyframeCurrent?.modalCornerRadius ?? keyframePrev?.modalCornerRadius - ?? Self.DefaultCornerRadius; + ?? 0; self.modalMaskedCorners = keyframeCurrent?.modalMaskedCorners ?? keyframePrev?.modalMaskedCorners @@ -112,8 +169,42 @@ struct AdaptiveModalInterpolationPoint: Equatable { ?? 1; }; + // MARK: - Functions + // ----------------- + + func getTransform( + shouldApplyRotation: Bool = true, + shouldApplyScale: Bool = true, + shouldApplyTranslate: Bool = true + ) -> CGAffineTransform { + + var transform: CGAffineTransform = .identity; + + if shouldApplyRotation { + transform = transform.rotated(by: self.modalRotation); + }; + + if shouldApplyScale { + transform = transform.scaledBy( + x: self.modalScaleX, + y: self.modalScaleY + ); + }; + + if shouldApplyTranslate { + transform = transform.translatedBy( + x: self.modalTranslateX, + y: self.modalTranslateY + ); + }; + + return transform; + }; + func apply(toModalView modalView: UIView){ modalView.frame = self.computedRect; + modalView.transform = self.transform; + modalView.layer.cornerRadius = self.modalCornerRadius; modalView.layer.maskedCorners = self.modalMaskedCorners; }; @@ -137,15 +228,6 @@ struct AdaptiveModalInterpolationPoint: Equatable { extension AdaptiveModalInterpolationPoint { - private static let DefaultCornerRadius: CGFloat = 0; - - private static let DefaultMaskedCorners: CACornerMask = [ - .layerMaxXMinYCorner, - .layerMinXMinYCorner, - .layerMaxXMaxYCorner, - .layerMinXMaxYCorner, - ]; - static func compute( usingModalConfig modalConfig: AdaptiveModalConfig, withTargetRect targetRect: CGRect, diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalKeyframePropertyAnimator.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalKeyframePropertyAnimator.swift new file mode 100644 index 00000000..e7a1d98c --- /dev/null +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalKeyframePropertyAnimator.swift @@ -0,0 +1,56 @@ +// +// AdaptiveModalPropertyAnimator.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 6/5/23. +// + +import UIKit + + +struct AdaptiveModalKeyframePropertyAnimator { + + var animator: UIViewPropertyAnimator; + + private weak var component: UIView?; + + init( + interpolationPoints: [AdaptiveModalInterpolationPoint], + forComponent component: T, + animation: @escaping ( + _ component: T, + _ interpolationPoint: AdaptiveModalInterpolationPoint + ) -> Void + ){ + let animator = UIViewPropertyAnimator(duration: 1, curve: .linear); + + self.animator = animator; + self.component = component; + + animator.addAnimations { + UIView.addKeyframe( + withRelativeStartTime: 0, + relativeDuration: 1 + ){ + component.transform = .identity; + }; + + for interpolationPoint in interpolationPoints { + UIView.addKeyframe( + withRelativeStartTime: interpolationPoint.percent, + relativeDuration: 0 + ){ + animation(component, interpolationPoint); + }; + }; + }; + }; + + func setFractionComplete(forPercent percent: CGFloat) { + self.animator.fractionComplete = 0; + }; + + func clear(){ + self.animator.stopAnimation(true); + }; +}; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift index cbf0c6d5..7193681a 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift @@ -14,7 +14,7 @@ class AdaptiveModalManager { var modalConfig: AdaptiveModalConfig; - var enableSnapping = true; + var enableSnapping = false; // MARK: - Properties - Refs // -------------------------- @@ -68,7 +68,7 @@ class AdaptiveModalManager { // MARK: - Computed Properties // --------------------------- - var modalFrame: CGRect? { + var modalFrame: CGRect! { set { guard let modalView = self.modalView, let newValue = newValue @@ -80,7 +80,7 @@ class AdaptiveModalManager { self.dummyModalView.frame = newValue; } get { - self.modalView?.frame; + self.dummyModalView.frame; } }; @@ -212,7 +212,7 @@ class AdaptiveModalManager { self.currentSizeProvider = currentSizeProvider; - self.setupdummyModalView(); + self.setupDummyModalView(); }; deinit { @@ -222,7 +222,7 @@ class AdaptiveModalManager { // MARK: - Functions - Setup // ------------------------- - private func setupdummyModalView(){ + private func setupDummyModalView(){ guard let targetView = self.targetView else { return }; let dummyModalView = self.dummyModalView; @@ -447,6 +447,99 @@ class AdaptiveModalManager { ); }; + func interpolateModalTransform( + forInputPercentValue inputPercentValue: CGFloat, + rangeInput: [CGFloat]? = nil, + rangeOutput: [AdaptiveModalInterpolationPoint]? = nil + ) -> CGAffineTransform? { + + guard let interpolationSteps = rangeOutput ?? self.interpolationStepsSorted, + let interpolationRangeInput = rangeInput ?? self.interpolationRangeInput + else { return nil }; + + let clampConfig = modalConfig.interpolationClampingConfig; + + let nextModalRotation = Self.interpolate( + inputValue: inputPercentValue, + rangeInput: interpolationRangeInput, + rangeOutput: interpolationSteps.map { + $0.modalRotation + }, + shouldClampMin: clampConfig.shouldClampModalInitRotation, + shouldClampMax: clampConfig.shouldClampModalLastRotation + ); + + let nextScaleX = Self.interpolate( + inputValue: inputPercentValue, + rangeInput: interpolationRangeInput, + rangeOutput: interpolationSteps.map { + $0.modalScaleX; + }, + shouldClampMin: clampConfig.shouldClampModalLastScaleX, + shouldClampMax: clampConfig.shouldClampModalLastScaleX + ); + + let nextScaleY = Self.interpolate( + inputValue: inputPercentValue, + rangeInput: interpolationRangeInput, + rangeOutput: interpolationSteps.map { + $0.modalScaleY + }, + shouldClampMin: clampConfig.shouldClampModalLastScaleY, + shouldClampMax: clampConfig.shouldClampModalLastScaleY + ); + + let nextTranslateX = Self.interpolate( + inputValue: inputPercentValue, + rangeInput: interpolationRangeInput, + rangeOutput: interpolationSteps.map { + $0.modalTranslateX + }, + shouldClampMin: clampConfig.shouldClampModalLastTranslateX, + shouldClampMax: clampConfig.shouldClampModalLastTranslateX + ); + + let nextTranslateY = Self.interpolate( + inputValue: inputPercentValue, + rangeInput: interpolationRangeInput, + rangeOutput: interpolationSteps.map { + $0.modalTranslateY + }, + shouldClampMin: clampConfig.shouldClampModalLastTranslateY, + shouldClampMax: clampConfig.shouldClampModalLastTranslateY + ); + + let nextTransform: CGAffineTransform = { + var transform: CGAffineTransform = .identity; + + if let rotation = nextModalRotation { + transform = transform.rotated(by: rotation); + }; + + if let nextScaleX = nextScaleX, + let nextScaleY = nextScaleY { + + transform = transform.scaledBy( + x: nextScaleX, + y: nextScaleY + ); + }; + + if let nextTranslateX = nextTranslateX, + let nextTranslateY = nextTranslateY { + + transform = transform.translatedBy( + x: nextTranslateX, + y: nextTranslateY + ); + }; + + return transform; + }(); + + return nextTransform; + }; + func interpolateModalBackgroundOpacity( forInputPercentValue inputPercentValue: CGFloat, rangeInput: [CGFloat]? = nil, @@ -607,6 +700,12 @@ class AdaptiveModalManager { self.modalFrame = nextModalRect; }; + if let nextModalTransform = self.interpolateModalTransform( + forInputPercentValue: inputPercentValue + ) { + modalView.transform = nextModalTransform; + }; + if let nextModalRadius = self.interpolateModalBorderRadius( forInputPercentValue: inputPercentValue, modalBounds: modalView.bounds @@ -666,7 +765,7 @@ class AdaptiveModalManager { let gestureInitialPoint = self.gestureInitialPoint else { return }; - let modalRect = modalView.frame; + let modalRect = self.modalFrame!; let gestureOffset = self.gestureOffset ?? { return CGPoint( @@ -751,6 +850,8 @@ class AdaptiveModalManager { interpolationPoint.apply(toDummyModalView: self.dummyModalView); interpolationPoint.apply(toModalBackgroundView: self.modalBackgroundView); interpolationPoint.apply(toBackgroundView: self.backgroundDimmingView); + + modalView.layoutIfNeeded(); }; if let completion = completion { @@ -769,12 +870,13 @@ class AdaptiveModalManager { snapPointConfig: AdaptiveModalSnapPointConfig, interpolationPoint: AdaptiveModalInterpolationPoint )? { - guard let interpolationSteps = self.interpolationSteps, - let modalView = self.modalView + guard let interpolationSteps = self.interpolationSteps else { return nil }; + let inputRect = self.modalFrame!; + let inputCoord = coord ?? - modalView.frame.origin[keyPath: self.modalConfig.inputValueKeyForPoint]; + inputRect.origin[keyPath: self.modalConfig.inputValueKeyForPoint]; let delta = interpolationSteps.map { abs($0.computedRect.origin[keyPath: self.modalConfig.inputValueKeyForPoint] - inputCoord); @@ -974,15 +1076,13 @@ class AdaptiveModalManager { }; func updateModal(){ - guard let modalView = self.modalView, - !self.isAnimating - else { return }; + guard !self.isAnimating else { return }; if let gesturePoint = self.gesturePoint { self.applyInterpolationToModal(forGesturePoint: gesturePoint); } else if let currentInterpolationStep = self.currentInterpolationStep, - currentInterpolationStep.computedRect != modalView.frame { + currentInterpolationStep.computedRect != self.modalFrame { self.applyInterpolationToModal( forInputPercentValue: currentInterpolationStep.percent @@ -991,10 +1091,9 @@ class AdaptiveModalManager { }; func snapToClosestSnapPoint(){ - guard let modalView = self.modalView, - let targetView = self.targetView, + guard let targetView = self.targetView, let closestSnapPoint = - self.getClosestSnapPoint(forRect: modalView.frame) + self.getClosestSnapPoint(forRect: self.modalFrame) else { return }; let interpolatedDuration = Self.interpolate( @@ -1013,8 +1112,6 @@ class AdaptiveModalManager { // ----------------------- func onModalWillSnap(){ - guard let _ = self.modalView else { return }; - guard let closestSnapPoint = self.getClosestSnapPoint() else { return }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalPropertyAnimator.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalRangePropertyAnimator.swift similarity index 95% rename from experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalPropertyAnimator.swift rename to experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalRangePropertyAnimator.swift index 01e65975..2439072d 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalPropertyAnimator.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalRangePropertyAnimator.swift @@ -7,7 +7,7 @@ import UIKit -struct AdaptiveModalPropertyAnimator { +struct AdaptiveModalRangePropertyAnimator { var interpolationRangeStart: AdaptiveModalInterpolationPoint; var interpolationRangeEnd: AdaptiveModalInterpolationPoint; @@ -106,12 +106,6 @@ struct AdaptiveModalPropertyAnimator { guard let percent = percent else { return }; self.setFractionComplete(forPercent: percent); - - print( - "\(self.component)" - + " - percent: \(percent)" - + " - rangeOutput" - ); }; func clear(){ diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/RNILayoutPreset.swift b/experiments/swift-programmatic-modal/AdaptiveModal/RNILayoutPreset.swift index 822d8e4d..ae676840 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/RNILayoutPreset.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/RNILayoutPreset.swift @@ -11,7 +11,7 @@ enum RNILayoutPreset { case offscreenBottom, offscreenTop, offscreenLeft, offscreenRight; case halfOffscreenBottom, halfOffscreenTop, halfOffscreenLeft, halfOffscreenRight; case edgeBottom, edgeTop, edgeLeft, edgeRight; - case fitScreen; + case fitScreen, fitScreenHorizontally, fitScreenVertically; case center; case layoutConfig(_ config: RNILayout); @@ -111,6 +111,7 @@ enum RNILayoutPreset { derivedFrom: baseLayoutConfig, verticalAlignment: .center ); + case .fitScreen: return .init( derivedFrom: baseLayoutConfig, @@ -123,6 +124,22 @@ enum RNILayoutPreset { mode: .stretch ) ); + + case .fitScreenHorizontally: + return .init( + derivedFrom: baseLayoutConfig, + width: RNIComputableValue( + mode: .stretch + ) + ); + + case .fitScreenVertically: + return .init( + derivedFrom: baseLayoutConfig, + height: RNIComputableValue( + mode: .stretch + ) + ); case let .layoutConfig(config): return config; diff --git a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift index cd4fbba3..6cd4b433 100644 --- a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift +++ b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift @@ -10,7 +10,9 @@ import UIKit enum AdaptiveModalConfigTestPresets: CaseIterable { - static let `default`: Self = .test03; + static let `default`: Self = .testModalTransform01; + + case testModalTransform01; case test01; case test02; @@ -18,6 +20,76 @@ enum AdaptiveModalConfigTestPresets: CaseIterable { var config: AdaptiveModalConfig { switch self { + case .testModalTransform01: return AdaptiveModalConfig( + snapPoints: [ + // snap point - 0 + AdaptiveModalSnapPointConfig( + snapPoint: RNILayout( + horizontalAlignment: .center, + verticalAlignment: .bottom, + width: RNIComputableValue( + mode: .percent(percentValue: 0.8) + ), + height: RNIComputableValue( + mode: .percent(percentValue: 0.2) + ) + ), + animationKeyframe: AdaptiveModalAnimationConfig( + modalRotation: 0, + modalScaleX: 1, + modalScaleY: 1, + modalTranslateX: 0, + modalTranslateY: 0 + ) + ), + + // snap point - 1 + AdaptiveModalSnapPointConfig( + snapPoint: RNILayout( + horizontalAlignment: .center, + verticalAlignment: .bottom, + width: RNIComputableValue( + mode: .percent(percentValue: 0.8) + ), + height: RNIComputableValue( + mode: .percent(percentValue: 0.4) + ) + ), + animationKeyframe: AdaptiveModalAnimationConfig( + modalRotation: 0.1, + modalScaleX: 0.5, + modalScaleY: 1, + modalTranslateX: 1000, + modalTranslateY: 0 + ) + ), + // snap point - 2 + AdaptiveModalSnapPointConfig( + snapPoint: RNILayout( + horizontalAlignment: .center, + verticalAlignment: .bottom, + width: RNIComputableValue( + mode: .percent(percentValue: 0.8) + ), + height: RNIComputableValue( + mode: .percent(percentValue: 0.6) + ) + ), + animationKeyframe: AdaptiveModalAnimationConfig( + modalRotation: -0.1, + modalScaleX: 1, + modalScaleY: 1, + modalTranslateX: -400, + modalTranslateY: 0 + ) + ), + ], + snapDirection: .bottomToTop, + overshootSnapPoint: AdaptiveModalSnapPointPreset( + snapPoint: .fitScreenVertically + ) + ); + case .test01: return AdaptiveModalConfig( snapPoints: [ AdaptiveModalSnapPointConfig(