Skip to content

Instantly share code, notes, and snippets.

@lifeutilityapps
Created April 1, 2025 12:01
Show Gist options
  • Save lifeutilityapps/af0a4ca7b11f604dc20f1ea34ee297ae to your computer and use it in GitHub Desktop.
Save lifeutilityapps/af0a4ca7b11f604dc20f1ea34ee297ae to your computer and use it in GitHub Desktop.
Interactive release notes screen with swipeable image preview carousel.
//
// 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