@@ -11,12 +11,11 @@ struct AltSource: Decodable {
1111
1212struct AppVersion : Decodable {
1313 let version : String ?
14- let buildVersion : String ?
1514 let date : String ?
1615 let downloadURL : URL ?
1716 let size : Int ?
1817 let minOSVersion : String ?
19- let maxOSVersion : String ?
18+ let localizedDescription : String ?
2019}
2120
2221struct AltApp : Decodable , Identifiable {
@@ -26,11 +25,8 @@ struct AltApp: Decodable, Identifiable {
2625 let developerName : String ?
2726 let subtitle : String ?
2827 let iconURL : URL ?
28+ let localizedDescription : String ?
2929 let versions : [ AppVersion ] ?
30-
31- var latestDownloadURL : URL ? {
32- versions? . first? . downloadURL
33- }
3430}
3531
3632// MARK: - ViewModel
@@ -97,9 +93,9 @@ final class RepoViewModel: ObservableObject {
9793// MARK: - AppsView
9894public struct AppsView : View {
9995 @StateObject private var vm : RepoViewModel
100-
10196 @State private var searchText : String = " "
10297 @FocusState private var searchFieldFocused : Bool
98+ @State private var selectedApp : AltApp ? = nil
10399
104100 public init ( repoURL: URL = URL ( string: " https://repository.apptesters.org/ " ) !) {
105101 _vm = StateObject ( wrappedValue: RepoViewModel ( sourceURL: repoURL) )
@@ -168,14 +164,22 @@ public struct AppsView: View {
168164 . padding ( )
169165 } else {
170166 List ( filteredApps) { app in
171- AppRowView ( app: app)
167+ Button {
168+ selectedApp = app
169+ } label: {
170+ AppRowView ( app: app)
171+ }
172+ . buttonStyle ( . plain)
172173 }
173174 . listStyle ( PlainListStyle ( ) )
174175 . refreshable { vm. refresh ( ) }
175176 }
176177 }
177178 . padding ( . top, 8 )
178179 }
180+ . sheet ( item: $selectedApp) { app in
181+ AppDetailView ( app: app)
182+ }
179183 . toolbar {
180184 ToolbarItem ( placement: . navigationBarTrailing) {
181185 Button ( action: { vm. refresh ( ) } ) {
@@ -205,13 +209,6 @@ private struct AppRowView: View {
205209 . scaledToFill ( )
206210 . frame ( width: 48 , height: 48 )
207211 . clipShape ( RoundedRectangle ( cornerRadius: 10 ) )
208- . shadow ( radius: 1 , y: 1 )
209- . onAppear {
210- let renderer = ImageRenderer ( content: image)
211- if let uiImage = renderer. uiImage {
212- ImageCache . shared. set ( uiImage, for: iconURL)
213- }
214- }
215212 case . failure:
216213 Image ( systemName: " app " )
217214 . resizable ( )
@@ -260,4 +257,134 @@ private struct AppRowView: View {
260257 }
261258 . padding ( . vertical, 6 )
262259 }
260+ }
261+
262+ // MARK: - AppDetailView
263+ private struct AppDetailView : View {
264+ let app : AltApp
265+
266+ private var latestVersion : AppVersion ? {
267+ app. versions? . first
268+ }
269+
270+ private func formatSize( _ size: Int ? ) -> String {
271+ guard let size = size else { return " Unknown " }
272+ return ByteCountFormatter . string ( fromByteCount: Int64 ( size) , countStyle: . file)
273+ }
274+
275+ private func formatDate( _ dateString: String ? ) -> String {
276+ guard let dateString = dateString, let date = ISO8601DateFormatter ( ) . date ( from: dateString) else {
277+ return " Unknown "
278+ }
279+ let formatter = DateFormatter ( )
280+ formatter. dateStyle = . medium
281+ return formatter. string ( from: date)
282+ }
283+
284+ var body : some View {
285+ ScrollView {
286+ VStack ( alignment: . leading, spacing: 16 ) {
287+ HStack ( alignment: . top, spacing: 16 ) {
288+ if let iconURL = app. iconURL {
289+ AsyncImage ( url: iconURL) { phase in
290+ switch phase {
291+ case . empty:
292+ ProgressView ( )
293+ . frame ( width: 80 , height: 80 )
294+ case . success( let image) :
295+ image
296+ . resizable ( )
297+ . scaledToFit ( )
298+ . frame ( width: 80 , height: 80 )
299+ . clipShape ( RoundedRectangle ( cornerRadius: 12 ) )
300+ case . failure:
301+ Image ( systemName: " app " )
302+ . resizable ( )
303+ . scaledToFit ( )
304+ . frame ( width: 60 , height: 60 )
305+ . foregroundColor ( . secondary)
306+ @unknown default :
307+ EmptyView ( )
308+ }
309+ }
310+ }
311+ VStack ( alignment: . leading, spacing: 4 ) {
312+ Text ( app. name)
313+ . font ( . title2)
314+ . bold ( )
315+ if let dev = app. developerName {
316+ Text ( dev)
317+ . font ( . subheadline)
318+ . foregroundColor ( . secondary)
319+ }
320+ Text ( app. bundleIdentifier)
321+ . font ( . caption)
322+ . foregroundColor ( . secondary)
323+ }
324+ }
325+
326+ if let generalDesc = app. localizedDescription, generalDesc != latestVersion? . localizedDescription {
327+ Text ( generalDesc)
328+ }
329+
330+ if let latest = latestVersion, latest. localizedDescription != nil , latest. localizedDescription != app. localizedDescription {
331+ VStack ( alignment: . leading, spacing: 8 ) {
332+ Text ( " What's New? " )
333+ . font ( . headline)
334+ Text ( latest. localizedDescription!)
335+ }
336+ }
337+
338+ if let latest = latestVersion {
339+ VStack ( alignment: . leading, spacing: 4 ) {
340+ HStack {
341+ Text ( " Version: " )
342+ . bold ( )
343+ Text ( latest. version ?? " Unknown " )
344+ }
345+ HStack {
346+ Text ( " Released: " )
347+ . bold ( )
348+ Text ( formatDate ( latest. date) )
349+ }
350+ HStack {
351+ Text ( " Size: " )
352+ . bold ( )
353+ Text ( formatSize ( latest. size) )
354+ }
355+ HStack {
356+ Text ( " Min OS: " )
357+ . bold ( )
358+ Text ( latest. minOSVersion ?? " Unknown " )
359+ }
360+ }
361+ }
362+
363+ if let screenshots = latestVersion? . downloadURL != nil ? [ ] : nil , !screenshots!. isEmpty {
364+ ScrollView ( . horizontal, showsIndicators: false ) {
365+ HStack {
366+ ForEach ( screenshots!, id: \. self) { url in
367+ AsyncImage ( url: url) { phase in
368+ switch phase {
369+ case . empty: ProgressView ( )
370+ case . success( let image) :
371+ image
372+ . resizable ( )
373+ . scaledToFit ( )
374+ . frame ( height: 200 )
375+ . cornerRadius ( 10 )
376+ case . failure: Image ( systemName: " photo " ) . resizable ( ) . scaledToFit ( ) . frame ( height: 200 )
377+ @unknown default : EmptyView ( )
378+ }
379+ }
380+ }
381+ }
382+ }
383+ }
384+
385+ Spacer ( )
386+ }
387+ . padding ( )
388+ }
389+ }
263390}
0 commit comments