Purpose: iOS-only development without backend dependency Architecture: SwiftUI + MVVM + Combine Dependencies: Zero third-party packages Accessibility: VoiceOver-first design
This is a standalone iOS project that works with your existing backend without requiring any backend changes. Use this if:
- ✅ Backend teammate is unavailable
- ✅ You want to develop iOS independently
- ✅ Backend is stable and doesn't need changes
- ✅ You prefer solo development workflow
pickforme-ios-only/
├── pickforme_ios_onlyApp.swift # App entry point
├── ContentView.swift # Main view
│
├── Core/ # Core infrastructure
│ ├── Networking/
│ │ ├── APIClient.swift # URLSession wrapper
│ │ ├── APIError.swift # Error handling
│ │ └── Endpoint.swift # API endpoints
│ ├── Storage/
│ │ └── KeychainManager.swift # Secure storage
│ ├── Managers/ # Coming soon
│ └── Utilities/
│ ├── Constants.swift # App constants
│ └── Extensions/
│ └── View+Accessibility.swift # Accessibility helpers
│
├── Features/ # Feature modules (MVVM)
│ ├── Authentication/
│ │ ├── Models/
│ │ │ └── User.swift # User model
│ │ ├── ViewModels/ # Coming soon
│ │ └── Views/ # Coming soon
│ ├── Products/
│ │ ├── Models/
│ │ │ └── Product.swift # Product model with accessibility
│ │ ├── ViewModels/ # Coming soon
│ │ └── Views/ # Coming soon
│ ├── AIFeatures/ # Coming soon
│ ├── Messaging/ # Coming soon
│ ├── Profile/ # Coming soon
│ └── Shared/
│ └── Views/
│ ├── LoadingView.swift # Accessible loading
│ ├── ErrorView.swift # Accessible error display
│ └── EmptyStateView.swift # Accessible empty states
│
└── Resources/
└── Assets.xcassets/ # Images, colors
Edit Core/Utilities/Constants.swift:
enum API {
static let baseURL = "https://api.pickforme.com" // ← Your backend URL
}cd ~/Desktop/pickforme-ios-only
open pickforme-ios-only.xcodeprojIn Xcode:
- Select iPhone 15 Pro simulator
- Press Cmd+R to run
- Enable VoiceOver: Cmd+F5
- Swipe right/left to navigate
- Every element should have Korean labels
CRITICAL: Every user uses VoiceOver!
-
Accessibility Label (what it is)
.accessibilityLabel("상품 버튼")
-
Accessibility Value (current state)
.accessibilityValue("10,000원")
-
Accessibility Hint (what happens)
.accessibilityHint("두 번 탭하여 상세 정보 보기")
-
Minimum Touch Target (44x44 pt)
.minimumTouchTarget(width: 44, height: 44)
- Enable VoiceOver (Cmd+F5)
- Navigate entire screen
- All content readable
- All actions performable
- Test with largest text size
// Simple async/await pattern
do {
let products: ProductListResponse = try await APIClient.shared.request(
.getProducts(page: 1, limit: 20)
)
// Handle success
} catch {
// Handle error with Korean message
}Features:
- Automatic JWT token injection
- Korean error messages
- Network error handling
- No third-party dependencies
// Save token
KeychainManager.shared.saveToken("jwt_token_here")
// Get token
if let token = KeychainManager.shared.getToken() {
// Use token
}
// Delete token (logout)
KeychainManager.shared.deleteToken()Features:
- Secure token storage
- Thread-safe
- Simple API
Text("상품명")
.accessibleElement(
label: "상품명, 가격 10,000원",
value: "즐겨찾기에 추가됨",
hint: "두 번 탭하여 상세 정보 보기",
traits: .isButton
)Features:
- Easy-to-use modifiers
- Korean labels
- Dynamic Type support
- Create
LoginViewandLoginViewModel - Implement Apple/Google/Kakao Sign In
- Save JWT token to Keychain
- Test with VoiceOver
- Create
ProductListViewModelandProductListView - Fetch products from API
- Display in accessible list
- Add pull-to-refresh
- Test with VoiceOver
- Create
ProductDetailView - Implement search
- Add favorites
- Test with VoiceOver
- AI features (report, summary, Q&A)
- Messaging
- Profile
- In-app purchases
- API keys
- Firebase config (
GoogleService-Info.plist) - Certificates (
.p8,.p12) - Environment files (
.env)
- Use Keychain for tokens
- HTTPS only
- Validate inputs
- Check
.gitignore
1. Enable VoiceOver: Cmd+F5
2. Close your eyes
3. Navigate using swipe gestures only
4. Can you complete all tasks?
5. Are all labels clear in Korean?
1. Settings → Display & Brightness → Text Size
2. Move to maximum
3. Open app
4. All text readable?
5. Buttons still work?
1. Enable Airplane Mode
2. Try to fetch data
3. Clear error message?
4. Retry works?
| Feature | Team Version | Standalone Version |
|---|---|---|
| Backend | Needs coordination | Works with existing |
| Development | Two teams | iOS only |
| Backend changes | May be needed | Not required |
| Timeline | 16 weeks | Flexible |
| Risk | Dependency on backend dev | Independent |
let response: T = try await APIClient.shared.request(.endpoint)KeychainManager.shared.saveToken(token)
let token = KeychainManager.shared.getToken().accessibilityLabel("레이블")
.accessibilityValue("값")
.accessibilityHint("힌트")
.minimumTouchTarget()@MainActor
class MyViewModel: ObservableObject {
@Published var data: [Item] = []
@Published var isLoading = false
func fetchData() async {
isLoading = true
// Fetch
isLoading = false
}
}- Use
letovervar - Prefer
guardfor early returns - Use
async/awaitover closures - Explicit
selfonly when required
- ViewModels:
ProductListViewModel - Views:
ProductListView - Models:
Product,User
- Always Korean labels
- Brief hints
- Test with VoiceOver
- Support Dynamic Type
- All views have accessibility labels
- Tested with VoiceOver
- Tested with largest text
- No sensitive data
- Code follows style guide
- No warnings
Solution: Check file target membership in File Inspector
Solution: Add explicit .accessibilityLabel()
Solution: Use .fixedSize(horizontal: false, vertical: true)
Solution: Use .minimumTouchTarget()
IOS_MIGRATION_PLAN.md- Detailed 16-week planCOORDINATION_PLAN.md- Team coordinationBACKEND_DEVELOPMENT_PLAN.md- Backend plan
You're ready to ship when:
- ✅ All features working
- ✅ 100% VoiceOver accessible
- ✅ Dynamic Type support
- ✅ No P0/P1 bugs
- ✅ TestFlight tested
- ✅ App Store ready
Remember: Every single user uses VoiceOver. Accessibility is not optional - it's THE feature.
Start building! 🚀
Last Updated: November 13, 2025 Status: Foundation Ready ✅