Slide 1

Slide 1 text

Bridging Procedural APIs with the Declarative World SwiftUI API Design Lessons @niw 4/11/2025 — Tokyo, Japan try! Swift Tokyo 2025 try! Swift TOKYO

Slide 2

Slide 2 text

Yoshimasa Niwa @niw

Slide 3

Slide 3 text

Use Camera with SwiftUI

Slide 4

Slide 4 text

Demo

Slide 5

Slide 5 text

AVFoundation Use Camera ❶ Create AVCaptureSession. ❷ Setup input and output, add preview. ❸ Capture and get a photo.

Slide 6

Slide 6 text

https://developer.apple.com/tutorials/sample-apps/capturingphotos-camerapreview, 2025

Slide 7

Slide 7 text

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.

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

That ’ s it!

Slide 10

Slide 10 text

Thank you for listening!

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No

Slide 13

Slide 13 text

Use Camera with SwiftUI

Slide 14

Slide 14 text

SwiftUI API Design

Slide 15

Slide 15 text

AVFoundation Use Camera ❶ Create AVCaptureSession. ❷ Setup input and output, add preview. ❸ Capture and get a photo.

Slide 16

Slide 16 text

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.

Slide 17

Slide 17 text

❶ Create

Slide 18

Slide 18 text

View is not View

Slide 19

Slide 19 text

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.

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

View Lifetime

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

@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.

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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.

Slide 29

Slide 29 text

YOLO InitializeOnce Basically an instance box of value T. Utilize @autoclosure to not immediately evaluate the initialization code of the object to workaround multiple initializations.

Slide 30

Slide 30 text

final class InitializeOnce { private let _value: () -> Value lazy var value: Value = _value() init(_ value: @autoclosure @escaping () -> Value) { _value = value } }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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.

Slide 34

Slide 34 text

❷ Setup

Slide 35

Slide 35 text

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.

Slide 36

Slide 36 text

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.

Slide 37

Slide 37 text

struct CaptureView: View { var body: some View { CaptureSession(isEnabled: true) { CapturePreviewView(device: .backCamera) CaptureOutput(device: .backCamera) ... } } }

Slide 38

Slide 38 text

Accumulate a configuration

Slide 39

Slide 39 text

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") } } } }

Slide 40

Slide 40 text

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.

Slide 41

Slide 41 text

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()) } } }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

struct CaptureSession: View { // ... var body: some View { VStack { content() } .onPreferenceChange(CaptureConfiguration.Key.self) { configurations in // ... } } }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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) } } }

Slide 53

Slide 53 text

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.

Slide 54

Slide 54 text

❸ Capture

Slide 55

Slide 55 text

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.

Slide 56

Slide 56 text

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.

Slide 57

Slide 57 text

public struct CaptureOutput: 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() } }

Slide 58

Slide 58 text

public struct CaptureOutput: 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(...)) } }

Slide 59

Slide 59 text

public struct CaptureView { var body: some View { CaptureOutput(device: .backCamera) { proxy in Button("Take a photo") { proxy.capture() } } } }

Slide 60

Slide 60 text

Callbacks

Slide 61

Slide 61 text

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.

Slide 62

Slide 62 text

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:_:).

Slide 63

Slide 63 text

public struct CaptureView { var body: some View { CaptureOutput(device: .backCamera) { proxy in Button("Take a photo") { proxy.capture() } } } }

Slide 64

Slide 64 text

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() } } } } }

Slide 65

Slide 65 text

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:)

Slide 66

Slide 66 text

That ’ s it!

Slide 67

Slide 67 text

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.

Slide 68

Slide 68 text

github.com/niw/CaptureUI CaptureUI

Slide 69

Slide 69 text

Thank you for listening!

Slide 70

Slide 70 text

No content