Building a Brewery Keg Tracker iOS App - Part 1: QR Scanner & Basic Inventory
Fri Dec 27 2024
Learn Swift while building a practical brewery keg tracking app. Part 1 covers creating a basic QR code scanner and inventory list.
Building a Brewery Keg Tracker iOS App - Part 1
In this tutorial series, we'll build a brewery keg tracking app from scratch while learning Swift and iOS development. By the end, you'll have an app that can scan QR codes, track kegs, and eventually use LiDAR for 3D mapping.
What We're Building in Part 1
A simple two-tab iOS app:
- Scanner Tab: QR code scanner to add kegs
- Inventory Tab: List view of all scanned kegs
Prerequisites
- Mac with Xcode installed
- iPhone (for testing camera features)
- Basic programming knowledge (any language)
Step 1: Create New Xcode Project
- Open Xcode
- Create new project → iOS → App
- Product Name:
KegTracker - organization Identifier:
yourname - Interface: SwiftUI
- Language: Swift
Step 2: Project Structure
We'll create these files:
KegTracker/
├── ContentView.swift (main tab view)
├── ScannerView.swift (QR scanner)
├── InventoryView.swift (keg list)
├── KegModel.swift (data model)
└── Info.plist (camera permissions)Step 3: Data Model
First, let's define what a keg looks like in our app:
// KegModel.swift
import Foundation
struct Keg: Identifiable, Codable {
let id = UUID()
let qrCode: String
let beerName: String
let beerType: String
let scanDate: Date
var location: String = "Unassigned"
var formattedDate: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: scanDate)
}
}
class KegStore: ObservableObject {
@Published var kegs: [Keg] = []
func addKeg(qrCode: String, beerName: String, beerType: String) {
let newKeg = Keg(
qrCode: qrCode,
beerName: beerName,
beerType: beerType,
scanDate: Date()
)
kegs.append(newKeg)
}
}Step 4: Main Tab View
Replace ContentView.swift with our tab structure:
// ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var kegStore = KegStore()
var body: some View {
TabView {
ScannerView()
.tabItem {
Image(systemName: "qrcode.viewfinder")
Text("Scanner")
}
.environmentObject(kegStore)
InventoryView()
.tabItem {
Image(systemName: "list.bullet")
Text("Inventory")
}
.environmentObject(kegStore)
}
}
}Step 5: QR Scanner View
Create the scanner interface:
// ScannerView.swift
import SwiftUI
import AVFoundation
struct ScannerView: View {
@EnvironmentObject var kegStore: KegStore
@State private var showingAddKeg = false
@State private var scannedCode = ""
var body: some View {
NavigationView {
VStack {
Text("Point camera at QR code")
.font(.title2)
.padding()
// QR Scanner will go here
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(width: 300, height: 300)
.overlay(
Text("Camera View\n(Coming next)")
.multilineTextAlignment(.center)
)
Button("Simulate Scan") {
scannedCode = "QR-\(Int.random(in: 1000...9999))"
showingAddKeg = true
}
.buttonStyle(.borderedProminent)
.padding()
Spacer()
}
.navigationTitle("Scanner")
.sheet(isPresented: $showingAddKeg) {
AddKegView(qrCode: scannedCode)
.environmentObject(kegStore)
}
}
}
}
struct AddKegView: View {
@EnvironmentObject var kegStore: KegStore
@Environment(\.dismiss) private var dismiss
let qrCode: String
@State private var beerName = ""
@State private var beerType = ""
let beerTypes = ["IPA", "Lager", "Stout", "Pilsner", "Wheat", "Sour", "Porter"]
var body: some View {
NavigationView {
Form {
Section("QR Code") {
Text(qrCode)
.foregroundColor(.secondary)
}
Section("Beer Details") {
TextField("Beer Name", text: $beerName)
Picker("Beer Type", selection: $beerType) {
ForEach(beerTypes, id: \.self) { type in
Text(type).tag(type)
}
}
}
}
.navigationTitle("Add Keg")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { dismiss() }
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
kegStore.addKeg(
qrCode: qrCode,
beerName: beerName,
beerType: beerType.isEmpty ? "Unknown" : beerType
)
dismiss()
}
.disabled(beerName.isEmpty)
}
}
}
}
}Step 6: Inventory View
Create the list to display kegs:
// InventoryView.swift
import SwiftUI
struct InventoryView: View {
@EnvironmentObject var kegStore: KegStore
var body: some View {
NavigationView {
List {
ForEach(kegStore.kegs) { keg in
KegRowView(keg: keg)
}
}
.navigationTitle("Inventory")
.overlay {
if kegStore.kegs.isEmpty {
ContentUnavailableView(
"No Kegs",
systemImage: "qrcode",
description: Text("Scan QR codes to add kegs to your inventory")
)
}
}
}
}
}
struct KegRowView: View {
let keg: Keg
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(keg.beerName)
.font(.headline)
Spacer()
Text(keg.beerType)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.2))
.cornerRadius(4)
}
Text("QR: \(keg.qrCode)")
.font(.caption)
.foregroundColor(.secondary)
Text("Scanned: \(keg.formattedDate)")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 2)
}
}Step 7: Camera Permissions
Add camera permission to Info.plist:
- Open Info.plist
- Add new entry:
Privacy - Camera Usage Description - Value:
"This app needs camera access to scan QR codes on kegs"
Testing Your App
- Build and run on device (camera won't work in simulator)
- Use "Simulate Scan" button to test the flow
- Add a few test kegs and see them in inventory
What's Next
In Part 2, we'll add:
- Real QR code scanning with camera
- Search and filter functionality
- Basic location assignment
Key Learning Points
- SwiftUI basics: Views, state management, navigation
- Data modeling: Structs, ObservableObject
- Tab navigation: TabView and environmentObject
- Forms and sheets: User input and modal presentation
Ready for Part 2? We'll make that QR scanner actually work with the camera!