Created
September 13, 2025 12:25
-
-
Save mariosaputra/9813f14da3aca6bf7dc920f665607f58 to your computer and use it in GitHub Desktop.
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
| 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