| name | ios-screenshot-preview |
| description | Add screenshot preview feature to iOS debug menu for App Store screenshots. Use when adding multi-language screenshot preview functionality, testing localization, or preparing App Store submissions. |
| metadata | {"author":"VoiLog Team","version":"1.0.0","category":"ui-development","tags":["ios","swiftui","screenshots","localization","debug"]} |
iOS Screenshot Preview Feature
Add a screenshot preview feature to your iOS app's debug menu. This allows you to preview app screens in different languages before taking App Store screenshots.
When to Use
- Preparing App Store screenshots in multiple languages
- Testing localization across different screens
- Quickly previewing UI in all supported languages
- Creating marketing materials with consistent UI
Quick Start
1. Create ScreenshotPreviewView.swift
Create a new file in your project's debug directory (e.g., ios/YourApp/DebugMode/ScreenshotPreviewView.swift):
import SwiftUI
#if DEBUG
struct ScreenshotPreviewView: View {
@State private var selectedLanguage: AppLanguage?
var body: some View {
NavigationStack {
List {
ForEach(AppLanguage.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language
}) {
HStack {
Text(language.displayName)
.font(.headline)
Spacer()
Text(language.appTitle)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("Select Language")
.navigationBarTitleDisplayMode(.inline)
.fullScreenCover(item: $selectedLanguage) { language in
FullscreenScreenshotView(language: language, onDismiss: {
selectedLanguage = nil
})
}
}
}
}
struct FullscreenScreenshotView: View {
let language: AppLanguage
let onDismiss: () -> Void
@State private var selectedTab = 0
@State private var dragOffset: CGSize = .zero
private var isLastTab: Bool {
selectedTab == ScreenshotScreen.allCases.count - 1
}
var body: some View {
TabView(selection: $selectedTab) {
ForEach(Array(ScreenshotScreen.allCases.enumerated()), id: \.element) { index, screen in
screenPreview(for: screen)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(edges: [])
.offset(x: isLastTab ? dragOffset.width : 0, y: dragOffset.height)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.height > 0 {
dragOffset = CGSize(width: 0, height: value.translation.height)
} else if isLastTab && value.translation.width > 0 {
dragOffset = CGSize(width: value.translation.width, height: 0)
}
}
.onEnded { value in
if value.translation.height > 150 {
onDismiss()
} else if isLastTab && value.translation.width > 150 {
onDismiss()
} else {
withAnimation(.spring()) {
dragOffset = .zero
}
}
}
)
}
@ViewBuilder
private func screenPreview(for screen: ScreenshotScreen) -> some View {
switch screen {
case .mainScreen:
MockMainScreenView(language: language)
case .listScreen:
MockListScreenView(language: language)
}
}
}
enum AppLanguage: String, CaseIterable, Identifiable {
case english = "en"
case japanese = "ja"
var id: String { rawValue }
var displayName: String {
switch self {
case .english: return "EN"
case .japanese: return "JA"
}
}
var appTitle: String {
switch self {
case .english: return "Your App Name"
case .japanese: return "あなたのアプリ名"
}
}
}
enum ScreenshotScreen: String, CaseIterable {
case mainScreen
case listScreen
}
struct MockMainScreenView: View {
let language: AppLanguage
var body: some View {
VStack {
Text("Main Screen")
Text(language.displayName)
}
}
}
struct MockListScreenView: View {
let language: AppLanguage
var body: some View {
List {
ForEach(0..<5) { index in
Text("Item \(index)")
}
}
}
}
#Preview {
ScreenshotPreviewView()
}
#endif
2. Add to Settings/Debug Menu
In your SettingsView or debug menu:
#if DEBUG
Section(header: Text("デバッグ")) {
NavigationLink(destination: ScreenshotPreviewView()) {
Text("スクリーンショットプレビュー")
}
}
#endif
Customization Guide
Adding Languages
- Add cases to
AppLanguage enum:
enum AppLanguage: String, CaseIterable, Identifiable {
case english = "en"
case japanese = "ja"
case german = "de"
case spanish = "es"
var displayName: String {
switch self {
case .english: return "EN"
case .japanese: return "JA"
case .german: return "DE"
case .spanish: return "ES"
}
}
}
- Add localized strings for each language:
var welcomeMessage: String {
switch self {
case .english: return "Welcome"
case .japanese: return "ようこそ"
case .german: return "Willkommen"
case .spanish: return "Bienvenido"
}
}
Adding Screens
- Add screen type to enum:
enum ScreenshotScreen: String, CaseIterable {
case mainScreen
case listScreen
case settingsScreen
}
- Create mock view:
struct MockSettingsScreenView: View {
let language: AppLanguage
var body: some View {
VStack(spacing: 0) {
HStack {
Text(language.settingsTitle)
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
}
.padding()
List {
Section(header: Text(language.generalSection)) {
ForEach(0..<3) { index in
HStack {
Text(language.settingItem(index))
Spacer()
Image(systemName: "chevron.right")
}
}
}
}
}
}
}
- Add to switch in
screenPreview:
@ViewBuilder
private func screenPreview(for screen: ScreenshotScreen) -> some View {
switch screen {
case .mainScreen:
MockMainScreenView(language: language)
case .listScreen:
MockListScreenView(language: language)
case .settingsScreen:
MockSettingsScreenView(language: language)
}
}
Creating Realistic Mock Views
Use VoiLog's implementation as reference (ios/VoiLog/DebugMode/ScreenshotPreviewView.swift):
List View Example:
struct MockListView: View {
let language: AppLanguage
var body: some View {
VStack(spacing: 0) {
HStack {
Text(language.screenTitle)
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Button(action: {}) {
Image(systemName: "plus")
.font(.title3)
}
}
.padding(.horizontal)
.padding(.top, 60)
.padding(.bottom, 20)
List {
ForEach(0..<5) { index in
HStack(spacing: 12) {
Image(systemName: "doc.text")
.font(.title2)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text(language.itemTitle(index))
.font(.headline)
Text(language.itemDate(index))
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text("1:\(15 + index)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
.listStyle(.plain)
}
}
}
Taking Screenshots
- Run app in Simulator (iPhone size for App Store)
- Navigate to Debug → Screenshot Preview
- Select language
- Swipe through screens
- Press
Cmd + S to save screenshot
- Screenshots saved to Desktop
Advanced Features
Custom Gestures
The default implementation supports:
- Swipe down: Dismiss from any screen
- Swipe right: Dismiss from last screen only
Customize thresholds:
.onEnded { value in
if value.translation.height > 200 {
onDismiss()
}
}
Animations
Add transition animations:
.offset(x: isLastTab ? dragOffset.width : 0, y: dragOffset.height)
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: dragOffset)
Dynamic Content
Use state for interactive previews:
struct MockRecordingView: View {
let language: AppLanguage
@State private var isRecording = true
@State private var timer = "00:05:23"
var body: some View {
}
}
Real-World Example
See VoiLog implementation for:
- 12 languages (English, Japanese, German, Spanish, etc.)
- 6 screens (Recording, Playback, Background, Waveform, Playlist, Share)
- Complex layouts with gradients, animations
- Lock screen simulation
- Share sheet mockup
Location: ios/VoiLog/DebugMode/ScreenshotPreviewView.swift
Best Practices
- Wrap in #if DEBUG: Prevents inclusion in release builds
- Realistic Mock Data: Use actual use case examples
- Match Real UI: Keep layouts identical to production
- Complete Localization: Provide all language strings
- Test on Device Sizes: Verify on all target iPhone models
Troubleshooting
Build Error: ScreenshotPreviewView not available in Release
Solution: Ensure all references are wrapped in #if DEBUG:
#if DEBUG
NavigationLink(destination: ScreenshotPreviewView()) {
Text("スクリーンショットプレビュー")
}
#endif
Mock View Doesn't Match Real UI
Solution:
- Compare with actual view code
- Use same fonts, colors, spacing
- Check safe area insets
- Verify navigation bar styling
Language Text Not Showing
Solution:
- Verify all AppLanguage cases return non-empty strings
- Check switch statement covers all cases
- Add default fallback:
default: return "Default Text"
References