zkit-ui 2.0.4 → 2.0.5

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 (63) hide show
  1. package/README.md +56 -0
  2. package/android/build.gradle +100 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaModule.kt +43 -0
  5. package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaView.kt +305 -0
  6. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryLayoutSupport.java +450 -0
  7. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryMediaItem.java +36 -0
  8. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryPagerAdapter.java +140 -0
  9. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementGeometry.java +25 -0
  10. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementNames.java +39 -0
  11. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementState.java +25 -0
  12. package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySourceViewRegistry.java +115 -0
  13. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryTransitionSnapshotView.java +60 -0
  14. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryVideoPlayerView.java +338 -0
  15. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerOverlayView.java +636 -0
  16. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerPageView.java +357 -0
  17. package/android/src/main/java/cn/fontree/zkit/ui/preview/GalleryViewerTransitionController.java +223 -0
  18. package/android/src/main/res/layout/galeria_player_view.xml +9 -0
  19. package/dist/all.d.ts +2 -0
  20. package/dist/all.d.ts.map +1 -1
  21. package/dist/all.js +24 -22
  22. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts +3 -0
  23. package/dist/ui/NativeImagePreview/NativeImagePreview.d.ts.map +1 -0
  24. package/dist/ui/NativeImagePreview/NativeImagePreview.js +99 -0
  25. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +3 -0
  26. package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -0
  27. package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +56 -0
  28. package/dist/ui/NativeImagePreview/context.d.ts +14 -0
  29. package/dist/ui/NativeImagePreview/context.d.ts.map +1 -0
  30. package/dist/ui/NativeImagePreview/context.js +38 -0
  31. package/dist/ui/NativeImagePreview/index.d.ts +3 -0
  32. package/dist/ui/NativeImagePreview/index.d.ts.map +1 -0
  33. package/dist/ui/NativeImagePreview/index.js +5 -0
  34. package/dist/ui/NativeImagePreview/types.d.ts +66 -0
  35. package/dist/ui/NativeImagePreview/types.d.ts.map +1 -0
  36. package/dist/ui/NativeImagePreview/types.js +2 -0
  37. package/dist/ui/NativeImagePreview/utils.d.ts +8 -0
  38. package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -0
  39. package/dist/ui/NativeImagePreview/utils.js +82 -0
  40. package/expo-module.config.json +11 -0
  41. package/ios/GaleriaModule.swift +46 -0
  42. package/ios/GaleriaView.swift +253 -0
  43. package/ios/GalleryViewer/GaleriaImageLoader.swift +73 -0
  44. package/ios/GalleryViewer/GaleriaImageViewInteraction.swift +245 -0
  45. package/ios/GalleryViewer/GaleriaLayoutSupport.swift +143 -0
  46. package/ios/GalleryViewer/GaleriaMedia.swift +28 -0
  47. package/ios/GalleryViewer/GaleriaSourceViewRegistry.swift +80 -0
  48. package/ios/GalleryViewer/GaleriaVideoPlayerView.swift +437 -0
  49. package/ios/GalleryViewer/GaleriaViewerConfiguration.swift +51 -0
  50. package/ios/GalleryViewer/GaleriaViewerController.swift +745 -0
  51. package/ios/GalleryViewer/GaleriaViewerPageView.swift +623 -0
  52. package/ios/GalleryViewer/GaleriaViewerTransitionCoordinator.swift +312 -0
  53. package/package.json +20 -12
  54. package/src/all.ts +18 -0
  55. package/src/services/ImagePreviewService/README.md +19 -0
  56. package/src/ui/NativeImagePreview/NativeImagePreview.tsx +146 -0
  57. package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +37 -0
  58. package/src/ui/NativeImagePreview/README.md +111 -0
  59. package/src/ui/NativeImagePreview/context.tsx +22 -0
  60. package/src/ui/NativeImagePreview/index.tsx +18 -0
  61. package/src/ui/NativeImagePreview/types.ts +92 -0
  62. package/src/ui/NativeImagePreview/utils.ts +107 -0
  63. package/zkit-ui.podspec +13 -1
@@ -0,0 +1,253 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ class GaleriaView: ExpoView {
5
+ private var childImageView: UIImageView?
6
+ private weak var registeredAnchorImageView: UIImageView?
7
+ private var registeredGalleryId: String?
8
+ private var registeredIndex: Int?
9
+
10
+ deinit {
11
+ unregisterCurrentAnchor()
12
+ }
13
+
14
+ override func didMoveToWindow() {
15
+ super.didMoveToWindow()
16
+ if window == nil {
17
+ unregisterCurrentAnchor()
18
+ } else {
19
+ registerCurrentAnchorIfNeeded()
20
+ }
21
+ }
22
+
23
+ func getChildImageView() -> UIImageView? {
24
+ var reactSubviews: [UIView]? = nil
25
+ if RCTIsNewArchEnabled() {
26
+ reactSubviews = self.subviews
27
+ } else {
28
+ reactSubviews = self.reactSubviews()
29
+ }
30
+
31
+ guard let reactSubviews = reactSubviews else { return nil }
32
+
33
+ for reactSubview in reactSubviews {
34
+ if let iv = findImageView(in: reactSubview) {
35
+ childImageView = iv
36
+ return iv
37
+ }
38
+ }
39
+
40
+ return nil
41
+ }
42
+
43
+ private func findImageView(in view: UIView) -> UIImageView? {
44
+ if let imageView = view as? UIImageView { return imageView }
45
+ for subview in view.subviews {
46
+ if let imageView = subview as? UIImageView { return imageView }
47
+ if let nested = findImageView(in: subview) { return nested }
48
+ }
49
+ return nil
50
+ }
51
+
52
+ #if !RCT_NEW_ARCH_ENABLED
53
+ override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
54
+ super.insertReactSubview(subview, at: atIndex)
55
+ setupImageView()
56
+ }
57
+ #endif
58
+
59
+ #if RCT_NEW_ARCH_ENABLED
60
+ // https://github.com/nandorojo/galeria/issues/19
61
+ // Cleanup gesture recognizers from the image view to work with fabric view recycling
62
+ override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
63
+ unregisterCurrentAnchor()
64
+ childImageView?.removeGaleriaViewerInteraction()
65
+ childImageView = nil
66
+ super.unmountChildComponentView(childComponentView, index: index)
67
+ }
68
+ #endif
69
+
70
+ var theme: Theme = .dark
71
+ var urls: [String]?
72
+ var items: [[String: Any]]?
73
+ var initialIndex: Int?
74
+ var galleryId: String?
75
+ var closeIconName: String?
76
+ var rightNavItemIconName: String?
77
+ let onPressRightNavItemIcon = EventDispatcher()
78
+ let onIndexChange = EventDispatcher()
79
+
80
+ public func setupImageView() {
81
+ registerCurrentAnchorIfNeeded()
82
+ guard let childImage = getChildImageView() else {
83
+ return
84
+ }
85
+
86
+ if let items = self.items, let initialIndex = self.initialIndex {
87
+ setupViewerWithItems(childImage, items: items, initialIndex: initialIndex)
88
+ } else if let urls = self.urls, let initialIndex = self.initialIndex {
89
+ setupViewerWithUrls(childImage, urls: urls, initialIndex: initialIndex)
90
+ } else {
91
+ setupViewerWithSingleImage(childImage)
92
+ }
93
+ }
94
+
95
+ private func setupViewerWithItems(
96
+ _ childImage: UIImageView,
97
+ items: [[String: Any]],
98
+ initialIndex: Int
99
+ ) {
100
+ let configuration = buildViewerConfiguration()
101
+
102
+ let imageItems: [GaleriaMediaItem] = items.compactMap { item in
103
+ guard let type = item["type"] as? String else { return nil }
104
+
105
+ if type == "video", let urlStr = item["url"] as? String, let url = URL(string: urlStr) {
106
+ var posterUrl: URL? = nil
107
+ if let posterStr = item["poster"] as? String {
108
+ posterUrl = URL(string: posterStr)
109
+ }
110
+ return GaleriaMediaItem.video(url: url, poster: posterUrl)
111
+ } else if type == "image", let urlStr = item["url"] as? String {
112
+ if urlStr.hasPrefix("http://") || urlStr.hasPrefix("https://") || urlStr.hasPrefix("file://") {
113
+ if let url = URL(string: urlStr) {
114
+ return GaleriaMediaItem.imageURL(url, placeholder: nil)
115
+ }
116
+ } else {
117
+ return GaleriaMediaItem.imageURL(URL(fileURLWithPath: urlStr), placeholder: nil)
118
+ }
119
+ }
120
+ return nil
121
+ }
122
+
123
+ let datasource = GaleriaArrayDataSource(items: imageItems)
124
+ childImage.setupGaleriaViewer(
125
+ dataSource: datasource,
126
+ initialIndex: initialIndex,
127
+ configuration: configuration,
128
+ galleryId: galleryId)
129
+ }
130
+
131
+
132
+ private func setupViewerWithUrls(
133
+ _ childImage: UIImageView,
134
+ urls: [String],
135
+ initialIndex: Int
136
+ ) {
137
+ let configuration = buildViewerConfiguration()
138
+
139
+ let urlObjects: [URL] = urls.compactMap { string in
140
+ if string.hasPrefix("http://") || string.hasPrefix("https://") || string.hasPrefix("file://") {
141
+ return URL(string: string)
142
+ }
143
+ return URL(fileURLWithPath: string)
144
+ }
145
+
146
+ childImage.setupGaleriaViewer(
147
+ urls: urlObjects,
148
+ initialIndex: initialIndex,
149
+ configuration: configuration,
150
+ galleryId: galleryId)
151
+ }
152
+
153
+ private func setupViewerWithSingleImage(_ childImage: UIImageView) {
154
+ guard let img = childImage.image else {
155
+ print("Missing image in childImage: \(childImage)")
156
+ return
157
+ }
158
+ let configuration = buildViewerConfiguration()
159
+
160
+ childImage.setupGaleriaViewer(images: [img], configuration: configuration, galleryId: galleryId)
161
+ }
162
+
163
+ private func buildViewerConfiguration() -> GaleriaViewerConfiguration {
164
+ var configuration = GaleriaViewerConfiguration(theme: theme.toViewerTheme())
165
+ let iconColor = theme.iconColor()
166
+
167
+ if let closeIconName = closeIconName,
168
+ let closeIconImage = UIImage(systemName: closeIconName)?.withTintColor(
169
+ iconColor, renderingMode: .alwaysOriginal)
170
+ {
171
+ configuration.closeIcon = closeIconImage
172
+ }
173
+
174
+ if let rightIconName = rightNavItemIconName,
175
+ let rightIconImage = UIImage(systemName: rightIconName)?.withTintColor(
176
+ iconColor, renderingMode: .alwaysOriginal)
177
+ {
178
+ configuration.rightBarButton = .icon(
179
+ rightIconImage,
180
+ onTap: { index in
181
+ self.onPressRightNavItemIcon(["index": index])
182
+ })
183
+ }
184
+
185
+ configuration.onIndexChange = { [weak self] index in
186
+ self?.onIndexChange(["currentIndex": index])
187
+ }
188
+ return configuration
189
+ }
190
+
191
+ private func registerCurrentAnchorIfNeeded() {
192
+ guard
193
+ window != nil,
194
+ let galleryId,
195
+ let initialIndex,
196
+ let childImage = getChildImageView()
197
+ else {
198
+ unregisterCurrentAnchor()
199
+ return
200
+ }
201
+
202
+ if
203
+ let registeredAnchorImageView,
204
+ (registeredAnchorImageView !== childImage
205
+ || registeredGalleryId != galleryId
206
+ || registeredIndex != initialIndex)
207
+ {
208
+ unregisterCurrentAnchor()
209
+ }
210
+
211
+ GaleriaSourceViewRegistry.shared.register(
212
+ childImage,
213
+ galleryId: galleryId,
214
+ index: initialIndex)
215
+ registeredAnchorImageView = childImage
216
+ registeredGalleryId = galleryId
217
+ registeredIndex = initialIndex
218
+ }
219
+
220
+ private func unregisterCurrentAnchor() {
221
+ if let registeredAnchorImageView = registeredAnchorImageView {
222
+ GaleriaSourceViewRegistry.shared.unregister(
223
+ registeredAnchorImageView,
224
+ galleryId: registeredGalleryId,
225
+ index: registeredIndex)
226
+ }
227
+ registeredAnchorImageView = nil
228
+ registeredGalleryId = nil
229
+ registeredIndex = nil
230
+ }
231
+ }
232
+
233
+ enum Theme: String, Enumerable {
234
+ case dark
235
+ case light
236
+
237
+ func toViewerTheme() -> GaleriaViewerTheme {
238
+ switch self {
239
+ case .dark:
240
+ return .dark
241
+ case .light:
242
+ return .light
243
+ }
244
+ }
245
+ func iconColor() -> UIColor {
246
+ switch self {
247
+ case .dark:
248
+ return .white
249
+ case .light:
250
+ return .black
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,73 @@
1
+ import Foundation
2
+ import UIKit
3
+ #if canImport(SDWebImage)
4
+ import SDWebImage
5
+ #endif
6
+
7
+ public protocol GaleriaImageLoading {
8
+ func loadImage(
9
+ _ url: URL,
10
+ placeholder: UIImage?,
11
+ imageView: UIImageView,
12
+ completion: @escaping (_ image: UIImage?) -> Void
13
+ )
14
+ }
15
+
16
+ public struct GaleriaURLSessionImageLoader: GaleriaImageLoading {
17
+ public init() {}
18
+
19
+ public func loadImage(
20
+ _ url: URL,
21
+ placeholder: UIImage?,
22
+ imageView: UIImageView,
23
+ completion: @escaping (UIImage?) -> Void
24
+ ) {
25
+ if let placeholder = placeholder {
26
+ imageView.image = placeholder
27
+ }
28
+
29
+ DispatchQueue.global(qos: .background).async {
30
+ guard let data = try? Data(contentsOf: url), let image = UIImage(data: data) else {
31
+ completion(nil)
32
+ return
33
+ }
34
+
35
+ DispatchQueue.main.async {
36
+ imageView.image = image
37
+ completion(image)
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ #if canImport(SDWebImage)
44
+ struct GaleriaSDWebImageLoader: GaleriaImageLoading {
45
+ func loadImage(
46
+ _ url: URL,
47
+ placeholder: UIImage?,
48
+ imageView: UIImageView,
49
+ completion: @escaping (UIImage?) -> Void
50
+ ) {
51
+ imageView.sd_setImage(
52
+ with: url,
53
+ placeholderImage: placeholder,
54
+ options: [],
55
+ progress: nil
56
+ ) { image, _, _, _ in
57
+ DispatchQueue.main.async {
58
+ completion(image)
59
+ }
60
+ }
61
+ }
62
+ }
63
+ #endif
64
+
65
+ public enum GaleriaImageLoaderFactory {
66
+ public static func makeDefault() -> GaleriaImageLoading {
67
+ #if canImport(SDWebImage)
68
+ return GaleriaSDWebImageLoader()
69
+ #else
70
+ return GaleriaURLSessionImageLoader()
71
+ #endif
72
+ }
73
+ }
@@ -0,0 +1,245 @@
1
+ import ObjectiveC
2
+ import UIKit
3
+
4
+ extension UIImageView {
5
+ private struct GaleriaAnchorRegistration {
6
+ let galleryId: String
7
+ let index: Int
8
+ }
9
+
10
+ private final class GaleriaTapGestureRecognizer: UITapGestureRecognizer {
11
+ weak var fromViewController: UIViewController?
12
+ var dataSource: GaleriaDataSource?
13
+ var imageLoader: GaleriaImageLoading?
14
+ var initialIndex = 0
15
+ var galleryId: String?
16
+ var configuration = GaleriaViewerConfiguration()
17
+ }
18
+
19
+ private static var anchorRegistrationKey: UInt8 = 0
20
+ private static var gestureRecognizerKey: UInt8 = 0
21
+
22
+ private var topMostHostViewController: UIViewController? {
23
+ let keyWindow: UIWindow? = {
24
+ if #available(iOS 13.0, *) {
25
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
26
+ let activeScene = scenes.first(where: { $0.activationState == .foregroundActive }) ?? scenes.first
27
+ return activeScene?.windows.first(where: { $0.isKeyWindow }) ?? activeScene?.windows.first
28
+ } else {
29
+ return UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
30
+ }
31
+ }()
32
+ guard let rootViewController = keyWindow?.rootViewController else { return nil }
33
+ return rootViewController.presentedViewController ?? rootViewController
34
+ }
35
+
36
+ public func setupGaleriaViewer(
37
+ configuration: GaleriaViewerConfiguration = GaleriaViewerConfiguration(),
38
+ from viewController: UIViewController? = nil,
39
+ imageLoader: GaleriaImageLoading? = nil,
40
+ galleryId: String? = nil
41
+ ) {
42
+ configureGaleriaViewer(
43
+ dataSource: GaleriaArrayDataSource(items: [.image(image)]),
44
+ initialIndex: 0,
45
+ configuration: configuration,
46
+ from: viewController,
47
+ imageLoader: imageLoader,
48
+ galleryId: galleryId
49
+ )
50
+ }
51
+
52
+ public func setupGaleriaViewer(
53
+ url: URL,
54
+ initialIndex: Int = 0,
55
+ placeholder: UIImage? = nil,
56
+ configuration: GaleriaViewerConfiguration = GaleriaViewerConfiguration(),
57
+ from viewController: UIViewController? = nil,
58
+ imageLoader: GaleriaImageLoading? = nil,
59
+ galleryId: String? = nil
60
+ ) {
61
+ configureGaleriaViewer(
62
+ dataSource: GaleriaArrayDataSource(items: [.imageURL(url, placeholder: placeholder)]),
63
+ initialIndex: initialIndex,
64
+ configuration: configuration,
65
+ from: viewController,
66
+ imageLoader: imageLoader,
67
+ galleryId: galleryId
68
+ )
69
+ }
70
+
71
+ public func setupGaleriaViewer(
72
+ images: [UIImage],
73
+ initialIndex: Int = 0,
74
+ configuration: GaleriaViewerConfiguration = GaleriaViewerConfiguration(),
75
+ from viewController: UIViewController? = nil,
76
+ imageLoader: GaleriaImageLoading? = nil,
77
+ galleryId: String? = nil
78
+ ) {
79
+ let items = images.map { GaleriaMediaItem.image($0) }
80
+ configureGaleriaViewer(
81
+ dataSource: GaleriaArrayDataSource(items: items),
82
+ initialIndex: initialIndex,
83
+ configuration: configuration,
84
+ from: viewController,
85
+ imageLoader: imageLoader,
86
+ galleryId: galleryId
87
+ )
88
+ }
89
+
90
+ public func setupGaleriaViewer(
91
+ urls: [URL],
92
+ initialIndex: Int = 0,
93
+ placeholder: UIImage? = nil,
94
+ configuration: GaleriaViewerConfiguration = GaleriaViewerConfiguration(),
95
+ from viewController: UIViewController? = nil,
96
+ imageLoader: GaleriaImageLoading? = nil,
97
+ galleryId: String? = nil
98
+ ) {
99
+ let items = urls.map { GaleriaMediaItem.imageURL($0, placeholder: placeholder) }
100
+ configureGaleriaViewer(
101
+ dataSource: GaleriaArrayDataSource(items: items),
102
+ initialIndex: initialIndex,
103
+ configuration: configuration,
104
+ from: viewController,
105
+ imageLoader: imageLoader,
106
+ galleryId: galleryId
107
+ )
108
+ }
109
+
110
+ public func setupGaleriaViewer(
111
+ dataSource: GaleriaDataSource,
112
+ initialIndex: Int = 0,
113
+ configuration: GaleriaViewerConfiguration = GaleriaViewerConfiguration(),
114
+ from viewController: UIViewController? = nil,
115
+ imageLoader: GaleriaImageLoading? = nil,
116
+ galleryId: String? = nil
117
+ ) {
118
+ configureGaleriaViewer(
119
+ dataSource: dataSource,
120
+ initialIndex: initialIndex,
121
+ configuration: configuration,
122
+ from: viewController,
123
+ imageLoader: imageLoader,
124
+ galleryId: galleryId
125
+ )
126
+ }
127
+
128
+ func removeGaleriaViewerInteraction() {
129
+ if let recognizer = galeriaTapGestureRecognizer {
130
+ removeGestureRecognizer(recognizer)
131
+ }
132
+ galeriaTapGestureRecognizer = nil
133
+
134
+ if let registration = galeriaAnchorRegistration {
135
+ GaleriaSourceViewRegistry.shared.unregister(
136
+ self,
137
+ galleryId: registration.galleryId,
138
+ index: registration.index
139
+ )
140
+ }
141
+ galeriaAnchorRegistration = nil
142
+ }
143
+
144
+ private func configureGaleriaViewer(
145
+ dataSource: GaleriaDataSource?,
146
+ initialIndex: Int,
147
+ configuration: GaleriaViewerConfiguration,
148
+ from viewController: UIViewController?,
149
+ imageLoader: GaleriaImageLoading?,
150
+ galleryId: String?
151
+ ) {
152
+ let recognizer = galeriaTapGestureRecognizer ?? {
153
+ let recognizer = GaleriaTapGestureRecognizer(target: self, action: #selector(showGaleriaViewer(_:)))
154
+ recognizer.numberOfTouchesRequired = 1
155
+ recognizer.numberOfTapsRequired = 1
156
+ galeriaTapGestureRecognizer = recognizer
157
+ return recognizer
158
+ }()
159
+
160
+ isUserInteractionEnabled = true
161
+ clipsToBounds = true
162
+
163
+ recognizer.dataSource = dataSource
164
+ recognizer.imageLoader = imageLoader
165
+ recognizer.initialIndex = initialIndex
166
+ recognizer.galleryId = galleryId
167
+ recognizer.configuration = configuration
168
+ recognizer.fromViewController = viewController
169
+
170
+ if recognizer.view !== self {
171
+ addGestureRecognizer(recognizer)
172
+ }
173
+
174
+ updateGaleriaAnchorRegistration(galleryId: galleryId, index: initialIndex)
175
+ }
176
+
177
+ private var galeriaTapGestureRecognizer: GaleriaTapGestureRecognizer? {
178
+ get {
179
+ objc_getAssociatedObject(self, &Self.gestureRecognizerKey) as? GaleriaTapGestureRecognizer
180
+ }
181
+ set {
182
+ objc_setAssociatedObject(
183
+ self,
184
+ &Self.gestureRecognizerKey,
185
+ newValue,
186
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
187
+ )
188
+ }
189
+ }
190
+
191
+ private var galeriaAnchorRegistration: GaleriaAnchorRegistration? {
192
+ get {
193
+ objc_getAssociatedObject(self, &Self.anchorRegistrationKey) as? GaleriaAnchorRegistration
194
+ }
195
+ set {
196
+ objc_setAssociatedObject(
197
+ self,
198
+ &Self.anchorRegistrationKey,
199
+ newValue,
200
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
201
+ )
202
+ }
203
+ }
204
+
205
+ @objc
206
+ private func showGaleriaViewer(_ recognizer: GaleriaTapGestureRecognizer) {
207
+ guard let sourceView = recognizer.view as? UIImageView else { return }
208
+ let resolvedImageLoader = recognizer.imageLoader ?? GaleriaImageLoaderFactory.makeDefault()
209
+ let viewerController = GaleriaViewerController(
210
+ sourceView: sourceView,
211
+ dataSource: recognizer.dataSource,
212
+ imageLoader: resolvedImageLoader,
213
+ configuration: recognizer.configuration,
214
+ initialIndex: recognizer.initialIndex,
215
+ galleryId: recognizer.galleryId
216
+ )
217
+
218
+ let rootViewController = recognizer.fromViewController ?? topMostHostViewController
219
+ let presenter = rootViewController.flatMap(topMostPresentedController(from:))
220
+ presenter?.present(viewerController, animated: false)
221
+ }
222
+
223
+ private func topMostPresentedController(from rootViewController: UIViewController) -> UIViewController {
224
+ var controller = rootViewController
225
+ while let presented = controller.presentedViewController {
226
+ controller = presented
227
+ }
228
+ return controller
229
+ }
230
+
231
+ private func updateGaleriaAnchorRegistration(galleryId: String?, index: Int) {
232
+ if let existing = galeriaAnchorRegistration {
233
+ GaleriaSourceViewRegistry.shared.unregister(
234
+ self,
235
+ galleryId: existing.galleryId,
236
+ index: existing.index
237
+ )
238
+ galeriaAnchorRegistration = nil
239
+ }
240
+
241
+ guard let galleryId = galleryId, galleryId.isEmpty == false else { return }
242
+ GaleriaSourceViewRegistry.shared.register(self, galleryId: galleryId, index: index)
243
+ galeriaAnchorRegistration = GaleriaAnchorRegistration(galleryId: galleryId, index: index)
244
+ }
245
+ }