Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUI API Design Lessons

SwiftUI API Design Lessons

SwiftUI APIs are inherently declarative, yet many existing Apple platform APIs are designed to be used in a procedural manner. In this session, we'll explore how to design and implement SwiftUI-friendly declarative APIs that integrate with procedural frameworks. Using AVFoundation as an example, we will build a photo and video capture app while uncovering best practices for adapting procedural APIs to the declarative paradigm of SwiftUI.

Yoshimasa Niwa

April 11, 2025
Tweet

More Decks by Yoshimasa Niwa

Other Decks in Programming

Transcript

  1. Bridging Procedural APIs with the Declarative World SwiftUI API Design

    Lessons @niw 4/11/2025 — Tokyo, Japan try! Swift Tokyo 2025 try! Swift TOKYO
  2. AVFoundation Use Camera ❶ Create AVCaptureSession. ❷ Setup input and

    output, add preview. ❸ Capture and get a photo.
  3. Sample Apps Tutorials What Apple Recommended Encapsulate all camera code

    within something like view model. Expose call API such as taking photo as method. Expose callbacks such as preview frame steam or taken photo via @Publish.
  4. Use Camera in SwiftUI Conclusion Encapsulate procedural APIs in your

    view model and hide it from SwiftUI. Implement necessary calls as methods. Implement necessary callbacks as @Publish.
  5. No

  6. AVFoundation Use Camera ❶ Create AVCaptureSession. ❷ Setup input and

    output, add preview. ❸ Capture and get a photo.
  7. AVFoundation with SwiftUI View Use Camera ❶ Create AVCaptureSession with

    SwiftUI View. ❷ Setup input and output, add preview with SwiftUI View. ❸ Capture and get a photo with SwiftUI View.
  8. The name may confuse you View is not View SwiftUI

    View is not an instance of UI element. SwiftUI View is a tree of app structure. SwiftUI View is much closer to JSON or YAML instead of UIView.
  9. View can represent non UI elements View is not View

    A View is just an element of the tree of the app structure, it represents any type of features in the app. Container View can represent a scope of application feature that can be used by the other views within the container.
  10. struct CaptureSession<Content: View>: View { var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) { self.content = content } var body: some View { content() } }
  11. class Session { } struct CaptureSession<Content: View>: View { var

    session: Session var content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.session = Session() self.content = content } var body: some View { content() } }
  12. It ’ s frequently recreated View lifetime is short View

    is not class but struct and it ’ s recreated frequently when its state is changed. Therefore, View ’ s initializer is frequently called.
  13. @State extends lifetime of properties View identity lifetime is long

    View maintains identity implicitly by structure and explicitly by id(_:). View identity has long lifetime. This lifetime matches to like UIView lifetime. @State uses a magic to give identity lifetime to properties marked with it.
  14. class Session { } struct CaptureSession<Content: View>: View { var

    session: Session var content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.session = Session() self.content = content } var body: some View { content() } }
  15. class Session { } struct CaptureSession<Content: View>: View { @State

    var session: Session var content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.session = Session() self.content = content } var body: some View { content() } }
  16. Not You Only Live Once NYOLO @State only retain the

    value assigned first time. Subsequent value are initialized but discarded. The valid assigned value can be only seen within body.
  17. YOLO InitializeOnce<T> Basically an instance box of value T. Utilize

    @autoclosure to not immediately evaluate the initialization code of the object to workaround multiple initializations.
  18. final class InitializeOnce<Value> { private let _value: () -> Value

    lazy var value: Value = _value() init(_ value: @autoclosure @escaping () -> Value) { _value = value } }
  19. struct CaptureSession<Content: View>: View { @State var session: Session var

    content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.session = Session() self.content = content } var body: some View { content() } }
  20. struct CaptureSession<Content: View>: View { @State var session: InitializeOnce<Session> var

    content: () -> Content init(@ViewBuilder content: @escaping () -> Content) { self.session = InitializeOnce(Session()) self.content = content } var body: some View { content() } }
  21. in SwiftUI View Create AVCaptureSession SwiftUI View represents an app

    structure. Use container view to represent a feature scope. Manage lifetime of properties and initialization of these.
  22. In a procedural way Setup AVCaptureSession Add AVCaptureInput for video

    and audio inputs. Add AVCaptureOutput to capture photo or video. Add AVCaptureVideoPreviewLayer for preview. Configure AVCaptureConnection with objects.
  23. In a declarative way Setup AVCaptureSession Represents input, output, and

    preview as View in the app structure and accumulate a configuration from it. Create AVCaptureInput, AVCaptureOutput, and AVCaptureVideoPreviewLayer from the configuration. Configure AVCaptureSession with these objects.
  24. struct CaptureView: View { var body: some View { CaptureSession(isEnabled:

    true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  25. struct CaptureView: View { @State private var isFrontCamera: Bool =

    false var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView( device: isFrontCamera ? .frontCamera : .backCamera ) CaptureOutput( device: isFrontCamera ? .frontCamera : .backCamera ) ... Toggle(isOn: $isFrontCamera) { Text("Toggle") } } } }
  26. Accumulate a configuration Use PreferenceKey Not well-known SwiftUI API compare

    to @State etc that let child views talk with parent view. It walks through the child view tree to tell parent view the accumulated value for each key.
  27. enum CaptureConfiguration: Equatable { case preview(Device) case output(Device) struct Key:

    PreferenceKey { typealias Value = [CaptureConfiguration] static var defaultValue: Value { [] } static func reduce( value: inout Value, nextValue: () -> Value ) { value.append(contentsOf: nextValue()) } } }
  28. struct CapturePreviewView: View { var device: Device var body: some

    View { AVCapturePreviewView() .preference( key: CaptureConfiguration.Key.self, value: [ .preview(device) ] ) } }
  29. struct CaptureSession: View { // ... var body: some View

    { VStack { content() } .onPreferenceChange(CaptureConfiguration.Key.self) { configurations in // ... } } }
  30. // Preferences [ CaptureConfiguration.Key: [ ] ] struct CaptureView: View

    { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  31. // Preferences [ CaptureConfiguration.Key: [ ] ] struct CaptureView: View

    { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  32. // Preferences [ CaptureConfiguration.Key: [ .preview(.backCamera) ] ] struct CaptureView:

    View { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  33. // Preferences [ CaptureConfiguration.Key: [ .preview(.backCamera) ] ] struct CaptureView:

    View { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  34. // Preferences [ CaptureConfiguration.Key: [ .preview(.backCamera) .output(.frontCamera) ] ] struct

    CaptureView: View { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  35. .onPreferenceChange( CaptureConfiguration.Key, [ .preview(.frontCamera), .output(.frontCamera) ] ) struct CaptureView: View

    { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  36. .onPreferenceChange( CaptureConfiguration.Key, [ .preview(.frontCamera), .output(.frontCamera) ] ) struct CaptureView: View

    { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }
  37. Apply the configuration changes Configure AVCaptureSession In declarative way, the

    configuration can be executed multiple times when the structure is changed. Take a difference between the previous configurations and the current configurations and reconfigure AVCaptureSession.
  38. func applyConfigurations(_ configurations: [CaptureConfiguration]) throws { var usedConnections: [AVCaptureConnection] =

    [] for configuration in configurations { switch configuration { case .output(let output, device: let device): if session.outputs.contains(output), output.device == device { usedConnections.append(contentsOf: output.connections) continue } // Add output and new connection to the session... usedConnections.append(connection) } } for connection in session.connections where !usedConnections.contains(usedConnections) { if let output = connection.output { session.removeOutput(output) } } }
  39. in SwiftUI View Setup input and output, add preview SwiftUI

    View can represents structured configuration. Use PreferenceKey to accumulate a configuration. Implement logic by taking a diff of each changes.
  40. in SwiftUI View Procedural Code There are very limited locations

    we can write such code. Action closure given to Button or similar View. onChange(of:_:), onReceive(_:perform:), or task(priority:_:) and similar other modifiers.
  41. View can ’ t provide actions Proxy Object Proxy object

    is the way to provide the way to call actions, can be given to the content View. The proxy object can be used to call actions within closure such as Button action closure.
  42. public struct CaptureOutput<Content>: View where Content: View { var device:

    Device var content: () -> Content public init( device: Device, @ViewBuilder content: @escaping () -> Content ) { self.device = device self.content = content } public var body: some View { content() } }
  43. public struct CaptureOutput<Content>: View where Content: View { var device:

    Device var content: (CaptureOutputProxy) -> Content public init( device: Device, @ViewBuilder content: @escaping (CaptureOutputProxy) -> Content ) { self.device = device self.content = content } public var body: some View { content(CaptureOutputProxy(...)) } }
  44. public struct CaptureView { var body: some View { CaptureOutput(device:

    .backCamera) { proxy in Button("Take a photo") { proxy.capture() } } } }
  45. View don ’ t like Closure Callback Closure Closure is

    not Equatable (==), nor pointer comparable (===). This is intentional Swift language design. This characteristic is anomaly in SwiftUI, It ’ s driven by taking a difference of states, which is not possible for such values.
  46. View don ’ t like Closure Callback Closure There are

    very limited ways to have callback in SwiftUI Use async on call that initiates callback. Use onReceive(_:perform:) or onChange(of:_:).
  47. public struct CaptureView { var body: some View { CaptureOutput(device:

    .backCamera) { proxy in Button("Take a photo") { proxy.capture() } } } }
  48. public struct CaptureView { @State private var image: UIImage? var

    body: some View { CaptureOutput(device: .backCamera) { proxy in Button("Take a photo") { Task { image = try await proxy.capture() } } } } }
  49. In SwiftUI View Capture and get a photo Use Proxy

    Object to call actions. Give callback on calls with async, or use onChange(of:_) onReceive(_:perform:)
  50. Bridging Procedural APIs with the Declarative World SwiftUI API Design

    Lessons SwiftUI View can represent app structure, not only UI elements. Use PreferenceKey, Proxy Object, async call to implement app features in SwiftUI View. Implement code always aware of taking a difference of state changes.