Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Adding synthesized speech to calls

At a glance

Item Summary
Purpose Adds synthesized app audio to phone and FaceTime calls after checking system and app permission.
App architecture A SwiftUI view owns a main-actor call-audio coordinator and a speech actor.
Main patterns View-owned services, observable state, actor isolation, async notification consumption.
Project style Small single-target app with one file per major type.

Project structure

AddSynthesizedSpeechToCalls/
├── AddSynthesizedSpeechToCallsApp.swift
├── ContentView.swift
├── CallAudio.swift
└── SpeechSynthesizer.swift

Structure observations

  • UI intent and alert state stay in ContentView; framework configuration and observation stay in CallAudio.
  • Speech synthesis is a separate actor rather than another responsibility of the view or call coordinator.

Overall architecture

Reference code

AddSynthesizedSpeechToCalls/ContentView.swift:33 — the toggle converts a UI intent into an asynchronous coordinator call.

private var toggleBinding: Binding<Bool> {
    Binding { addAudioToCalls } set: { newValue in
        Task {
            switch await callAudio.setAppAudioEnabled(newValue) {
            case .success(let state): addAudioToCalls = state
            case .failure: addAudioToCalls = false
            }
        }
    }
}

Interpretation

The view presents intent and results, while the two service types contain AVFAudio operations. CallAudio is the authoritative source for call availability and microphone-injection state; SpeechSynthesizer serializes access to its synthesizer through actor isolation.

Ownership and state

Ownership evidence

AddSynthesizedSpeechToCalls/ContentView.swift:13 — the root content view creates both long-lived collaborators.

@State private var callAudio = CallAudio()
@State private var speechSynth = SpeechSynthesizer()
Owner Object or state Relationship Mutation authority
ContentView UI text, toggle, alerts Owns SwiftUI state ContentView
ContentView CallAudio, SpeechSynthesizer Creates and retains through @State Each service mutates its own internals
CallAudio Injection and call state Coordinates shared system services CallAudio
SpeechSynthesizer AVSpeechSynthesizer Creates and actor-isolates SpeechSynthesizer

Class and protocol design

Type Responsibility Depends on
ContentView Collect text, present status, translate controls into intents CallAudio, SpeechSynthesizer
CallAudio Check permission, set injection mode, observe call/media-service changes AVAudioApplication, AVAudioSession
SpeechSynthesizer Select a voice and speak utterances serially AVSpeechSynthesizer
AppAudioError Express user-actionable enablement failures Swift Error

No app-defined protocol is needed because there is one concrete implementation for each narrow capability.

Access control

Symbol Access Verified effect Likely rationale
CallAudio.isCallActive private(set) Views can read it; only CallAudio can update it. Protect notification-derived truth.
CallAudio.isAppAudioEnabled private Hidden outside the coordinator. Keep system mode and cached state synchronized.
setMicrophoneInjectionMode private Only permission-checked paths can change the mode. Preserve the enablement invariant.
SpeechSynthesizer.speechSynth private The underlying mutable synthesizer cannot escape its actor. Maintain actor isolation.
App types implicit internal Visible only within the app module. No reusable framework API is being published.

Reference code

AddSynthesizedSpeechToCalls/CallAudio.swift:14 — readable state exposes no external setter.

private(set) var isCallActive = false
private var isAppAudioEnabled = false

Logic ownership and placement

Logic Owning type or file Placement rationale
Toggle/alert presentation ContentView View-specific state and rendering.
Permission and injection policy CallAudio One boundary around coupled system APIs.
Call and reset observation CallAudio Events mutate coordinator-owned state.
Utterance construction and playback SpeechSynthesizer Keeps mutable synthesizer work off the view.

Design patterns

Pattern Source evidence Purpose or tradeoff
Observable coordinator AddSynthesizedSpeechToCalls/CallAudio.swift:10 Main-actor state can drive SwiftUI without exposing system details.
Actor-isolated service AddSynthesizedSpeechToCalls/SpeechSynthesizer.swift:10 Serializes a mutable speech engine behind an async boundary.
Async event stream AddSynthesizedSpeechToCalls/CallAudio.swift:82 Consumes notification sequences with structured asynchronous loops.
Result-based boundary AddSynthesizedSpeechToCalls/CallAudio.swift:32 Returns domain errors instead of making the view infer permission outcomes.

Naming conventions

  • Types use responsibility nouns: CallAudio, SpeechSynthesizer, and AppAudioError.
  • Boolean names use is/state phrasing: isCallActive, isAppAudioEnabled, addAudioToCalls.
  • Commands use verbs: setAppAudioEnabled, synthesizeSpeech, observeCallState.
  • Files match their principal type; UI and service boundaries are immediately visible in the tree.

Architecture takeaways

  • Keep permission and system-mode changes in one coordinator with a domain result type.
  • Expose notification-derived state as read-only and retain mutation authority in the observer.
  • Use a separate actor when a mutable framework object represents an independent capability.
  • A small sample benefits from explicit boundaries without requiring a protocol for every class.

Source map

Source file Relevant symbols
AddSynthesizedSpeechToCalls/ContentView.swift ContentView, toggleBinding, textToSpeech
AddSynthesizedSpeechToCalls/CallAudio.swift CallAudio, setAppAudioEnabled, observers
AddSynthesizedSpeechToCalls/SpeechSynthesizer.swift SpeechSynthesizer
AddSynthesizedSpeechToCalls/AddSynthesizedSpeechToCallsApp.swift App entry