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.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Beginner Estimated time: 1–2 weekends Updated: May 12, 2026

Prerequisites

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

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.