Created
April 1, 2025 12:01
-
-
Save lifeutilityapps/af0a4ca7b11f604dc20f1ea34ee297ae to your computer and use it in GitHub Desktop.
Interactive release notes screen with swipeable image preview carousel.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// InteractiveReleaseNotes.swift | |
// DownPay for iOS | |
// | |
// Created by Life Utility Apps | |
// | |
import SwiftUI | |
import NukeUI | |
/* | |
This implementation uses JWAutumn's ACarousel library: | |
ACarousel: https://github.com/JWAutumn/ACarousel | |
*/ | |
struct WhatsNewModal: View { | |
@EnvironmentObject var dm: UIDataModel | |
let releases = StandardAppReleaseVersionItem.releaseNotes | |
@State private var activePreviewImage: StandardAppReleaseVersionItemPreview? = nil | |
@State private var activePreviewVideo: StandardAppReleaseVersionVideoPreview? = nil | |
@State private var isSheetVideoPreviewOpen = false | |
@State private var currentIndex: Int = 0 | |
@State private var currentPreviews: [StandardAppReleaseVersionItemPreview] = [] | |
func handleClose() { | |
dm.closeFullScreenCover() | |
} | |
func handleSetPreviewImage(_ newValue: ActivePreviewCarosuelState?) { | |
if let newValue { | |
// Need to get the image indexes | |
withAnimation { | |
activePreviewImage = newValue.currentPreview | |
currentIndex = newValue.currentIndex | |
currentPreviews = newValue.previews | |
} | |
} else { | |
withAnimation { | |
activePreviewImage = nil | |
currentIndex = 0 | |
currentPreviews = [] | |
} | |
} | |
} | |
func handleSetPreviewVideo(_ newValue: StandardAppReleaseVersionVideoPreview?) { | |
if(activePreviewVideo != nil) { | |
isSheetVideoPreviewOpen = true | |
} | |
activePreviewVideo = newValue | |
} | |
var body: some View { | |
NavigationStack { | |
VStack(alignment: .leading, spacing: 0) { | |
StandardSheetTitle(title: "What's New", subtitle: "Explore the \(Global.Legal.appDisplayName) \(SCL.config.appVersion) Update", submitLabel: "Close", onSubmit: handleClose, onClose: handleClose) | |
.animation(.none) | |
ScrollView { | |
VStack(alignment: .leading, spacing: SCL.ui.spacing * 3) { | |
ForEach(releases) { version in | |
StandardReleaseVersionDisplay(releaseVersion: version, onSetActivePreview: handleSetPreviewImage, onSetActiveVideo: handleSetPreviewVideo) | |
} | |
HStack { | |
Spacer() | |
VStack { | |
AppLogo(size: 60, hideTitle: true) | |
.grayscale(1) | |
.shadow(color: SCL.colors.labelSecondaryColor.opacity(0.15), radius: 5) | |
Text("\(Global.Legal.appDisplayName) was created.") | |
.font(.headline) | |
.gradientText(colors: SCL.colors.colorArray.label) | |
StandardDateText(date: Global.appDetails.dateAppCreated, variant: .fullDate) | |
.font(.caption) | |
.foregroundStyle(SCL.colors.labelSecondaryColor) | |
} | |
Spacer() | |
}.padding(.top) | |
} | |
.padding(.vertical) | |
.padding(.bottom, SCL.ui.spacing * 15) | |
} | |
.scrollIndicators(.never) | |
Spacer() | |
} | |
.edgesIgnoringSafeArea(.bottom) | |
.overlay { | |
VStack { | |
Spacer() | |
AppFloatingRateCTA() | |
.padding(.horizontal, SCL.ui.spacing * 3) | |
} | |
} | |
.overlay { | |
if activePreviewImage != nil { | |
WhatsNewPreviewCarosuel(previews: currentPreviews, currentIndex: currentIndex, onSetActivePreview: handleSetPreviewImage) | |
} | |
} | |
.sheet(isPresented: $isSheetVideoPreviewOpen) { | |
if let video = activePreviewVideo { | |
ReleasePreviewVideoSheet(videoPreview: video, onClose: { | |
isSheetVideoPreviewOpen = false | |
}) | |
} else { | |
Text("Something went wrong playing this video.") | |
} | |
} | |
.onChange(of: activePreviewVideo) { _, newValue in | |
if(newValue != nil) { | |
isSheetVideoPreviewOpen = true | |
} | |
} | |
} | |
.onDisappear { | |
SCL.util.asyncAfter(useWithAnimation: false) { | |
SCL.util.clearTemporaryFiles() | |
} | |
} | |
} | |
} | |
struct WhatsNewPreviewCarosuel: View { | |
var previews: [StandardAppReleaseVersionItemPreview] | |
var currentIndex: Int | |
var onSetActivePreview: (ActivePreviewCarosuelState?) -> Void | |
var activeItem: StandardAppReleaseVersionItemPreview? { | |
if(previews.count == 0 || currentIndex < 0) { | |
return nil | |
} else if currentIndex >= previews.count { | |
return previews[0] | |
} else { | |
return previews[currentIndex] | |
} | |
} | |
struct HashablePreview: Hashable, Identifiable { | |
var id: UUID | |
var imgSrc: String | |
} | |
var previewsHashable: [HashablePreview] { | |
var previewsHashable: [HashablePreview] = [] | |
for preview in previews { | |
previewsHashable.append(HashablePreview(id: UUID(), imgSrc: preview.imgSrc)) | |
} | |
return previewsHashable | |
} | |
@State private var index: Int = 0 | |
func handleAppear() { | |
withAnimation { | |
index = currentIndex | |
} | |
} | |
var body: some View { | |
ZStack { | |
Color.black.opacity(0.05) | |
.edgesIgnoringSafeArea(.vertical) | |
VStack { | |
if activeItem != nil { | |
// ACarousel -> https://github.com/JWAutumn/ACarousel | |
ACarousel(previewsHashable, | |
id: \.imgSrc, | |
index: $index, | |
spacing: 10, | |
headspace: 10, | |
sidesScaling: 0.7, | |
isWrap: true) { item in | |
PreviewImageDisplay(imgSrc: item.imgSrc) | |
} | |
StandardCircleListIndicator(index: index, totalCount: previews.count) | |
.padding(.top, SCL.ui.spacing * 3) | |
} else { | |
Text("Something went wrong.") | |
} | |
} | |
} | |
.background(.ultraThinMaterial) | |
.overlay { | |
VStack { | |
HStack { | |
Spacer() | |
Button { | |
onSetActivePreview(nil) | |
} label: { | |
Label("Close", systemImage: SCL.icons.close) | |
} | |
.buttonStyle(.borderedProminent) | |
} | |
.padding() | |
Spacer() | |
} | |
} | |
.onAppear { | |
handleAppear() | |
} | |
} | |
} | |
struct PreviewImageDisplay: View { | |
let imgSrc: String | |
var url: URL { | |
return URL(string: imgSrc) ?? Global.Config.testImage.url | |
} | |
var body: some View { | |
LazyImage(url: url) { phase in | |
if let image = phase.image { | |
image.resizable() | |
.aspectRatio(contentMode: .fit) | |
.mask { | |
RoundedRectangle(cornerRadius: 15) | |
} | |
.frame(width: SCL.ui.screenWidth * 0.9, height: SCL.ui.screenHeight * 0.9) | |
.transition( | |
.asymmetric( | |
insertion: .opacity.animation(.easeIn), | |
removal: .opacity.animation(.easeOut) | |
) | |
) | |
} else if phase.error != nil { | |
StandardErrorState(title: "Something went wrong", subtitle: "Please try again later. Sorry for the inconvenience.") | |
} else { | |
// Placeholder | |
ProgressView() | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment