
Photo by
matsuri365d
SwiftUI ReplayKitで画面録画する
お久しぶりです!りーさんです。
次回の開発で画面録画する機能を作ることになったのでサンプルで作ってみました。誰かの参考になればと思います。
ReplayKitとは?
デベロッパが収録やライブ・ブロードキャスト機能をAppに追加することを可能にするフレームワークです。 また、ユーザはデバイスの前面のカメラとマイクを使用して、収録やブロードキャストに注釈を加えることもできます。
参考コード
ContentView
import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ContentViewModel()
@State private var showCountdown = false
var body: some View {
VStack {
Text("ReplayKit")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.blue)
.padding(.top, 20)
if viewModel.isRecording {
Button(action: {
viewModel.onTapRecordingButton()
}) {
Image(systemName: "stop.circle")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.red)
}
} else if viewModel.showCountdown {
VStack {
Text("Recording will start in:")
.font(.headline)
.foregroundColor(.primary)
.padding(.bottom, 20)
Text("\(viewModel.countdownValue)")
.font(.system(size: 60, weight: .bold, design: .default))
.foregroundColor(.blue)
.padding()
.background(
Circle()
.fill(Color.white)
.shadow(radius: 10)
)
}
} else {
Button(action: {
viewModel.onTapRecordingButton()
}) {
Image(systemName: "record.circle")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
}
}
}
.padding()
.alert(isPresented: $viewModel.isSaved) {
Alert(
title: Text("保存完了"),
message: Text("動画をカメラロールに保存しました!"),
dismissButton: .default(Text("OK"))
)
}
}
}
#Preview {
ContentView()
}
ContentViewModel
import ReplayKit
import Foundation
import Photos
final class ContentViewModel: ObservableObject {
let recorder = RPScreenRecorder.shared()
@Published var isRecording = false
@Published var url: URL?
@Published var isSaved = false
@Published var showCountdown = false
@Published var countdownValue = 3
// 録画ボタンを押した時の処理
func onTapRecordingButton() {
if isRecording {
Task { @MainActor in
do {
self.url = try await stopRecording()
print(self.url ?? "")
self.isRecording = false
if self.url != nil {
self.saveVideoToCameraRoll()
}
} catch {
print(error.localizedDescription)
}
}
} else {
startRecordingWithCountdown()
}
}
// 録画開始のカウントダウン + 録画開始
func startRecordingWithCountdown() {
showCountdown = true
countdownValue = 3
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.countdownValue -= 1
if self.countdownValue == 0 {
timer.invalidate()
self.showCountdown = false
startRecording { error in
if let error = error {
print(error.localizedDescription)
return
}
}
Task { @MainActor in
self.isRecording = true
}
}
}
timer.tolerance = 0.1
}
// 録画開始処理
private func startRecording(enableMicorphone: Bool = false, completion: @escaping (Error?) -> ()) {
recorder.isMicrophoneEnabled = false
recorder.startRecording(handler: completion)
}
// 録画停止処理
private func stopRecording() async throws -> URL {
let name = UUID().uuidString + ".mov"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(name)
try await recorder.stopRecording(withOutput: url)
return url
}
// キャンセル処理
private func cancelRecording() {
recorder.discardRecording {}
}
// カメラロールへ保存
private func saveVideoToCameraRoll() {
self.isSaved = true
guard let videoURL = self.url else {
print("No video URL found.")
return
}
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
} completionHandler: { success, error in
if success {
print("Video saved to Camera Roll successfully.")
self.isSaved = true
} else {
if let error = error {
print("Error saving video to Camera Roll: \(error.localizedDescription)")
}
}
}
}
}