clox-view-switcher 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.eslintrc.js +2 -0
  2. package/README.md +35 -0
  3. package/android/build.gradle +42 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/expo/modules/cloxviewswitcher/CloxViewSwitcherModule.kt +21 -0
  6. package/android/src/main/java/expo/modules/cloxviewswitcher/CloxViewSwitcherView.kt +34 -0
  7. package/build/CloxViewSwitcher.types.d.ts +48 -0
  8. package/build/CloxViewSwitcher.types.d.ts.map +1 -0
  9. package/build/CloxViewSwitcher.types.js +2 -0
  10. package/build/CloxViewSwitcher.types.js.map +1 -0
  11. package/build/CloxViewSwitcherModule.d.ts +8 -0
  12. package/build/CloxViewSwitcherModule.d.ts.map +1 -0
  13. package/build/CloxViewSwitcherModule.js +4 -0
  14. package/build/CloxViewSwitcherModule.js.map +1 -0
  15. package/build/CloxViewSwitcherView.d.ts +4 -0
  16. package/build/CloxViewSwitcherView.d.ts.map +1 -0
  17. package/build/CloxViewSwitcherView.js +22 -0
  18. package/build/CloxViewSwitcherView.js.map +1 -0
  19. package/build/index.d.ts +5 -0
  20. package/build/index.d.ts.map +1 -0
  21. package/build/index.js +10 -0
  22. package/build/index.js.map +1 -0
  23. package/example/App.tsx +89 -0
  24. package/example/android/app/build.gradle +182 -0
  25. package/example/android/app/debug.keystore +0 -0
  26. package/example/android/app/proguard-rules.pro +14 -0
  27. package/example/android/app/src/debug/AndroidManifest.xml +7 -0
  28. package/example/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
  29. package/example/android/app/src/main/AndroidManifest.xml +31 -0
  30. package/example/android/app/src/main/java/expo/modules/cloxviewswitcher/example/MainActivity.kt +61 -0
  31. package/example/android/app/src/main/java/expo/modules/cloxviewswitcher/example/MainApplication.kt +56 -0
  32. package/example/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  33. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  34. package/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  35. package/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  36. package/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  37. package/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  38. package/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  39. package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  40. package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  41. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  42. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  43. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  44. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  45. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  46. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  47. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  48. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  49. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  50. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  51. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  52. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  53. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  54. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  55. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  56. package/example/android/app/src/main/res/values/colors.xml +6 -0
  57. package/example/android/app/src/main/res/values/strings.xml +5 -0
  58. package/example/android/app/src/main/res/values/styles.xml +11 -0
  59. package/example/android/app/src/main/res/values-night/colors.xml +1 -0
  60. package/example/android/build.gradle +24 -0
  61. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  62. package/example/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  63. package/example/android/gradle.properties +65 -0
  64. package/example/android/gradlew +251 -0
  65. package/example/android/gradlew.bat +94 -0
  66. package/example/android/settings.gradle +39 -0
  67. package/example/app.json +31 -0
  68. package/example/assets/adaptive-icon.png +0 -0
  69. package/example/assets/favicon.png +0 -0
  70. package/example/assets/icon.png +0 -0
  71. package/example/assets/splash-icon.png +0 -0
  72. package/example/index.ts +8 -0
  73. package/example/ios/.xcode.env +11 -0
  74. package/example/ios/Podfile +60 -0
  75. package/example/ios/Podfile.lock +2211 -0
  76. package/example/ios/Podfile.properties.json +5 -0
  77. package/example/ios/cloxviewswitcherexample/AppDelegate.swift +70 -0
  78. package/example/ios/cloxviewswitcherexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  79. package/example/ios/cloxviewswitcherexample/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  80. package/example/ios/cloxviewswitcherexample/Images.xcassets/Contents.json +6 -0
  81. package/example/ios/cloxviewswitcherexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  82. package/example/ios/cloxviewswitcherexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
  83. package/example/ios/cloxviewswitcherexample/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
  84. package/example/ios/cloxviewswitcherexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
  85. package/example/ios/cloxviewswitcherexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
  86. package/example/ios/cloxviewswitcherexample/Info.plist +82 -0
  87. package/example/ios/cloxviewswitcherexample/PrivacyInfo.xcprivacy +48 -0
  88. package/example/ios/cloxviewswitcherexample/SplashScreen.storyboard +48 -0
  89. package/example/ios/cloxviewswitcherexample/Supporting/Expo.plist +12 -0
  90. package/example/ios/cloxviewswitcherexample/cloxviewswitcherexample-Bridging-Header.h +3 -0
  91. package/example/ios/cloxviewswitcherexample/cloxviewswitcherexample.entitlements +5 -0
  92. package/example/ios/cloxviewswitcherexample.xcodeproj/project.pbxproj +552 -0
  93. package/example/ios/cloxviewswitcherexample.xcodeproj/xcshareddata/xcschemes/cloxviewswitcherexample.xcscheme +88 -0
  94. package/example/metro.config.js +34 -0
  95. package/example/package.json +34 -0
  96. package/example/tsconfig.json +11 -0
  97. package/example/yarn.lock +5771 -0
  98. package/expo-module.config.json +9 -0
  99. package/ios/AppSwitcherView.swift +283 -0
  100. package/ios/AppView.swift +98 -0
  101. package/ios/CloxViewSwitcher.podspec +30 -0
  102. package/ios/CloxViewSwitcherModule.swift +66 -0
  103. package/ios/CloxViewSwitcherView.swift +138 -0
  104. package/package.json +58 -0
  105. package/src/CloxViewSwitcher.types.ts +55 -0
  106. package/src/CloxViewSwitcherModule.ts +10 -0
  107. package/src/CloxViewSwitcherView.tsx +44 -0
  108. package/src/index.ts +11 -0
  109. package/tsconfig.json +9 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["CloxViewSwitcherModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.cloxviewswitcher.CloxViewSwitcherModule"]
8
+ }
9
+ }
@@ -0,0 +1,283 @@
1
+ //
2
+ // AppSwitcherView.swift
3
+ // CloxViewSwitcher
4
+ //
5
+ // Based on swiftui-app-switcher by Marcus Crafter
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ /// Model representing an item in the switcher
11
+ struct SwitcherItem: Identifiable {
12
+ var id: String
13
+ var title: String
14
+ var imageURL: String
15
+ var iconURL: String
16
+ }
17
+
18
+ /// Container view that handles gestures and renders the switcher
19
+ struct AppSwitcherContainerView: View {
20
+ let items: [SwitcherItem]
21
+ let springResponse: Double
22
+ let springDamping: Double
23
+ let scrollSpeed: Double
24
+ let cardBackgroundColor: String
25
+ let cardBorderRadius: Double
26
+ let titleFontColor: String
27
+ let titleFontSize: Double
28
+ let titleFontWeight: String
29
+ let onItemPress: (String) -> Void
30
+ let onCardChange: (String) -> Void
31
+
32
+ // Use @State instead of @GestureState to prevent automatic reset causing snap-back
33
+ @State private var activeDragX: CGFloat = 0
34
+ @State private var committedDragX: CGFloat = 0
35
+ @State private var isDragging: Bool = false
36
+
37
+ // Direct visual offset for rubber band (bypasses exponential curveX)
38
+ @State private var rubberBandOffset: CGFloat = 0
39
+
40
+ // Track the currently visible card to fire onCardChange
41
+ @State private var lastVisibleCardId: String? = nil
42
+
43
+ // Total drag position (clamped to bounds - rubber band is separate)
44
+ private var dragX: CGFloat {
45
+ activeDragX + committedDragX
46
+ }
47
+
48
+ // Bounds for drag
49
+ // maxDragX: how far right you can scroll (to see the last card)
50
+ // minDragX: how far left you can scroll (when first card is pushed left)
51
+ private var maxDragX: CGFloat { CGFloat(max(items.count - 1, 0)) * 50 + 20 }
52
+ private var minDragX: CGFloat { -50 } // Trigger rubber band early - don't let first card go too far left
53
+
54
+ // Rubber band settings - VISUAL pixels (not dragX), applied directly
55
+ private let rubberBandMultiplier: CGFloat = 0.4 // Resistance factor
56
+ private let maxVisualOvershoot: CGFloat = 40 // Max visual movement in pixels
57
+
58
+ // Constants for offset calculation (must match AppSwitcherView)
59
+ private let openingPanOffset: CGFloat = 200
60
+ private let openingAlignmentOffset: CGFloat = 80
61
+ private let appViewSpacing: CGFloat = 50
62
+ private let minStackingPerCard: CGFloat = 15
63
+ private let curveDampingFactor: CGFloat = 20
64
+
65
+ private func curveX(_ inputX: CGFloat) -> CGFloat {
66
+ pow(1.8, inputX / curveDampingFactor)
67
+ }
68
+
69
+ private func card0BaseOffset() -> CGFloat {
70
+ curveX(dragX + openingPanOffset - appViewSpacing) - openingAlignmentOffset
71
+ }
72
+
73
+ private func rawOffset(for index: Int) -> CGFloat {
74
+ curveX(dragX + openingPanOffset - CGFloat(index + 1) * appViewSpacing) - openingAlignmentOffset
75
+ }
76
+
77
+ private func offset(for index: Int) -> CGFloat {
78
+ let baseOffset = rawOffset(for: index)
79
+ if index == 0 { return baseOffset }
80
+ let card0Pos = card0BaseOffset()
81
+ let stackingFloor = card0Pos - CGFloat(index) * minStackingPerCard
82
+ return min(baseOffset, stackingFloor)
83
+ }
84
+
85
+ /// Find the currently visible card (the one with title showing)
86
+ private var visibleCardId: String? {
87
+ for index in items.indices {
88
+ let currentOffset = offset(for: index)
89
+ if currentOffset > -50 && currentOffset < 180 {
90
+ return items[index].id
91
+ }
92
+ }
93
+ return items.first?.id
94
+ }
95
+
96
+ var body: some View {
97
+ ZStack {
98
+ Color.clear
99
+
100
+ AppSwitcherView(
101
+ items: items,
102
+ dragX: dragX,
103
+ cardBackgroundColor: cardBackgroundColor,
104
+ cardBorderRadius: cardBorderRadius,
105
+ titleFontColor: titleFontColor,
106
+ titleFontSize: titleFontSize,
107
+ titleFontWeight: titleFontWeight,
108
+ onItemPress: { id in
109
+ if !isDragging {
110
+ onItemPress(id)
111
+ }
112
+ }
113
+ )
114
+ .offset(x: rubberBandOffset) // Apply rubber band as direct visual offset
115
+ }
116
+ .contentShape(Rectangle())
117
+ .gesture(
118
+ DragGesture(minimumDistance: 5)
119
+ .onChanged { value in
120
+ isDragging = true
121
+ // Apply scroll speed multiplier to translation
122
+ let scaledTranslation = value.translation.width * CGFloat(scrollSpeed)
123
+ let proposedTotal = committedDragX + scaledTranslation
124
+
125
+ // Clamp dragX to bounds, calculate overshoot for rubber band
126
+ if proposedTotal > maxDragX {
127
+ // Past right bound - clamp dragX, apply visual rubber band
128
+ activeDragX = maxDragX - committedDragX
129
+ let overshoot = proposedTotal - maxDragX
130
+ rubberBandOffset = min(overshoot * rubberBandMultiplier, maxVisualOvershoot)
131
+ print("[DRAG] RIGHT RUBBER - overshoot: \(overshoot), visualOffset: \(rubberBandOffset)")
132
+ } else if proposedTotal < minDragX {
133
+ // Past left bound - clamp dragX, apply visual rubber band (negative)
134
+ activeDragX = minDragX - committedDragX
135
+ let overshoot = minDragX - proposedTotal
136
+ rubberBandOffset = -min(overshoot * rubberBandMultiplier, maxVisualOvershoot)
137
+ print("[DRAG] LEFT RUBBER - overshoot: \(overshoot), visualOffset: \(rubberBandOffset)")
138
+ } else {
139
+ // Within bounds - normal drag, no rubber band
140
+ activeDragX = scaledTranslation
141
+ rubberBandOffset = 0
142
+ }
143
+
144
+ print("[DRAG] translation: \(value.translation.width), dragX: \(dragX), rubberBandOffset: \(rubberBandOffset)")
145
+ }
146
+ .onEnded { value in
147
+ // Calculate momentum from velocity
148
+ let momentum = value.predictedEndTranslation.width - value.translation.width
149
+
150
+ // Current clamped position
151
+ let currentPosition = committedDragX + activeDragX
152
+
153
+ print("[END] currentPosition: \(currentPosition), rubberBandOffset: \(rubberBandOffset)")
154
+ print("[END] momentum: \(momentum)")
155
+
156
+ // Commit to current position
157
+ committedDragX = currentPosition
158
+ activeDragX = 0
159
+
160
+ // Animate rubber band back to zero (bounce back)
161
+ withAnimation(.spring(response: springResponse, dampingFraction: springDamping)) {
162
+ rubberBandOffset = 0
163
+ }
164
+
165
+ // If we were within bounds, apply momentum
166
+ if rubberBandOffset == 0 {
167
+ let targetWithMomentum = currentPosition + momentum * 0.5
168
+ let finalPosition = max(minDragX, min(maxDragX, targetWithMomentum))
169
+
170
+ print("[END] targetWithMomentum: \(targetWithMomentum), finalPosition: \(finalPosition)")
171
+
172
+ withAnimation(.spring(response: springResponse, dampingFraction: springDamping)) {
173
+ committedDragX = finalPosition
174
+ }
175
+ }
176
+
177
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
178
+ isDragging = false
179
+ }
180
+ }
181
+ )
182
+ .onChange(of: visibleCardId) { newValue in
183
+ if let id = newValue, id != lastVisibleCardId {
184
+ lastVisibleCardId = id
185
+ onCardChange(id)
186
+ }
187
+ }
188
+ .onAppear {
189
+ // Fire initial card change on appear
190
+ if let id = visibleCardId {
191
+ lastVisibleCardId = id
192
+ onCardChange(id)
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /// The app switcher view showing overlapping app cards
199
+ struct AppSwitcherView: View {
200
+ let items: [SwitcherItem]
201
+ var dragX: CGFloat
202
+ let cardBackgroundColor: String
203
+ let cardBorderRadius: Double
204
+ let titleFontColor: String
205
+ let titleFontSize: Double
206
+ let titleFontWeight: String
207
+ let onItemPress: (String) -> Void
208
+
209
+ var body: some View {
210
+ ZStack {
211
+ ForEach(items.indices.reversed(), id: \.self) { index in
212
+ AppCardView(
213
+ item: items[index],
214
+ titleOpacity: titleOpacity(for: index),
215
+ backgroundColor: cardBackgroundColor,
216
+ borderRadius: cardBorderRadius,
217
+ titleFontColor: titleFontColor,
218
+ titleFontSize: titleFontSize,
219
+ titleFontWeight: titleFontWeight,
220
+ onPress: { onItemPress(items[index].id) }
221
+ )
222
+ .offset(x: offset(for: index))
223
+ .scaleEffect(scale(for: index))
224
+ }
225
+ }
226
+ }
227
+
228
+ private let openingPanOffset: CGFloat = 200
229
+ private let openingAlignmentOffset: CGFloat = 80
230
+ private let appViewSpacing: CGFloat = 50
231
+ private let minStackingPerCard: CGFloat = 15 // Minimum pixels between stacked cards
232
+
233
+ /// Calculate offset for card 0 (used as reference for stacking)
234
+ private func card0BaseOffset() -> CGFloat {
235
+ curveX(dragX + openingPanOffset - appViewSpacing) - openingAlignmentOffset
236
+ }
237
+
238
+ /// Calculate raw curved offset without stacking adjustment
239
+ private func rawOffset(for index: Int) -> CGFloat {
240
+ curveX(dragX + openingPanOffset - CGFloat(index + 1) * appViewSpacing) - openingAlignmentOffset
241
+ }
242
+
243
+ private func offset(for index: Int) -> CGFloat {
244
+ let baseOffset = rawOffset(for: index)
245
+
246
+ // Card 0 uses its natural offset
247
+ if index == 0 {
248
+ return baseOffset
249
+ }
250
+
251
+ // For other cards: ensure minimum stacking relative to card 0
252
+ // This prevents all cards from converging to the same position when dragged left
253
+ let card0Pos = card0BaseOffset()
254
+ let stackingFloor = card0Pos - CGFloat(index) * minStackingPerCard
255
+
256
+ // Use the more-left position (min) to ensure stacking is maintained
257
+ return min(baseOffset, stackingFloor)
258
+ }
259
+
260
+ private let scaleBandingFactor: CGFloat = 2000
261
+
262
+ private func scale(for index: Int) -> CGFloat {
263
+ let cardOffset = offset(for: index)
264
+ if cardOffset > 0 {
265
+ return 1
266
+ }
267
+ return max(0.94, (1 - abs(cardOffset) / scaleBandingFactor))
268
+ }
269
+
270
+ private func titleOpacity(for index: Int) -> Double {
271
+ let currentOffset = offset(for: index)
272
+ if currentOffset > -50 && currentOffset < 180 {
273
+ return 1
274
+ }
275
+ return 0
276
+ }
277
+
278
+ private let curveDampingFactor: CGFloat = 20
279
+
280
+ private func curveX(_ inputX: CGFloat) -> CGFloat {
281
+ pow(1.8, inputX / curveDampingFactor)
282
+ }
283
+ }
@@ -0,0 +1,98 @@
1
+ //
2
+ // AppView.swift
3
+ // CloxViewSwitcher
4
+ //
5
+ // Based on swiftui-app-switcher by Marcus Crafter
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ /// View representing an app card in the switcher
11
+ struct AppCardView: View {
12
+ let item: SwitcherItem
13
+ var titleOpacity: Double
14
+ var backgroundColor: String
15
+ var borderRadius: Double
16
+ var titleFontColor: String
17
+ var titleFontSize: Double
18
+ var titleFontWeight: String
19
+ let onPress: () -> Void
20
+
21
+ /// Convert hex string to Color
22
+ private func colorFromHex(_ hex: String) -> Color {
23
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
24
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
25
+
26
+ var rgb: UInt64 = 0
27
+ Scanner(string: hexSanitized).scanHexInt64(&rgb)
28
+
29
+ let red = Double((rgb & 0xFF0000) >> 16) / 255.0
30
+ let green = Double((rgb & 0x00FF00) >> 8) / 255.0
31
+ let blue = Double(rgb & 0x0000FF) / 255.0
32
+
33
+ return Color(red: red, green: green, blue: blue)
34
+ }
35
+
36
+ /// Convert weight string to Font.Weight
37
+ private func fontWeight(from string: String) -> Font.Weight {
38
+ switch string.lowercased() {
39
+ case "regular": return .regular
40
+ case "medium": return .medium
41
+ case "semibold": return .semibold
42
+ case "bold": return .bold
43
+ case "heavy": return .heavy
44
+ default: return .bold
45
+ }
46
+ }
47
+
48
+ var body: some View {
49
+ VStack(alignment: .leading) {
50
+ // Header with icon and title
51
+ HStack {
52
+ // App icon
53
+ AsyncImage(url: URL(string: item.iconURL)) { phase in
54
+ switch phase {
55
+ case .success(let image):
56
+ image
57
+ .resizable()
58
+ .aspectRatio(contentMode: .fill)
59
+ default:
60
+ RoundedRectangle(cornerRadius: 6)
61
+ .fill(colorFromHex(backgroundColor))
62
+ }
63
+ }
64
+ .frame(width: 30, height: 30)
65
+ .clipShape(RoundedRectangle(cornerRadius: 6))
66
+
67
+ Text(item.title)
68
+ .font(.system(size: CGFloat(titleFontSize), weight: fontWeight(from: titleFontWeight)))
69
+ .foregroundColor(colorFromHex(titleFontColor))
70
+ .opacity(titleOpacity)
71
+ }
72
+ .offset(x: 15)
73
+
74
+ // Main app screenshot
75
+ ZStack {
76
+ AsyncImage(url: URL(string: item.imageURL)) { phase in
77
+ switch phase {
78
+ case .success(let image):
79
+ image
80
+ .resizable()
81
+ .aspectRatio(contentMode: .fill)
82
+ default:
83
+ Rectangle()
84
+ .fill(colorFromHex(backgroundColor))
85
+ }
86
+ }
87
+ }
88
+ .frame(width: 260, height: 540)
89
+ .clipShape(RoundedRectangle(cornerRadius: CGFloat(borderRadius)))
90
+ .shadow(radius: 3)
91
+ }
92
+ .frame(width: 260, height: 580)
93
+ .contentShape(Rectangle())
94
+ .onTapGesture {
95
+ onPress()
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CloxViewSwitcher'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.9'
18
+ s.source = { git: 'https://github.com/prolific-life/clox-view-switcher' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ # Swift/Objective-C compatibility
24
+ s.pod_target_xcconfig = {
25
+ 'DEFINES_MODULE' => 'YES',
26
+ }
27
+
28
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
29
+ s.exclude_files = "build/**/*"
30
+ end
@@ -0,0 +1,66 @@
1
+ import ExpoModulesCore
2
+
3
+ public class CloxViewSwitcherModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("CloxViewSwitcher")
6
+
7
+ // Defines event names that the module can send to JavaScript.
8
+ Events("onChange", "onItemPress", "onCardChange")
9
+
10
+ // Defines a JavaScript synchronous function
11
+ Function("hello") {
12
+ return "Hello World! 👋"
13
+ }
14
+
15
+ // Enables the module to be used as a native view.
16
+ View(CloxViewSwitcherView.self) {
17
+ // Items prop - array of app items
18
+ Prop("items") { (view: CloxViewSwitcherView, items: [[String: String]]) in
19
+ view.setItems(items)
20
+ }
21
+
22
+ // Spring animation response (duration)
23
+ Prop("springResponse") { (view: CloxViewSwitcherView, value: Double) in
24
+ view.setSpringResponse(value)
25
+ }
26
+
27
+ // Spring animation damping fraction
28
+ Prop("springDamping") { (view: CloxViewSwitcherView, value: Double) in
29
+ view.setSpringDamping(value)
30
+ }
31
+
32
+ // Scroll speed multiplier
33
+ Prop("scrollSpeed") { (view: CloxViewSwitcherView, value: Double) in
34
+ view.setScrollSpeed(value)
35
+ }
36
+
37
+ // Card background color (placeholder before images load)
38
+ Prop("cardBackgroundColor") { (view: CloxViewSwitcherView, value: String) in
39
+ view.setCardBackgroundColor(value)
40
+ }
41
+
42
+ // Card border radius
43
+ Prop("cardBorderRadius") { (view: CloxViewSwitcherView, value: Double) in
44
+ view.setCardBorderRadius(value)
45
+ }
46
+
47
+ // Title font color
48
+ Prop("titleFontColor") { (view: CloxViewSwitcherView, value: String) in
49
+ view.setTitleFontColor(value)
50
+ }
51
+
52
+ // Title font size
53
+ Prop("titleFontSize") { (view: CloxViewSwitcherView, value: Double) in
54
+ view.setTitleFontSize(value)
55
+ }
56
+
57
+ // Title font weight
58
+ Prop("titleFontWeight") { (view: CloxViewSwitcherView, value: String) in
59
+ view.setTitleFontWeight(value)
60
+ }
61
+
62
+ // Events
63
+ Events("onItemPress", "onCardChange")
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,138 @@
1
+ //
2
+ // CloxViewSwitcherView.swift
3
+ // CloxViewSwitcher
4
+ //
5
+ // Native view that wraps the SwiftUI app switcher
6
+ //
7
+
8
+ import ExpoModulesCore
9
+ import UIKit
10
+ import SwiftUI
11
+
12
+ class CloxViewSwitcherView: ExpoView {
13
+
14
+ private var hostingController: UIHostingController<AnyView>?
15
+ private var items: [SwitcherItem] = []
16
+ private var springResponse: Double = 0.5
17
+ private var springDamping: Double = 0.85
18
+ private var scrollSpeed: Double = 1.0
19
+ private var cardBackgroundColor: String = "#FFFFFF"
20
+ private var cardBorderRadius: Double = 20.0
21
+ private var titleFontColor: String = "#FFFFFF"
22
+ private var titleFontSize: Double = 14.0
23
+ private var titleFontWeight: String = "bold"
24
+
25
+ // Event emitters
26
+ let onItemPress = EventDispatcher()
27
+ let onCardChange = EventDispatcher()
28
+
29
+ required init(appContext: AppContext? = nil) {
30
+ super.init(appContext: appContext)
31
+ setupView()
32
+ }
33
+
34
+ private func setupView() {
35
+ backgroundColor = .clear
36
+ updateSwiftUIView()
37
+ }
38
+
39
+ func setItems(_ itemsData: [[String: String]]) {
40
+ self.items = itemsData.compactMap { dict in
41
+ guard let id = dict["id"],
42
+ let title = dict["title"],
43
+ let image = dict["image"],
44
+ let icon = dict["icon"] else {
45
+ return nil
46
+ }
47
+ return SwitcherItem(id: id, title: title, imageURL: image, iconURL: icon)
48
+ }
49
+ updateSwiftUIView()
50
+ }
51
+
52
+ func setSpringResponse(_ value: Double) {
53
+ self.springResponse = value
54
+ updateSwiftUIView()
55
+ }
56
+
57
+ func setSpringDamping(_ value: Double) {
58
+ self.springDamping = value
59
+ updateSwiftUIView()
60
+ }
61
+
62
+ func setScrollSpeed(_ value: Double) {
63
+ self.scrollSpeed = value
64
+ updateSwiftUIView()
65
+ }
66
+
67
+ func setCardBackgroundColor(_ value: String) {
68
+ self.cardBackgroundColor = value
69
+ updateSwiftUIView()
70
+ }
71
+
72
+ func setCardBorderRadius(_ value: Double) {
73
+ self.cardBorderRadius = value
74
+ updateSwiftUIView()
75
+ }
76
+
77
+ func setTitleFontColor(_ value: String) {
78
+ self.titleFontColor = value
79
+ updateSwiftUIView()
80
+ }
81
+
82
+ func setTitleFontSize(_ value: Double) {
83
+ self.titleFontSize = value
84
+ updateSwiftUIView()
85
+ }
86
+
87
+ func setTitleFontWeight(_ value: String) {
88
+ self.titleFontWeight = value
89
+ updateSwiftUIView()
90
+ }
91
+
92
+ private func updateSwiftUIView() {
93
+ // Remove existing hosting controller view
94
+ hostingController?.view.removeFromSuperview()
95
+
96
+ // Create the SwiftUI view with items
97
+ let switcherView = AppSwitcherContainerView(
98
+ items: items,
99
+ springResponse: springResponse,
100
+ springDamping: springDamping,
101
+ scrollSpeed: scrollSpeed,
102
+ cardBackgroundColor: cardBackgroundColor,
103
+ cardBorderRadius: cardBorderRadius,
104
+ titleFontColor: titleFontColor,
105
+ titleFontSize: titleFontSize,
106
+ titleFontWeight: titleFontWeight,
107
+ onItemPress: { [weak self] id in
108
+ self?.onItemPress(["id": id])
109
+ },
110
+ onCardChange: { [weak self] id in
111
+ self?.onCardChange(["id": id])
112
+ }
113
+ )
114
+
115
+ // Wrap it in a hosting controller
116
+ let hostingController = UIHostingController(rootView: AnyView(switcherView))
117
+ hostingController.view.backgroundColor = .clear
118
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
119
+
120
+ self.hostingController = hostingController
121
+
122
+ // Add the hosting controller's view
123
+ addSubview(hostingController.view)
124
+
125
+ // Constrain to fill the parent view
126
+ NSLayoutConstraint.activate([
127
+ hostingController.view.topAnchor.constraint(equalTo: topAnchor),
128
+ hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
129
+ hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
130
+ hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor)
131
+ ])
132
+ }
133
+
134
+ override func layoutSubviews() {
135
+ super.layoutSubviews()
136
+ hostingController?.view.frame = bounds
137
+ }
138
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "clox-view-switcher",
3
+ "version": "0.1.1",
4
+ "description": "A native Expo module for view switching animations",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android",
17
+ "android": "expo run:android",
18
+ "ios": "expo run:ios"
19
+ },
20
+ "keywords": [
21
+ "react-native",
22
+ "expo",
23
+ "clox-view-switcher",
24
+ "CloxViewSwitcher",
25
+ "view-switcher",
26
+ "animation"
27
+ ],
28
+ "repository": "https://github.com/prolific-life/clox-view-switcher",
29
+ "bugs": {
30
+ "url": "https://github.com/prolific-life/clox-view-switcher/issues"
31
+ },
32
+ "author": "Yash Saxena <yash.saxena1217@gmail.com> (https://github.com/prolific-life/clox-view-switcher)",
33
+ "license": "MIT",
34
+ "homepage": "https://github.com/prolific-life/clox-view-switcher#readme",
35
+ "engines": {
36
+ "node": ">=18.14.0 || >=23.0.0"
37
+ },
38
+ "dependencies": {
39
+ "expo": "~54.0.12",
40
+ "react": "19.1.0",
41
+ "react-native": "0.81.4"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "~19.1.0",
45
+ "@react-native-community/cli": "latest",
46
+ "expo-module-scripts": "^5.0.7"
47
+ },
48
+ "peerDependencies": {
49
+ "expo": "*",
50
+ "react": "*",
51
+ "react-native": "*"
52
+ },
53
+ "overrides": {
54
+ "glob": "^9.3.5",
55
+ "rimraf": "^5.0.0",
56
+ "pretty-format": "^29.7.0"
57
+ }
58
+ }