iOS SDK
🚀 クイックスタート
SkyWay のメディア通信を体験できる以下のシンプルなサンプルアプリケーションを作成します。
- Room 名を指定して入室する
- 自分のマイク音声とカメラ映像を送信する
- 相手のマイク音声とカメラ映像を受信する
このクイックスタートの実行には iPhone 実機が必要です。
完成品は https://github.com/skyway/ios-sdk/tree/main/Tutorial にあります。
開発環境
- Xcode 26.6
- iOS 26.5
アプリケーション ID とシークレットキーの取得
※SkyWay への登録がまだの方はこちらから
SkyWay コンソールへログインし、以下の 3 つを行います。
- 「アプリケーションを作成」ボタンを押す

- アプリケーション名を入力して作成ボタンを押す
- アプリケーション一覧からアプリケーション ID とシークレットキーをコピーする
SkyWay Auth Token の作成
SkyWay を利用するためには、初めに JWT(JSON Web Token)を用いて Context を初期化します。
SkyWay Auth Token は本来サーバーサイドで生成するため、 iOS SDK にはトークンの生成機能はございません。
クイックスタートでは、 Dev 環境専用の API である Context.setupForDev(withAppId:secretKey:options:completion:) を用いて初期化するため、 SkyWay Auth Token の作成は省略します。
認証認可について、詳しくはこちらをご覧ください。
XcodeProjectの作成・フレームワークの設定
新規で XcodeProject を作成してください。
作成時、Interface は SwiftUI を選択してください。
パーミッションの設定
info.plist の Privacy - Microphone Usage Description と Privacy - Camera Usage Description を追加して、value にはユーザーに利用許可を確認するプロンプトのメッセージを登録してください。
ここでは、マイクを利用します と カメラを利用します と登録します。

SDKのダウンロード
今回は Swift Package Manager にてダウンロードします。
Xcode から Project を選択し、 Package Dependencies を選択します。
左下 + ボタンからパッケージ検索のモーダルを表示させ、右上の URL 検索ボックスに https://github.com/skyway/ios-sdk.git と入力します。
Package Product SkyWayRoom がチェックされていることを確認し、 Add Package を押下します。
映像ビューのセットアップ
今回は、カメラからキャプチャしている Local の View (プレビュー)と受信した Remote の View を描画します。
SwiftUI では UIKit ベースの VideoView を UIViewRepresentable でラップして使用します。
LocalVideoView.swift を新規作成します。LocalVideoStream を受け取り、updateUIView 内でアタッチすることで自身の映像を描画します。
import SkyWayRoom
import SwiftUI
struct LocalVideoView: UIViewRepresentable {
typealias UIViewType = VideoView
typealias Context = UIViewRepresentableContext<Self>
let stream: LocalVideoStream?
func makeUIView(context: Context) -> VideoView {
let view = VideoView()
view.videoContentMode = .scaleAspectFit
return view
}
func updateUIView(_ uiView: VideoView, context: Context) {
stream?.attach(uiView)
}
}次に、 RemoteVideoView.swift を新規作成します。RemoteVideoStream を受け取り、updateUIView 内でアタッチすることで受信した映像を描画します。
import SkyWayRoom
import SwiftUI
struct RemoteVideoView: UIViewRepresentable {
typealias UIViewType = VideoView
typealias Context = UIViewRepresentableContext<Self>
let stream: RemoteVideoStream?
func makeUIView(context: Context) -> VideoView {
let view = VideoView()
view.videoContentMode = .scaleAspectFit
return view
}
func updateUIView(_ uiView: VideoView, context: Context) {
stream?.attach(uiView)
}
}ViewModel の作成
RoomViewModel.swift を新規作成し、SkyWay のロジックをまとめます。
import SkyWayRoom でフレームワークを import してください。
@MainActor を指定、 ObservableObject と RoomDelegate に準拠します。
また、後ほど必要になるプロパティを宣言します。
import Foundation
import SkyWayRoom
@MainActor
final class RoomViewModel: ObservableObject, RoomDelegate {
@Published var localVideoStream: LocalVideoStream?
@Published var remoteVideoStream: RemoteVideoStream?
private var localAudioStream: LocalAudioStream?
private var room: Room?
private var localRoomMember: LocalRoomMember?SkyWay のセットアップ
RoomViewModel にて、 start(roomName:) という関数を定義し、アプリケーション ID とシークレットキーを宣言します。
Context.setupForDev(withAppId:secretKey:options:completion:) で SkyWay のセットアップを行います。
Context.setupForDev(withAppId:secretKey:options:completion:)は Dev 環境での利用が想定されている API です。 本番環境では、 SecretKey を秘匿するためContext.setup(withToken:options:completion:)をご利用ください。
func start(roomName: String) async {
let appId = "アプリケーションIDを入力してください"
let secretKey = "シークレットキーを入力してください"
// SkyWayのセットアップ
let contextOptions: ContextOptions = .init()
contextOptions.logLevel = .trace
try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOptions)Room の作成
SkyWay のセットアップが完了したら Room を作成します。
Room.findOrCreate(with:) で Room を作成し、プロパティに保持します。
// Roomの作成
let roomInit: Room.InitOptions = .init()
roomInit.name = roomName
guard let room = try? await Room.findOrCreate(with: roomInit) else {
print("[Tutorial] Creating room failed.")
return
}
self.room = roomRoom への参加
Room.join(with:) で Room に参加し、 Member を作成します。
作成した Member はプロパティに保持します。
// Roomへの参加
let memberInit: Room.MemberInitOptions = .init()
memberInit.name = "member_\(UUID().uuidString)"
guard let member = try? await room.join(with: memberInit) else {
print("[Tutorial] Join failed.")
return
}
localRoomMember = memberPublish の準備
カメラ映像とマイク音声を取得し、 Room に Publish する準備を行います。
カメラ映像ソースの Stream の作成と UI への表示
自身のカメラ映像を取得し UI へ反映します。
CameraVideoSource.supportedCameras() で SkyWay がサポートしているカメラの一覧を取得できます。
今回は前面カメラデバイスを取得します。
CameraVideoSource.shared().startCapturing(with:options:) でキャプチャを開始します。
CameraVideoSource.shared().createStream() で LocalVideoStream を作成します。
localVideoStream プロパティを更新することで、ContentView の LocalVideoView で描画されます。
// カメラからの映像取得とUIへの表示
// カメラリソースの取得
if let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) {
// カメラ映像のキャプチャを開始します
try? await CameraVideoSource.shared().startCapturing(with: camera, options: nil)
} else {
print("[Tutorial] Supported camera is not found.")
}
// 描画やPublishが可能なStreamを作成します
// @Publishedプロパティを更新し、ContentViewのLocalVideoViewで描画します
localVideoStream = CameraVideoSource.shared().createStream()マイク音声の Stream の作成
自身のマイク音声を取得します。
MicrophoneAudioSource を作成し、createStream() で LocalAudioStream を作成します。
// マイクからの音声取得
// Publishが可能なStreamを作成します
localAudioStream = MicrophoneAudioSource().createStream()Stream の Publish
Room に入室している他の Member へ Stream を Publish します。
LocalRoomMember.publish(_:options:) で Room に Publish します。
Publish することで、 Room に Publication が作成されます。
他の Member は対象の Publication を Subscribe することで、 Stream を受信することが可能になります。
// StreamのPublish
_ = try? await member.publish(localVideoStream!, options: nil)
_ = try? await member.publish(localAudioStream!, options: nil)Publication の Subscribe
Publication を Subscribe し Stream を受信します。
まず、 RoomDelegate の room(_:didPublishStreamOf:) を実装することで、 Room 内で新たに Publish された Publication を Subscribe することが可能です。
次に、入室時に存在する Publication を Subscribe します。
LocalRoomMember.subscribe(publicationId:options:) で Publication を Subscribe します。
受信した映像を表示するため、 Subscribe 後に Stream を取得し、RemoteVideoStream へのキャストを試みます。
remoteVideoStream プロパティを更新することで、ContentView の RemoteVideoView で描画されます。
なお、 AudioStream の場合は Subscribe が完了したタイミングでスピーカーから音声が流れます。
自身で Publish した Publication は Subscribe できないことに注意してください。
// Room内でStreamがPublishされるとroom(_:didPublishStreamOf:)が呼ばれるようにdelegateを登録します
room.delegate = self
// PublicationのSubscribe
// 入室時に他のMemberのPublicationをSubscribeします
for publication in room.publications {
await subscribe(publication)
}
}
// Room内のMemberがPublishしているPublicationをSubscribeします
private func subscribe(_ publication: RoomPublication) async {
// 自身のPublicationは除く
if publication.publisher == localRoomMember {
return
}
// PublicationをSubscribeします
guard let subscription = try? await localRoomMember?.subscribe(publicationId: publication.id, options: nil) else {
return
}
// Videoの場合はremoteVideoStreamを更新して描画します
if let videoStream = subscription.stream as? RemoteVideoStream {
self.remoteVideoStream = videoStream
}
}
// MARK: - RoomDelegate
nonisolated func room(_ room: Room, didPublishStreamOf publication: RoomPublication) {
Task {
await subscribe(publication)
}
}View と ViewModel を接続
ContentView.swift で RoomViewModel を @StateObject として保持します。
Room 名を入力して Join ボタンを押すと、 RoomViewModel.start(roomName:) が呼ばれ、 Room への入室と Publish が行われます。
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = RoomViewModel()
@State private var roomName: String = ""
var body: some View {
VStack {
HStack {
TextField("Room name", text: $roomName)
.textFieldStyle(.roundedBorder)
Button("Join") {
Task {
await viewModel.start(roomName: roomName)
}
}
.buttonStyle(.borderedProminent)
.disabled(roomName.isEmpty)
}
LocalVideoView(stream: viewModel.localVideoStream)
.frame(maxWidth: .infinity, maxHeight: .infinity)
RemoteVideoView(stream: viewModel.remoteVideoStream)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding()
}
}
Tutorial完成コード
LocalVideoView.swift
import SkyWayRoom
import SwiftUI
struct LocalVideoView: UIViewRepresentable {
typealias UIViewType = VideoView
typealias Context = UIViewRepresentableContext<Self>
let stream: LocalVideoStream?
func makeUIView(context: Context) -> VideoView {
let view = VideoView()
view.videoContentMode = .scaleAspectFit
return view
}
func updateUIView(_ uiView: VideoView, context: Context) {
stream?.attach(uiView)
}
}RemoteVideoView.swift
import SkyWayRoom
import SwiftUI
struct RemoteVideoView: UIViewRepresentable {
typealias UIViewType = VideoView
typealias Context = UIViewRepresentableContext<Self>
let stream: RemoteVideoStream?
func makeUIView(context: Context) -> VideoView {
let view = VideoView()
view.videoContentMode = .scaleAspectFit
return view
}
func updateUIView(_ uiView: VideoView, context: Context) {
stream?.attach(uiView)
}
}RoomViewModel.swift
import Foundation
import SkyWayRoom
@MainActor
final class RoomViewModel: ObservableObject, RoomDelegate {
@Published var localVideoStream: LocalVideoStream?
@Published var remoteVideoStream: RemoteVideoStream?
private var localAudioStream: LocalAudioStream?
private var room: Room?
private var localRoomMember: LocalRoomMember?
func start(roomName: String) async {
let appId = "アプリケーションIDを入力してください"
let secretKey = "シークレットキーを入力してください"
// SkyWayのセットアップ
let contextOptions: ContextOptions = .init()
contextOptions.logLevel = .trace
try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOptions)
// Roomの作成
let roomInit: Room.InitOptions = .init()
roomInit.name = roomName
guard let room = try? await Room.findOrCreate(with: roomInit) else {
print("[Tutorial] Creating room failed.")
return
}
self.room = room
// Roomへの参加
let memberInit: Room.MemberInitOptions = .init()
memberInit.name = "member_\(UUID().uuidString)"
guard let member = try? await room.join(with: memberInit) else {
print("[Tutorial] Join failed.")
return
}
localRoomMember = member
// カメラからの映像取得とUIへの表示
// カメラリソースの取得
if let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) {
// カメラ映像のキャプチャを開始します
try? await CameraVideoSource.shared().startCapturing(with: camera, options: nil)
} else {
print("[Tutorial] Supported camera is not found.")
}
// 描画やPublishが可能なStreamを作成します
// @Publishedプロパティを更新し、ContentViewのLocalVideoViewで描画します
localVideoStream = CameraVideoSource.shared().createStream()
// マイクからの音声取得
// Publishが可能なStreamを作成します
localAudioStream = MicrophoneAudioSource().createStream()
// StreamのPublish
_ = try? await member.publish(localVideoStream!, options: nil)
_ = try? await member.publish(localAudioStream!, options: nil)
// Room内でStreamがPublishされるとroom(_:didPublishStreamOf:)が呼ばれるようにdelegateを登録します
room.delegate = self
// PublicationのSubscribe
// 入室時に他のMemberのPublicationをSubscribeします
for publication in room.publications {
await subscribe(publication)
}
}
// Room内のMemberがPublishしているPublicationをSubscribeします
private func subscribe(_ publication: RoomPublication) async {
// 自身のPublicationは除く
if publication.publisher == localRoomMember {
return
}
// PublicationをSubscribeします
guard let subscription = try? await localRoomMember?.subscribe(publicationId: publication.id, options: nil) else {
return
}
// Videoの場合はremoteVideoStreamを更新して描画します
if let videoStream = subscription.stream as? RemoteVideoStream {
self.remoteVideoStream = videoStream
}
}
// MARK: - RoomDelegate
nonisolated func room(_ room: Room, didPublishStreamOf publication: RoomPublication) {
Task {
await subscribe(publication)
}
}
}ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = RoomViewModel()
@State private var roomName: String = ""
var body: some View {
VStack {
HStack {
TextField("Room name", text: $roomName)
.textFieldStyle(.roundedBorder)
Button("Join") {
Task {
await viewModel.start(roomName: roomName)
}
}
.buttonStyle(.borderedProminent)
.disabled(roomName.isEmpty)
}
LocalVideoView(stream: viewModel.localVideoStream)
.frame(maxWidth: .infinity, maxHeight: .infinity)
RemoteVideoView(stream: viewModel.remoteVideoStream)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding()
}
}動作確認
iPhone(実機)にて Run します。
iPhone Simulator ではカメラが利用できないので実機にて実行してください。
次のステップ
今回はメディアを Publish, Subscribe するシンプルな例でした。
その他のサンプルアプリケーションはこちらをご参照ください。
また、開発の前に開発ドキュメントもご一読ください。