Skip to content

Instantly share code, notes, and snippets.

@mariosaputra
Created September 13, 2025 12:25
Show Gist options
  • Select an option

  • Save mariosaputra/9813f14da3aca6bf7dc920f665607f58 to your computer and use it in GitHub Desktop.

Select an option

Save mariosaputra/9813f14da3aca6bf7dc920f665607f58 to your computer and use it in GitHub Desktop.
import Foundation
import SwiftUI
struct WelcomeGridView: View {
// Replace with your real asset names
private var imageNames: [String] {
let baseImages = [
"gen_plc_1","gen_plc_2","gen_plc_3",
"gen_plc_4","gen_plc_5","gen_plc_6",
"gen_plc_1","gen_plc_6","gen_plc_3",
"gen_plc_5","gen_plc_2","gen_plc_4",
"gen_plc_4","gen_plc_1","gen_plc_2"
]
let screenWidth = UIScreen.main.bounds.width
if screenWidth > 768 { // iPad - add more items
return baseImages + [
"gen_plc_3","gen_plc_5","gen_plc_1",
"gen_plc_6","gen_plc_4","gen_plc_2",
"gen_plc_1","gen_plc_3","gen_plc_5",
"gen_plc_2","gen_plc_6","gen_plc_4",
"gen_plc_4","gen_plc_1","gen_plc_2",
"gen_plc_3","gen_plc_5","gen_plc_6",
"gen_plc_3","gen_plc_5","gen_plc_6"
]
} else {
return baseImages
}
}
// Animation states
@State private var gridOffset: CGFloat = 90
@State private var tilesVisible = false
// Visual constants (tweak to taste)
private let gutterSpacing: CGFloat = 1
private let innerPadding: CGFloat = 3
private let cornerRadiusBig: CGFloat = 12
private let cornerRadiusInner: CGFloat = 10
private let accentCyan = Color(red: 0/255, green: 230/255, blue: 210/255)
// grid layout - responsive for iPhone and iPad
private var columns: [GridItem] {
let screenWidth = UIScreen.main.bounds.width
let columnCount = screenWidth > 768 ? 5 : 3 // iPad gets 5 columns, iPhone gets 3
return Array(repeating: GridItem(.flexible(), spacing: 1), count: columnCount)
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
// Grid area (behind overlay)
VStack(spacing: 0) {
EmptyView().frame(height: 18)
GeometryReader { geo in
LazyVGrid(columns: columns, spacing: 1) {
ForEach(Array(imageNames.enumerated()), id: \.offset) { idx, name in
tileView(name: name)
// square tiles — responsive sizing for iPhone and iPad
.frame(height: responsiveTileHeight(geo: geo))
.opacity(tilesVisible ? 1 : 0)
.offset(y: tilesVisible ? 0 : 30)
.scaleEffect(tilesVisible ? 1 : 0.8)
.rotationEffect(.degrees(tilesVisible ? 0 : 5))
.animation(.interpolatingSpring(stiffness: 120, damping: 15).delay(Double(idx) * 0.08), value: tilesVisible)
}
}
.padding(.horizontal, 0)
.offset(y: gridOffset) // whole grid moves up on appear
.scaleEffect(gridOffset == 0 ? 1 : 0.95)
.opacity(gridOffset == 0 ? 1 : 0.7)
.onAppear {
withAnimation(.easeOut(duration: 1.0)) {
gridOffset = 0
}
// reveal tiles with beautiful staggered animation
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tilesVisible = true
// Add subtle haptic feedback when tiles appear
Haptics.impact(.soft)
}
}
} // GeometryReader
Spacer(minLength: 18)
} // VStack
.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
// Overlay gradient / blur background to darken grid
ZStack {
// smooth fade to black so text is readable over images
LinearGradient(gradient: Gradient(colors: [
Color.clear,
Color.clear,
Color.black.opacity(0.05),
Color.black.opacity(0.15),
Color.black.opacity(0.3),
Color.black.opacity(0.5),
Color.black.opacity(0.7),
Color.black.opacity(0.85),
Color.black.opacity(0.95)
]),
startPoint: .top, endPoint: .bottom)
.frame(height: calculateOverlayHeight())
.ignoresSafeArea(edges: .bottom)
// Content inside overlay
VStack(alignment: .center, spacing: 2) {
Spacer()
HStack(alignment: .top) {
Text("Welcome to")
.font(.system(size: 25, weight: .bold))
.foregroundColor(.white)
.minimumScaleFactor(0.6)
HStack( spacing: 8) {
Text("Tattoo")
.font(.system(size: 25, weight: .bold))
.lineLimit(1)
Text("Simulator")
.font(.system(size: 25, weight: .bold))
.lineLimit(1)
}
.foregroundStyle(
LinearGradient(
gradient: Gradient(colors: [Color.appPrimary, Color.appAccent]),
startPoint: .leading,
endPoint: .trailing
)
)
}
.padding(.horizontal, 24)
.padding(.top, 6)
// Subtitle
Text("Step into the future of personalized tattoos. Envision it, adore it, ink it!")
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color(white: 0.86))
.lineLimit(2)
.multilineTextAlignment(.center)
.padding(.horizontal, 26)
.frame(maxWidth: .infinity, alignment: .center)
// CTA
Button(action: {
// Add haptic feedback for button press
Haptics.impact(.medium)
// Navigate to onboarding
NotificationCenter.default.post(name: .navigateToOnboarding, object: nil)
}) {
Text("Let's Get Started!")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.background(
LinearGradient(
gradient: Gradient(colors: [Color.appPrimary, Color.appAccent]),
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(12)
.padding(.horizontal, 22)
}
.padding(.top, 16)
.padding(.bottom, 40)
} // VStack content
} // ZStack gradient
.frame(height: calculateOverlayHeight())
} // VStack overlay
.edgesIgnoringSafeArea(.bottom)
} // ZStack
}
// MARK: - Responsive Helper Functions
private func responsiveTileHeight(geo: GeometryProxy) -> CGFloat {
let screenWidth = UIScreen.main.bounds.width
let columnCount = screenWidth > 768 ? 5 : 3
return (geo.size.width - (gutterSpacing * CGFloat(columnCount - 1))) / CGFloat(columnCount)
}
private func calculateOverlayHeight() -> CGFloat {
let screenWidth = UIScreen.main.bounds.width
// Text content heights
let welcomeTextHeight: CGFloat = 25 // "Welcome to" font size
let titleTextHeight: CGFloat = 25 // "Tattoo Simulator" font size
let subtitleTextHeight: CGFloat = 16 // Subtitle font size
let buttonHeight: CGFloat = 20 + 36 // Button text + vertical padding
let buttonCornerRadius: CGFloat = 12
// Spacing between elements
let textSpacing: CGFloat = 2
let titleSpacing: CGFloat = 6
let subtitleSpacing: CGFloat = 16
let buttonSpacing: CGFloat = 40 // Bottom padding
// Calculate total content height
let contentHeight = welcomeTextHeight +
titleTextHeight +
subtitleTextHeight +
buttonHeight +
textSpacing +
titleSpacing +
subtitleSpacing +
buttonSpacing
// Add buffer for smooth gradient transition
let buffer: CGFloat = screenWidth > 768 ? 80 : 120
return contentHeight + buffer + 200
}
// MARK: - Tile view that exactly reproduces dark gutter + white inner card
@ViewBuilder
private func tileView(name: String) -> some View {
ZStack {
// dark gutter card (visible between tiles)
RoundedRectangle(cornerRadius: cornerRadiusBig, style: .continuous)
.fill(Color(white: 0.03))
// inner white card for the image
RoundedRectangle(cornerRadius: cornerRadiusInner, style: .continuous)
.fill(Color.white)
.padding(innerPadding)
.overlay(
Group {
if let ui = UIImage(named: name) {
Image(uiImage: ui)
.resizable()
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: cornerRadiusInner, style: .continuous))
.padding(innerPadding)
.clipped()
} else {
// placeholder if asset missing - with debug info
RoundedRectangle(cornerRadius: cornerRadiusInner)
.fill(Color.red.opacity(0.5)) // Red background to see missing assets
.overlay(
VStack {
Image(systemName: "photo")
.font(.system(size: 26))
.foregroundColor(.white)
Text(name)
.font(.system(size: 8))
.foregroundColor(.white)
}
)
.padding(innerPadding)
}
}
)
// enhanced shadow with animation
.shadow(color: Color.black.opacity(0.3), radius: 6, x: 0, y: 3)
}
.onTapGesture {
// Add haptic feedback for tile interaction
Haptics.selectionChanged()
// Add subtle tap animation
withAnimation(.easeInOut(duration: 0.1)) {
// Tile tap animation could be added here
}
}
}
}
struct WelcomeGridView_Previews: PreviewProvider {
static var previews: some View {
Group {
WelcomeGridView()
.previewDevice("iPhone 14 Pro")
WelcomeGridView()
.previewDevice("iPhone 13 mini")
WelcomeGridView()
.previewDevice("iPad Pro (12.9-inch) (6th generation)")
WelcomeGridView()
.previewDevice("iPad Air (5th generation)")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment