iOS/APIExample/.agent/skills/upsert-case/SKILL.md
Add a new API demo case or modify an existing one in the APIExample (UIKit + Swift) project. Covers folder creation, Entry/Main Swift file, storyboard, MenuItem registration, and Case Index update.
npx skillsauth add agoraio/api-examples upsert-caseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 + .swift file + .storyboard, ViewController.swift (MenuItem), ARCHITECTURE.md (Case Index) |
| Modify existing case | Existing .swift file(s), optionally .storyboard, ARCHITECTURE.md (Case Index) |
APIExample/Examples/[Basic|Advanced]/<ExampleName>/
Use Basic/ for fundamental channel join demos, Advanced/ for everything else.
Create <ExampleName>.swift containing both Entry and Main classes:
import UIKit
import AgoraRtcKit
class <ExampleName>Entry: UIViewController {
@IBOutlet weak var channelTextField: UITextField!
@IBAction func onJoinPressed(_ sender: UIButton) {
guard let channelName = channelTextField.text, !channelName.isEmpty else { return }
let storyboard = UIStoryboard(name: "<ExampleName>", bundle: nil)
guard let mainVC = storyboard.instantiateViewController(
withIdentifier: "<ExampleName>") as? <ExampleName>Main else { return }
mainVC.configs = ["channelName": channelName]
navigationController?.pushViewController(mainVC, animated: true)
}
}
class <ExampleName>Main: BaseViewController {
var agoraKit: AgoraRtcEngineKit?
override func viewDidLoad() {
super.viewDidLoad()
guard let channelName = configs["channelName"] as? String else { return }
let config = AgoraRtcEngineConfig()
config.appId = KeyCenter.AppId
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
// configure engine, request permissions, then join
NetworkManager.shared.generateToken(channelName: channelName) { [weak self] token in
let option = AgoraRtcChannelMediaOptions()
self?.agoraKit?.joinChannel(byToken: token, channelId: channelName,
uid: 0, mediaOptions: option)
}
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if parent == nil {
agoraKit?.leaveChannel()
AgoraRtcEngineKit.destroy()
}
}
}
extension <ExampleName>Main: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String,
withUid uid: UInt, elapsed: Int) {
LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info)
}
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error)
}
}
Create APIExample/Base.lproj/<ExampleName>.storyboard with two scenes:
| Scene | Storyboard ID | Class |
|-------|--------------|-------|
| Entry | EntryViewController | <ExampleName>Entry |
| Main | <ExampleName> | <ExampleName>Main |
Connect a Show segue or use the manual push in onJoinPressed.
Unless the user explicitly asks for a different flow, use this default Entry layout and interaction:
UITextField) with placeholder "Enter channel name".localizedUIButton) with title "Join".localizedconfigs = ["channelName": channelName]Rationale:
Add to the menus array in APIExample/ViewController.swift:
MenuItem(name: "<Display Name>".localized,
storyboard: "<ExampleName>",
controller: "<ExampleName>")
Place it in the correct section (Basic / Advanced).
Add a row to the ## Case Index table in ARCHITECTURE.md:
| <ExampleName> | `Examples/[Basic|Advanced]/<ExampleName>/<ExampleName>.swift` | `keyApi1()`, `keyApi2()` | One-line description |
Key APIs: list 2–5 core SDK methods the case demonstrates. Do not list joinChannel, leaveChannel, destroy, or sharedEngine unless they are the primary focus.
BaseViewControllerViewController.swiftleaveChannel() + AgoraRtcEngineKit.destroy() called in willMove(toParent:) when parent == nilDispatchQueue.mainjoinChannel()ARCHITECTURE.mdAgoraRtcEngineKit in the Entry VCleaveChannel or destroy in viewDidDisappear — use willMove(toParent:) with parent == nilAgoraRtcEngineDelegate callbacks — always DispatchQueue.main.async { }Main.storyboard — each case must have its own .storyboard fileAgoraRtcEngineKit instance between casesjoinChannel before requesting camera/microphone permissionsARCHITECTURE.mddevelopment
Add a new API example or modify an existing one. Covers both creation and modification scenarios, including dialog class structure, message map registration, and ARCHITECTURE.md updates.
development
Code review for API examples. Ensures examples follow project conventions, handle lifecycle correctly, manage threads safely, and use APIs properly.
development
Add a new API example or modify an existing one. Covers both creation and modification scenarios, including file structure, registration, and ARCHITECTURE.md updates.
development
Code review for API examples. Ensures examples follow project conventions, handle lifecycle correctly, manage threads safely, and use APIs properly.