3
3

More than 1 year has passed since last update.

iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる

Last updated at Posted at 2023-01-01

iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる

Photos Frameworkを利用して、写真アプリに保存された画像や動画のPHAssetを取得し、iOSの写真アプリのように画像をリスト表示する機能を作る事がよくあります
自作した画像リストでも、iOSの写真アプリにある「〇〇から保存」という表示を実現できないか調べてみました

iOS 15.1で動作確認しています
(手元で動作確認しただけで、互換性など含めて諸々調べきれていないため、実際に使う場合はご注意ください🙇)

xattr を使って保存したアプリを調べる

Photos Framework には、PHAssetを保存したアプリの情報を提供する機能は見当たりませんでした
しかし、保存されたファイルのExtended Attributesの com.apple.assetsd.creatorBundleIDに保存したアプリのBundle IDが設定されていました
そこで、xattrを使って、Extended Attributesを取得して、保存したアプリを調べます

xattr を使って Extended Attributes を取得する

listxattrgetxattrをSwiftから使いやすいようにWrapperを作成しました

public struct XattrError: Error {
    private let str: String? = String(utf8String: strerror(errno))
    public var localizedDescription: String { str ?? "unknown" }
}

public enum xattr {
    public static func list(from url: URL) throws -> [String] {
        let path = url.path
        let len = listxattr(path, nil, 0, 0)
        guard len > 0 else { throw XattrError() }

        let namebuff = UnsafeMutablePointer<CChar>.allocate(capacity: len)
        defer { namebuff.deallocate() }
        let res = listxattr(path, namebuff, len, 0)
        guard res > 0 else { throw XattrError() }

        guard let str = NSString(bytes: namebuff, length: len, encoding: String.Encoding.utf8.rawValue) else {
            throw XattrError()
        }
        var strs = str.components(separatedBy: "\0")
        strs.removeLast()
        return strs
    }

    public static func get(from url: URL, name: String) throws -> Data {
        let path = url.path
        let len = getxattr(path, name, nil, 0, 0, 0)
        guard len > 0 else { throw XattrError() }

        let value = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: MemoryLayout<UnsafeMutableRawPointer.Pointee>.alignment)
        defer { value.deallocate() }
        let res = getxattr(path, name, value, len, 0, 0)
        guard res > 0 else { throw XattrError() }

        let data = Data(bytes: value, count: len)
        return data
    }
}

この xattrを使って、指定したURLのファイルのExtended Attributesに com.apple.assetsd.creatorBundleIDが含まれていた場合は、Dataを取得して文字列として表示します

let url: URL

let list = try! xattr.list(from: url)

guard list.contains("com.apple.assetsd.creatorBundleID") else { return }

let creatorBundleID = try! xattr.get(from: url, name: "com.apple.assetsd.creatorBundleID")
print("creatorBundleID: \(String(data: creatorBundleID, encoding: .utf8)!)")

PHAsset のファイルの URL を取得する

前項のxattrはファイルのURLを対象としているため、PHAssetのファイルのURLを取得します
適当にファイルのURLを取得して、Extended Attributesを表示させます

let options = PHFetchOptions()
options.sortDescriptors = [.init(key: "creationDate", ascending: false)]
let assets = PHAsset.fetchAssets(with: options)
assets.enumerateObjects { (obj: PHAsset, idx: Int, stopPtr: UnsafeMutablePointer<ObjCBool>) in
    getUrl(asset: obj) { (url: URL) in
        print("obj: \(obj)")
        print("url: \(url)")
        
        let list = try! xattr.list(from: url)
        print("list: \(list)")
        
        guard list.contains("com.apple.assetsd.creatorBundleID") else { return }
        
        let creatorBundleID = try! xattr.get(from: url, name: "com.apple.assetsd.creatorBundleID")
        print("creatorBundleID: \(String(data: creatorBundleID, encoding: .utf8)!)")
    }
}

func getUrl(asset: PHAsset, completion: @escaping (URL) -> Void) {
    switch asset.mediaType {
    case .image:
        let options = PHContentEditingInputRequestOptions()
        asset.requestContentEditingInput(with: options) { (contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) in
            completion(contentEditingInput!.fullSizeImageURL!)
        }
    case .video:
        let options = PHVideoRequestOptions()
        PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) in
            let urlAsset = asset as! AVURLAsset
            completion(urlAsset.url)
        }
    default:
        break
    }
}

結果

PHAssetのURLとExtended Attributesのリストと、com.apple.assetsd.creatorBundleIDのプロパティを取得できました

url: file:///var/mobile/Media/DCIM/XXXAPPLE/IMG_XXX.MP4
list: ["com.apple.assetsd.UUID", "com.apple.assetsd.addedDate", "com.apple.assetsd.assetType", "com.apple.assetsd.avalanche.type", "com.apple.assetsd.cloudAsset.UUID", "com.apple.assetsd.creatorBundleID", "com.apple.assetsd.customCreationDate", "com.apple.assetsd.dbRebuildUuid", "com.apple.assetsd.deferredProcessing", "com.apple.assetsd.favorite", "com.apple.assetsd.hidden", "com.apple.assetsd.importedBy", "com.apple.assetsd.importedByDisplayName", "com.apple.assetsd.originalFilename", "com.apple.assetsd.publicGlobalUUID", "com.apple.assetsd.sceneAnalysisIsFromPreivew", "com.apple.assetsd.syndicationHistory", "com.apple.assetsd.timeZoneName", "com.apple.assetsd.timeZoneOffset", "com.apple.assetsd.trashed", "com.apple.assetsd.videoComplementVisibility"]
creatorBundleID: hoge.fuga.piyo.bundle.identifier

まとめ

iOSの写真アプリの画像や動画のPHAssetがどのアプリから保存されたのかを調べる方法を調査してみました
最初は、Photos.sqlite を直接読み込むのも検討していましたが、結果的に比較的シンプルに画像を保存したアプリを調べる事ができました
最初にも書きましたが、互換性など含めて諸々調べきれていないので、実際に使う場合はご注意頂けると幸いです

3
3
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
3
3