
Release Review
- 390 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
release-review is an Apple app release skill that prepares pre-submission review artifacts for developers who need a regression checklist and release notes before App Store submission.
About
release-review is an Apple app release skill that helps developers prepare and self-review a release right before App Store submission by generating build notes, review notes, and a regression checklist. release-review also surfaces common App Store rejection patterns so a developer can sanity-check compliance, UX, and edge cases before uploading in App Store Connect. Developers reach for release-review when a release has multiple changes, when QA coverage is uneven, or when the team needs a repeatable pre-submit checklist for iOS/iPadOS apps. release-review is best used as a final gate in a release train alongside TestFlight validation and release-candidate tagging.
- Structured pre-submission review checklist
- Reviewer notes and build metadata templates
- Maps common Apple rejection causes
- Coordinates test evidence for release builds
- Shortens resubmission cycles
Release Review by the numbers
- 390 all-time installs (skills.sh)
- +18 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #60 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill release-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 390 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
How do I self-review an iOS release before submission?
Prepare and self-review an Apple app release—build notes, review notes, regression checklist, and common rejection patterns—immediately before App Store submission.
Who is it for?
iOS developers shipping a release candidate and needing a repeatable pre-submission checklist.
Skip if: Teams that only need automated CI checks without human review notes or checklists.
When should I use this skill?
Invoke when a prompt mentions App Store submission, iOS release review, release notes, regression checklist, or rejection patterns.
What you get
Release notes, App Store review notes, regression checklist, and a rejection-risk checklist.
- release checklist
- release notes
Files
Release Review for Apple Platforms
Performs a comprehensive pre-release audit of macOS and iOS applications from a senior developer's perspective. Identifies critical issues that could cause rejection, security vulnerabilities, privacy concerns, and UX problems—with actionable fixes.
When This Skill Activates
Use this skill when the user:
- Says "review for release", "release review", or "pre-release audit"
- Asks for "senior developer review" or "critical review"
- Mentions preparing for "App Store", "TestFlight", or "notarization"
- Wants to know what "power users might complain about"
- Asks to "review before shipping" or "check before release"
Review Process
Phase 1: Project Discovery
First, understand the project:
# Find project type
Glob: **/*.xcodeproj or **/*.xcworkspace
Glob: **/Info.plist
Glob: **/project.pbxprojIdentify:
- Platform (macOS, iOS, or both)
- App type (standard app, menu bar app, widget, extension)
- Distribution method (App Store, direct download, TestFlight)
Phase 2: Security Review
Load and apply: security-checklist.md
Key areas:
- Credential storage (Keychain patterns, no hardcoded secrets)
- Data transmission (HTTPS, certificate validation)
- Input validation (injection prevention)
- Entitlements audit
- Hardened runtime (macOS)
Phase 3: Privacy Review
Load and apply: privacy-checklist.md
Key areas:
- Data collection transparency
- Privacy manifest (iOS 17+)
- User consent flows
- Third-party SDK disclosure
- GDPR compliance basics
Phase 4: UX Polish Review
Load and apply: ux-polish-checklist.md
Key areas:
- First launch / onboarding
- Empty states and error handling
- Loading states
- Text truncation and accessibility
- Platform-specific UX patterns
Phase 5: Distribution Review
Load and apply: distribution-checklist.md
Key areas:
- Bundle identifier format
- Code signing configuration
- Info.plist completeness
- App icons
- Platform-specific requirements (notarization, App Store)
Phase 6: API Design Review
Load and apply: api-design-checklist.md
Key areas:
- User-Agent headers (honest identification)
- Error handling patterns
- Token expiration handling
- Rate limiting
- Offline handling
Output Format
Present findings in this structure:
# Release Review: [App Name]
**Platform**: macOS / iOS / Universal
**Distribution**: App Store / Direct Download / TestFlight
**Review Date**: [Date]
## Summary
| Priority | Count |
|----------|-------|
| Critical | X |
| High | X |
| Medium | X |
| Low | X |
---
## 🔴 Critical Issues (Must Fix)
Issues that will cause rejection, crashes, or security vulnerabilities.
### [Category]: [Issue Title]
**File**: `path/to/file.swift:123`
**Impact**: [Why this matters]
**Current Code**:// problematic code
**Suggested Fix**:// fixed code
---
## 🟠 High Priority (Should Fix)
Issues that significantly impact user experience or trust.
[Same format as above]
---
## 🟡 Medium Priority (Fix Soon)
Issues that should be addressed but won't block release.
[Same format as above]
---
## 🟢 Low Priority / Suggestions
Nice-to-have improvements and polish.
[Same format as above]
---
## ✅ Strengths
What the app does well:
- [Strength 1]
- [Strength 2]
- [Strength 3]
---
## Recommended Action Plan
1. **[Critical]** [First thing to fix]
2. **[Critical]** [Second thing to fix]
3. **[High]** [Third thing to fix]
...Priority Classification
🔴 Critical
- Security vulnerabilities (credential exposure, injection)
- Crashes or data loss scenarios
- App Store rejection causes
- Privacy violations
- Hardcoded secrets or spoofed identifiers
🟠 High
- Poor error handling (silent failures)
- Missing user consent or transparency
- Accessibility blockers
- Missing required Info.plist keys
- Broken functionality
🟡 Medium
- Incomplete onboarding
- Suboptimal UX patterns
- Missing empty states
- Performance concerns
- Minor accessibility issues
🟢 Low
- Code style improvements
- Additional features
- Polish and refinement
- Documentation improvements
Platform-Specific Considerations
macOS
- Menu bar app window activation (
NSApp.activate) - Sandbox exceptions justification
- Notarization requirements
- Hardened runtime
- Developer ID signing
- DMG/installer considerations
iOS
- App Tracking Transparency
- Privacy nutrition labels
- Launch screen requirements
- Export compliance
- In-app purchase requirements
- TestFlight configuration
References
- security-checklist.md - Detailed security review items
- privacy-checklist.md - Privacy and data handling
- ux-polish-checklist.md - User experience review
- distribution-checklist.md - Release and distribution
- api-design-checklist.md - Network and API patterns
API Design Checklist
Network and API patterns review for macOS and iOS applications.
User-Agent Headers
Why This Matters
User-Agent identifies your app to API servers. Spoofing (pretending to be a browser or another app) is:
- Dishonest - Misrepresents your app's identity
- Risky - Can get your app blocked when detected
- Unprofessional - Shows lack of engineering maturity
✅ Good Pattern
// Honest identification
let userAgent = "MyApp/1.2.0 (macOS 14.0; com.company.myapp)"
var request = URLRequest(url: url)
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")❌ Anti-patterns
// Browser spoofing - NEVER do this
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", forHTTPHeaderField: "User-Agent")
// Pretending to be official app
request.setValue("OfficialAPIClient/1.0", forHTTPHeaderField: "User-Agent")
// Empty or missing - also problematic
// (no User-Agent set at all)Checklist
- [ ] User-Agent honestly identifies your app
- [ ] Includes app name and version
- [ ] No browser or other app spoofing
- [ ] Consistent across all API calls
Search Pattern
Grep: "User-Agent|userAgent|Mozilla|Chrome|Safari"Error Handling
HTTP Status Codes
Handle all relevant status codes gracefully:
func handleResponse(_ response: HTTPURLResponse, data: Data) throws -> Data {
switch response.statusCode {
case 200...299:
return data
case 401:
throw APIError.unauthorized("Session expired. Please re-authenticate.")
case 403:
throw APIError.forbidden("Access denied. Check your permissions.")
case 404:
throw APIError.notFound("Resource not found.")
case 429:
throw APIError.rateLimited("Too many requests. Please wait and try again.")
case 500...599:
throw APIError.serverError("Server error. Please try again later.")
default:
throw APIError.unknown("Unexpected error (HTTP \(response.statusCode)).")
}
}User-Friendly Error Messages
✅ Good Pattern
enum APIError: LocalizedError {
case networkUnavailable
case unauthorized(String)
case rateLimited(String)
var errorDescription: String? {
switch self {
case .networkUnavailable:
return "Unable to connect. Please check your internet connection."
case .unauthorized(let message):
return message
case .rateLimited(let message):
return message
}
}
var recoverySuggestion: String? {
switch self {
case .networkUnavailable:
return "Try again when you have a stable connection."
case .unauthorized:
return "Sign out and sign back in to refresh your session."
case .rateLimited:
return "Wait a few minutes before making more requests."
}
}
}❌ Anti-patterns
// Technical jargon in user messages
throw NSError(domain: "HTTP", code: 401, userInfo: nil)
// Generic unhelpful messages
throw APIError.error("Something went wrong")
// Exposing internal details
throw APIError.error("JSON parsing failed at key 'data.user.id'")Checklist
- [ ] All HTTP status codes handled
- [ ] Error messages are user-friendly
- [ ] Error messages explain what to do
- [ ] No technical jargon in user-facing errors
- [ ] Errors logged for debugging (not shown to user)
Token Expiration
The Problem
API tokens expire. If not handled, users experience silent failures or confusing errors.
✅ Good Pattern
class APIClient {
private var tokenExpiresAt: Date?
func makeRequest() async throws -> Data {
// Check expiration before request
if let expiresAt = tokenExpiresAt, Date() >= expiresAt {
throw APIError.tokenExpired("Your session has expired. Please restart the app to refresh.")
}
// Make request...
let (data, response) = try await session.data(for: request)
// Handle 401 from server (token revoked or expired early)
if (response as? HTTPURLResponse)?.statusCode == 401 {
throw APIError.tokenExpired("Your session has expired. Please restart the app to refresh.")
}
return data
}
func refreshTokenIfNeeded() async throws {
guard let expiresAt = tokenExpiresAt else { return }
// Refresh proactively when close to expiration
let refreshThreshold: TimeInterval = 300 // 5 minutes
if Date().addingTimeInterval(refreshThreshold) >= expiresAt {
try await refreshToken()
}
}
}Token Refresh Flow
// Option 1: Automatic refresh
func refreshToken() async throws {
let newToken = try await authService.refreshToken()
self.token = newToken.accessToken
self.tokenExpiresAt = newToken.expiresAt
}
// Option 2: Notify user to re-authenticate
NotificationCenter.default.post(
name: .tokenExpired,
object: nil,
userInfo: ["message": "Please sign in again to continue."]
)Checklist
- [ ] Token expiration time tracked
- [ ] Expiration checked before requests
- [ ] 401 responses handled as potential expiration
- [ ] User notified with clear action when token expires
- [ ] Proactive refresh implemented (if supported by API)
Rate Limiting
Handling 429 Responses
func makeRequestWithRetry(maxRetries: Int = 3) async throws -> Data {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
return try await makeRequest()
} catch APIError.rateLimited {
lastError = APIError.rateLimited("Rate limited")
// Exponential backoff
let delay = pow(2.0, Double(attempt)) // 1s, 2s, 4s
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}
throw lastError ?? APIError.unknown("Request failed after retries")
}Respect Retry-After Header
if response.statusCode == 429 {
if let retryAfter = response.value(forHTTPHeaderField: "Retry-After"),
let seconds = Int(retryAfter) {
throw APIError.rateLimited("Please wait \(seconds) seconds before trying again.")
}
}Checklist
- [ ] 429 status code handled
- [ ] Retry-After header respected
- [ ] Exponential backoff implemented
- [ ] User informed when rate limited
- [ ] Request queuing for high-volume operations
Timeout Configuration
Guidelines
- Short timeouts for user-initiated actions (10-30 seconds)
- Longer timeouts for background operations (60-120 seconds)
- Very short timeouts for connectivity checks (5 seconds)
// User-initiated request
var request = URLRequest(url: url)
request.timeoutInterval = 30
// Background sync
let config = URLSessionConfiguration.background(withIdentifier: "sync")
config.timeoutIntervalForRequest = 60
config.timeoutIntervalForResource = 300
// Connectivity check
var pingRequest = URLRequest(url: healthCheckURL)
pingRequest.timeoutInterval = 5Checklist
- [ ] Appropriate timeouts for each request type
- [ ] Timeout errors show user-friendly message
- [ ] Long operations use background session
- [ ] No infinite timeouts
Offline Handling
Network Reachability
import Network
class NetworkMonitor: ObservableObject {
private let monitor = NWPathMonitor()
@Published var isConnected = true
init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
}
}
monitor.start(queue: DispatchQueue.global())
}
}Graceful Degradation
func fetchData() async throws -> [Item] {
if !networkMonitor.isConnected {
// Return cached data when offline
if let cached = cache.loadItems() {
return cached
}
throw APIError.networkUnavailable
}
// Fetch fresh data
let items = try await api.fetchItems()
cache.saveItems(items)
return items
}Checklist
- [ ] Network connectivity monitored
- [ ] Offline state shown to user
- [ ] Cached data available offline (if applicable)
- [ ] Auto-retry when connection restored
- [ ] No silent failures when offline
Request/Response Logging
Debug Logging (Development Only)
#if DEBUG
func logRequest(_ request: URLRequest) {
print("📤 \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
}
func logResponse(_ response: HTTPURLResponse, data: Data) {
print("📥 \(response.statusCode) (\(data.count) bytes)")
}
#endif❌ Anti-patterns
// NEVER log sensitive data
print("Token: \(apiToken)") // Security risk!
print("Request body: \(String(data: body, encoding: .utf8))") // May contain PII
// NEVER log in production
func makeRequest() {
print("Making request...") // Should be #if DEBUG
}Checklist
- [ ] Request/response logging only in DEBUG
- [ ] No sensitive data in logs (tokens, passwords, PII)
- [ ] No production logging of request bodies
- [ ] Structured logging for debugging
Caching Strategy
HTTP Caching
// Respect cache headers
let config = URLSessionConfiguration.default
config.requestCachePolicy = .useProtocolCachePolicy
// Custom cache for specific needs
let cache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB
diskCapacity: 50 * 1024 * 1024, // 50 MB
diskPath: "api_cache"
)
config.urlCache = cacheApplication-Level Caching
actor APICache {
private var cache: [String: (data: Data, timestamp: Date)] = [:]
private let maxAge: TimeInterval = 300 // 5 minutes
func get(_ key: String) -> Data? {
guard let entry = cache[key] else { return nil }
if Date().timeIntervalSince(entry.timestamp) > maxAge {
cache.removeValue(forKey: key)
return nil
}
return entry.data
}
func set(_ key: String, data: Data) {
cache[key] = (data, Date())
}
}Checklist
- [ ] Caching strategy defined
- [ ] Cache invalidation implemented
- [ ] Stale data handling defined
- [ ] Cache size limits set
API Versioning
Handling API Changes
struct APIClient {
static let apiVersion = "v1"
static let baseURL = "https://api.example.com/\(apiVersion)"
// Check minimum supported version
func checkAPICompatibility() async throws {
let serverVersion = try await fetchServerVersion()
if serverVersion < minimumSupportedVersion {
throw APIError.updateRequired("Please update the app to continue using this feature.")
}
}
}Checklist
- [ ] API version included in requests
- [ ] Graceful handling of deprecated endpoints
- [ ] User prompted to update when API incompatible
- [ ] Fallback behavior for missing features
Search Patterns
// Find potential API issues
Grep: "URLRequest|URLSession"
Grep: "User-Agent|userAgent"
Grep: "401|403|429|500"
Grep: "timeout|Timeout"
Grep: "print.*request|print.*response|NSLog.*API"References
Distribution Checklist
Release and distribution review for macOS and iOS applications.
Bundle Configuration
Bundle Identifier
✅ Good Pattern
com.companyname.appname
com.yourname.AppName❌ Anti-patterns
// Generic or placeholder identifiers
com.example.app
org.cocoapods.demo
com.apple.product-type.application
// Missing reverse-domain format
MyApp
app.mycompanyChecklist
- [ ] Bundle identifier uses reverse-domain format
- [ ] Bundle identifier matches App Store Connect (if applicable)
- [ ] Bundle identifier is unique (not copied from template)
- [ ] Team ID configured correctly
Search Pattern
Grep in project.pbxproj: "PRODUCT_BUNDLE_IDENTIFIER"Version Numbers
Guidelines
- Version (CFBundleShortVersionString): User-facing, semantic versioning (1.0.0)
- Build (CFBundleVersion): Internal, incremented each build
✅ Good Pattern
<key>CFBundleShortVersionString</key>
<string>1.2.0</string>
<key>CFBundleVersion</key>
<string>42</string>Checklist
- [ ] Version number follows semantic versioning
- [ ] Build number increments with each release
- [ ] Version matches marketing expectations
- [ ] Build number is higher than previous releases
Info.plist
Required Keys (Universal)
<!-- Always required -->
<key>CFBundleDisplayName</key>
<string>App Name</string>
<key>CFBundleIdentifier</key>
<string>com.company.app</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>Permission Usage Descriptions
If your app requests permissions, these keys are required or App Store will reject:
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to scan QR codes.</string>
<!-- Photos -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo access to let you choose a profile picture.</string>
<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby stores.</string>
<!-- Microphone -->
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for voice messages.</string>
<!-- Contacts -->
<key>NSContactsUsageDescription</key>
<string>We need contacts access to help you invite friends.</string>
<!-- Calendars -->
<key>NSCalendarsUsageDescription</key>
<string>We need calendar access to add event reminders.</string>Checklist
- [ ] All required Info.plist keys present
- [ ] Usage descriptions for all requested permissions
- [ ] Usage descriptions are user-friendly (not technical)
- [ ] No placeholder text in descriptions
Code Signing
macOS
Developer ID (Direct Distribution)
CODE_SIGN_IDENTITY = "Developer ID Application: Your Name (TEAM_ID)"App Store
CODE_SIGN_IDENTITY = "Apple Distribution: Your Name (TEAM_ID)"iOS
Development
CODE_SIGN_IDENTITY = "iPhone Developer"Distribution
CODE_SIGN_IDENTITY = "iPhone Distribution"Checklist
- [ ] Signing certificate valid and not expired
- [ ] Provisioning profile matches bundle ID
- [ ] Provisioning profile includes required capabilities
- [ ] Team selected in Xcode project
Search Pattern
Grep in project.pbxproj: "CODE_SIGN_IDENTITY|PROVISIONING_PROFILE"App Icons
macOS Requirements
| Size | Scale | Filename |
|---|---|---|
| 16x16 | 1x, 2x | icon_16x16.png, icon_16x16@2x.png |
| 32x32 | 1x, 2x | icon_32x32.png, icon_32x32@2x.png |
| 128x128 | 1x, 2x | icon_128x128.png, icon_128x128@2x.png |
| 256x256 | 1x, 2x | icon_256x256.png, icon_256x256@2x.png |
| 512x512 | 1x, 2x | icon_512x512.png, icon_512x512@2x.png |
iOS Requirements
| Size | Usage |
|---|---|
| 1024x1024 | App Store |
| 180x180 | iPhone @3x |
| 120x120 | iPhone @2x |
| 167x167 | iPad Pro @2x |
| 152x152 | iPad @2x |
Checklist
- [ ] All required icon sizes present
- [ ] Icons are square with no transparency (iOS)
- [ ] Icons match brand guidelines
- [ ] No placeholder or default icons
Platform-Specific: macOS
Notarization
Required for apps distributed outside App Store (macOS 10.15+).
Prerequisites
- [ ] Hardened Runtime enabled
- [ ] Developer ID certificate
- [ ] App-specific password for notarytool
Notarization Script
#!/bin/bash
APP_PATH="$1"
BUNDLE_ID="com.company.app"
APPLE_ID="developer@email.com"
TEAM_ID="XXXXXXXXXX"
# Create ZIP
ditto -c -k --keepParent "$APP_PATH" "app.zip"
# Submit for notarization
xcrun notarytool submit "app.zip" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "@keychain:AC_PASSWORD" \
--wait
# Staple ticket
xcrun stapler staple "$APP_PATH"Checklist
- [ ] Hardened Runtime enabled
- [ ] All binaries signed with Developer ID
- [ ] App submitted to notarization service
- [ ] Notarization ticket stapled to app
- [ ] Gatekeeper accepts app (
spctl -a -v App.app)
DMG Creation
For professional distribution:
# Create DMG with Applications symlink
hdiutil create -volname "AppName" \
-srcfolder build/ \
-ov -format UDZO \
AppName.dmgChecklist
- [ ] DMG contains app and Applications alias
- [ ] DMG is signed and notarized
- [ ] DMG opens cleanly with drag-to-install
- [ ] Background image (optional but professional)
Sandbox Entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<!-- Required for sandboxing -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Network access -->
<key>com.apple.security.network.client</key>
<true/>
<!-- User-selected files -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>Checklist
- [ ] Only necessary entitlements requested
- [ ] Each entitlement justified
- [ ] Temporary exceptions have migration plan
Platform-Specific: iOS
App Store Connect
Required Information
- [ ] App name (30 characters max)
- [ ] Subtitle (30 characters max)
- [ ] Privacy Policy URL
- [ ] Support URL
- [ ] Marketing URL (optional)
- [ ] App category selected
- [ ] Age rating questionnaire completed
Screenshots
| Device | Size | Required |
|---|---|---|
| iPhone 6.7" | 1290 x 2796 | Yes |
| iPhone 6.5" | 1242 x 2688 | Yes |
| iPhone 5.5" | 1242 x 2208 | Optional |
| iPad Pro 12.9" | 2048 x 2732 | If universal |
Checklist
- [ ] All required screenshots uploaded
- [ ] Screenshots show actual app (not mockups)
- [ ] App preview video (optional but recommended)
- [ ] Promotional text (170 characters)
- [ ] Description (4000 characters max)
- [ ] Keywords (100 characters max)
TestFlight
Checklist
- [ ] Beta App Description provided
- [ ] What to Test notes included
- [ ] Test Information contact email
- [ ] Beta App Review Information (if using restricted features)
- [ ] External testers invited (if needed)
Export Compliance
If your app uses encryption:
<key>ITSAppUsesNonExemptEncryption</key>
<false/> <!-- or true if using custom encryption -->Checklist
- [ ] Export compliance question answered in App Store Connect
- [ ] ITSAppUsesNonExemptEncryption key set correctly
- [ ] Encryption registration filed (if required)
In-App Purchases (if applicable)
Checklist
- [ ] Products created in App Store Connect
- [ ] Products submitted for review
- [ ] Restore purchases implemented
- [ ] Receipt validation implemented
- [ ] Sandbox testing completed
Launch Preparation
Final Checklist
Universal
- [ ] All debug code removed
- [ ] No test/placeholder content
- [ ] Crash reporting configured
- [ ] Analytics configured (if any)
- [ ] Version/build numbers correct
macOS
- [ ] App launches correctly
- [ ] Menu bar items work
- [ ] Preferences accessible (Cmd+,)
- [ ] About window shows correct info
- [ ] App quits cleanly (Cmd+Q)
iOS
- [ ] App launches on all supported devices
- [ ] Launch screen matches initial UI
- [ ] All orientations work (if supported)
- [ ] Background modes work correctly
- [ ] Push notifications work (if applicable)
References
Privacy Checklist
Privacy and data handling review for macOS and iOS applications.
Data Collection Transparency
What to Document
Users should understand: 1. What data is collected 2. Why it's collected 3. Where it's stored (local vs. cloud) 4. Who has access (first-party only vs. third parties) 5. How long it's retained
In-App Transparency
✅ Good Pattern
// Privacy section in Settings
Section("Privacy & Data") {
DisclosureGroup {
VStack(alignment: .leading, spacing: 12) {
Label("What we access", systemImage: "folder")
Text("Local session files for usage calculation")
.font(.caption)
Label("What we send", systemImage: "arrow.up.circle")
Text("Only your token to fetch YOUR quota data")
.font(.caption)
Label("What stays local", systemImage: "lock.shield")
Text("All calculations happen on your Mac")
.font(.caption)
}
} label: {
Label("What data does this app access?", systemImage: "hand.raised")
}
}Checklist
- [ ] App explains what data it collects (in Settings or onboarding)
- [ ] Data usage is justified and proportionate
- [ ] Users can see what data is stored
- [ ] Clear distinction between local and cloud data
Privacy Manifest (iOS 17+)
Required APIs
If your app uses these APIs, you need a Privacy Manifest:
<!-- PrivacyInfo.xcprivacy -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string> <!-- App functionality -->
</array>
</dict>
</array>APIs Requiring Declaration
- UserDefaults
- File timestamp APIs
- System boot time APIs
- Disk space APIs
- Active keyboard APIs
Checklist
- [ ] Privacy Manifest created if using required APIs
- [ ] All required API reasons documented
- [ ] Third-party SDK privacy manifests included
User Consent
Permission Requests
✅ Good Patterns
// Request permission with context
Button("Enable Notifications") {
// User initiated - good UX
requestNotificationPermission()
}
// Explain before requesting
Alert("Location Access",
message: "We use your location to show nearby stores. Your location is never shared.")❌ Anti-patterns
// Don't request on launch without context
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) {
// BAD: Immediate permission request
CLLocationManager().requestWhenInUseAuthorization()
}Checklist
- [ ] Permissions requested in context (not on first launch)
- [ ] Clear explanation before each permission request
- [ ] App works (with reduced functionality) if permission denied
- [ ] No repeated permission requests after denial
Data Retention & Deletion
User Control
✅ Good Pattern
// Clear data option in Settings
Button("Delete All Data", role: .destructive) {
DataManager.shared.deleteAllData()
// Also clear Keychain if applicable
KeychainHelper.clearCredentials()
}
// Export before delete option
Button("Export My Data") {
ExportManager.exportUserData()
}Checklist
- [ ] Users can delete their data
- [ ] Data deletion is complete (including Keychain, caches)
- [ ] Users can export their data
- [ ] Uninstall removes all user data (document if not)
Third-Party SDKs
Audit Checklist
- [ ] List all third-party SDKs used
- [ ] Understand what data each SDK collects
- [ ] SDK privacy policies reviewed
- [ ] SDKs included in privacy manifest (iOS)
- [ ] No unnecessary analytics SDKs
Common SDKs to Review
- Analytics (Firebase, Mixpanel, Amplitude)
- Crash reporting (Crashlytics, Sentry)
- Advertising (AdMob, Facebook)
- Social login (Sign in with Apple, Google, Facebook)
Platform-Specific Privacy
macOS
Checklist
- [ ] App doesn't access files outside sandbox without permission
- [ ] Keychain access explained to users
- [ ] No silent background data collection
- [ ] Menu bar apps explain their persistent presence
Keychain Access Dialog
When accessing another app's Keychain item:
// Users will see: "[App] wants to access [Item] in your keychain"
// Prepare users in onboarding:
Text("On first launch, macOS will ask to access credentials from your Keychain. Click 'Always Allow' for permanent access.")iOS
App Tracking Transparency (ATT)
// Required if tracking users across apps
import AppTrackingTransparency
ATTrackingManager.requestTrackingAuthorization { status in
// Handle response
}Privacy Nutrition Labels
Checklist for App Store Connect:
- [ ] Data types collected documented
- [ ] Data linked to user identity marked
- [ ] Data used for tracking marked
- [ ] Third-party data collection included
GDPR Compliance Basics
Checklist
- [ ] Privacy policy accessible in app
- [ ] Consent obtained before data collection
- [ ] Users can access their data
- [ ] Users can request data deletion
- [ ] Data processing purposes documented
Privacy Policy Link
Link("Privacy Policy", destination: URL(string: "https://yourapp.com/privacy")!)Search Patterns
// Find potential privacy issues
Grep: "CLLocationManager|locationManager"
Grep: "ATTrackingManager|advertisingIdentifier"
Grep: "UNUserNotificationCenter|requestAuthorization"
Grep: "PHPhotoLibrary|AVCaptureDevice"
Grep: "CNContactStore|EventKit"References
Security Checklist
Comprehensive security review for macOS and iOS applications.
Credential Storage
Keychain Usage
✅ Good Patterns
// Store sensitive data in Keychain
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.app.credentials",
kSecAttrAccount as String: account,
kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)❌ Anti-patterns
// Never store credentials in UserDefaults
UserDefaults.standard.set(apiKey, forKey: "apiKey")
// Never hardcode secrets
let apiKey = "sk-ant-api03-xxxxx"
// Never store in plain text files
try apiKey.write(to: credentialsFile, atomically: true, encoding: .utf8)Checklist
- [ ] API keys stored in Keychain, not UserDefaults
- [ ] No hardcoded secrets in source code
- [ ] No secrets in Info.plist
- [ ] Keychain items have appropriate access controls
- [ ] Credentials cleared on logout/sign-out
Search Patterns
Grep: "UserDefaults.*password|UserDefaults.*token|UserDefaults.*key|UserDefaults.*secret"
Grep: "sk-ant-|api-key-|Bearer [A-Za-z0-9]"
Grep: "hardcoded|TODO.*key|FIXME.*secret"Data Transmission
Network Security
✅ Good Patterns
// Use HTTPS
let url = URL(string: "https://api.example.com")
// Validate SSL certificates (default behavior)
let session = URLSession.shared
// Set appropriate timeouts
var request = URLRequest(url: url)
request.timeoutInterval = 30❌ Anti-patterns
// Never disable SSL validation in production
class InsecureDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// DANGEROUS: Accepts any certificate
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
// Never use HTTP for sensitive data
let url = URL(string: "http://api.example.com/login")Checklist
- [ ] All API calls use HTTPS
- [ ] No disabled SSL certificate validation
- [ ] Sensitive data not logged to console
- [ ] Request/response bodies not logged in production
- [ ] Appropriate request timeouts set
Search Patterns
Grep: "http://" (excluding localhost/127.0.0.1)
Grep: "URLAuthenticationChallenge|serverTrust"
Grep: "print.*token|print.*password|NSLog.*credential"Input Validation
User Input
✅ Good Patterns
// Validate and sanitize user input
func validateEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email)
}
// Use parameterized queries for database
let query = "SELECT * FROM users WHERE id = ?"❌ Anti-patterns
// String interpolation in queries = SQL injection risk
let query = "SELECT * FROM users WHERE name = '\(userInput)'"
// Unsanitized HTML rendering = XSS risk
webView.loadHTMLString("<div>\(userInput)</div>", baseURL: nil)Checklist
- [ ] User input validated before use
- [ ] No string interpolation in database queries
- [ ] HTML content sanitized before rendering
- [ ] File paths validated (no path traversal)
- [ ] URL schemes validated
Entitlements & Sandboxing
macOS Sandbox
Checklist
- [ ] App is sandboxed (unless justified exception)
- [ ] Only necessary entitlements requested
- [ ] Sandbox exceptions documented and justified
- [ ] Temporary exception entitlements have migration plan
Common Entitlements to Review
<!-- Justify each of these -->
<key>com.apple.security.files.user-selected.read-write</key>
<key>com.apple.security.files.downloads.read-write</key>
<key>com.apple.security.network.client</key>
<key>com.apple.security.network.server</key>iOS Capabilities
Checklist
- [ ] Only necessary capabilities enabled
- [ ] Background modes justified
- [ ] Push notification entitlement if using push
- [ ] Keychain sharing groups appropriate
Hardened Runtime (macOS)
Checklist
- [ ] Hardened Runtime enabled for notarization
- [ ] Runtime exceptions minimized
- [ ] JIT exceptions only if absolutely necessary
- [ ] Unsigned executable memory exceptions justified
Search in project.pbxproj
Grep: "ENABLE_HARDENED_RUNTIME"
Grep: "CODE_SIGN_INJECT_BASE_ENTITLEMENTS"Platform-Specific Security
macOS
- [ ] Keychain access properly scoped
- [ ] App Groups used appropriately
- [ ] XPC services secured
- [ ] Helper tools signed and validated
iOS
- [ ] Keychain access groups configured
- [ ] App Transport Security not disabled globally
- [ ] Jailbreak detection considered (if needed)
- [ ] Data Protection class appropriate for sensitive data
Common Vulnerabilities
Checklist
- [ ] No force unwrapping of security-critical optionals
- [ ] Cryptographic operations use Security framework
- [ ] Random numbers use SecRandomCopyBytes for security
- [ ] Sensitive data cleared from memory when done
- [ ] Debug code removed from release builds
Search Patterns
Grep: "print.*debug|#if DEBUG.*secret"
Grep: "arc4random|rand\(\)" (should use SecRandomCopyBytes)References
UX Polish Checklist
User experience review for macOS and iOS applications.
First Launch / Onboarding
What Good Onboarding Includes
1. Value proposition - What does the app do? 2. Key features - 3-4 main capabilities 3. Requirements - What's needed to use the app 4. Permissions context - Why permissions are needed 5. Get started - Clear call to action
✅ Good Pattern
struct WelcomeView: View {
var body: some View {
VStack(spacing: 24) {
// App icon and title
Image(systemName: "app.icon")
Text("Welcome to AppName")
.font(.largeTitle)
// Features
FeatureRow(icon: "star", title: "Feature 1", description: "...")
FeatureRow(icon: "heart", title: "Feature 2", description: "...")
// Requirements
RequirementsSection()
// CTA
Button("Get Started") { ... }
}
}
}Checklist
- [ ] Onboarding shown on first launch
- [ ] Clear value proposition
- [ ] Features explained concisely
- [ ] Requirements/prerequisites listed
- [ ] Permission requests explained before shown
- [ ] Skip option available (if appropriate)
- [ ] Onboarding state persisted
Empty States
Types of Empty States
1. First use - No data yet 2. No results - Search/filter returned nothing 3. Error - Something went wrong 4. Cleared - User deleted data
✅ Good Pattern
if items.isEmpty {
ContentUnavailableView {
Label("No Projects Yet", systemImage: "folder")
} description: {
Text("Start using Claude Code to see your projects here.")
} actions: {
Button("Learn More") { ... }
}
}❌ Anti-pattern
// Empty view with no guidance
if items.isEmpty {
Text("No data")
}Checklist
- [ ] All lists have empty states
- [ ] Empty states explain why it's empty
- [ ] Empty states provide actionable guidance
- [ ] Search empty states differentiate from "no data"
Error States
Error Message Guidelines
1. What happened - Clear description 2. Why it happened - If known 3. What to do - Actionable next step
✅ Good Pattern
enum AppError: LocalizedError {
case tokenExpired
case networkUnavailable
var errorDescription: String? {
switch self {
case .tokenExpired:
return "Your session has expired. Please restart the app to refresh."
case .networkUnavailable:
return "Unable to connect. Check your internet connection and try again."
}
}
}❌ Anti-pattern
// Unhelpful error messages
case .error:
return "An error occurred"
// Technical jargon
case .httpError(let code):
return "HTTP \(code)"Checklist
- [ ] All errors have user-friendly messages
- [ ] Error messages explain what to do
- [ ] No technical jargon in user-facing errors
- [ ] Errors are recoverable where possible
- [ ] Retry options provided where appropriate
Loading States
Guidelines
✅ Good Pattern
// Show loading indicator
if isLoading {
ProgressView("Loading projects...")
}
// Skeleton loading for lists
ForEach(0..<5) { _ in
SkeletonRow()
.redacted(reason: .placeholder)
}Checklist
- [ ] Loading states shown for async operations
- [ ] Loading indicators have context (what's loading)
- [ ] Long operations show progress (not just spinner)
- [ ] Loading doesn't block entire UI unnecessarily
Text & Accessibility
Text Truncation
✅ Good Pattern
Text(longText)
.lineLimit(2)
.help(longText) // Tooltip shows full text
// Or allow selection
Text(longText)
.textSelection(.enabled)❌ Anti-pattern
// Truncated with no way to see full text
Text(longText)
.lineLimit(1)
// No help or selectionChecklist
- [ ] Truncated text has tooltip (
.help()) or expansion - [ ] Dynamic Type supported (iOS)
- [ ] VoiceOver labels for all interactive elements
- [ ] Sufficient color contrast
- [ ] No information conveyed by color alone
Search Patterns
Grep: "lineLimit.*[^help]" (truncation without tooltip)
Grep: "accessibilityLabel|accessibilityHint"Dark Mode
Checklist
- [ ] App supports dark mode
- [ ] Uses system colors (not hardcoded)
- [ ] Images/icons adapt to color scheme
- [ ] No readability issues in either mode
✅ Good Pattern
// Use system colors
Color(nsColor: .controlBackgroundColor)
Color(nsColor: .textColor)
Color.primary
Color.secondary❌ Anti-pattern
// Hardcoded colors
Color.white
Color(red: 0.2, green: 0.2, blue: 0.2)Platform-Specific UX
macOS
Menu Bar Apps
// Window activation - activate BEFORE dismissing menu
Button("Open Dashboard") {
openWindow(id: "dashboard")
NSApp.activate(ignoringOtherApps: true) // First
dismiss() // Then dismiss
}Keyboard Navigation
- [ ] Tab navigation works
- [ ] Keyboard shortcuts for common actions
- [ ] Focus rings visible
- [ ] Escape closes modals/popovers
Checklist
- [ ] Windows activate properly from menu bar
- [ ] Keyboard navigation complete
- [ ] Standard shortcuts work (Cmd+W, Cmd+Q, etc.)
- [ ] Preferences accessible via Cmd+,
iOS
Launch Screen
- [ ] Launch screen matches initial UI
- [ ] No jarring transition from launch to app
Orientation
- [ ] Supported orientations declared
- [ ] UI adapts to orientation changes
- [ ] iPad multitasking supported (if applicable)
Safe Areas
- [ ] Content respects safe areas
- [ ] No content under notch/Dynamic Island
- [ ] Home indicator area respected
Interactive Feedback
Checklist
- [ ] Buttons show pressed state
- [ ] Destructive actions require confirmation
- [ ] Success/failure feedback provided
- [ ] Animations are subtle and purposeful
✅ Good Pattern
Button("Delete", role: .destructive) { ... }
.confirmationDialog("Delete this item?", isPresented: $showConfirm) {
Button("Delete", role: .destructive) { delete() }
Button("Cancel", role: .cancel) { }
}Performance UX
Checklist
- [ ] App launches quickly (< 2 seconds)
- [ ] UI remains responsive during background work
- [ ] No spinning beach ball (macOS)
- [ ] Scrolling is smooth (60fps)
- [ ] Memory warnings handled gracefully
References
Related skills
How it compares
Pick this when you need release notes and a human-run checklist rather than only CI automation.
FAQ
What should be in App Store review notes?
release-review structures App Store review notes around what changed, how to test key flows, and any special reviewer instructions. release-review is designed to produce reviewer-friendly notes that pair with a regression checklist so you can validate behavior before submitting i
How do I run a pre-submit regression pass?
release-review outputs a regression checklist that a developer can execute on a release candidate build, typically a TestFlight or signed release build. release-review emphasizes validating critical user flows, permissions prompts, purchase flows if present, and crash-free startu