Add a new API demo case or modify an existing one in the APIExample-SwiftUI project. Covers folder creation, Entry view, RTC class, MenuItem registration, and Case Index update.
Examples/Basic/ or Examples/Advanced/Before adding, search the Case Index in ARCHITECTURE.md to confirm the case does not already exist.
| Scenario | Files |
|---|---|
| Add new case | New folder + <ExampleName>RTC.swift + <ExampleName>.swift, ContentView.swift (MenuItem), ARCHITECTURE.md (Case Index) |
| Modify existing case | Existing *RTC.swift and/or *.swift view files, ARCHITECTURE.md (Case Index) |
APIExample-SwiftUI/Examples/[Basic|Advanced]/<ExampleName>/
Create <ExampleName>RTC.swift — owns the engine lifecycle:
import AgoraRtcKit
import SwiftUI
class <ExampleName>RTC: NSObject, ObservableObject {
var agoraKit: AgoraRtcEngineKit!
private var isJoined = false
func setupRTC(configs: [String: Any]) {
let config = AgoraRtcEngineConfig()
config.appId = KeyCenter.AppId
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
guard let channelName = configs["channelName"] as? String else { return }
let option = AgoraRtcChannelMediaOptions()
option.clientRoleType = .broadcaster
NetworkManager.shared.generateToken(channelName: channelName) { [weak self] token in
self?.agoraKit.joinChannel(byToken: token, channelId: channelName,
uid: 0, mediaOptions: option)
}
}
func onDestroy() {
if isJoined { agoraKit.leaveChannel(nil) }
AgoraRtcEngineKit.destroy()
}
}
extension <ExampleName>RTC: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String,
withUid uid: UInt, elapsed: Int) {
isJoined = true
LogUtils.log(message: "Joined: \(channel)", level: .info)
}
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
LogUtils.log(message: "Error: \(errorCode)", level: .error)
}
}
Create <ExampleName>.swift with Entry and Main views:
import SwiftUI
struct <ExampleName>Entry: View {
@State private var channelName = ""
@State private var isActive = false
@State private var configs: [String: Any] = [:]
var body: some View {
VStack {
TextField("Enter channel name".localized, text: $channelName)
.textFieldStyle(.roundedBorder).padding()
Button("Join".localized) {
configs = ["channelName": channelName]
isActive = true
}.disabled(channelName.isEmpty)
NavigationLink(destination: <ExampleName>(configs: configs),
isActive: $isActive) { EmptyView() }
}
}
}
struct <ExampleName>: View {
@State var configs: [String: Any] = [:]
@ObservedObject private var rtc = <ExampleName>RTC()
var body: some View {
VStack { /* UI here */ }
.onAppear { rtc.setupRTC(configs: configs) }
.onDisappear { rtc.onDestroy() }
}
}
Add to the menus array in APIExample-SwiftUI/ContentView.swift:
MenuItem(name: "<Display Name>".localized, view: AnyView(<ExampleName>Entry()))
Add a row to the ## Case Index table in ARCHITECTURE.md:
| <ExampleName> | `Examples/[Basic|Advanced]/<ExampleName>/` | `keyApi1()`, `keyApi2()` | One-line description |
NSObject, conforms to ObservableObject and AgoraRtcEngineDelegatesetupRTC, destroyed in onDestroy@ObservedObject (not @StateObject) for the RTC objectsetupRTC called in .onAppear, onDestroy called in .onDisappearleaveChannel + AgoraRtcEngineKit.destroy() called in onDestroyDispatchQueue.mainContentView.swiftARCHITECTURE.mdAgoraRtcEngineKit in the Entry view@StateObject for the RTC object in the Main view — the Main view does not own its lifetimebody — only in .onAppear, .onDisappear, or explicit user action handlersAgoraRtcEngineDelegate callbacks — always DispatchQueue.main.async { }AgoraRtcEngineKit instance between casesjoinChannel before requesting camera/microphone permissionsARCHITECTURE.md