diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalAnimationConfig.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalAnimationConfig.swift index 534cc9a6..69bbca02 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalAnimationConfig.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalAnimationConfig.swift @@ -7,6 +7,73 @@ import UIKit + +struct AdaptiveModalInterpolationPoint { + + static func compute( + modalConfig: AdaptiveModalConfig, + withTargetRect targetRect: CGRect, + currentSize: CGSize + ) -> [Self] { + var items: [Self] = []; + + for snapConfig in modalConfig.snapPoints { + let keyframe = snapConfig.animationKeyframe; + let prevKeyframe = items.last; + + items.append( + Self.init( + withTargetRect : targetRect, + currentSize : currentSize, + snapPointConfig: snapConfig, + + modalRadiusTopLeft: keyframe?.modalRadiusTopLeft + ?? prevKeyframe?.modalRadiusTopLeft ?? 0, + + modalRadiusTopRight: keyframe?.modalRadiusTopRight + ?? prevKeyframe?.modalRadiusTopRight ?? 0, + + modalRadiusBottomLeft: keyframe?.modalRadiusBottomLeft + ?? prevKeyframe?.modalRadiusBottomLeft ?? 0, + + modalRadiusBottomRight: keyframe?.modalRadiusBottomRight + ?? prevKeyframe?.modalRadiusBottomRight ?? 0 + ) + ); + }; + + return items; + }; + + /// The computed frames of the modal based on the snap points + let computedRect: CGRect; + + let modalRadiusTopLeft: CGFloat; + let modalRadiusTopRight: CGFloat; + let modalRadiusBottomLeft: CGFloat; + let modalRadiusBottomRight: CGFloat; + + init( + withTargetRect targetRect: CGRect, + currentSize: CGSize, + snapPointConfig: AdaptiveModalSnapPointConfig, + modalRadiusTopLeft: CGFloat, + modalRadiusTopRight: CGFloat, + modalRadiusBottomLeft: CGFloat, + modalRadiusBottomRight: CGFloat + ) { + self.computedRect = snapPointConfig.snapPoint.computeRect( + withTargetRect: targetRect, + currentSize: currentSize + ); + + self.modalRadiusTopLeft = modalRadiusTopLeft; + self.modalRadiusTopRight = modalRadiusTopRight; + self.modalRadiusBottomLeft = modalRadiusBottomLeft; + self.modalRadiusBottomRight = modalRadiusBottomRight; + }; +}; + struct AdaptiveModalAnimationConfig { let modalRotation: CGFloat?; @@ -19,6 +86,11 @@ struct AdaptiveModalAnimationConfig { let modalBackgroundColor: UIColor?; let modalBackgroundOpacity: CGFloat?; + let modalRadiusTopLeft: CGFloat?; + let modalRadiusTopRight: CGFloat?; + let modalRadiusBottomLeft: CGFloat?; + let modalRadiusBottomRight: CGFloat?; + let modalBlurEffectStyle: UIBlurEffect.Style?; let modalBlurEffectIntensity: CGFloat?; @@ -36,6 +108,10 @@ struct AdaptiveModalAnimationConfig { modalTranslateY: CGFloat? = nil, modalBackgroundColor: UIColor? = nil, modalBackgroundOpacity: CGFloat? = nil, + modalRadiusTopLeft: CGFloat? = nil, + modalRadiusTopRight: CGFloat? = nil, + modalRadiusBottomLeft: CGFloat? = nil, + modalRadiusBottomRight: CGFloat? = nil, modalBlurEffectStyle: UIBlurEffect.Style? = nil, modalBlurEffectIntensity: CGFloat? = nil, backgroundColor: UIColor? = nil, @@ -44,16 +120,27 @@ struct AdaptiveModalAnimationConfig { backgroundBlurEffectIntensity: CGFloat? = nil ) { self.modalRotation = modalRotation; + self.modalScaleX = modalScaleX; self.modalScaleY = modalScaleY; + self.modalTranslateX = modalTranslateX; self.modalTranslateY = modalTranslateY; + self.modalBackgroundColor = modalBackgroundColor; self.modalBackgroundOpacity = modalBackgroundOpacity; + + self.modalRadiusTopLeft = modalRadiusTopLeft; + self.modalRadiusTopRight = modalRadiusTopRight; + self.modalRadiusBottomLeft = modalRadiusBottomLeft; + self.modalRadiusBottomRight = modalRadiusBottomRight; + self.modalBlurEffectStyle = modalBlurEffectStyle; self.modalBlurEffectIntensity = modalBlurEffectIntensity; + self.backgroundColor = backgroundColor; self.backgroundOpacity = backgroundOpacity; + self.backgroundBlurEffectStyle = backgroundBlurEffectStyle; self.backgroundBlurEffectIntensity = backgroundBlurEffectIntensity; }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift index 24024096..0f386a8e 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalConfig.swift @@ -9,8 +9,10 @@ import UIKit struct AdaptiveModalConfig { enum Direction { - case horizontal; - case vertical; + case bottomToTop; + case topToBottom; + case leftToRight; + case rightToLeft; }; // MARK: - Properties diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift index 3bf9af60..f8932590 100644 --- a/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift +++ b/experiments/swift-programmatic-modal/AdaptiveModal/AdaptiveModalManager.swift @@ -35,7 +35,7 @@ class AdaptiveModalManager { // ------------------- /// The computed frames of the modal based on the snap points - var computedSnapRects: [CGRect]?; + var interpolationPoints: [AdaptiveModalInterpolationPoint]?; // MARK: - Computed Properties // --------------------------- @@ -49,8 +49,8 @@ class AdaptiveModalManager { /// var inputAxisKey: KeyPath { switch self.modalConfig.snapDirection { - case .vertical : return \.y; - case .horizontal: return \.x; + case .topToBottom, .bottomToTop: return \.y; + case .leftToRight, .rightToLeft: return \.x; }; }; @@ -113,6 +113,24 @@ class AdaptiveModalManager { return CGPoint(x: nextX, y: nextY); }; + + // sorted based on the modal direction + var interpolationPointsSorted: [AdaptiveModalInterpolationPoint]? { + + switch modalConfig.snapDirection { + case .bottomToTop, .rightToLeft: + return self.interpolationPoints?.reversed(); + + case .topToBottom, .leftToRight: + return self.interpolationPoints + }; + }; + + var gestureRangeInput: [CGFloat]? { + self.interpolationPointsSorted?.map { + $0.computedRect.origin[keyPath: self.inputAxisKey]; + }; + }; // MARK: - Init // ------------ @@ -122,7 +140,7 @@ class AdaptiveModalManager { modalView: UIView, targetView: UIView, currentSizeProvider: @escaping () -> CGSize - ){ + ) { self.modalConfig = modalConfig; self.modalView = modalView; @@ -141,7 +159,7 @@ class AdaptiveModalManager { }; func animateModal( - toRect nextRect: CGRect, + to interpolationPoint: AdaptiveModalInterpolationPoint, duration: CGFloat? = nil ) { guard let modalView = self.modalView else { return }; @@ -178,7 +196,7 @@ class AdaptiveModalManager { }(); animator.addAnimations { - modalView.frame = nextRect; + modalView.frame = interpolationPoint.computedRect; }; animator.startAnimation(); @@ -189,19 +207,19 @@ class AdaptiveModalManager { ) -> ( snapPointIndex: Int, snapPointConfig: AdaptiveModalSnapPointConfig, - computedRect: CGRect + interpolationPoint: AdaptiveModalInterpolationPoint )? { - guard let snapRects = self.computedSnapRects else { return nil }; + guard let interpolationPoints = self.interpolationPoints else { return nil }; let gestureOffset = self.gestureOffset ?? 0; let gestureCoordAdj = gestureCoord - gestureOffset; - let delta = snapRects.map { - abs($0.origin[keyPath: self.inputAxisKey] - gestureCoordAdj); + let delta = interpolationPoints.map { + abs($0.computedRect.origin[keyPath: self.inputAxisKey] - gestureCoordAdj); }; print( - "snapRects: \(snapRects.map { $0.origin[keyPath: self.inputAxisKey] })" + "snapRects: \(interpolationPoints.map { $0.computedRect.origin[keyPath: self.inputAxisKey] })" + "\n - delta: \(delta)" + "\n - gestureCoord: \(gestureCoord)" + "\n - gestureOffset: \(gestureOffset)" @@ -219,7 +237,7 @@ class AdaptiveModalManager { return ( snapPointIndex: closestSnapPointIndex, snapPointConfig: self.modalConfig.snapPoints[closestSnapPointIndex], - computedRect: snapRects[closestSnapPointIndex] + interpolationPoint: interpolationPoints[closestSnapPointIndex] ); }; @@ -228,17 +246,17 @@ class AdaptiveModalManager { ) -> ( snapPointIndex: Int, snapPointConfig: AdaptiveModalSnapPointConfig, - computedRect: CGRect, + interpolationPoint: AdaptiveModalInterpolationPoint, snapDistance: CGFloat )? { - guard let snapRects = self.computedSnapRects else { return nil }; + guard let interpolationPoints = self.interpolationPoints else { return nil }; - let delta = snapRects.map { + let delta = interpolationPoints.map { CGRect( - x: abs($0.origin.x - currentRect.origin.x), - y: abs($0.origin.y - currentRect.origin.y), - width : abs($0.size.height - currentRect.size.height), - height: abs($0.size.height - currentRect.size.height) + x: abs($0.computedRect.origin.x - currentRect.origin.x), + y: abs($0.computedRect.origin.y - currentRect.origin.y), + width : abs($0.computedRect.size.height - currentRect.size.height), + height: abs($0.computedRect.size.height - currentRect.size.height) ); }; @@ -256,7 +274,7 @@ class AdaptiveModalManager { return ( snapPointIndex: closestSnapPointIndex, snapPointConfig: self.modalConfig.snapPoints[closestSnapPointIndex], - computedRect: snapRects[closestSnapPointIndex], + interpolationPoint: interpolationPoints[closestSnapPointIndex], snapDistance: deltaAvg[closestSnapPointIndex] ); }; @@ -267,14 +285,14 @@ class AdaptiveModalManager { guard let modalView = self.modalView, let targetView = self.targetView, - let computedSnapRects = self.computedSnapRects + let interpolationPoints = self.interpolationPointsSorted, + let gestureRangeInput = self.gestureRangeInput else { return .zero }; let targetRect = targetView.frame; let modalRect = modalView.frame; let gestureCoord = gesturePoint[keyPath: self.inputAxisKey]; - let snapRects = computedSnapRects.reversed(); let gestureOffset = self.gestureOffset ?? { let modalCoord = modalRect.origin[keyPath: self.inputAxisKey]; @@ -286,8 +304,6 @@ class AdaptiveModalManager { }; let gestureInput = gestureCoord - gestureOffset; - let rangeInputGesture = snapRects.map { $0.minY }; - let clampConfig = modalConfig.interpolationClampingConfig; print( @@ -295,14 +311,14 @@ class AdaptiveModalManager { + "\n" + " - targetRect: \(targetRect)" + "\n" + " - gestureInput: \(gestureInput)" + "\n" + " - offset: \(gestureOffset)" - + "\n" + " - snapRects: \(snapRects)" - + "\n" + " - rangeInputGesture: \(rangeInputGesture)" + + "\n" + " - interpolationPoints: \(interpolationPoints)" + + "\n" + " - gestureRangeInput: \(gestureRangeInput)" ); let nextHeight = Self.interpolate( inputValue: gestureInput, - rangeInput: rangeInputGesture, - rangeOutput: snapRects.map { $0.height }, + rangeInput: gestureRangeInput, + rangeOutput: interpolationPoints.map { $0.computedRect.height }, shouldClampMin: clampConfig.shouldClampModalLastHeight, shouldClampMax: clampConfig.shouldClampModalInitHeight ); @@ -311,8 +327,8 @@ class AdaptiveModalManager { let nextWidth = Self.interpolate( inputValue: gestureInput, - rangeInput: rangeInputGesture, - rangeOutput: snapRects.map { $0.width }, + rangeInput: gestureRangeInput, + rangeOutput: interpolationPoints.map { $0.computedRect.width }, shouldClampMin: clampConfig.shouldClampModalLastWidth, shouldClampMax: clampConfig.shouldClampModalInitWidth ); @@ -321,8 +337,8 @@ class AdaptiveModalManager { let nextX = Self.interpolate( inputValue: gestureInput, - rangeInput: rangeInputGesture, - rangeOutput: snapRects.map { $0.minX }, + rangeInput: gestureRangeInput, + rangeOutput: interpolationPoints.map { $0.computedRect.minX }, shouldClampMin: clampConfig.shouldClampModalLastX, shouldClampMax: clampConfig.shouldClampModalInitX ); @@ -331,8 +347,8 @@ class AdaptiveModalManager { let nextY = Self.interpolate( inputValue: gestureInput, - rangeInput: rangeInputGesture, - rangeOutput: snapRects.map { $0.minY }, + rangeInput: gestureRangeInput, + rangeOutput: interpolationPoints.map { $0.computedRect.minY }, shouldClampMin: clampConfig.shouldClampModalLastY, shouldClampMax: clampConfig.shouldClampModalInitY )!; @@ -357,16 +373,16 @@ class AdaptiveModalManager { // MARK: - User-Invoked Functions // ------------------------------ - func computeSnapPoints(){ + + func computeSnapPoints() { guard let targetView = self.targetView else { return }; let currentSize = self.currentSizeProvider(); - self.computedSnapRects = self.modalConfig.snapPoints.map { - $0.snapPoint.computeRect( - withTargetRect: targetView.frame, - currentSize : currentSize - ); - }; + self.interpolationPoints = .Element.compute( + modalConfig: self.modalConfig, + withTargetRect: targetView.frame, + currentSize: currentSize + ); }; func notifyOnDragPanGesture(_ gesture: UIPanGestureRecognizer){ @@ -399,12 +415,12 @@ class AdaptiveModalManager { }; print( - "closestSnapPoint: \(closestSnapPoint.computedRect)" + "closestSnapPoint: \(closestSnapPoint)" + "\n - gesturePoint: \(gesturePoint)" + "\n - gestureFinalPoint: \(gestureFinalPoint)" ); - self.animateModal(toRect: closestSnapPoint.computedRect); + self.animateModal(to: closestSnapPoint.interpolationPoint); self.currentSnapPointIndex = closestSnapPoint.snapPointIndex; self.clearGestureValues(); @@ -459,7 +475,7 @@ class AdaptiveModalManager { ); self.animateModal( - toRect: closestSnapPoint.computedRect, + to: closestSnapPoint.interpolationPoint, duration: interpolatedDuration ); }; diff --git a/experiments/swift-programmatic-modal/AdaptiveModal/UIBezierPath+VariadicCornerRadius.swift b/experiments/swift-programmatic-modal/AdaptiveModal/UIBezierPath+VariadicCornerRadius.swift new file mode 100644 index 00000000..5b3cfcc8 --- /dev/null +++ b/experiments/swift-programmatic-modal/AdaptiveModal/UIBezierPath+VariadicCornerRadius.swift @@ -0,0 +1,118 @@ +// +// UIBezierPath+VariadicCornerRadius.swift +// swift-programmatic-modal +// +// Created by Dominic Go on 5/28/23. +// + +import UIKit + +extension UIBezierPath { + + convenience init( + shouldRoundRect rect: CGRect, + topLeftRadius: CGFloat, + topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, + bottomRightRadius: CGFloat + ) { + self.init(); + + let path = CGMutablePath() + + let topLeft = rect.origin + let topRight = CGPoint(x: rect.maxX, y: rect.minY) + let bottomRight = CGPoint(x: rect.maxX, y: rect.maxY) + let bottomLeft = CGPoint(x: rect.minX, y: rect.maxY) + + if topLeftRadius != 0 { + path.move(to: CGPoint( + x: topLeft.x + topLeftRadius, + y: topLeft.y + )); + + } else { + path.move(to: topLeft); + } + + if topRightRadius != 0 { + path.addLine(to: CGPoint( + x: topRight.x - topRightRadius, + y: topRight.y + )); + + path.addArc( + tangent1End: topRight, + tangent2End: CGPoint( + x: topRight.x, + y: topRight.y + topRightRadius + ), + radius: topRightRadius + ); + + } else { + path.addLine(to: topRight); + }; + + if bottomRightRadius != 0 { + path.addLine(to: CGPoint( + x: bottomRight.x, + y: bottomRight.y - bottomRightRadius + )); + + path.addArc( + tangent1End: bottomRight, + tangent2End: CGPoint( + x: bottomRight.x - bottomRightRadius, + y: bottomRight.y + ), + radius: bottomRightRadius + ); + + } else { + path.addLine(to: bottomRight); + }; + + if bottomLeftRadius != 0 { + path.addLine(to: CGPoint( + x: bottomLeft.x + bottomLeftRadius, + y: bottomLeft.y + )); + + path.addArc( + tangent1End: bottomLeft, + tangent2End: CGPoint( + x: bottomLeft.x, + y: bottomLeft.y - bottomLeftRadius + ), + radius: bottomLeftRadius + ); + + } else { + path.addLine(to: bottomLeft); + }; + + if topLeftRadius != 0 { + path.addLine(to: CGPoint( + x: topLeft.x, + y: topLeft.y + topLeftRadius + )); + + path.addArc( + tangent1End: topLeft, + tangent2End: CGPoint( + x: topLeft.x + topLeftRadius, + y: topLeft.y + ), + radius: topLeftRadius + ); + + } else { + path.addLine(to: topLeft); + }; + + path.closeSubpath(); + cgPath = path; + }; +}; + diff --git a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift index a40c661b..b39d62c4 100644 --- a/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift +++ b/experiments/swift-programmatic-modal/Test/RNIDraggableTestViewController.swift @@ -56,7 +56,7 @@ enum AdaptiveModalConfigTestPresets: CaseIterable { ) ), ], - snapDirection: .vertical + snapDirection: .bottomToTop ); case .test02: return AdaptiveModalConfig( snapPoints: [ @@ -87,7 +87,7 @@ enum AdaptiveModalConfigTestPresets: CaseIterable { ) ), ], - snapDirection: .vertical, + snapDirection: .bottomToTop, interpolationClampingConfig: .init( shouldClampModalLastHeight: true, shouldClampModalLastWidth: true, diff --git a/experiments/swift-programmatic-modal/Test/RNILayoutTestViewController.swift b/experiments/swift-programmatic-modal/Test/RNILayoutTestViewController.swift index 883239eb..7fa7dd5a 100644 --- a/experiments/swift-programmatic-modal/Test/RNILayoutTestViewController.swift +++ b/experiments/swift-programmatic-modal/Test/RNILayoutTestViewController.swift @@ -366,6 +366,10 @@ class RNILayoutTestViewController : UIViewController { self.updateFloatingView(); }; + override func viewDidLayoutSubviews() { + self.applyRadiusMaskFor(); + }; + func updateFloatingView(){ let layoutConfig = self.layoutConfig; @@ -378,10 +382,26 @@ class RNILayoutTestViewController : UIViewController { floatingView.frame = computedRect; self.floatingViewLabel.text = "\(self.layoutConfigIndex)"; + self.applyRadiusMaskFor(); }; @objc func onPressFloatingView(_ sender: UITapGestureRecognizer){ self.layoutConfigCount += 1; self.updateFloatingView(); }; + + func applyRadiusMaskFor() { + let path = UIBezierPath( + shouldRoundRect : self.floatingView.bounds, + topLeftRadius : 20, + topRightRadius : 20, + bottomLeftRadius : 20, + bottomRightRadius: 20 + ); + + let shape = CAShapeLayer(); + shape.path = path.cgPath; + + self.floatingView.layer.mask = shape; + }; }; diff --git a/experiments/swift-programmatic-modal/swift-programmatic-modal.xcodeproj/project.pbxproj b/experiments/swift-programmatic-modal/swift-programmatic-modal.xcodeproj/project.pbxproj index 8d6e3af2..d7043d93 100644 --- a/experiments/swift-programmatic-modal/swift-programmatic-modal.xcodeproj/project.pbxproj +++ b/experiments/swift-programmatic-modal/swift-programmatic-modal.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 88E8C0182A224A8D008C2FF8 /* AdaptiveModalSnapAnimationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8C0172A224A8D008C2FF8 /* AdaptiveModalSnapAnimationConfig.swift */; }; 88E8C01A2A228289008C2FF8 /* AdaptiveModalClampingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8C0192A228289008C2FF8 /* AdaptiveModalClampingConfig.swift */; }; 88E8C01C2A23203E008C2FF8 /* AdaptiveModalSnapPointPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8C01B2A23203E008C2FF8 /* AdaptiveModalSnapPointPreset.swift */; }; + 88E8C01E2A234B0A008C2FF8 /* UIBezierPath+VariadicCornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8C01D2A234B0A008C2FF8 /* UIBezierPath+VariadicCornerRadius.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -170,6 +171,7 @@ 88E8C0172A224A8D008C2FF8 /* AdaptiveModalSnapAnimationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveModalSnapAnimationConfig.swift; sourceTree = ""; }; 88E8C0192A228289008C2FF8 /* AdaptiveModalClampingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveModalClampingConfig.swift; sourceTree = ""; }; 88E8C01B2A23203E008C2FF8 /* AdaptiveModalSnapPointPreset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveModalSnapPointPreset.swift; sourceTree = ""; }; + 88E8C01D2A234B0A008C2FF8 /* UIBezierPath+VariadicCornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+VariadicCornerRadius.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -508,6 +510,7 @@ 88075E262A2121FE00B78388 /* AdaptiveModalManager+Helpers.swift */, 88E8C0172A224A8D008C2FF8 /* AdaptiveModalSnapAnimationConfig.swift */, 88E8C0192A228289008C2FF8 /* AdaptiveModalClampingConfig.swift */, + 88E8C01D2A234B0A008C2FF8 /* UIBezierPath+VariadicCornerRadius.swift */, ); path = AdaptiveModal; sourceTree = ""; @@ -629,6 +632,7 @@ 88B7D0EF29C593F400490628 /* AppDelegate.swift in Sources */, 88D018222A1B3030004664D2 /* UIImage+Init.swift in Sources */, 88D018702A1B3030004664D2 /* RNIModalFlags.swift in Sources */, + 88E8C01E2A234B0A008C2FF8 /* UIBezierPath+VariadicCornerRadius.swift in Sources */, 88B7D0F129C593F400490628 /* SceneDelegate.swift in Sources */, 88D0187E2A1B6CB3004664D2 /* BlurEffectTestViewController.swift in Sources */, 88D018272A1B3030004664D2 /* RNIInternalCleanupMode.swift in Sources */,