How to Build a QR Code Generator App in SwiftUI
A QR Code Generator lets users turn any URL, contact card, or plain text into a styled, scannable code they can save and share instantly. It's a focused first App Store project with immediate visual feedback and a natural one-time-purchase upsell for color customization.
Prerequisites
- Mac with Xcode 16+
- Apple Developer Program ($99/year) — required for TestFlight and App Store distribution
- Basic Swift/SwiftUI knowledge — familiarity with
@StateandVStackis enough - A real device is needed to test saving to the Photos library; the simulator blocks PHPhotoLibrary writes
Architecture overview
The data layer uses SwiftData to persist each generated QR code alongside its hex colors and a user-supplied label. A standalone QRCodeGenerator enum wraps CoreImage's filter pipeline so views stay free of image-processing logic. PhotosUI's ImageTransferable protocol handles saving the final UIImage to the camera roll. State is lightweight @State inside views — no view model needed at this complexity level.
QRCodeGenerator/ ├── Models/ │ └── QRCodeItem.swift # SwiftData @Model ├── Views/ │ ├── ContentView.swift # Tab / nav root │ ├── GeneratorView.swift # Input + live preview │ └── HistoryView.swift # Saved codes list ├── Helpers/ │ └── QRCodeGenerator.swift # CoreImage pipeline └── PrivacyInfo.xcprivacy # Required for App Store
Step-by-step
1. Data model
Define a SwiftData @Model class so each QR code — text, colors, and label — survives app restarts without any manual persistence code.
import SwiftData
import Foundation
@Model
final class QRCodeItem {
var id: UUID
var inputText: String
var label: String
var foregroundHex: String
var backgroundHex: String
var createdAt: Date
init(
inputText: String,
label: String = "",
foregroundHex: String = "#000000",
backgroundHex: String = "#FFFFFF"
) {
self.id = UUID()
self.inputText = inputText
self.label = label
self.foregroundHex = foregroundHex
self.backgroundHex = backgroundHex
self.createdAt = Date()
}
}
2. Core UI
Wire up the text input, color pickers, and live QR preview in one focused view — the Generate button stays disabled until there's real input.
import SwiftUI
struct GeneratorView: View {
@State private var inputText = ""
@State private var fgColor: Color = .black
@State private var bgColor: Color = .white
@State private var qrImage: UIImage?
var body: some View {
VStack(spacing: 20) {
TextField("URL, text, or contact…", text: $inputText)
.textFieldStyle(.roundedBorder)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
HStack(spacing: 24) {
ColorPicker("Foreground", selection: $fgColor)
ColorPicker("Background", selection: $bgColor)
}
if let image = qrImage {
Image(uiImage: image)
.interpolation(.none)
.resizable().scaledToFit()
.frame(maxWidth: 220)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 8)
}
Button("Generate QR Code") {
qrImage = QRCodeGenerator.make(
text: inputText,
foreground: UIColor(fgColor),
background: UIColor(bgColor))
}
.buttonStyle(.borderedProminent)
.disabled(inputText.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding()
.navigationTitle("QR Generator")
}
}
3. Custom QR code styling
Chain CoreImage's qrCodeGenerator and falseColor filters, then scale the output up so it renders crisp at any display size.
import CoreImage
import CoreImage.CIFilterBuiltins
import UIKit
enum QRCodeGenerator {
static func make(
text: String,
foreground: UIColor,
background: UIColor,
scale: CGFloat = 12
) -> UIImage? {
let ctx = CIContext()
let qr = CIFilter.qrCodeGenerator()
qr.message = Data(text.utf8)
qr.correctionLevel = "M"
guard let raw = qr.outputImage else { return nil }
// color0 = dark modules, color1 = light modules
let colorize = CIFilter.falseColor()
colorize.inputImage = raw
colorize.color0 = CIColor(color: foreground)
colorize.color1 = CIColor(color: background)
guard let colored = colorize.outputImage else { return nil }
let scaled = colored.transformed(
by: CGAffineTransform(scaleX: scale, y: scale))
guard let cg = ctx.createCGImage(scaled, from: scaled.extent) else {
return nil
}
return UIImage(cgImage: cg)
}
}
4. Privacy Manifest
Add PrivacyInfo.xcprivacy to your app target — App Store Connect has rejected builds missing this file since May 2024.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key><false/>
<key>NSPrivacyTrackingDomains</key><array/>
<key>NSPrivacyCollectedDataTypes</key><array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>C617.1</string></array>
</dict>
</array>
</dict>
</plist>
Common pitfalls
- Blurry QR codes in the Image view: SwiftUI's default
.mediuminterpolation antialiases pixel-art images. Always set.interpolation(.none)on theImagedisplaying your QR output. - Inverted color mapping:
falseColormapscolor0to dark modules andcolor1to light. Swapping them produces a visually inverted QR that most scanners refuse to read. - Missing
NSPhotoLibraryAddUsageDescription: If you let users save to Photos, add this key toInfo.plist. Its absence causes a silent denial — no error alert, no save, confused users. - App Store review — low-contrast codes: Reviewers have flagged apps where color pickers allow near-identical hues. Add a contrast ratio guard (WCAG 3:1 minimum) before enabling the Generate button.
- Empty-input QR codes: CoreImage generates a valid QR for an empty string. Trim whitespace and disable the button so users cannot generate and share a blank code.
Adding monetization: One-time purchase
Use StoreKit 2's Product.purchase() API to gate the color pickers and save-to-Photos behind a non-consumable IAP (e.g. com.yourapp.pro_unlock). Load the product at launch with Product.products(for:), present a paywall when the user taps a locked control, and persist the unlocked state after a .verified transaction. Re-verify on each launch with Transaction.currentEntitlement(for:) to stay receipt-honest. Keep core QR generation free so users experience the value before seeing the paywall — conversion rates drop sharply when the free tier is empty.
Shipping this faster with Soarias
Soarias scaffolds the full Xcode project from a one-line prompt — SwiftData model, CoreImage helper, tabbed navigation, StoreKit paywall stub, and PrivacyInfo.xcprivacy all pre-wired — then runs fastlane snapshot to capture App Store screenshots across every required device size. It also generates App Store Connect metadata (title, subtitle, description, keywords, privacy nutrition labels) and submits the build via the ASC API, cutting out the manual form-filling that eats a full afternoon on its own.
For a beginner app like this one, most developers spend their first weekend on Xcode project setup, provisioning profiles, and ASC metadata rather than actual features. Soarias collapses that overhead to under an hour — leaving your 1–2 weekends for the parts that differentiate your app: logo overlays, QR history, share sheets, or widget extensions.
Related guides
FAQ
Do I need a paid Apple Developer account?
Yes. A $99/year Apple Developer Program membership is required to distribute on TestFlight or the App Store. You can build and run on a personal device for free with a regular Apple ID, but submission requires the paid account.
How do I submit this to the App Store?
Archive the build in Xcode (Product → Archive), then upload via Xcode Organizer or fastlane deliver. Complete the App Store Connect listing — screenshots, privacy nutrition labels, and your PrivacyInfo.xcprivacy target membership. Submit for review; a simple utility app like this typically clears review within 24–48 hours.
Can I add a logo or icon to the center of the QR code?
Yes — use UIGraphicsImageRenderer to composite a logo UIImage over the generated QR code after the CoreImage pipeline. Set the correction level to "H" first (30% data recovery) so the code remains scannable even with up to 30% of its modules covered by the logo.
Last reviewed: 2026-05-12 by the Soarias team.