Skip to content

Commit

Permalink
Merge pull request #41 from enebin/release/0.5.2
Browse files Browse the repository at this point in the history
Release/0.5.2
  • Loading branch information
enebin authored Apr 28, 2024
2 parents e50a92d + c61b7c6 commit 334e5ab
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 91 deletions.
60 changes: 0 additions & 60 deletions .github/workflows/unit-test.yml

This file was deleted.

35 changes: 35 additions & 0 deletions Demo/Aespa-iOS/VideoContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class VideoContentViewModel: ObservableObject {
@Published var photoFiles: [PhotoAsset] = []

init() {
// If you don't want to make an album, you can set `albumName` to `nil`
let option = AespaOption(albumName: "YOUR_ALBUM_NAME")
self.aespaSession = Aespa.session(with: option)

Expand Down Expand Up @@ -81,6 +82,40 @@ class VideoContentViewModel: ObservableObject {
}
.assign(to: \.photoAlbumCover, on: self)
.store(in: &subscription)

aespaSession.videoAssetEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
guard let self else { return }

if case .deleted = event {
self.fetchVideoFiles()

// It works, but not recommended
// videoFiles.remove(assets)

// Update thumbnail
self.videoAlbumCover = self.videoFiles.first?.thumbnailImage
}
}
.store(in: &subscription)

aespaSession.photoAssetEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
guard let self else { return }

if case .deleted = event {
self.fetchPhotoFiles()

// It works, but not recommended
// photoFiles.remove(assets)

// Update thumbnail
self.photoAlbumCover = self.photoFiles.first?.image
}
}
.store(in: &subscription)
}

func fetchVideoFiles() {
Expand Down
15 changes: 11 additions & 4 deletions Sources/Aespa/AespaOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public struct AespaOption {
///
/// - Parameters:
/// - albumName: The name of the album where recorded videos will be saved.
/// If you don't want to make an album, you can set this value to `nil`
/// - enableLogging: A Boolean value that determines whether logging is enabled.
public init(albumName: String, enableLogging: Bool = true) {
public init(albumName: String?, enableLogging: Bool = true) {
self.init(
asset: Asset(albumName: albumName),
session: Session(),
Expand Down Expand Up @@ -73,18 +74,24 @@ public extension AespaOption {
public var fileExtension: String

init(
albumName: String,
albumName: String?,
videoDirectoryName: String = "video",
photoDirectoryName: String = "photo",
synchronizeWithLocalAlbum: Bool = true,
fileExtension: FileExtension = .mp4,
fileNameHandler: @escaping FileNamingRule = FileNamingRulePreset.Timestamp().rule
) {
self.albumName = albumName
if let albumName {
self.albumName = albumName
self.synchronizeWithLocalAlbum = synchronizeWithLocalAlbum
} else {
Logger.log(message: "Album name is not specified. It will not save and load any assets.")
self.albumName = "Temp" // Use a temporary album name
self.synchronizeWithLocalAlbum = false
}
self.videoDirectoryName = videoDirectoryName
self.photoDirectoryName = photoDirectoryName

self.synchronizeWithLocalAlbum = true
self.fileExtension = fileExtension.rawValue
self.fileNameHandler = fileNameHandler
}
Expand Down
28 changes: 24 additions & 4 deletions Sources/Aespa/AespaSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ open class AespaSession {
let option: AespaOption
let coreSession: AespaCoreSession
private let albumManager: AespaCoreAlbumManager
private let eventManager: AespaEventManager

private let recorder: AespaCoreRecorder
private let camera: AespaCoreCamera
Expand All @@ -38,31 +39,37 @@ open class AespaSession {
///
/// - Note: If you're looking for a `View` for `SwiftUI`, use `preview`
public let previewLayer: AVCaptureVideoPreviewLayer

convenience init(option: AespaOption) {
let session = AespaCoreSession(option: option)
let eventManager = AespaEventManager()

self.init(
option: option,
session: session,
recorder: .init(core: session),
camera: .init(core: session),
albumManager: .init(albumName: option.asset.albumName)
)
albumManager: .init(
albumName: option.asset.albumName,
videoAssetEventSubject: eventManager.videoAssetEventPublihser,
photoAssetEventSubject: eventManager.photoAssetEventPublihser),
eventManager: eventManager)
}

init(
option: AespaOption,
session: AespaCoreSession,
recorder: AespaCoreRecorder,
camera: AespaCoreCamera,
albumManager: AespaCoreAlbumManager
albumManager: AespaCoreAlbumManager,
eventManager: AespaEventManager
) {
self.option = option
self.coreSession = session
self.recorder = recorder
self.camera = camera
self.albumManager = albumManager
self.eventManager = eventManager

self.previewLayerSubject = .init(nil)

Expand Down Expand Up @@ -132,6 +139,19 @@ open class AespaSession {
return device.position
}

/// Publishes events related to video assets,
/// allowing subscribers to react to delete or add event in video assets.
public var videoAssetEventPublisher: AnyPublisher<AssetEvent, Never> {
eventManager.videoAssetEventPublihser.eraseToAnyPublisher()
}


/// Publishes events related to photo assets,
/// allowing subscribers to react to delete or add event in photo assets.
public var photoAssetEventPublisher: AnyPublisher<AssetEvent, Never> {
eventManager.photoAssetEventPublihser.eraseToAnyPublisher()
}

/// This property indicates whether the camera device is set to monitor changes in the subject area.
///
/// Enabling subject area change monitoring allows the device to adjust focus and exposure settings automatically
Expand Down
108 changes: 102 additions & 6 deletions Sources/Aespa/Core/AespaCoreAlbumManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,48 @@ import Photos

/// Retreive the video(url) from `FileManager` based local storage
/// and add the video to the pre-defined album roll
class AespaCoreAlbumManager {
class AespaCoreAlbumManager: NSObject {
// Dependencies
private let cachingProxy: AssetCachingProxy
private let photoLibrary: PHPhotoLibrary
private let albumName: String
private let videoAssetEventSubject: AssetEventSubject
private let photoAssetEventSubject: AssetEventSubject

private var album: PHAssetCollection?

convenience init(albumName: String) {
private var latestVideoFetchResult: PHFetchResult<PHAsset>?
private var latestPhotoFetchResult: PHFetchResult<PHAsset>?

convenience init(
albumName: String,
videoAssetEventSubject: AssetEventSubject,
photoAssetEventSubject: AssetEventSubject
) {
let photoLibrary = PHPhotoLibrary.shared()
let cachingProxy = AssetCachingProxy()
self.init(albumName: albumName, cachingProxy, photoLibrary)
self.init(
albumName: albumName,
cachingProxy,
photoLibrary,
videoAssetEventSubject,
photoAssetEventSubject
)
}

init(
albumName: String,
_ cachingProxy: AssetCachingProxy,
_ photoLibrary: PHPhotoLibrary
_ photoLibrary: PHPhotoLibrary,
_ videoAssetEventSubject: AssetEventSubject,
_ photoAssetEventSubject: AssetEventSubject
) {
self.cachingProxy = cachingProxy
self.albumName = albumName
self.photoLibrary = photoLibrary
self.videoAssetEventSubject = videoAssetEventSubject
self.photoAssetEventSubject = photoAssetEventSubject

super.init()
}

func run<T: AespaAssetProcessing>(processor: T) async throws {
Expand All @@ -42,6 +63,7 @@ class AespaCoreAlbumManager {
}

if let album {
ensureLatestFetchResults(for: album)
try await processor.process(photoLibrary, album)
} else {
album = try AlbumImporter.getAlbum(name: albumName, in: photoLibrary)
Expand All @@ -59,7 +81,8 @@ class AespaCoreAlbumManager {
}

if let album {
return try loader.load(photoLibrary, album)
ensureLatestFetchResults(for: album)
return try loader.loadAssets(photoLibrary, album)
} else {
album = try AlbumImporter.getAlbum(name: albumName, in: photoLibrary)
return try await run(loader: loader)
Expand Down Expand Up @@ -102,3 +125,76 @@ extension AespaCoreAlbumManager {
}
}
}

extension AespaCoreAlbumManager: PHPhotoLibraryChangeObserver {
func photoLibraryDidChange(_ changeInstance: PHChange) {
if let latestVideoFetchResult {
handleChange(changeInstance, with: latestVideoFetchResult, for: .video)
}

if let latestPhotoFetchResult {
handleChange(changeInstance, with: latestPhotoFetchResult, for: .image)
}
}

private func ensureLatestFetchResults(for album: PHAssetCollection) {
if latestVideoFetchResult == nil {
latestVideoFetchResult = try? AssetLoader(limit: 0, assetType: .video).laodFetchResult(photoLibrary, album)
observePhotoLibraryChanges(album: album)
}

if latestPhotoFetchResult == nil {
latestPhotoFetchResult = try? AssetLoader(limit: 0, assetType: .image).laodFetchResult(photoLibrary, album)
observePhotoLibraryChanges(album: album)
}
}

private func observePhotoLibraryChanges(album: PHAssetCollection) {
photoLibrary.register(self)
}

private func handleChange(
_ changeInstance: PHChange,
with fetchResult: PHFetchResult<PHAsset>,
for assetType: PHAssetMediaType
) {
if let details = changeInstance.changeDetails(for: fetchResult) {
switch assetType {
case .video:
self.latestVideoFetchResult = details.fetchResultAfterChanges
handleVideoAssetChanges(details)
case .image:
self.latestPhotoFetchResult = details.fetchResultAfterChanges
handlePhotoAssetChanges(details)
default:
break
}
}
}

private func handleVideoAssetChanges(_ details: PHFetchResultChangeDetails<PHAsset>) {
let addedObjects = details.insertedObjects
let removedObjects = details.removedObjects

if !addedObjects.isEmpty {
videoAssetEventSubject.send(.added(addedObjects))
}

if !removedObjects.isEmpty {
videoAssetEventSubject.send(.deleted(removedObjects))
}
}

private func handlePhotoAssetChanges(_ details: PHFetchResultChangeDetails<PHAsset>) {
let addedObjects = details.insertedObjects
let removedObjects = details.removedObjects

if !addedObjects.isEmpty {
photoAssetEventSubject.send(.added(addedObjects))
}

if !removedObjects.isEmpty {
photoAssetEventSubject.send(.deleted(removedObjects))
}
}
}
14 changes: 14 additions & 0 deletions Sources/Aespa/Core/AespaEventManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// AespaEventManager.swift
//
//
// Created by YoungBin Lee on 4/18/24.
//

import Foundation
import Combine

class AespaEventManager {
let videoAssetEventPublihser = PassthroughSubject<AssetEvent, Never>()
let photoAssetEventPublihser = PassthroughSubject<AssetEvent, Never>()
}
4 changes: 2 additions & 2 deletions Sources/Aespa/Core/Context/AespaPhotoContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ extension AespaPhotoContext: PhotoContext {
guard option.asset.synchronizeWithLocalAlbum else {
Logger.log(
message:
"'option.asset.synchronizeWithLocalAlbum' is set to false" +
"'option.asset.synchronizeWithLocalAlbum' is set to false " +
"so no photos will be fetched from the local album. " +
"If you intended to fetch photos," +
"If you intended to fetch photos, " +
"please ensure 'option.asset.synchronizeWithLocalAlbum' is set to true."
)
return []
Expand Down
Loading

0 comments on commit 334e5ab

Please sign in to comment.