Building tools. Learning to build tools. Learning to build learning tools.
Before the app can stream anything, it needs to know where the printer is.
PrinterSettings.swift is the data model: 25 lines of Swift that store the
printer’s IP address and access code, persist them across app launches, and derive
the RTSPS URL from them. Small file, but it demonstrates three important iOS patterns in one place.
import Foundation
import SwiftUI
class PrinterSettings: ObservableObject {
@Published var printerIP: String {
didSet { UserDefaults.standard.set(printerIP, forKey: "printerIP") }
}
@Published var accessCode: String {
didSet { UserDefaults.standard.set(accessCode, forKey: "accessCode") }
}
var isConfigured: Bool {
!printerIP.isEmpty && !accessCode.isEmpty
}
var streamURL: URL? {
guard isConfigured else { return nil }
return URL(string: "rtsps://bblp:\(accessCode)@\(printerIP):322/streaming/live/1")
}
init() {
self.printerIP = UserDefaults.standard.string(forKey: "printerIP") ?? ""
self.accessCode = UserDefaults.standard.string(forKey: "accessCode") ?? ""
}
}
ObservableObject is a protocol that enables a class to participate in SwiftUI’s
reactive update system. When a property marked @Published changes, SwiftUI automatically
re-renders any views that are observing this object.
If you’ve written C# with WPF, this maps closely to:
| Swift / SwiftUI | C# / WPF |
|---|---|
class PrinterSettings: ObservableObject |
class PrinterSettings : INotifyPropertyChanged |
@Published var printerIP: String |
[ObservableProperty] private string _printerIP; (CommunityToolkit) |
| SwiftUI auto-subscribes when a view reads the property | Binding in XAML subscribes to PropertyChanged |
The @Published property wrapper synthesizes a Combine Publisher
behind the scenes. When the value changes, it sends a notification to SwiftUI’s
observation system, which schedules a re-render of any view that read the property.
ObservableObject requires a class (reference type), not a
struct. This is because multiple views need to share a single
instance and observe changes to it. If PrinterSettings were a struct,
each copy in each view would be independent, and changes in one view wouldn’t
propagate to others. The reference semantics of a class guarantee that
@StateObject in ContentView and
@ObservedObject in SettingsView point to the same object.
iOS apps are regularly terminated and relaunched. The printer IP and access code need to survive across sessions. UserDefaults is the right tool for small, user-level preferences — it’s essentially a persistent dictionary backed by a property list file in the app’s container.
The persistence is wired through Swift’s didSet property observer:
@Published var printerIP: String {
didSet { UserDefaults.standard.set(printerIP, forKey: "printerIP") }
}
didSet is a block that runs immediately after the property’s value
changes. Every time printerIP is assigned a new value, the new value is
written to UserDefaults. The init() reads it back on startup:
init() {
self.printerIP = UserDefaults.standard.string(forKey: "printerIP") ?? ""
self.accessCode = UserDefaults.standard.string(forKey: "accessCode") ?? ""
}
The ?? "" is the nil-coalescing operator — if no value has ever been
saved for that key, string(forKey:) returns nil, and we
substitute an empty string. This is equivalent to C#’s ?? operator:
Properties.Settings.Default.PrinterIP ?? string.Empty.
For a production app, storing a security credential like an access code in
UserDefaults is not best practice — the Keychain is the right place for
secrets. UserDefaults is unencrypted and accessible to anyone with a full device
backup. For a personal-use app on your own phone, the tradeoff is acceptable.
A future improvement would be to store accessCode using
SecItemAdd/SecItemCopyMatching in the Keychain, or
use the KeychainAccess
library as a friendlier wrapper.
Rather than storing a Bool for “is the user configured?” or
recomputing the stream URL in multiple places, these are computed properties
— properties with no storage that compute their value on demand:
var isConfigured: Bool {
!printerIP.isEmpty && !accessCode.isEmpty
}
var streamURL: URL? {
guard isConfigured else { return nil }
return URL(string: "rtsps://bblp:\(accessCode)@\(printerIP):322/streaming/live/1")
}
streamURL returns an Optional<URL> (written as URL?).
It returns nil when not configured, and a URL when it is.
Swift’s string interpolation (\(accessCode)) is equivalent to C#’s
interpolated strings ($"{accessCode}").
Notice that streamURL is not @Published. It doesn’t need
to be — because it’s derived from printerIP and
accessCode, SwiftUI knows to recompute it whenever either of those
@Published properties changes. A computed property based on observed state
is automatically reactive.
PrinterSettings is created once and shared across two views:
// ContentView.swift
@StateObject private var settings = PrinterSettings()
// SettingsView.swift — receives the same instance as a parameter
@ObservedObject var settings: PrinterSettings
The distinction matters:
@StateObject is the owner. SwiftUI creates the
object once when the view first appears and holds a strong reference to it. If
ContentView is rebuilt (which SwiftUI does constantly), the
PrinterSettings instance persists — it’s not recreated.
@ObservedObject is the observer. The object was
created elsewhere and passed in. SettingsView observes the same instance
that ContentView owns. Changes in SettingsView propagate
back to ContentView automatically because they share the same object.
A common mistake is to use @ObservedObject in the root view that creates
the object. If you write @ObservedObject private var settings = PrinterSettings()
in ContentView, SwiftUI may create a new PrinterSettings instance
every time the view rebuilds, losing the reference and the data. Use @StateObject
for the view that owns the lifetime of the object.
PrinterSettings is an ObservableObject with two
@Published properties that auto-save to UserDefaults via didSet.
The RTSPS URL is a computed property derived from those two values, so it’s
always consistent. ContentView owns the instance with @StateObject;
SettingsView observes it with @ObservedObject.