SwiftDataに画像や映像を保存する

どうも！こんにちは！

題名通り、SwiftDataに画像や映像を保存してランニングコストを極限まで落とそう！というゲスいことを考えています。(個人開発を始めようと画策中な今日この頃

画像や映像をアプリで表示するのに、サーバー借りたり、Supabaseの有料プラン登録するのめんどくさいですよね〜

そんなあなたに向けて、今日はこの記事をお届けます

では、行ってみましょう〜

## Swift Dataのモデルを作成

~~SwiftDataのセットアップは他の記事見てください。まとまっている記事をよく見かけるので(適当)~~

```Item.swift
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から映像や画像を選択できるようにしてあげて〜

```view.swift
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へ登録

登録する情報をまとめて〜

 ```viewModel.swift
 @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)
            }
        }
    }
}
 ```

 サービス経由でデータを登録してあげるぅ〜

```Service.swift
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指定で保存したデータを取得して〜

```Service.swift
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から値を取得して

```viewModel.swift
@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に反映させる

```view.swift
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](https://developer.apple.com/documentation/uikit/uiimage/pngdata())や[jpegData](https://developer.apple.com/documentation/uikit/uiimage/jpegdata(compressionquality:))に変換してからImageとして表示すればOK

こんな感じでできるはず〜
SwiftDataは触り始めだから、色々見落としているかもしれないですので引き続き使って何かいい方法見つかったら記事にしていこうかな〜