どうも!こんにちは!
題名通り、SwiftDataに画像や映像を保存してランニングコストを極限まで落とそう!というゲスいことを考えています。(個人開発を始めようと画策中な今日この頃
画像や映像をアプリで表示するのに、サーバー借りたり、Supabaseの有料プラン登録するのめんどくさいですよね〜
そんなあなたに向けて、今日はこの記事をお届けます
では、行ってみましょう〜
Swift Dataのモデルを作成
SwiftDataのセットアップは他の記事見てください。まとまっている記事をよく見かけるので(適当)
import Foundation
import SwiftData
@Model
internal final class Item {
var id: UUID = UUID()
var videoData: Data = Data()
init(i: UUID, videoData: Data) {
self.id = id
self.videoData = videoData
}
}
はい、ここで察しの良い方は気付きましたね。
そう、Data型で登録してしまえばいいんです
私は、Swift、Appleデバイス向けアプリ開発と心中する予定なのでこれでOK
映像データの取得
PhotosPickerから映像や画像を選択できるようにしてあげて〜
struct AddDataView: View {
@State var viewModel: ViewModel
VSTack {
PhotosPicker(
selection: $viewModel.selectedItems,
maxSelectionCount: 1,
matching: .videos,
photoLibrary: .shared()
) {
Text("映像紐付け")
}
// ...省略
}
.onChange(of: viewModel.selectedItems, { _, new in
viewModel.addItem(pickerItems: new)
})
}
SwiftDataへ登録
登録する情報をまとめて〜
@Observable
@MainActor
internal final class MotionChartViewModel {
var selectedItems: [PhotosPickerItem] = []
func addItem(pickerItems: [PhotosPickerItem]) {
Task {
do {
guard let video: PhotosPickerItem = pickerItems.first else {
return
}
guard let videoData = try await video.loadTransferable(type: Data.self) else {
return
}
let item: Item = Item(
id: UUID(),
videoData: videoData
)
let service: Service = .init()
service.addItem(item)
} catch {
print(error)
}
}
}
}
サービス経由でデータを登録してあげるぅ〜
import Foundation
import SwiftData
internal class MotionVideoService {
var modelContext: ModelContext?
var modelContainer: ModelContainer?
@MainActor
init() {
do {
let configuration: ModelConfiguration = ModelConfiguration(isStoredInMemoryOnly: false)
let container: ModelContainer = try ModelContainer(
for: Item.self, configurations: configuration
)
modelContainer = container
modelContext = container.mainContext
modelContext?.autosaveEnabled = true
} catch {
print(
"DEBUG: init failed: \(error.localizedDescription)"
)
}
}
func addItem(_ item: Item) {
guard let modelContext: ModelContext else {
return
}
modelContext.insert(item)
save()
}
private func save() {
guard let modelContext: ModelContext else {
return
}
do {
try modelContext.save()
} catch {
print("DEBUG: Failed to save: \(error.localizedDescription)")
}
}
}
登録処理は上記で完了!!
データの取得
ID指定で保存したデータを取得して〜
import Foundation
import SwiftData
internal class MotionVideoService {
// ...省略
func fetchMotionVideo(id: UUID) -> Item? {
do {
guard let modelContext: ModelContext else {
return nil
}
// 条件を指定して fetch
let filteredItems: [Item] = try modelContext.fetch(
FetchDescriptor<Item>(
predicate: #Predicate { motion in
motion.motionId == id
}
)
)
return filteredItems.first
} catch {
return nil
}
}
// ...省略
}
Viewに反映
viewModelでSwiftDataから値を取得して
@Observable
@MainActor
internal final class ViewModel {
let motionVideoPlayer: AVPlayer?
init(motion: MotionItem) {
do {
guard let video = motionVideoService.fetchMotionVideo(id: motion.id) else {
throw MyError.videoNotFound
}
let tempURL: URL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".mov")
try video.videoData.write(to: tempURL)
self.motionVideoPlayer = AVPlayer(url: tempURL)
} catch {
print("error is \(error)")
self.motionVideoPlayer = nil
}
}
}
Viewに反映させる
if let player = viewModel.motionVideoPlayer {
VideoPlayer(player: player)
.frame(height: 200)
.onAppear {
player.play()
}
} else {
Text("映像を再生できません")
}
}
これで映像が表示されるはず〜
ローカルに適宜保存してあげればいいじゃん、って意見もあるだろうけど、
SwiftDataと連携してIDなどを外部キーに指定して保存する場合はこっちのが管理楽だな〜と、
あとは、Viewから離れた際にファイルの削除処理などをはさんであげる
(消し忘れると、容量圧迫しちゃう...
https://forums.developer.apple.com/forums/thread/743014
↑
ForumではAVPlayerを使わずに映像を表示する方法を見つけたが、m未検証(気が向いたらやる)
画像を保存する場合
1. PhotospickerのOptionを
matching: .videos,
↓
matching: .images,
に変更して
2. 取得したものを同様にData型に変換
3. SwiftDataへ保存して
4. 画面へ表示する際にpngDataやjpegDataに変換してからImageとして表示すればOK
こんな感じでできるはず〜
SwiftDataは触り始めだから、色々見落としているかもしれないですので引き続き使って何かいい方法見つかったら記事にしていこうかな〜