0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iOSアプリ開発でアプリ用のローカルストレージを作成する場合

Posted at

Xcodeで作成するアプリケーションで複数の動画を再生するアプリを作成するとき、もし、プロジェクトに直接組み込んだ場合、動画ファイルを入れ替えるのにアプリをビルドし直す必要があるという懸念点がありました。

そこで、iOSのデバイスにあるストレージを活用してアプリと動画を切り離して作成するためにどうすればいいか考えました。

iOSのdocumentを使用

ファイル.png

写真のような保存場所がiphonやiPadにはあります。
ここをアプリで使用する動画の保存場所として使用することを試しました。
使用する方法は試したところ2つありました

1. 直接フォルダから指定する

アプリを起動してフォルダを手動で指定する方法です
この方法でも使用することは出来ますが、アクセス権限などが厳しいため、毎回指定し直す必要が出たり、動画を自動再生することが出来ないなど制限が大きいため、現実的な方法ではありませんでした。
参考にした記事のリンク

2. アプリ用のフォルダを作成し、その中に保存する

※こちらのやり方が正しいようです
参考にした記事のリンク

アプリの設定ファイルの追記

プロジェクト名-Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- ✅ iTunes でのファイル共有を有効化 -->
    <key>UIFileSharingEnabled</key>
    <true/>

    <!-- ✅ 他のアプリからのファイルアクセスを許可 -->
    <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>

    <!-- ✅ バックグラウンド再生を許可 -->
    <key>UIBackgroundModes</key>
    <array>
        <string>audio</string> <!-- ✅ バックグラウンドで音声・動画を再生 -->
        <string>fetch</string> <!-- ✅ バックグラウンドでデータ取得 -->
        <string>processing</string> <!-- ✅ バックグラウンド処理を許可 -->
    </array>

    <!-- ✅ App Transport Security (ATS) を無効化 -->
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
</dict>
</plist>

SwiftUIのコード

ContentView.swift

import SwiftUI
import SwiftData

struct ContentView: View {
    @EnvironmentObject var userSettings: UserSettings // ✅ `UserSettings` を環境オブジェクトとして使用
    var body: some View {
        FileManagerView().environmentObject(UserSettings())
    }

}

#Preview {
    ContentView()
}

FileManagerView.swift

import SwiftUI

struct FileManagerView: View {
    @StateObject private var fileManagerHelper = FileManagerHelper.shared
    @State private var selectedVideoURL: URL? // 🎯 選択した動画のURL
    @State private var isVideoPlayerPresented = false // 🎥 VideoPlayerView の表示制御
    @State private var debugMessage: String = "📂 ファイルを選択してください" // 🛠 デバッグ用メッセージ
    @State private var isShowingDeleteAlert = false // 🗑 削除アラートの表示制御
    @State private var fileToDelete: String? // 🗑 削除対象のファイル

    var body: some View {
        NavigationView {
            VStack {
                List {
                    // 📂 MyAppData 内のファイル一覧を表示
                    Section(header: Text("📂 MyAppDataのファイル一覧")) {
                        ForEach(fileManagerHelper.appFiles, id: \.self) { file in
                            HStack {
                                // 🎥 動画をタップで再生
                                Text(file)
                                    .onTapGesture {
                                        if let videoURL = getVideoURL(fileName: file) {
                                            debugMessage = "🎥 選択された動画: \(file)" // ✅ デバッグメッセージ更新
                                            selectedVideoURL = videoURL
                                            isVideoPlayerPresented = true
                                        } else {
                                            debugMessage = "❌ \(file) のURLを取得できませんでした"
                                        }
                                    }
                                    .contentShape(Rectangle()) // 🎯 タップ領域を `Text` のみに限定

                                Spacer() // 🔹 削除ボタンと動画名の間隔を確保

                                // 🗑 削除ボタン (タップすると確認アラートを表示)
                                Button(action: {
                                    fileToDelete = file
                                    isShowingDeleteAlert = true
                                }) {
                                    Image(systemName: "trash")
                                        .foregroundColor(.red)
                                }
                                .buttonStyle(BorderlessButtonStyle()) // 🎯 `List` 内での誤動作防止
                            }
                        }
                    }
                }
                .listStyle(InsetGroupedListStyle())

                // 🔽 デバッグ情報を表示
                Text(debugMessage)
                    .foregroundColor(.gray)
                    .padding()

                Button(action: {
                    debugMessage = "🔄 ファイルリストを更新中..."
                    fileManagerHelper.fetchAppFiles() // 🔄 MyAppData の中身を更新
                }) {
                    HStack {
                        Image(systemName: "arrow.clockwise")
                        Text("更新")
                    }
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
                    .padding(.horizontal)
                }
            }
            .navigationTitle("MyAppDataの管理")
            .onAppear {
                debugMessage = "📂 MyAppData のファイルを取得中..."
                fileManagerHelper.fetchAppFiles() // 初回表示時にリストを取得
            }
            // 🎥 動画プレイヤー表示用の `fullScreenCover`
            .fullScreenCover(isPresented: $isVideoPlayerPresented) {
                if let videoURL = selectedVideoURL {
                    VideoPlayerView(videoURL: videoURL)
                } else {
                    VStack {
                        Text("⚠️ `selectedVideoURL` が nil なので動画を再生できません")
                            .foregroundColor(.red)
                            .padding()
                        Button("閉じる") {
                            isVideoPlayerPresented = false
                        }
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                    }
                }
            }
            // 🗑 削除確認アラート
            .alert(isPresented: $isShowingDeleteAlert) {
                Alert(
                    title: Text("削除確認"),
                    message: Text("\(fileToDelete ?? "") を削除しますか?"),
                    primaryButton: .destructive(Text("削除")) {
                        if let fileName = fileToDelete {
                            fileManagerHelper.deleteAppFile(fileName: fileName)
                            fileManagerHelper.fetchAppFiles() // 🔄 削除後リスト更新
                            debugMessage = "🗑️ \(fileName) を削除しました"
                        }
                        fileToDelete = nil
                    },
                    secondaryButton: .cancel {
                        fileToDelete = nil
                    }
                )
            }
        }
    }

    /// 🎯 指定した `fileName` の動画ファイルのURLを取得
    private func getVideoURL(fileName: String) -> URL? {
        guard let appFolderPath = fileManagerHelper.getAppFolder() else {
            debugMessage = "❌ `MyAppData` フォルダの取得に失敗"
            return nil
        }
        let videoURL = appFolderPath.appendingPathComponent(fileName)
        
        if FileManager.default.fileExists(atPath: videoURL.path) {
            debugMessage = "✅ `\(fileName)` は存在します"
            return videoURL
        } else {
            debugMessage = "❌ `\(fileName)` が見つかりません"
            return nil
        }
    }
}

FileManagerHelper.swift

今回の1番重要なファイルはFileManagerHelper.swiftになります。

import Foundation

class FileManagerHelper: ObservableObject {
    static let shared = FileManagerHelper()
    private let fileManager = FileManager.default

    @Published var currentFiles: [String] = [] // 現在表示しているフォルダ内のファイル・フォルダ一覧
    @Published var currentFolderPath: URL? // 現在のフォルダのパス
    private let movieFolder = "Movies"

    private init() {
        fetchAppFiles()
    }

    /// 📂 Documentsフォルダのパスを取得
    func getDocumentsDirectory() -> URL? {
        return fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
    }

    /// 📂 MyAppDataフォルダのパスを取得 or 作成
    func getAppFolder() -> URL? {
        guard let documentsPath = getDocumentsDirectory() else { return nil }
        let appFolderPath = documentsPath.appendingPathComponent(movieFolder)

        if !fileManager.fileExists(atPath: appFolderPath.path) {
            try? fileManager.createDirectory(at: appFolderPath, withIntermediateDirectories: true)
        }
        return appFolderPath
    }



    /// 📜 MyAppDataフォルダ内のファイル・フォルダ一覧を取得
    func fetchAppFiles() {
        guard let appFolderPath = getAppFolder() else { return }
        fetchFiles(in: appFolderPath)
    }

    /// 📜 指定したフォルダの中のファイル・フォルダ一覧を取得
    func fetchFiles(in folderPath: URL) {
        do {
            let items = try fileManager.contentsOfDirectory(at: folderPath, includingPropertiesForKeys: nil)
            let sortedItems = items.map { $0.lastPathComponent }
            DispatchQueue.main.async {
                self.currentFolderPath = folderPath
                self.currentFiles = sortedItems
            }
        } catch {
            print("❌ ファイル一覧取得失敗: \(error)")
        }
    }

    /// 📂 指定したパスがフォルダかどうかを判定
    func isDirectory(at path: URL) -> Bool {
        var isDir: ObjCBool = false
        return fileManager.fileExists(atPath: path.path, isDirectory: &isDir) && isDir.boolValue
    }
    /// 🔄 指定したファイルを MyAppData フォルダへコピー
    func copyFileToAppFolder(fileName: String) {
        guard let documentsPath = getDocumentsDirectory(),
              let appFolderPath = getAppFolder() else {
            print("❌ フォルダの取得に失敗")
            return
        }

        let sourceURL = documentsPath.appendingPathComponent(fileName)
        let destinationURL = appFolderPath.appendingPathComponent(fileName)

        do {
            try fileManager.copyItem(at: sourceURL, to: destinationURL)
            print("✅ \(fileName) を MyAppData にコピー成功")
            fetchAppFiles()
        } catch {
            print("❌ ファイルコピー失敗: \(error)")
        }
    }

    /// 🗑️ 指定したファイルまたはフォルダを削除
    func deleteItem(at path: URL) {
        do {
            try fileManager.removeItem(at: path)
            print("🗑️ \(path.lastPathComponent) を削除しました")
            if let folderPath = currentFolderPath {
                fetchFiles(in: folderPath) // 現在のフォルダを再読み込み
            } else {
                fetchAppFiles()
            }
        } catch {
            print("❌ 削除失敗: \(error)")
        }
    }
}

ViewController.swift

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let button = UIButton()
        button.frame = CGRect(x: 0, y:0, width: 100 , height: 100)
        button.backgroundColor = .orange
        button.center = self.view.center
        button.addTarget(self, action: #selector(createFile), for: .touchUpInside)
        self.view.addSubview(button)
    }
    
    @objc  func createFile() {
        let fileManager = FileManager.default
        let docPath =  NSHomeDirectory() + "/Documents"
        let filePath = docPath + "/sample.txt"
        
        if !fileManager.fileExists(atPath: filePath) {
            fileManager.createFile(atPath:filePath, contents: nil, attributes: [:])
        }else{
            print("既に存在します。")
        }
    }
}
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?