diff --git a/Modules/Capabilities/AppNavigation/Sources/DocumentScannerPresenting.swift b/Modules/Capabilities/AppNavigation/Sources/DocumentScannerPresenting.swift index a4a03282..1b3973ed 100644 --- a/Modules/Capabilities/AppNavigation/Sources/DocumentScannerPresenting.swift +++ b/Modules/Capabilities/AppNavigation/Sources/DocumentScannerPresenting.swift @@ -3,7 +3,7 @@ import UIKit -@MainActor public protocol DocumentScannerPresenting { +@MainActor @objc public protocol DocumentScannerPresenting { func presentDocumentCameraViewController() } diff --git a/Modules/Capabilities/AppNavigation/Sources/LimitedLibraryPresenting.swift b/Modules/Capabilities/AppNavigation/Sources/LimitedLibraryPresenting.swift index 70b39bf8..9ac2f3a6 100644 --- a/Modules/Capabilities/AppNavigation/Sources/LimitedLibraryPresenting.swift +++ b/Modules/Capabilities/AppNavigation/Sources/LimitedLibraryPresenting.swift @@ -3,7 +3,7 @@ import SwiftUI -public protocol LimitedLibraryPresenting { +@objc public protocol LimitedLibraryPresenting { func presentLimitedLibrary() } diff --git a/Modules/Capabilities/BarBuilder/Sources/BarBuilder.swift b/Modules/Capabilities/BarBuilder/Sources/BarBuilder.swift new file mode 100644 index 00000000..5305331e --- /dev/null +++ b/Modules/Capabilities/BarBuilder/Sources/BarBuilder.swift @@ -0,0 +1,35 @@ +// Created by Geoff Pado on 12/6/21. +// Copyright © 2021 Cocoatype, LLC. All rights reserved. + +import UIKit + +public protocol BarSection { + @MainActor var barButtonItems: [UIBarButtonItem] { get } +} + +extension UIBarButtonItem: BarSection { + public var barButtonItems: [UIBarButtonItem] { [self] } +} + +extension Array: BarSection where Element == UIBarButtonItem { + public var barButtonItems: [UIBarButtonItem] { self } +} + +@MainActor @resultBuilder +public struct BarBuilder { + public static func buildBlock(_ components: BarSection...) -> [UIBarButtonItem] { + components.flatMap(\.barButtonItems) + } + + public static func buildOptional(_ component: [BarSection]?) -> [UIBarButtonItem] { + component?.flatMap(\.barButtonItems) ?? [] + } + + public static func buildEither(first component: [BarSection]) -> [UIBarButtonItem] { + component.flatMap(\.barButtonItems) + } + + public static func buildEither(second component: [BarSection]) -> [UIBarButtonItem] { + component.flatMap(\.barButtonItems) + } +} diff --git a/Modules/Capabilities/BarBuilder/Tests/BarBuilderTests.swift b/Modules/Capabilities/BarBuilder/Tests/BarBuilderTests.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Modules/Capabilities/BarBuilder/Tests/BarBuilderTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Modules/Capabilities/DocumentScanning/Resources/en.lproj/Localizable.strings b/Modules/Capabilities/DocumentScanning/Resources/en.lproj/Localizable.strings index 4a979de2..8ed4d9f9 100644 --- a/Modules/Capabilities/DocumentScanning/Resources/en.lproj/Localizable.strings +++ b/Modules/Capabilities/DocumentScanning/Resources/en.lproj/Localizable.strings @@ -1,3 +1,6 @@ +// Accessibility label for the document scanner cell +"DocumentScannerBarButtonItem.accessibilityLabel" = "Scan Document"; + // Title for the page count alert when scanning documents "PageCountAlertFactory.alertTitle" = "Too Many Pages"; // Message for the page count alert when scanning documents diff --git a/Modules/Capabilities/DocumentScanning/Sources/DocumentScannerBarButtonItem.swift b/Modules/Capabilities/DocumentScanning/Sources/DocumentScannerBarButtonItem.swift new file mode 100644 index 00000000..d5110c8c --- /dev/null +++ b/Modules/Capabilities/DocumentScanning/Sources/DocumentScannerBarButtonItem.swift @@ -0,0 +1,44 @@ +// Created by Geoff Pado on 9/1/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import UIKit + +import FactoryKit + +import AppNavigation +import DesignSystem +import Logging + +public class DocumentScannerBarButtonItem: UIBarButtonItem { + public static var standard: DocumentScannerBarButtonItem { + let standard = DocumentScannerBarButtonItem( + image: Icons.scanDocument, + style: .plain, + target: self, + action: #selector(presentDocumentScanner(_:)) + ) + standard.accessibilityLabel = DocumentScanningStrings.DocumentScannerBarButtonItem.accessibilityLabel + return standard + } + + @objc static func presentDocumentScanner(_ sender: Any) { + let logger = Container.shared.logger() + logger.log(EventFactory().scannerPresentationEvent(for: .library)) + UIApplication.shared + .sendAction( + #selector(DocumentScannerPresenting.presentDocumentCameraViewController), + to: nil, + from: sender, + for: nil + ) + } + + // MARK: Boilerplate + + override init() { super.init() } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError("init(coder:) is not implemented") + } +} diff --git a/Modules/Capabilities/EditingToolbar/Sources/ActionSet.swift b/Modules/Capabilities/EditingToolbar/Sources/ActionSet.swift index e2155cd4..6e4cbdc6 100644 --- a/Modules/Capabilities/EditingToolbar/Sources/ActionSet.swift +++ b/Modules/Capabilities/EditingToolbar/Sources/ActionSet.swift @@ -6,6 +6,7 @@ import UIKit import FactoryKit +import BarBuilder import Defaults import FeatureFlagging import Purchasing @@ -13,7 +14,7 @@ import Tools @MainActor public struct ActionSet { - @ToolbarBuilder public var leadingNavigationItems: [UIBarButtonItem] { + @BarBuilder public var leadingNavigationItems: [UIBarButtonItem] { DismissBarButtonItem(asset: asset) if #unavailable(iOS 16), sizeClass == .regular { @@ -23,7 +24,7 @@ public struct ActionSet { } @available(iOS 16, *) - @ToolbarBuilder public var centerNavigationItems: [UIBarButtonItem] { + @BarBuilder public var centerNavigationItems: [UIBarButtonItem] { if sizeClass == .regular { UndoBarButtonItem(undoManager: undoManager, target: target) RedoBarButtonItem(undoManager: undoManager, target: target) @@ -38,7 +39,7 @@ public struct ActionSet { // 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣 by @Eskeminha on 2024-05-03 // the standard set of trailing navigation items - @ToolbarBuilder private var 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣: [UIBarButtonItem] { + @BarBuilder private var 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣: [UIBarButtonItem] { if featureFlagProvider.shouldShowDebugOverlay { DebugPreferencesBarButtonItem(target: target) } ShareBarButtonItem(target: target) @@ -48,7 +49,7 @@ public struct ActionSet { if shouldShowQuickRedact { QuickRedactBarButtonItem(target: target) } } - @ToolbarBuilder public var trailingNavigationItems: [UIBarButtonItem] { + @BarBuilder public var trailingNavigationItems: [UIBarButtonItem] { if #unavailable(iOS 16) { 🧑‍💻👋👋👋🧑‍💻🏠🕖🤣💜😍💜😍🕙😒😒😒🕙👋😍🤣 } @@ -71,7 +72,7 @@ public struct ActionSet { } } - @ToolbarBuilder public var toolbarItems: [UIBarButtonItem] { + @BarBuilder public var toolbarItems: [UIBarButtonItem] { if sizeClass != .regular { UndoBarButtonItem(undoManager: undoManager, target: target) UIBarButtonItem.flexibleSpace() diff --git a/Modules/Capabilities/EditingToolbar/Sources/ToolbarBuilder.swift b/Modules/Capabilities/EditingToolbar/Sources/ToolbarBuilder.swift deleted file mode 100644 index fccbb382..00000000 --- a/Modules/Capabilities/EditingToolbar/Sources/ToolbarBuilder.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Created by Geoff Pado on 12/6/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import UIKit - -protocol ToolbarSection { - @MainActor var barButtonItems: [UIBarButtonItem] { get } -} - -extension UIBarButtonItem: ToolbarSection { - var barButtonItems: [UIBarButtonItem] { [self] } -} - -extension Array: ToolbarSection where Element == UIBarButtonItem { - var barButtonItems: [UIBarButtonItem] { self } -} - -@MainActor @resultBuilder -struct ToolbarBuilder { - static func buildBlock(_ components: ToolbarSection...) -> [UIBarButtonItem] { - components.flatMap(\.barButtonItems) - } - - static func buildOptional(_ component: [ToolbarSection]?) -> [UIBarButtonItem] { - component?.flatMap(\.barButtonItems) ?? [] - } - - static func buildEither(first component: [ToolbarSection]) -> [UIBarButtonItem] { - component.flatMap(\.barButtonItems) - } - - static func buildEither(second component: [ToolbarSection]) -> [UIBarButtonItem] { - component.flatMap(\.barButtonItems) - } -} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/LibraryDataSource.swift b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/LibraryDataSource.swift index b61275a8..3ec8340e 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/LibraryDataSource.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/LibraryDataSource.swift @@ -3,5 +3,4 @@ protocol LibraryDataSource { var itemsCount: Int { get } - func item(at index: Int) -> PhotoLibraryItem } diff --git a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSource.swift b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSource.swift index d445891a..36399047 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSource.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSource.swift @@ -31,29 +31,12 @@ class PhotoLibraryDataSource: NSObject, LibraryDataSource, UICollectionViewDataS } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - switch item(at: indexPath) { - case .asset: - return assetsProvider.cell(for: collectionView, at: indexPath) - case .documentScan: - return extraItemsProvider.documentScannerCell(for: collectionView, at: indexPath) - case .limitedLibrary: - return extraItemsProvider.limitedLibraryCell(for: collectionView, at: indexPath) - } + return assetsProvider.cell(for: collectionView, at: indexPath) } // MARK: Photos - var itemsCount: Int { assetsProvider.photosCount + extraItemsProvider.itemsCount } - - func item(at index: Int) -> PhotoLibraryItem { - let photosCount = assetsProvider.photosCount - guard index < photosCount else { return extraItemsProvider.item(atIndex: index - photosCount) } - return assetsProvider.item(atIndex: index) - } - - func item(at indexPath: IndexPath) -> PhotoLibraryItem { - item(at: indexPath.item) - } + var itemsCount: Int { assetsProvider.photosCount } var lastItemIndexPath: IndexPath { IndexPath(row: itemsCount - 1, section: 0) @@ -62,6 +45,5 @@ class PhotoLibraryDataSource: NSObject, LibraryDataSource, UICollectionViewDataS // MARK: Providers private let assetsProvider: PhotoLibraryDataSourceAssetsProvider - private let extraItemsProvider = PhotoLibraryDataSourceExtraItemsProvider() private let changeCalculator: PhotoLibraryDataSourceChangeCalculator } diff --git a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceAssetsProvider.swift b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceAssetsProvider.swift index bd392889..c59bad7a 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceAssetsProvider.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceAssetsProvider.swift @@ -15,10 +15,6 @@ public class PhotoLibraryDataSourceAssetsProvider: NSObject { self.collection = collection } - func item(atIndex index: Int) -> PhotoLibraryItem { - return .asset(allPhotos[index]) - } - @Injected(\.errorHandler) private var errorHandler func cell(for collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AssetPhotoLibraryViewCell.identifier, for: indexPath) diff --git a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift deleted file mode 100644 index 2a46eee1..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift +++ /dev/null @@ -1,86 +0,0 @@ -// Created by Geoff Pado on 5/31/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import VisionKit - -import FactoryKit - -import Defaults -import Editing -import ErrorHandling -import PhotoPermissions -import Purchasing - -@MainActor -class PhotoLibraryDataSourceExtraItemsProvider: NSObject { - convenience override init() { - self.init( - isDocumentScannerSupported: VNDocumentCameraViewController.isSupported, - permissionsRequester: PhotoLibraryPermissionsRequester() - ) - } - - init( - isDocumentScannerSupported: Bool, - permissionsRequester: any PhotoPermissionsRequester - ) { - self.permissionsRequester = permissionsRequester - self.isDocumentScannerSupported = isDocumentScannerSupported - } - - var itemsCount: Int { extraItems.count } - func item(atIndex index: Int) -> PhotoLibraryItem { - extraItems[index] - } - - // MARK: Document Scanning - private var shouldShowDocumentScannerCell: Bool { - let hasPurchased = thatsFineThatsOnlyThree.withCheese == .purchased - return isDocumentScannerSupported && (hideDocumentScanner == false || hasPurchased) - } - - func documentScannerCell(for collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { - return collectionView.dequeueReusableCell(withReuseIdentifier: DocumentScannerPhotoLibraryViewCell.identifier, for: indexPath) - } - - // MARK: Limited Library - - private var shouldShowLimitedLibraryCell: Bool { - permissionsRequester.authorizationStatus() == .limited - } - - func limitedLibraryCell(for collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { - #if targetEnvironment(macCatalyst) - errorHandler.crash("Tried to display a limited library cell on macOS") - #else - return collectionView.dequeueReusableCell(withReuseIdentifier: LimitedLibraryPhotoLibraryViewCell.identifier, for: indexPath) - #endif - } - - // MARK: Boilerplate - - private var hideDocumentScanner: Bool { defaults.value(for: Keys.hideDocumentScanner) } - private var extraItems: [PhotoLibraryItem] { - var extraItems = [PhotoLibraryItem]() - - if shouldShowDocumentScannerCell { - extraItems.append(.documentScan) - } - - if shouldShowLimitedLibraryCell { - extraItems.append(.limitedLibrary) - } - - return extraItems - } - - @Injected(\.defaults) private var defaults - @Injected(\.errorHandler) private var errorHandler - private let permissionsRequester: any PhotoPermissionsRequester - - // thatsFineThatsOnlyThree by @nutterfi on 2024-05-15 - // the purchase repository - @Injected(\.purchaseRepository) private var thatsFineThatsOnlyThree - - private let isDocumentScannerSupported: Bool -} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryItem.swift b/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryItem.swift deleted file mode 100644 index e10f456e..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/Data Source/PhotoLibraryItem.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Created by Geoff Pado on 5/31/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Photos - -enum PhotoLibraryItem { - case asset(PHAsset) - case documentScan - case limitedLibrary -} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCell.swift b/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCell.swift deleted file mode 100644 index 46f7c06f..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// Created by Geoff Pado on 8/5/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import UIKit - -class DocumentScannerPhotoLibraryViewCell: UICollectionViewCell { - static let identifier = "DocumentScannerPhotoLibraryViewCell.identifier" - - override init(frame: CGRect) { - iconView = DocumentScannerPhotoLibraryViewCellIconView() - super.init(frame: frame) - - contentView.addSubview(iconView) - NSLayoutConstraint.activate([ - iconView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.5), - iconView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.5), - iconView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ]) - - isAccessibilityElement = true - accessibilityLabel = PhotoLibraryStrings.DocumentScannerPhotoLibraryViewCell.defaultAccessibilityLabel - accessibilityTraits = .button - backgroundColor = .primary - } - - override func draw(_ rect: CGRect) { - super.draw(rect) - - UIColor.primaryLight.setStroke() - - let borderRect = bounds.insetBy(dx: 8.5, dy: 8.5) - let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: 8.0) - borderPath.setLineDash([4, 2], count: 2, phase: 0) - borderPath.stroke() - } - - // MARK: Boilerplate - - private let iconView: DocumentScannerPhotoLibraryViewCellIconView - - @available(*, unavailable) - required init(coder: NSCoder) { - let typeName = NSStringFromClass(type(of: self)) - fatalError("\(typeName) does not implement init(coder:)") - } -} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCellIconView.swift b/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCellIconView.swift deleted file mode 100644 index d83eaa0b..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/DocumentScannerPhotoLibraryViewCellIconView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// Created by Geoff Pado on 8/5/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import DesignSystem -import UIKit - -class DocumentScannerPhotoLibraryViewCellIconView: UIImageView { - init() { - super.init(image: Icons.scanDocument) - - contentMode = .scaleAspectFit - tintColor = .primaryExtraLight - translatesAutoresizingMaskIntoConstraints = false - } - - // MARK: Boilerplate - - @available(*, unavailable) - required init(coder: NSCoder) { - let typeName = NSStringFromClass(type(of: self)) - fatalError("\(typeName) does not implement init(coder:)") - } -} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/LibrarySplitViewController.swift b/Modules/Capabilities/PhotoLibrary/Sources/LibrarySplitViewController.swift index fd7a94b0..2e4e706f 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/LibrarySplitViewController.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/LibrarySplitViewController.swift @@ -42,7 +42,7 @@ public class LibrarySplitViewController: SplitViewController, PhotoCollectionPre // MARK: Limited Library - public func presentLimitedLibrary() { + @objc public func presentLimitedLibrary() { PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self) } diff --git a/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryBarButtonItem.swift b/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryBarButtonItem.swift new file mode 100644 index 00000000..588cfcf9 --- /dev/null +++ b/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryBarButtonItem.swift @@ -0,0 +1,29 @@ +// Created by Geoff Pado on 9/1/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import UIKit + +import AppNavigation +import DesignSystem + +public class LimitedLibraryBarButtonItem: UIBarButtonItem { + public static var standard: LimitedLibraryBarButtonItem { + let standard = LimitedLibraryBarButtonItem( + image: Icons.limitedLibrary, + style: .plain, + target: nil, + action: #selector(LimitedLibraryPresenting.presentLimitedLibrary) + ) + standard.accessibilityLabel = PhotoLibraryStrings.DocumentScannerPhotoLibraryViewCell.defaultAccessibilityLabel + return standard + } + + // MARK: Boilerplate + + override init() { super.init() } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError("init(coder:) is not implemented") + } +} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCell.swift b/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCell.swift deleted file mode 100644 index a9ec8e9f..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCell.swift +++ /dev/null @@ -1,50 +0,0 @@ -// Created by Geoff Pado on 5/28/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import UIKit - -#if !targetEnvironment(macCatalyst) -class LimitedLibraryPhotoLibraryViewCell: UICollectionViewCell { - static let identifier = "LimitedLibraryPhotoLibraryViewCell.identifier" - - override init(frame: CGRect) { - iconView = LimitedLibraryPhotoLibraryViewCellIconView() - super.init(frame: frame) - - contentView.addSubview(iconView) - NSLayoutConstraint.activate([ - iconView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.5), - iconView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.5), - iconView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ]) - - isAccessibilityElement = true - accessibilityLabel = PhotoLibraryStrings.LimitedLibraryPhotoLibraryViewCell.defaultAccessibilityLabel - accessibilityTraits = .button - backgroundColor = .primary - } - - override func draw(_ rect: CGRect) { - super.draw(rect) - - UIColor.primaryLight.setStroke() - - let borderRect = bounds.insetBy(dx: 8.5, dy: 8.5) - let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: 8.0) - borderPath.setLineDash([4, 2], count: 2, phase: 0) - borderPath.stroke() - } - - // MARK: Boilerplate - - private let iconView: LimitedLibraryPhotoLibraryViewCellIconView - - @available(*, unavailable) - required init(coder: NSCoder) { - let typeName = NSStringFromClass(type(of: self)) - fatalError("\(typeName) does not implement init(coder:)") - } - -} -#endif diff --git a/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCellIconView.swift b/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCellIconView.swift deleted file mode 100644 index 2940ad87..00000000 --- a/Modules/Capabilities/PhotoLibrary/Sources/LimitedLibraryPhotoLibraryViewCellIconView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// Created by Geoff Pado on 5/28/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import DesignSystem -import UIKit - -#if !targetEnvironment(macCatalyst) -class LimitedLibraryPhotoLibraryViewCellIconView: UIImageView { - init() { - super.init(image: Icons.limitedLibrary) - - contentMode = .scaleAspectFit - tintColor = .primaryExtraLight - translatesAutoresizingMaskIntoConstraints = false - } - - // MARK: Boilerplate - - @available(*, unavailable) - required init(coder: NSCoder) { - let typeName = NSStringFromClass(type(of: self)) - fatalError("\(typeName) does not implement init(coder:)") - } -} -#endif diff --git a/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryBarButtonsProvider.swift b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryBarButtonsProvider.swift new file mode 100644 index 00000000..275dcb29 --- /dev/null +++ b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryBarButtonsProvider.swift @@ -0,0 +1,65 @@ +// Created by Geoff Pado on 9/1/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import UIKit +import VisionKit + +import FactoryKit + +import BarBuilder +import Defaults +import DocumentScanning +import MobileSettingsUI +import PhotoPermissions +import Purchasing + +@MainActor +struct PhotoLibraryBarButtonsProvider { + init() { + self.init( + isDocumentScannerSupported: VNDocumentCameraViewController.isSupported, + permissionsRequester: PhotoLibraryPermissionsRequester() + ) + } + + private let isDocumentScannerSupported: Bool + private let permissionsRequester: any PhotoPermissionsRequester + init( + isDocumentScannerSupported: Bool, + permissionsRequester: any PhotoPermissionsRequester + ) { + self.permissionsRequester = permissionsRequester + self.isDocumentScannerSupported = isDocumentScannerSupported + } + + @BarBuilder public var trailingNavigationItems: [UIBarButtonItem] { + if shouldShowDocumentScannerCell { + DocumentScannerBarButtonItem.standard + } + + if shouldShowLimitedLibraryCell { + LimitedLibraryBarButtonItem.standard + } + + if shouldShowDocumentScannerCell || shouldShowLimitedLibraryCell { + UIBarButtonItem.fixedSpace(0) + } + + SettingsBarButtonItem.standard + } + + // MARK: Helpers + + @Injected(\.defaults) private var defaults + @Injected(\.purchaseRepository) private var purchaseRepository + + private var shouldShowDocumentScannerCell: Bool { + let hideDocumentScanner = defaults.value(for: Keys.hideDocumentScanner) + let hasPurchased = purchaseRepository.withCheese == .purchased + return isDocumentScannerSupported && (hideDocumentScanner == false || hasPurchased) + } + + private var shouldShowLimitedLibraryCell: Bool { + permissionsRequester.authorizationStatus() == .limited + } +} diff --git a/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryView.swift b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryView.swift index fe285acc..e58099b7 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryView.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryView.swift @@ -11,11 +11,6 @@ class PhotoLibraryView: UICollectionView { isAccessibilityElement = false register(AssetPhotoLibraryViewCell.self, forCellWithReuseIdentifier: AssetPhotoLibraryViewCell.identifier) - register(DocumentScannerPhotoLibraryViewCell.self, forCellWithReuseIdentifier: DocumentScannerPhotoLibraryViewCell.identifier) - - #if !targetEnvironment(macCatalyst) - register(LimitedLibraryPhotoLibraryViewCell.self, forCellWithReuseIdentifier: LimitedLibraryPhotoLibraryViewCell.identifier) - #endif backgroundColor = .primary } diff --git a/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryViewController.swift b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryViewController.swift index 73bbb882..0d595c73 100644 --- a/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryViewController.swift +++ b/Modules/Capabilities/PhotoLibrary/Sources/PhotoLibraryViewController.swift @@ -23,7 +23,7 @@ class PhotoLibraryViewController: UIViewController, UICollectionViewDelegate, UI PHPhotoLibrary.shared().register(self) navigationItem.title = collection.title ?? PhotoLibraryStrings.PhotoLibraryViewController.navigationItemTitle - navigationItem.rightBarButtonItem = SettingsBarButtonItem.standard + navigationItem.rightBarButtonItems = PhotoLibraryBarButtonsProvider().trailingNavigationItems.reversed() hideDocumentScannerObserver = NotificationCenter.default.addObserver( for: Keys.hideDocumentScanner @@ -81,8 +81,9 @@ class PhotoLibraryViewController: UIViewController, UICollectionViewDelegate, UI // MARK: UICollectionViewDragDelegate func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - let item = dataSource.item(at: indexPath) - guard case .asset(let asset) = item else { return [] } + guard let cell = collectionView.cellForItem(at: indexPath) as? AssetPhotoLibraryViewCell, + let asset = cell.asset + else { return [] } let userActivity = EditingUserActivity(assetLocalIdentifier: asset.localIdentifier) let dragItemProvider = NSItemProvider(object: userActivity) @@ -116,16 +117,7 @@ class PhotoLibraryViewController: UIViewController, UICollectionViewDelegate, UI // MARK: UICollectionViewDelegate func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - switch dataSource.item(at: indexPath) { - case .asset(let asset): - self.logger.log(EventFactory().editorPresentationEvent(for: .library)) - photoEditorPresenter?.presentPhotoEditingViewController(for: asset, redactions: nil, animated: true) - case .documentScan: - self.logger.log(EventFactory().scannerPresentationEvent(for: .library)) - documentScannerPresenter?.presentDocumentCameraViewController() - case .limitedLibrary: - limitedLibraryPresenter?.presentLimitedLibrary() - } + logger.log(EventFactory().editorPresentationEvent(for: .library)) } // MARK: Photo Library Changes diff --git a/Modules/Capabilities/PhotoLibrary/Tests/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift b/Modules/Capabilities/PhotoLibrary/Tests/PhotoLibraryBarButtonsProviderTests.swift similarity index 76% rename from Modules/Capabilities/PhotoLibrary/Tests/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift rename to Modules/Capabilities/PhotoLibrary/Tests/PhotoLibraryBarButtonsProviderTests.swift index c10d155f..b5645e5b 100644 --- a/Modules/Capabilities/PhotoLibrary/Tests/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift +++ b/Modules/Capabilities/PhotoLibrary/Tests/PhotoLibraryBarButtonsProviderTests.swift @@ -1,5 +1,5 @@ -// Created by Geoff Pado on 5/16/24. -// Copyright © 2024 Cocoatype, LLC. All rights reserved. +// Created by Geoff Pado on 9/1/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. import Testing @@ -7,6 +7,7 @@ import FactoryKit import FactoryTesting import DefaultsDoubles +import DocumentScanning import PhotoPermissions import Purchasing import PurchasingDoubles @@ -15,7 +16,7 @@ import PurchasingDoubles @testable import PhotoLibrary @MainActor @Suite(.container) -struct PhotoLibraryDataSourceExtraItemsProviderTests { +struct PhotoLibraryBarButtonsProviderTests { @Test(arguments: [ (true, false, PurchaseState.purchased, true), (true, false, .unavailable, true), @@ -39,15 +40,15 @@ struct PhotoLibraryDataSourceExtraItemsProviderTests { SpyRepository(withCheese: purchaseState) } - let provider = PhotoLibraryDataSourceExtraItemsProvider( + let provider = PhotoLibraryBarButtonsProvider( isDocumentScannerSupported: isDocumentScannerSupported, permissionsRequester: PhotoLibraryPermissionsRequester() ) - let isIncluded = (0..