1
+ import DesignSystem
1
2
import Foundation
3
+ import Models
2
4
import Nuke
3
5
import NukeUI
4
6
import SwiftUI
5
7
6
8
public struct FullScreenMediaView : View {
7
- let images : [ URL ]
9
+ @Environment ( \. dismiss) private var dismiss
10
+
11
+ let images : [ Media ]
8
12
let preloadedImage : URL ?
9
13
let namespace : Namespace . ID
10
14
11
15
@State private var isFirstImageLoaded : Bool = false
16
+ @State private var isOverlayVisible : Bool = false
17
+ @State private var isSaved : Bool = false
18
+ @State private var scrollPosition : Media ?
19
+ @State private var isAltVisible : Bool = false
20
+
21
+ @GestureState private var zoom = 1.0
12
22
13
- public init ( images: [ URL ] , preloadedImage: URL ? , namespace: Namespace . ID ) {
23
+ public init ( images: [ Media ] , preloadedImage: URL ? , namespace: Namespace . ID ) {
14
24
self . images = images
15
25
self . preloadedImage = preloadedImage
16
26
self . namespace = namespace
@@ -20,39 +30,136 @@ public struct FullScreenMediaView: View {
20
30
if let preloadedImage, !isFirstImageLoaded {
21
31
return preloadedImage
22
32
}
23
- return images. first
33
+ return images. first? . url
24
34
}
25
35
26
36
public var body : some View {
27
- ScrollView ( . horizontal) {
37
+ ScrollView ( . horizontal, showsIndicators : false ) {
28
38
LazyHStack {
29
39
ForEach ( images. indices, id: \. self) { index in
30
- LazyImage ( url: index == 0 ? firstImageURL : images [ index] ) { state in
40
+ LazyImage (
41
+ request: . init(
42
+ url: index == 0 ? firstImageURL : images [ index] . url,
43
+ priority: . veryHigh)
44
+ ) { state in
31
45
if let image = state. image {
32
46
image
33
47
. resizable ( )
34
48
. scaledToFill ( )
35
49
. aspectRatio ( contentMode: . fit)
50
+ . scaleEffect ( zoom)
51
+ . gesture (
52
+ MagnifyGesture ( )
53
+ . updating ( $zoom) { value, gestureState, transaction in
54
+ gestureState = value. magnification
55
+ }
56
+ )
36
57
} else {
37
58
RoundedRectangle ( cornerRadius: 8 )
38
59
. fill ( . thinMaterial)
39
60
}
40
61
}
41
62
. containerRelativeFrame ( [ . horizontal, . vertical] )
63
+ . id ( images [ index] )
42
64
}
43
65
}
44
66
. scrollTargetLayout ( )
45
67
}
68
+ . scrollPosition ( id: $scrollPosition)
69
+ . overlay ( alignment: . topTrailing) {
70
+ topActionsView
71
+ }
72
+ . overlay ( alignment: . bottom) {
73
+ bottomActionsView
74
+ }
46
75
. scrollContentBackground ( . hidden)
47
76
. scrollTargetBehavior ( . viewAligned)
48
- . navigationTransition ( . zoom( sourceID: images [ 0 ] , in: namespace) )
77
+ . navigationTransition ( . zoom( sourceID: images [ 0 ] . id, in: namespace) )
78
+ . onTapGesture {
79
+ withAnimation {
80
+ isOverlayVisible. toggle ( )
81
+ }
82
+ }
49
83
. task {
84
+ scrollPosition = images. first
50
85
do {
51
- let data = try await Nuke . ImagePipeline. shared. data ( for: . init( url: images. first) )
86
+ let data = try await ImagePipeline . shared. data ( for: . init( url: images. first? . url ) )
52
87
if !data. 0 . isEmpty {
53
88
self . isFirstImageLoaded = true
54
89
}
55
90
} catch { }
56
91
}
57
92
}
93
+
94
+ private var topActionsView : some View {
95
+ HStack {
96
+ if isOverlayVisible {
97
+ Button {
98
+ dismiss ( )
99
+ } label: {
100
+ Image ( systemName: " xmark " )
101
+ . padding ( )
102
+ }
103
+ . buttonStyle ( . circle)
104
+ . foregroundColor ( . indigo)
105
+ . padding ( . trailing, 16 )
106
+ . transition ( . move( edge: . top) . combined ( with: . opacity) )
107
+ }
108
+ }
109
+ }
110
+
111
+ private var bottomActionsView : some View {
112
+ HStack ( spacing: 16 ) {
113
+ if isOverlayVisible {
114
+ saveButton
115
+ shareButton
116
+ }
117
+ }
118
+ }
119
+
120
+ private var saveButton : some View {
121
+ Button {
122
+ Task {
123
+ do {
124
+ guard let imageURL = scrollPosition? . url else { return }
125
+ let data = try await ImagePipeline . shared. data ( for: . init( url: imageURL) )
126
+ if !data. 0 . isEmpty, let image = UIImage ( data: data. 0 ) {
127
+ UIImageWriteToSavedPhotosAlbum ( image, nil , nil , nil )
128
+ withAnimation {
129
+ isSaved = true
130
+ }
131
+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 1.0 ) {
132
+ withAnimation {
133
+ isSaved = false
134
+ }
135
+ }
136
+ }
137
+ } catch { }
138
+ }
139
+ } label: {
140
+ if isSaved {
141
+ Label ( " Saved " , systemImage: " checkmark " )
142
+ . padding ( )
143
+ } else {
144
+ Label ( " Save " , systemImage: " square.and.arrow.down " )
145
+ . padding ( )
146
+ }
147
+ }
148
+ . foregroundColor ( . indigo)
149
+ . buttonStyle ( . pill)
150
+ . transition ( . move( edge: . bottom) . combined ( with: . opacity) )
151
+ }
152
+
153
+ @ViewBuilder
154
+ private var shareButton : some View {
155
+ if let imageURL = scrollPosition? . url {
156
+ ShareLink ( item: imageURL) {
157
+ Label ( " Share " , systemImage: " square.and.arrow.up " )
158
+ . padding ( )
159
+ }
160
+ . foregroundColor ( . indigo)
161
+ . buttonStyle ( . pill)
162
+ . transition ( . move( edge: . bottom) . combined ( with: . opacity) )
163
+ }
164
+ }
58
165
}
0 commit comments