diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..51de011
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,3 @@
+[*.{swift}]
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/Examples/Example-iOS/AppDelegate.swift b/Examples/Example-iOS/AppDelegate.swift
deleted file mode 100644
index 4b66d7a..0000000
--- a/Examples/Example-iOS/AppDelegate.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-//
-// AppDelegate.swift
-// Example-iOS
-//
-// Created by Alexandra Afonasova on 20.10.2022.
-//
-
-import Foundation
-import UIKit
-
-final class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
-
- private var orientationLock = UIInterfaceOrientationMask.all
-
- func lockOrientationToPortrait() {
- if #available(iOS 16, *) {
- if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
- scene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
- }
- UIViewController.attemptRotationToDeviceOrientation()
- } else {
- UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation")
- }
- orientationLock = .portrait
- }
-
- func unlockOrientation() {
- orientationLock = .all
- UIViewController.attemptRotationToDeviceOrientation()
- }
-
- func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
- return orientationLock
- }
-
-}
diff --git a/Examples/Example-iOS/Info.plist b/Examples/Example-iOS/Info.plist
deleted file mode 100644
index f9ff86c..0000000
--- a/Examples/Example-iOS/Info.plist
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- NSPhotoLibraryUsageDescription
- Grant access to photo library to be able to select photos
- NSMicrophoneUsageDescription
- Grant access to microphone to be able to take videos
- NSCameraUsageDescription
- Grant access to camera to be able to take photos and videos
-
-
diff --git a/ExyteMediaPicker.podspec b/ExyteMediaPicker.podspec
deleted file mode 100644
index b5cc127..0000000
--- a/ExyteMediaPicker.podspec
+++ /dev/null
@@ -1,23 +0,0 @@
-Pod::Spec.new do |s|
- s.name = "ExyteMediaPicker"
- s.version = "1.2.4"
- s.summary = "MediaPicker is a customizable photo/video picker for iOS written in pure SwiftUI"
-
- s.homepage = 'https://github.com/exyte/MediaPicker.git'
- s.license = 'MIT'
- s.author = { 'Exyte' => 'info@exyte.com' }
- s.source = { :git => 'https://github.com/exyte/MediaPicker.git', :tag => s.version.to_s }
- s.social_media_url = 'http://exyte.com'
-
- s.ios.deployment_target = '15.0'
-
- s.requires_arc = true
- s.swift_version = "5.2"
-
- s.source_files = [
- 'Sources/*.h',
- 'Sources/*.swift',
- 'Sources/**/*.swift'
- ]
-
-end
diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/MediaPickerExample/MediaPickerExample.xcodeproj/project.pbxproj
similarity index 52%
rename from Examples/Examples.xcodeproj/project.pbxproj
rename to MediaPickerExample/MediaPickerExample.xcodeproj/project.pbxproj
index 5dd69f1..a9c3491 100644
--- a/Examples/Examples.xcodeproj/project.pbxproj
+++ b/MediaPickerExample/MediaPickerExample.xcodeproj/project.pbxproj
@@ -3,218 +3,191 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 55;
+ objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
- 130FBF5528740D3E0086478A /* CustomizedMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130FBF5428740D3E0086478A /* CustomizedMediaPicker.swift */; };
- 1384FF0B283F912E00C9AAC1 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1384FF0A283F912E00C9AAC1 /* Application.swift */; };
- 1384FF0D283F912E00C9AAC1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1384FF0C283F912E00C9AAC1 /* ContentView.swift */; };
- 1384FF0F283F912F00C9AAC1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1384FF0E283F912F00C9AAC1 /* Assets.xcassets */; };
- 1384FF12283F912F00C9AAC1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1384FF11283F912F00C9AAC1 /* Preview Assets.xcassets */; };
- 5B1CA4E029F137E100245B8B /* ExyteMediaPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 5B1CA4DF29F137E100245B8B /* ExyteMediaPicker */; };
- 5B5AA87329F2923A001306F9 /* MediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5AA87229F2923A001306F9 /* MediaCell.swift */; };
- 5B92AABC2A162DB3002F9EB2 /* FilterMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B92AABB2A162DB3002F9EB2 /* FilterMediaPicker.swift */; };
- A82559832901A437008D9CC5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82559822901A437008D9CC5 /* AppDelegate.swift */; };
+ 5B2068782D9BE32700485134 /* ExyteMediaPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 5B2068772D9BE32700485134 /* ExyteMediaPicker */; };
+ 5BFF684D2AD68B990099D333 /* MediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF68432AD68B990099D333 /* MediaCell.swift */; };
+ 5BFF684E2AD68B990099D333 /* FilterMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF68442AD68B990099D333 /* FilterMediaPicker.swift */; };
+ 5BFF684F2AD68B990099D333 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BFF68452AD68B990099D333 /* Assets.xcassets */; };
+ 5BFF68502AD68B990099D333 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BFF68472AD68B990099D333 /* Preview Assets.xcassets */; };
+ 5BFF68512AD68B990099D333 /* CustomizedMediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF68482AD68B990099D333 /* CustomizedMediaPicker.swift */; };
+ 5BFF68522AD68B990099D333 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF68492AD68B990099D333 /* AppDelegate.swift */; };
+ 5BFF68532AD68B990099D333 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF684A2AD68B990099D333 /* Application.swift */; };
+ 5BFF68542AD68B990099D333 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFF684B2AD68B990099D333 /* ContentView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
- 130FBF5428740D3E0086478A /* CustomizedMediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizedMediaPicker.swift; sourceTree = ""; };
- 1384FF07283F912E00C9AAC1 /* Example-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- 1384FF0A283F912E00C9AAC1 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; };
- 1384FF0C283F912E00C9AAC1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
- 1384FF0E283F912F00C9AAC1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 1384FF11283F912F00C9AAC1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
- 1384FF19283F920A00C9AAC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 5B1CA4DE29F137A600245B8B /* MediaPicker */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaPicker; path = ..; sourceTree = ""; };
- 5B5AA87229F2923A001306F9 /* MediaCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCell.swift; sourceTree = ""; };
- 5B92AABB2A162DB3002F9EB2 /* FilterMediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMediaPicker.swift; sourceTree = ""; };
- A82559822901A437008D9CC5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 5B2068742D9BE17100485134 /* MediaPicker */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaPicker; path = ..; sourceTree = ""; };
+ 5B2068752D9BE32400485134 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 5BFF68312AD689C80099D333 /* MediaPickerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MediaPickerExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 5BFF68432AD68B990099D333 /* MediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaCell.swift; sourceTree = ""; };
+ 5BFF68442AD68B990099D333 /* FilterMediaPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterMediaPicker.swift; sourceTree = ""; };
+ 5BFF68452AD68B990099D333 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 5BFF68472AD68B990099D333 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 5BFF68482AD68B990099D333 /* CustomizedMediaPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizedMediaPicker.swift; sourceTree = ""; };
+ 5BFF68492AD68B990099D333 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 5BFF684A2AD68B990099D333 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; };
+ 5BFF684B2AD68B990099D333 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
- 1384FF04283F912E00C9AAC1 /* Frameworks */ = {
+ 5BFF682E2AD689C80099D333 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 5B1CA4E029F137E100245B8B /* ExyteMediaPicker in Frameworks */,
+ 5B2068782D9BE32700485134 /* ExyteMediaPicker in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 130FBF5628740FB30086478A /* Support Files */ = {
+ 5B2068762D9BE32700485134 /* Frameworks */ = {
isa = PBXGroup;
children = (
- 1384FF0E283F912F00C9AAC1 /* Assets.xcassets */,
- 1384FF19283F920A00C9AAC1 /* Info.plist */,
- 1384FF10283F912F00C9AAC1 /* Preview Content */,
- );
- name = "Support Files";
- sourceTree = "";
- };
- 1384FEFA283F90C800C9AAC1 = {
- isa = PBXGroup;
- children = (
- 1384FF01283F90FA00C9AAC1 /* Packages */,
- 1384FF09283F912E00C9AAC1 /* Example-iOS */,
- 1384FF08283F912E00C9AAC1 /* Products */,
- 1384FF16283F919C00C9AAC1 /* Frameworks */,
);
+ name = Frameworks;
sourceTree = "";
};
- 1384FF01283F90FA00C9AAC1 /* Packages */ = {
+ 5BFF68282AD689C80099D333 = {
isa = PBXGroup;
children = (
- 5B1CA4DE29F137A600245B8B /* MediaPicker */,
+ 5B2068742D9BE17100485134 /* MediaPicker */,
+ 5BFF68422AD68B990099D333 /* MediaPickerExample */,
+ 5BFF68322AD689C80099D333 /* Products */,
+ 5B2068762D9BE32700485134 /* Frameworks */,
);
- name = Packages;
sourceTree = "";
};
- 1384FF08283F912E00C9AAC1 /* Products */ = {
+ 5BFF68322AD689C80099D333 /* Products */ = {
isa = PBXGroup;
children = (
- 1384FF07283F912E00C9AAC1 /* Example-iOS.app */,
+ 5BFF68312AD689C80099D333 /* MediaPickerExample.app */,
);
name = Products;
sourceTree = "";
};
- 1384FF09283F912E00C9AAC1 /* Example-iOS */ = {
+ 5BFF68422AD68B990099D333 /* MediaPickerExample */ = {
isa = PBXGroup;
children = (
- 130FBF5628740FB30086478A /* Support Files */,
- 1384FF0A283F912E00C9AAC1 /* Application.swift */,
- 130FBF5428740D3E0086478A /* CustomizedMediaPicker.swift */,
- 1384FF0C283F912E00C9AAC1 /* ContentView.swift */,
- A82559822901A437008D9CC5 /* AppDelegate.swift */,
- 5B5AA87229F2923A001306F9 /* MediaCell.swift */,
- 5B92AABB2A162DB3002F9EB2 /* FilterMediaPicker.swift */,
+ 5B2068752D9BE32400485134 /* Info.plist */,
+ 5BFF684A2AD68B990099D333 /* Application.swift */,
+ 5BFF68492AD68B990099D333 /* AppDelegate.swift */,
+ 5BFF684B2AD68B990099D333 /* ContentView.swift */,
+ 5BFF68432AD68B990099D333 /* MediaCell.swift */,
+ 5BFF68482AD68B990099D333 /* CustomizedMediaPicker.swift */,
+ 5BFF68442AD68B990099D333 /* FilterMediaPicker.swift */,
+ 5BFF68452AD68B990099D333 /* Assets.xcassets */,
+ 5BFF68462AD68B990099D333 /* Preview Content */,
);
- path = "Example-iOS";
+ path = MediaPickerExample;
sourceTree = "";
};
- 1384FF10283F912F00C9AAC1 /* Preview Content */ = {
+ 5BFF68462AD68B990099D333 /* Preview Content */ = {
isa = PBXGroup;
children = (
- 1384FF11283F912F00C9AAC1 /* Preview Assets.xcassets */,
+ 5BFF68472AD68B990099D333 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "";
};
- 1384FF16283F919C00C9AAC1 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- );
- name = Frameworks;
- sourceTree = "";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- 1384FF06283F912E00C9AAC1 /* Example-iOS */ = {
+ 5BFF68302AD689C80099D333 /* MediaPickerExample */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 1384FF13283F912F00C9AAC1 /* Build configuration list for PBXNativeTarget "Example-iOS" */;
+ buildConfigurationList = 5BFF683F2AD689C90099D333 /* Build configuration list for PBXNativeTarget "MediaPickerExample" */;
buildPhases = (
- 1384FF03283F912E00C9AAC1 /* Sources */,
- 1384FF04283F912E00C9AAC1 /* Frameworks */,
- 1384FF05283F912E00C9AAC1 /* Resources */,
+ 5BFF682D2AD689C80099D333 /* Sources */,
+ 5BFF682E2AD689C80099D333 /* Frameworks */,
+ 5BFF682F2AD689C80099D333 /* Resources */,
);
buildRules = (
);
dependencies = (
);
- name = "Example-iOS";
+ name = MediaPickerExample;
packageProductDependencies = (
- 5B1CA4DF29F137E100245B8B /* ExyteMediaPicker */,
+ 5B2068772D9BE32700485134 /* ExyteMediaPicker */,
);
- productName = "Example-iOS";
- productReference = 1384FF07283F912E00C9AAC1 /* Example-iOS.app */;
+ productName = MediaPickerExample;
+ productReference = 5BFF68312AD689C80099D333 /* MediaPickerExample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
- 1384FEFB283F90C800C9AAC1 /* Project object */ = {
+ 5BFF68292AD689C80099D333 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1340;
- LastUpgradeCheck = 1340;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
TargetAttributes = {
- 1384FF06283F912E00C9AAC1 = {
- CreatedOnToolsVersion = 13.4;
+ 5BFF68302AD689C80099D333 = {
+ CreatedOnToolsVersion = 15.0;
};
};
};
- buildConfigurationList = 1384FEFE283F90C800C9AAC1 /* Build configuration list for PBXProject "Examples" */;
- compatibilityVersion = "Xcode 13.0";
+ buildConfigurationList = 5BFF682C2AD689C80099D333 /* Build configuration list for PBXProject "MediaPickerExample" */;
+ compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
- mainGroup = 1384FEFA283F90C800C9AAC1;
- productRefGroup = 1384FF08283F912E00C9AAC1 /* Products */;
+ mainGroup = 5BFF68282AD689C80099D333;
+ packageReferences = (
+ );
+ productRefGroup = 5BFF68322AD689C80099D333 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
- 1384FF06283F912E00C9AAC1 /* Example-iOS */,
+ 5BFF68302AD689C80099D333 /* MediaPickerExample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
- 1384FF05283F912E00C9AAC1 /* Resources */ = {
+ 5BFF682F2AD689C80099D333 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 1384FF12283F912F00C9AAC1 /* Preview Assets.xcassets in Resources */,
- 1384FF0F283F912F00C9AAC1 /* Assets.xcassets in Resources */,
+ 5BFF68502AD68B990099D333 /* Preview Assets.xcassets in Resources */,
+ 5BFF684F2AD68B990099D333 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- 1384FF03283F912E00C9AAC1 /* Sources */ = {
+ 5BFF682D2AD689C80099D333 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- A82559832901A437008D9CC5 /* AppDelegate.swift in Sources */,
- 1384FF0D283F912E00C9AAC1 /* ContentView.swift in Sources */,
- 5B5AA87329F2923A001306F9 /* MediaCell.swift in Sources */,
- 130FBF5528740D3E0086478A /* CustomizedMediaPicker.swift in Sources */,
- 5B92AABC2A162DB3002F9EB2 /* FilterMediaPicker.swift in Sources */,
- 1384FF0B283F912E00C9AAC1 /* Application.swift in Sources */,
+ 5BFF68522AD68B990099D333 /* AppDelegate.swift in Sources */,
+ 5BFF68532AD68B990099D333 /* Application.swift in Sources */,
+ 5BFF68542AD68B990099D333 /* ContentView.swift in Sources */,
+ 5BFF68512AD68B990099D333 /* CustomizedMediaPicker.swift in Sources */,
+ 5BFF684D2AD68B990099D333 /* MediaCell.swift in Sources */,
+ 5BFF684E2AD68B990099D333 /* FilterMediaPicker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
- 1384FEFF283F90C800C9AAC1 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- };
- name = Debug;
- };
- 1384FF00283F90C800C9AAC1 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- };
- name = Release;
- };
- 1384FF14283F912F00C9AAC1 /* Debug */ = {
+ 5BFF683D2AD689C90099D333 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -240,16 +213,12 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
- DEVELOPMENT_ASSET_PATHS = "\"Example-iOS/Preview Content\"";
- DEVELOPMENT_TEAM = FZXCM5CJ7P;
- ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
@@ -263,42 +232,26 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "Example-iOS/Info.plist";
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 1.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.example.media-picker";
- PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ SWIFT_VERSION = "";
};
name = Debug;
};
- 1384FF15283F912F00C9AAC1 /* Release */ = {
+ 5BFF683E2AD689C90099D333 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -324,16 +277,12 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DEVELOPMENT_ASSET_PATHS = "\"Example-iOS/Preview Content\"";
- DEVELOPMENT_TEAM = FZXCM5CJ7P;
ENABLE_NS_ASSERTIONS = NO;
- ENABLE_PREVIEWS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
@@ -341,50 +290,110 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_VERSION = "";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 5BFF68402AD689C90099D333 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"MediaPickerExample/Preview Content\"";
+ DEVELOPMENT_TEAM = FZXCM5CJ7P;
+ ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = "Example-iOS/Info.plist";
+ INFOPLIST_FILE = MediaPickerExample/Info.plist;
+ INFOPLIST_KEY_NSCameraUsageDescription = "Grant access to camera to be able to take photos and videos";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Grant access to microphone to be able to take videos";
+ INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Grant access to photo library to be able to select photos";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.example.media-picker";
+ PRODUCT_BUNDLE_IDENTIFIER = com.exyte.MediaPickerExample;
PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- VALIDATE_PRODUCT = YES;
+ TARGETED_DEVICE_FAMILY = 1;
+ };
+ name = Debug;
+ };
+ 5BFF68412AD689C90099D333 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"MediaPickerExample/Preview Content\"";
+ DEVELOPMENT_TEAM = FZXCM5CJ7P;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = MediaPickerExample/Info.plist;
+ INFOPLIST_KEY_NSCameraUsageDescription = "Grant access to camera to be able to take photos and videos";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Grant access to microphone to be able to take videos";
+ INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Grant access to photo library to be able to select photos";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.exyte.MediaPickerExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- 1384FEFE283F90C800C9AAC1 /* Build configuration list for PBXProject "Examples" */ = {
+ 5BFF682C2AD689C80099D333 /* Build configuration list for PBXProject "MediaPickerExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 1384FEFF283F90C800C9AAC1 /* Debug */,
- 1384FF00283F90C800C9AAC1 /* Release */,
+ 5BFF683D2AD689C90099D333 /* Debug */,
+ 5BFF683E2AD689C90099D333 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 1384FF13283F912F00C9AAC1 /* Build configuration list for PBXNativeTarget "Example-iOS" */ = {
+ 5BFF683F2AD689C90099D333 /* Build configuration list for PBXNativeTarget "MediaPickerExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 1384FF14283F912F00C9AAC1 /* Debug */,
- 1384FF15283F912F00C9AAC1 /* Release */,
+ 5BFF68402AD689C90099D333 /* Debug */,
+ 5BFF68412AD689C90099D333 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
@@ -392,11 +401,11 @@
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
- 5B1CA4DF29F137E100245B8B /* ExyteMediaPicker */ = {
+ 5B2068772D9BE32700485134 /* ExyteMediaPicker */ = {
isa = XCSwiftPackageProductDependency;
productName = ExyteMediaPicker;
};
/* End XCSwiftPackageProductDependency section */
};
- rootObject = 1384FEFB283F90C800C9AAC1 /* Project object */;
+ rootObject = 5BFF68292AD689C80099D333 /* Project object */;
}
diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename to MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 100%
rename from Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
diff --git a/MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/MediaPickerExample/MediaPickerExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/MediaPickerExample/MediaPickerExample.xcodeproj/xcshareddata/xcschemes/MediaPickerExample.xcscheme b/MediaPickerExample/MediaPickerExample.xcodeproj/xcshareddata/xcschemes/MediaPickerExample.xcscheme
new file mode 100644
index 0000000..949dbd1
--- /dev/null
+++ b/MediaPickerExample/MediaPickerExample.xcodeproj/xcshareddata/xcschemes/MediaPickerExample.xcscheme
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MediaPickerExample/MediaPickerExample/AppDelegate.swift b/MediaPickerExample/MediaPickerExample/AppDelegate.swift
new file mode 100644
index 0000000..34e976e
--- /dev/null
+++ b/MediaPickerExample/MediaPickerExample/AppDelegate.swift
@@ -0,0 +1,44 @@
+//
+// AppDelegate.swift
+// Example-iOS
+//
+// Created by Alexandra Afonasova on 20.10.2022.
+//
+
+import Foundation
+import UIKit
+
+final class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
+
+ private var orientationLock = UIInterfaceOrientationMask.all
+
+ func lockOrientationToPortrait() {
+ orientationLock = .portrait
+ if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
+ scene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
+ }
+ }
+
+ func unlockOrientation() {
+ orientationLock = .all
+ if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
+ let currentOrientation = UIDevice.current.orientation
+ let newOrientation: UIInterfaceOrientationMask
+
+ switch currentOrientation {
+ case .portrait: newOrientation = .portrait
+ case .portraitUpsideDown: newOrientation = .portraitUpsideDown
+ case .landscapeLeft: newOrientation = .landscapeLeft
+ case .landscapeRight: newOrientation = .landscapeRight
+ default: newOrientation = .all
+ }
+
+ scene.requestGeometryUpdate(.iOS(interfaceOrientations: newOrientation))
+ }
+ }
+
+ func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
+ return orientationLock
+ }
+
+}
diff --git a/Examples/Example-iOS/Application.swift b/MediaPickerExample/MediaPickerExample/Application.swift
similarity index 88%
rename from Examples/Example-iOS/Application.swift
rename to MediaPickerExample/MediaPickerExample/Application.swift
index 9f3f296..c3dddd7 100644
--- a/Examples/Example-iOS/Application.swift
+++ b/MediaPickerExample/MediaPickerExample/Application.swift
@@ -16,6 +16,7 @@ struct Application: App {
WindowGroup {
ContentView()
.environmentObject(appDelegate)
+ .preferredColorScheme(.dark)
}
}
}
diff --git a/Examples/Example-iOS/Assets.xcassets/AccentColor.colorset/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/AccentColor.colorset/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/AccentColor.colorset/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/AccentColor.colorset/Contents.json
diff --git a/Examples/Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/Examples/Example-iOS/Assets.xcassets/chevron.imageset/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/Chevron.imageset/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/chevron.imageset/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/Chevron.imageset/Contents.json
diff --git a/Examples/Example-iOS/Assets.xcassets/chevron.imageset/chevron.pdf b/MediaPickerExample/MediaPickerExample/Assets.xcassets/Chevron.imageset/chevron.pdf
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/chevron.imageset/chevron.pdf
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/Chevron.imageset/chevron.pdf
diff --git a/Examples/Example-iOS/Assets.xcassets/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/Contents.json
diff --git a/Examples/Example-iOS/Assets.xcassets/CustomGreen.colorset/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/CustomGreen.colorset/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/CustomGreen.colorset/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/CustomGreen.colorset/Contents.json
diff --git a/Examples/Example-iOS/Assets.xcassets/CustomPurple.colorset/Contents.json b/MediaPickerExample/MediaPickerExample/Assets.xcassets/CustomPurple.colorset/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Assets.xcassets/CustomPurple.colorset/Contents.json
rename to MediaPickerExample/MediaPickerExample/Assets.xcassets/CustomPurple.colorset/Contents.json
diff --git a/Examples/Example-iOS/ContentView.swift b/MediaPickerExample/MediaPickerExample/ContentView.swift
similarity index 94%
rename from Examples/Example-iOS/ContentView.swift
rename to MediaPickerExample/MediaPickerExample/ContentView.swift
index 9785f6a..5ae8aae 100644
--- a/Examples/Example-iOS/ContentView.swift
+++ b/MediaPickerExample/MediaPickerExample/ContentView.swift
@@ -7,7 +7,6 @@
import SwiftUI
import ExyteMediaPicker
-import Combine
struct ContentView: View {
@@ -54,12 +53,13 @@ struct ContentView: View {
}
// MARK: - Default media picker
- .sheet(isPresented: $showDefaultMediaPicker) {
+ .fullScreenCover(isPresented: $showDefaultMediaPicker) {
MediaPicker(
isPresented: $showDefaultMediaPicker,
onChange: { medias = $0 }
)
- .showLiveCameraCell()
+ .mediaSelectionType(.photoAndVideo)
+ .mediaSelectionStyle(.count)
.orientationHandler {
switch $0 {
case .lock: appDelegate.lockOrientationToPortrait()
diff --git a/Examples/Example-iOS/CustomizedMediaPicker.swift b/MediaPickerExample/MediaPickerExample/CustomizedMediaPicker.swift
similarity index 61%
rename from Examples/Example-iOS/CustomizedMediaPicker.swift
rename to MediaPickerExample/MediaPickerExample/CustomizedMediaPicker.swift
index 4eca860..8f47832 100644
--- a/Examples/Example-iOS/CustomizedMediaPicker.swift
+++ b/MediaPickerExample/MediaPickerExample/CustomizedMediaPicker.swift
@@ -17,7 +17,9 @@ struct CustomizedMediaPicker: View {
@State private var mediaPickerMode = MediaPickerMode.photos
@State private var selectedAlbum: Album?
+ @State private var currentFullscreenMedia: Media?
@State private var showAlbumsDropDown: Bool = false
+ @State private var videoIsBeingRecorded: Bool = false
let maxCount: Int = 5
@@ -25,12 +27,13 @@ struct CustomizedMediaPicker: View {
MediaPicker(
isPresented: $isPresented,
onChange: { medias = $0 },
- albumSelectionBuilder: { _, albumSelectionView in
+ albumSelectionBuilder: { _, albumSelectionView, _ in
VStack {
headerView
albumSelectionView
Spacer()
footerView
+ .background(Color.black)
}
.background(Color.black)
},
@@ -40,28 +43,50 @@ struct CustomizedMediaPicker: View {
Spacer()
Button("Done", action: { isPresented = false })
}
+ .padding()
cameraSelectionView
HStack {
Button("Cancel", action: cancelClosure)
Spacer()
Button(action: addMoreClosure) {
Text("Take more photos")
- .font(.headline)
- .foregroundColor(.black)
- .padding()
- }
- .background {
- Color("CustomGreen")
- .cornerRadius(16)
+ .greenButtonStyle()
}
}
+ .padding()
}
.background(Color.black)
+ },
+ cameraViewBuilder: { cameraSheetView, cancelClosure, showPreviewClosure, takePhotoClosure, startVideoCaptureClosure, stopVideoCaptureClosure, _, _ in
+ cameraSheetView
+ .overlay(alignment: .topLeading) {
+ HStack {
+ Button("Cancel") { cancelClosure() }
+ .foregroundColor(Color("CustomPurple"))
+ Spacer()
+ Button("Done") { showPreviewClosure() }
+ .foregroundColor(Color("CustomPurple"))
+ }
+ .padding()
+ }
+ .overlay(alignment: .bottom) {
+ HStack {
+ Button("Take photo") { takePhotoClosure() }
+ .greenButtonStyle()
+ Button(videoIsBeingRecorded ? "Stop video capture" : "Capture video") {
+ videoIsBeingRecorded ? stopVideoCaptureClosure() : startVideoCaptureClosure()
+ videoIsBeingRecorded.toggle()
+ }
+ .greenButtonStyle()
+ }
+ .padding()
+ }
}
)
- .showLiveCameraCell()
+ .liveCameraCell(.prominant)
.albums($albums)
.pickerMode($mediaPickerMode)
+ .currentFullscreenMedia($currentFullscreenMedia)
.orientationHandler {
switch $0 {
case .lock: appDelegate.lockOrientationToPortrait()
@@ -72,15 +97,15 @@ struct CustomizedMediaPicker: View {
.mediaSelectionLimit(maxCount)
.mediaPickerTheme(
main: .init(
- albumSelectionBackground: .black,
+ pickerBackground: .black,
fullscreenPhotoBackground: .black
),
selection: .init(
- emptyTint: .white,
- emptyBackground: .black.opacity(0.25),
- selectedTint: Color("CustomPurple"),
- fullscreenTint: .white
- )
+ cellSelectedBorder: .customPurple,
+ cellSelectedBackground: .customPurple,
+ fullscreenEmptyBorder: .customPurple,
+ fullscreenSelectedBorder: .customPurple,
+ fullscreenSelectedBackground: .customPurple)
)
.overlay(alignment: .topLeading) {
if showAlbumsDropDown {
@@ -116,13 +141,12 @@ struct CustomizedMediaPicker: View {
var footerView: some View {
HStack {
- Button {
- medias = []
- isPresented = false
- } label: {
- Text("Cancel")
- .foregroundColor(.white.opacity(0.7))
- }
+ TextField("", text: .constant(""), prompt: Text("Add a caption").foregroundColor(.gray))
+ .padding()
+ .background {
+ Color.white.opacity(0.2)
+ .cornerRadius(6)
+ }
Spacer(minLength: 70)
@@ -137,18 +161,12 @@ struct CustomizedMediaPicker: View {
.background(Color.white)
.clipShape(Circle())
}
- .font(.headline)
- .foregroundColor(.black)
- .padding(.horizontal)
- .padding(.vertical, 15)
.frame(maxWidth: .infinity)
}
- .background {
- Color("CustomGreen")
- .cornerRadius(16)
- }
+ .greenButtonStyle()
}
.padding(.horizontal)
+ .padding(.vertical, 8)
}
var albumsDropdown: some View {
@@ -167,3 +185,15 @@ struct CustomizedMediaPicker: View {
.frame(maxHeight: 300)
}
}
+
+extension View {
+ func greenButtonStyle() -> some View {
+ self.font(.headline)
+ .foregroundColor(.black)
+ .padding()
+ .background {
+ Color("CustomGreen")
+ .cornerRadius(16)
+ }
+ }
+}
diff --git a/Examples/Example-iOS/FilterMediaPicker.swift b/MediaPickerExample/MediaPickerExample/FilterMediaPicker.swift
similarity index 90%
rename from Examples/Example-iOS/FilterMediaPicker.swift
rename to MediaPickerExample/MediaPickerExample/FilterMediaPicker.swift
index 08b879f..cd734a4 100644
--- a/Examples/Example-iOS/FilterMediaPicker.swift
+++ b/MediaPickerExample/MediaPickerExample/FilterMediaPicker.swift
@@ -19,18 +19,18 @@ struct FilterMediaPicker: View {
isPresented: $isPresented,
onChange: { medias = $0 }
)
- .applyFilter { await isMostlyBlueAndGreen($0) }
+ .applyFilter { await isMostlyRed($0) }
}
- private func isMostlyBlueAndGreen(_ media: Media) async -> Media? {
+ private func isMostlyRed(_ media: Media) async -> Media? {
guard let data = await media.getThumbnailData() else { return nil }
guard let uiImage = UIImage(data: data) else { return nil }
let color = uiImage.averageColor
var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
- if blue > red, green > red {
- return media
+ if red > blue, red > green {
+ return media
} else {
return nil
}
diff --git a/MediaPickerExample/MediaPickerExample/Info.plist b/MediaPickerExample/MediaPickerExample/Info.plist
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/MediaPickerExample/MediaPickerExample/Info.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Examples/Example-iOS/MediaCell.swift b/MediaPickerExample/MediaPickerExample/MediaCell.swift
similarity index 90%
rename from Examples/Example-iOS/MediaCell.swift
rename to MediaPickerExample/MediaPickerExample/MediaCell.swift
index ef09dfc..b263ea5 100644
--- a/Examples/Example-iOS/MediaCell.swift
+++ b/MediaPickerExample/MediaPickerExample/MediaCell.swift
@@ -34,8 +34,8 @@ struct MediaCell: View {
}
}
.frame(width: g.size.width, height: g.size.height)
- .clipped()
}
+ .clipped()
.task {
await viewModel.onStart()
}
@@ -45,6 +45,7 @@ struct MediaCell: View {
}
}
+@MainActor
final class MediaCellViewModel: ObservableObject {
let media: Media
@@ -64,12 +65,12 @@ final class MediaCellViewModel: ObservableObject {
let url = await media.getURL()
guard let url = url else { return }
- DispatchQueue.main.async {
- switch self.media.type {
+ DispatchQueue.main.async { [weak self, media] in
+ switch media.type {
case .image:
- self.imageUrl = url
+ self?.imageUrl = url
case .video:
- self.player = AVPlayer(url: url)
+ self?.player = AVPlayer(url: url)
}
}
}
diff --git a/Examples/Example-iOS/Preview Content/Preview Assets.xcassets/Contents.json b/MediaPickerExample/MediaPickerExample/Preview Content/Preview Assets.xcassets/Contents.json
similarity index 100%
rename from Examples/Example-iOS/Preview Content/Preview Assets.xcassets/Contents.json
rename to MediaPickerExample/MediaPickerExample/Preview Content/Preview Assets.xcassets/Contents.json
diff --git a/Package.swift b/Package.swift
index 4b56e26..3288c9d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,24 +1,32 @@
-// swift-tools-version: 5.6
-// The swift-tools-version declares the minimum version of Swift required to build this package.
+// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "ExyteMediaPicker",
platforms: [
- .iOS(.v15),
- .macOS(.v11)
+ .iOS(.v17)
],
products: [
.library(
name: "ExyteMediaPicker",
targets: ["ExyteMediaPicker"]),
],
- dependencies: [],
+ dependencies: [
+ .package(
+ url: "https://github.com/exyte/AnchoredPopup.git",
+ from: "1.0.0"
+ )
+ ],
targets: [
.target(
name: "ExyteMediaPicker",
- dependencies: []
+ dependencies: [
+ .product(name: "AnchoredPopup", package: "AnchoredPopup")
+ ],
+ swiftSettings: [
+ .enableExperimentalFeature("StrictConcurrency")
+ ]
),
.testTarget(
name: "MediaPickerTests",
diff --git a/README.md b/README.md
index ec4faf0..ae039fc 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,8 @@

[](https://swiftpackageindex.com/exyte/MediaPicker)
[](https://swiftpackageindex.com/exyte/MediaPicker)
-[](https://swiftpackageindex.com/exyte/MediaPicker)
-[](https://cocoapods.org/pods/ExyteMediaPicker)
-[](https://github.com/Carthage/Carthage)
+[](https://swiftpackageindex.com/exyte/MediaPicker)
+[](https://cocoapods.org/pods/ExyteMediaPicker)
[](https://opensource.org/licenses/MIT)
# Features
@@ -68,14 +67,16 @@ After making one photo, you see a preview of it and a little plus icon, by tappi
`onChange` - a closure that returns the selected media every time the selection changes
### Init - optional view builders
-You can pass two view builders in order to add your own buttons and other elements to media picker screens. First screen you can customize is default photos grid view. Pass `albumSelectionBuilder` closure like this to replace the standard one with your own view:
+You can pass 1-3 view builders in order to add your own buttons and other elements to media picker screens. You can pass all, some or none of these when creating your `MediaPicker` (see the custom picker in the example project for usage example). First screen you can customize is default photos grid view. Pass `albumSelectionBuilder` closure like this to replace the standard one with your own view:
```swift
MediaPicker(
isPresented: $isPresented,
onChange: { selectedMedia = $0 },
- albumSelectionBuilder: { defaultHeaderView, albumSelectionView in
+ albumSelectionBuilder: { defaultHeaderView, albumSelectionView, isInFullscreen in
VStack {
- defaultHeaderView
+ if !isInFullscreen {
+ defaultHeaderView
+ }
albumSelectionView
Spacer()
footerView
@@ -88,6 +89,7 @@ MediaPicker(
`albumSelectionBuilder` gives you two views to work with:
- `defaultHeaderView` - a default looking `header` with photos/albums mode switcher
- `albumSelectionView` - the photos grid itself
+- `isInFullscreen` - is fullscreen photo details screen displayed. Use for example to hide the header while in fullscreen mode.
The second customizable screen is the one you see after taking a photo. Pass `cameraSelectionBuilder` like this:
```swift
@@ -117,13 +119,56 @@ MediaPicker(
- `cancelClosure` - show confirmation and return to photos grid screen if confirmed
- `cameraSelectionView` - swipable camera photos preview collection itself
-You can pass one, both or none of these when creating your `MediaPicker`. (see the custom picker in the example project for usage example)
+The last one is live camera screen
+
+```swift
+MediaPicker(
+ isPresented: $isPresented,
+ onChange: { selectedMedia = $0 },
+ cameraViewBuilder: { cameraSheetView, cancelClosure, showPreviewClosure, takePhotoClosure, startVideoCaptureClosure, stopVideoCaptureClosure, toggleFlash, flipCamera in
+ cameraSheetView
+ .overlay(alignment: .topLeading) {
+ HStack {
+ Button("Cancel") { cancelClosure() }
+ .foregroundColor(Color("CustomPurple"))
+ Spacer()
+ Button("Done") { showPreviewClosure() }
+ .foregroundColor(Color("CustomPurple"))
+ }
+ .padding()
+ }
+ .overlay(alignment: .bottom) {
+ HStack {
+ Button("Take photo") { takePhotoClosure() }
+ .greenButtonStyle()
+ Button(videoIsBeingRecorded ? "Stop video capture" : "Capture video") {
+ videoIsBeingRecorded ? stopVideoCaptureClosure() : startVideoCaptureClosure()
+ videoIsBeingRecorded.toggle()
+ }
+ .greenButtonStyle()
+ }
+ .padding()
+ }
+ }
+)
+```
+
+`cameraViewBuilder` live camera capture view and a lot of closures to do with as you please:
+- `cameraSheetView` - live camera capture view
+- `cancelClosure` - if you want to display "are you sure" before closing
+- `showPreviewClosure` - shows preview of taken photos
+- `cancelClosure` - if you want to display "are you sure" before closing
+- `startVideoCaptureClosure` - starts video capture, you'll need a bollean varialbe to track recording state
+- `stopVideoCaptureClosure` - stops video capture
+- `toggleFlash` - flash off/on
+- `flipCamera` - camera back/front
## Available modifiers
-`showLiveCameraCell` - show live camera feed cell in the top left corner of the gallery grid
+`liveCameraCell` - show live camera feed cell in the top left corner of the gallery grid, type .none, .small, .prominant (2 cell height) defaults to .small
`mediaSelectionType` - limit displayed media type: .photo, .video or both
`mediaSelectionStyle` - a way to display selected/unselected media state: a counter or a simple checkmark
`mediaSelectionLimit` - the maximum selection quantity allowed, 'nil' for unlimited selection
+`showFullscreenPreview` - if true - tap on media opens fullscreen preview, if false - tap on image immediately selects this image and closes the picker
### Available modifiers - filtering
`applyFilter((Media) async -> Media?)` - pass a closure to apply to each of medias individually. Closures's return type is `Media?`: return `Media` the closure gives to you if you want it to be displayed on photo grid, or `nil` if you want to exclude it. The code you apply to each media can be asyncronous (using async/await syntactics, check out `FilterMediaPicker` in example project)
@@ -169,8 +214,8 @@ Here is an example of how you can customize colors and elements to create a cust
To try out the MediaPicker examples:
- Clone the repo `https://github.com/exyte/MediaPicker.git`
-- Open `Examples/Examples.xcworkspace` in the Xcode
-- Run it!
+- Open `MediaPickerExample.xcodeproj` in the Xcode
+- Try it!
## Installation
@@ -182,32 +227,24 @@ dependencies: [
]
```
-### CocoaPods
-
-```ruby
-pod 'ExyteMediaPicker'
-```
-
-### Carthage
-
-```ogdl
-github "Exyte/MediaPicker"
-```
-
## Requirements
-* iOS 15+
-* Xcode 13+
+* iOS 17+
+* Xcode 15+
## Our other open source SwiftUI libraries
[PopupView](https://github.com/exyte/PopupView) - Toasts and popups library
+[AnchoredPopup](https://github.com/exyte/AnchoredPopup) - Anchored Popup grows "out" of a trigger view (similar to Hero animation)
[Grid](https://github.com/exyte/Grid) - The most powerful Grid container
-[ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll
-[AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with number of preset animations
-[Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker
+[ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll
+[AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with a number of preset animations
+[Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker
+[OpenAI](https://github.com/exyte/OpenAI) Wrapper lib for [OpenAI REST API](https://platform.openai.com/docs/api-reference/introduction)
+[AnimatedGradient](https://github.com/exyte/AnimatedGradient) - Animated linear gradient
[ConcentricOnboarding](https://github.com/exyte/ConcentricOnboarding) - Animated onboarding flow
[FloatingButton](https://github.com/exyte/FloatingButton) - Floating button menu
[ActivityIndicatorView](https://github.com/exyte/ActivityIndicatorView) - A number of animated loading indicators
[ProgressIndicatorView](https://github.com/exyte/ProgressIndicatorView) - A number of animated progress indicators
+[FlagAndCountryCode](https://github.com/exyte/FlagAndCountryCode) - Phone codes and flags for every country
[SVGView](https://github.com/exyte/SVGView) - SVG parser
[LiquidSwipe](https://github.com/exyte/LiquidSwipe) - Liquid navigation animation
diff --git a/Sources/ExyteMediaPicker/Drivers/AVFoundation/LiveCameraViewModel.swift b/Sources/ExyteMediaPicker/Drivers/AVFoundation/LiveCameraViewModel.swift
deleted file mode 100644
index 30c3f51..0000000
--- a/Sources/ExyteMediaPicker/Drivers/AVFoundation/LiveCameraViewModel.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-// Created by Alex.M on 07.06.2022.
-//
-
-import Foundation
-import AVFoundation
-import CoreImage
-import UIKit.UIImage
-
-/**
- Object with live camera preview. Use `captureImage: UIImage` publisher.
-
- Used solution (with adapt for new iOs version): https://medium.com/ios-os-x-development/ios-camera-frames-extraction-d2c0f80ed05a
- */
-class LiveCameraViewModel: NSObject, ObservableObject {
-
- private let quality = AVCaptureSession.Preset.medium
-
- private let sessionQueue = DispatchQueue(label: "LiveCameraPreviewQueue")
- let captureSession = AVCaptureSession()
-
- @Published public var capturedImage: UIImage = UIImage()
-
- override init() {
- super.init()
- sessionQueue.async { [weak self] in
- self?.configureSession()
- self?.captureSession.startRunning()
- }
- }
-
- func startSession() {
- sessionQueue.async { [weak self] in
- self?.captureSession.startRunning()
- }
- }
-
- func stopSession() {
- sessionQueue.async { [weak self] in
- self?.captureSession.stopRunning()
- }
- }
-
- deinit {
- captureSession.stopRunning()
- }
-
- private func configureSession() {
- captureSession.beginConfiguration()
- captureSession.sessionPreset = quality
- guard let captureDevice = captureDevice else { return }
- guard let captureDeviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { return }
- guard captureSession.canAddInput(captureDeviceInput) else { return }
- captureSession.addInput(captureDeviceInput)
- captureSession.commitConfiguration()
- }
-
- private var captureDevice: AVCaptureDevice? {
- AVCaptureDevice.default(for: .video)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/Permissions/PermissionsService.swift b/Sources/ExyteMediaPicker/Drivers/Permissions/PermissionsService.swift
deleted file mode 100644
index cb98df9..0000000
--- a/Sources/ExyteMediaPicker/Drivers/Permissions/PermissionsService.swift
+++ /dev/null
@@ -1,117 +0,0 @@
-//
-// Created by Alex.M on 08.06.2022.
-//
-
-import Foundation
-import Combine
-import AVFoundation
-import Photos
-
-final class PermissionsService: ObservableObject {
- @Published var cameraAction: CameraAction?
- @Published var photoLibraryAction: PhotoLibraryAction?
-
- private var subscriptions = Set()
-
- init() {
- photoLibraryChangePermissionPublisher
- .sink { [weak self] in
- self?.reload()
- }
- .store(in: &subscriptions)
- reload()
- }
-
- func reload() {
- checkCameraAuthorizationStatus()
- checkPhotoLibraryAuthorizationStatus()
- }
-
- /// photoLibraryChangePermissionPublisher gets called multiple times even when nothing changed in photo library, so just use this one to make sure the closure runs exactly once
- static func requestPermission(_ permissionGrantedClosure: @escaping ()->()) {
- PHPhotoLibrary.requestAuthorization { status in
- if status == .authorized || status == .limited {
- permissionGrantedClosure()
- }
- }
- }
-}
-
-private extension PermissionsService {
- func checkCameraAuthorizationStatus() {
- let status = AVCaptureDevice.authorizationStatus(for: .video)
- handle(camera: status)
- }
-
- func handle(camera status: AVAuthorizationStatus) {
- var result: CameraAction?
-#if targetEnvironment(simulator)
- result = .unavailable
-#else
- switch status {
- case .notDetermined:
- AVCaptureDevice.requestAccess(for: .video) { [weak self] _ in
- self?.checkCameraAuthorizationStatus()
- }
- case .restricted:
- result = .unavailable
- case .denied:
- result = .authorize
- case .authorized:
- // Do nothing
- break
- @unknown default:
- result = .unknown
- }
-#endif
- DispatchQueue.main.async { [weak self] in
- self?.cameraAction = result
- }
- }
-
- func checkPhotoLibraryAuthorizationStatus() {
- let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
- handle(photoLibrary: status)
- }
-
- func handle(photoLibrary status: PHAuthorizationStatus) {
- var result: PhotoLibraryAction?
- switch status {
- case .notDetermined:
- PHPhotoLibrary.requestAuthorization { [weak self] status in
- self?.handle(photoLibrary: status)
- }
- case .restricted:
- // TODO: Make sure that access can't change when status == .restricted
- result = .unavailable
- case .denied:
- result = .authorize
- case .authorized:
- // Do nothing
- break
- case .limited:
- result = .selectMore
- @unknown default:
- result = .unknown
- }
-
- DispatchQueue.main.async { [weak self] in
- self?.photoLibraryAction = result
- }
- }
-}
-
-extension PermissionsService {
- enum CameraAction {
- case authorize
- case unavailable
- case unknown
- }
-
- enum PhotoLibraryAction {
- case selectMore
- case authorize
- case unavailable
- case unknown
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/AlbumsProviderProtocol.swift b/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/AlbumsProviderProtocol.swift
deleted file mode 100644
index 7f47f20..0000000
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/AlbumsProviderProtocol.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-//
-// Created by Alex.M on 09.06.2022.
-//
-
-import Foundation
-import Combine
-
-protocol AlbumsProviderProtocol {
- var albums: AnyPublisher<[AlbumModel], Never> { get }
-
- func reload()
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/DefaultAlbumsProvider.swift b/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/DefaultAlbumsProvider.swift
deleted file mode 100644
index 28b4cbb..0000000
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Albums/DefaultAlbumsProvider.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-//
-// Created by Alex.M on 10.06.2022.
-//
-
-import Foundation
-import Combine
-import Photos
-
-final class DefaultAlbumsProvider: AlbumsProviderProtocol {
-
- private var subject = CurrentValueSubject<[AlbumModel], Never>([])
- private var albumsCancellable: AnyCancellable?
- private var permissionCancellable: AnyCancellable?
-
- var albums: AnyPublisher<[AlbumModel], Never> {
- subject.eraseToAnyPublisher()
- }
-
- var mediaSelectionType: MediaSelectionType = .photoAndVideo
-
- func reload() {
- PermissionsService.requestPermission { [ weak self] in
- self?.reloadInternal()
- }
- }
-
- func reloadInternal() {
- albumsCancellable = [PHAssetCollectionType.album, .smartAlbum]
- .publisher
- .map { fetchAlbums(type: $0) }
- .scan([], +)
- .receive(on: DispatchQueue.main)
- .sink { [weak self] in
- self?.subject.send($0)
- }
- }
-}
-
-private extension DefaultAlbumsProvider {
-
- func fetchAlbums(type: PHAssetCollectionType) -> [AlbumModel] {
- let options = PHFetchOptions()
- options.includeAssetSourceTypes = [.typeUserLibrary, .typeiTunesSynced, .typeCloudShared]
- options.sortDescriptors = [NSSortDescriptor(key: "localizedTitle", ascending: true)]
-
- let collections = PHAssetCollection.fetchAssetCollections(
- with: type,
- subtype: .any,
- options: options
- )
-
- if collections.count == 0 {
- return []
- }
- var albums: [AlbumModel] = []
-
- for index in 0...(collections.count - 1) {
- let collection = collections[index]
- let options = PHFetchOptions()
- options.sortDescriptors = [
- NSSortDescriptor(key: "creationDate", ascending: false)
- ]
- options.fetchLimit = 1
- let fetchResult = PHAsset.fetchAssets(in: collection, options: options)
- if fetchResult.count == 0 {
- continue
- }
- let preview = MediasProvider.map(fetchResult: fetchResult, mediaSelectionType: mediaSelectionType).first
- let album = AlbumModel(preview: preview, source: collection)
- albums.append(album)
- }
- return albums
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/MediasProviderProtocol.swift b/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/MediasProviderProtocol.swift
deleted file mode 100644
index ce73c1c..0000000
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/MediasProviderProtocol.swift
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// Created by Alex.M on 09.06.2022.
-//
-
-import Foundation
-import Combine
-import Photos
-import SwiftUI
-
-protocol MediasProviderProtocol {
- func reload()
- func cancel()
-
- var assetMediaModelsPublisher: CurrentValueSubject<[AssetMediaModel], Never> { get }
-}
-
-class BaseMediasProvider: MediasProviderProtocol {
- var selectionParamsHolder: SelectionParamsHolder
- var filterClosure: MediaPicker.FilterClosure?
- var massFilterClosure: MediaPicker.MassFilterClosure?
-
- @Binding var showingLoadingCell: Bool
-
- var assetMediaModelsPublisher = CurrentValueSubject<[AssetMediaModel], Never>([])
-
- @Published var cancellableTask: Task?
-
- private var cancellable: AnyCancellable?
-
- init(selectionParamsHolder: SelectionParamsHolder, filterClosure: MediaPicker.FilterClosure?, massFilterClosure: MediaPicker.MassFilterClosure?, showingLoadingCell: Binding) {
- self.selectionParamsHolder = selectionParamsHolder
- self.filterClosure = filterClosure
- self.massFilterClosure = massFilterClosure
- self._showingLoadingCell = showingLoadingCell
-
- cancellable = photoLibraryChangeLimitedPhotosPublisher
- .receive(on: RunLoop.main)
- .sink { [weak self] in
- self?.reload()
- }
- }
-
- func filterAndPublish(assets: [AssetMediaModel]) {
- if let filterClosure = filterClosure {
- showLoading(true)
- cancellableTask = Task {
- let serialQueue = DispatchQueue(label: "filterSerialQueue")
- var result = [AssetMediaModel]()
- await assets.asyncForEach {
- if cancellableTask?.isCancelled ?? false {
- return
- }
- if let media = await filterClosure(Media(source: $0)), let model = media.source as? AssetMediaModel {
- serialQueue.sync {
- result.append(model)
- print(result.count)
- assetMediaModelsPublisher.send(result)
- }
- }
- }
- showLoading(false)
- }
- } else if let massFilterClosure = massFilterClosure {
- showLoading(true)
- cancellableTask = Task {
- let result = await massFilterClosure(assets.map { Media(source: $0) })
- assetMediaModelsPublisher.send(result.compactMap { $0.source as? AssetMediaModel })
- showLoading(false)
- }
- }
- else {
- DispatchQueue.main.async { [weak self] in
- self?.assetMediaModelsPublisher.send(assets)
- }
- }
- }
-
- func showLoading(_ show: Bool) {
- DispatchQueue.main.async { [weak self] in
- self?.$showingLoadingCell.wrappedValue = show
- }
- }
-
- func reload() { }
-
- func cancel() {
- cancellableTask?.cancel()
- }
-}
-
-class MediasProvider {
-
- static func map(fetchResult: PHFetchResult, mediaSelectionType: MediaSelectionType) -> [AssetMediaModel] {
- var assetMediaModels: [AssetMediaModel] = []
-
- if fetchResult.count == 0 {
- return assetMediaModels
- }
-
- for index in 0...(fetchResult.count - 1) {
- let asset = fetchResult[index]
- if (asset.mediaType == .image && mediaSelectionType.allowsPhoto) || (asset.mediaType == .video && mediaSelectionType.allowsVideo) {
- assetMediaModels.append(AssetMediaModel(asset: asset))
- }
- }
- return assetMediaModels
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/PhotoLibraryChangeNotifier.swift b/Sources/ExyteMediaPicker/Drivers/PhotoKit/PhotoLibraryChangeNotifier.swift
deleted file mode 100644
index c28bced..0000000
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/PhotoLibraryChangeNotifier.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// Created by Alex.M on 08.06.2022.
-//
-
-import Foundation
-import Combine
-import Photos
-
-let photoLibraryChangePermissionNotification = Notification.Name(rawValue: "PhotoLibraryChangePermissionNotification")
-
-let photoLibraryChangeLimitedPhotosNotification = Notification.Name(rawValue: "PhotoLibraryChangeLimitedPhotosNotification")
-
-let photoLibraryChangePermissionPublisher = NotificationCenter.default
- .publisher(for: photoLibraryChangePermissionNotification)
- .map { _ in }
- .share()
-
-let photoLibraryChangeLimitedPhotosPublisher = NotificationCenter.default
- .publisher(for: photoLibraryChangeLimitedPhotosNotification)
- .map { _ in }
- .share()
-
-final class PhotoLibraryChangePermissionWatcher: NSObject, PHPhotoLibraryChangeObserver {
- override init() {
- super.init()
- PHPhotoLibrary.shared().register(self)
- }
-
- deinit {
- PHPhotoLibrary.shared().unregisterChangeObserver(self)
- }
-
- func photoLibraryDidChange(_ changeInstance: PHChange) {
- // gets called too often, even if nothing changed - a bug?
- NotificationCenter.default.post(
- name: photoLibraryChangePermissionNotification,
- object: nil)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/Selection/SelectionParamsHolder.swift b/Sources/ExyteMediaPicker/Drivers/Selection/SelectionParamsHolder.swift
deleted file mode 100644
index 6b98d2f..0000000
--- a/Sources/ExyteMediaPicker/Drivers/Selection/SelectionParamsHolder.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-//
-// SelectionParamsHolder.swift
-//
-//
-// Created by Alisa Mylnikova on 05.05.2023.
-//
-
-import SwiftUI
-
-final class SelectionParamsHolder: ObservableObject {
-
- @Published var mediaType: MediaSelectionType = .photoAndVideo
- @Published var selectionStyle: MediaSelectionStyle = .checkmark
- @Published var selectionLimit: Int? // if nill - unlimited
-}
-
-public enum MediaSelectionStyle {
- case checkmark
- case count
-}
-
-public enum MediaSelectionType {
- case photoAndVideo
- case photo
- case video
-
- var allowsPhoto: Bool {
- [.photoAndVideo, .photo].contains(self)
- }
-
- var allowsVideo: Bool {
- [.photoAndVideo, .video].contains(self)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/Theme/MediaPickerTheme.swift b/Sources/ExyteMediaPicker/Drivers/Theme/MediaPickerTheme.swift
deleted file mode 100644
index efb885f..0000000
--- a/Sources/ExyteMediaPicker/Drivers/Theme/MediaPickerTheme.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// Created by Alex.M on 06.07.2022.
-//
-
-import Foundation
-import SwiftUI
-
-public struct MediaPickerTheme {
- public let main: Main
- public let selection: Selection
- public let error: Error
-
- public init(main: MediaPickerTheme.Main = .init(),
- selection: MediaPickerTheme.Selection = .init(),
- error: MediaPickerTheme.Error = .init()) {
- self.main = main
- self.selection = selection
- self.error = error
- }
-}
-
-extension MediaPickerTheme {
- public struct Main {
- public let text: Color
- public let albumSelectionBackground: Color
- public let fullscreenPhotoBackground: Color
- public let cameraBackground: Color
- public let cameraSelectionBackground: Color
-
- public init(text: Color = Color(uiColor: .label),
- albumSelectionBackground: Color = Color(uiColor: .systemGroupedBackground),
- fullscreenPhotoBackground: Color = Color(uiColor: .systemGroupedBackground),
- cameraBackground: Color = .black,
- cameraSelectionBackground: Color = .black) {
- self.text = text
- self.albumSelectionBackground = albumSelectionBackground
- self.fullscreenPhotoBackground = fullscreenPhotoBackground
- self.cameraBackground = cameraBackground
- self.cameraSelectionBackground = cameraSelectionBackground
- }
- }
-
- public struct Selection {
- public let emptyTint: Color
- public let emptyBackground: Color
- public let selectedTint: Color
- public let selectedBackground: Color
- public let fullscreenTint: Color
-
- public init(emptyTint: Color = .white,
- emptyBackground: Color = .clear,
- selectedTint: Color = .blue,
- selectedBackground: Color = .white,
- fullscreenTint: Color = .blue) {
- self.emptyTint = emptyTint
- self.emptyBackground = emptyBackground
- self.selectedTint = selectedTint
- self.selectedBackground = selectedBackground
- self.fullscreenTint = fullscreenTint
- }
- }
-
- public struct Error {
- public let background: Color
- public let tint: Color
-
- public init(background: Color = .red.opacity(0.7),
- tint: Color = .white) {
- self.background = background
- self.tint = tint
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/UIKit/UIKit+Config.swift b/Sources/ExyteMediaPicker/Drivers/UIKit/UIKit+Config.swift
deleted file mode 100644
index da2b5be..0000000
--- a/Sources/ExyteMediaPicker/Drivers/UIKit/UIKit+Config.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// Created by Alex.M on 06.07.2022.
-//
-
-import UIKit
-
-func setupNavigationBarAppearance() {
- let appearance = UINavigationBarAppearance()
- appearance.backgroundColor = .white
- UINavigationBar.appearance().standardAppearance = appearance
-}
diff --git a/Sources/ExyteMediaPicker/Extensions/Set+Cancellable.swift b/Sources/ExyteMediaPicker/Extensions/Set+Cancellable.swift
deleted file mode 100644
index 5472465..0000000
--- a/Sources/ExyteMediaPicker/Extensions/Set+Cancellable.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-//
-// Created by Alex.M on 07.06.2022.
-//
-
-import Foundation
-import Combine
-
-extension Set where Element == AnyCancellable {
- mutating func cancelAll() {
- self = Set()
- }
-}
diff --git a/Sources/ExyteMediaPicker/Extensions/View+NotificationCenter.swift b/Sources/ExyteMediaPicker/Extensions/View+NotificationCenter.swift
deleted file mode 100644
index 048c15b..0000000
--- a/Sources/ExyteMediaPicker/Extensions/View+NotificationCenter.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// View+NotificationCenter.swift
-//
-//
-// Created by Alexandra Afonasova on 17.10.2022.
-//
-
-import SwiftUI
-
-extension View {
-
- func onEnteredBackground(perform: @escaping () -> Void) -> some View {
- let publisher = NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
- return onReceive(publisher) { _ in perform() }
- }
-
- func onEnteredForeground(perform: @escaping () -> Void) -> some View {
- let publisher = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
- return onReceive(publisher) { _ in perform() }
- }
-
- func onRotate(perform: @escaping (UIDeviceOrientation) -> Void) -> some View {
- let publisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
- return onReceive(publisher) { _ in perform(UIDevice.current.orientation) }
- }
-
-}
diff --git a/Sources/ExyteMediaPicker/Drivers/FileManager/FileManager.swift b/Sources/ExyteMediaPicker/Managers/FileManager.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Drivers/FileManager/FileManager.swift
rename to Sources/ExyteMediaPicker/Managers/FileManager.swift
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AlbumMediasProvider.swift b/Sources/ExyteMediaPicker/Managers/Medias/AlbumMediasProvider.swift
similarity index 74%
rename from Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AlbumMediasProvider.swift
rename to Sources/ExyteMediaPicker/Managers/Medias/AlbumMediasProvider.swift
index aba4f69..29eed7e 100644
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AlbumMediasProvider.swift
+++ b/Sources/ExyteMediaPicker/Managers/Medias/AlbumMediasProvider.swift
@@ -3,7 +3,6 @@
//
import Foundation
-import Combine
import Photos
import SwiftUI
@@ -11,25 +10,31 @@ final class AlbumMediasProvider: BaseMediasProvider {
let album: AlbumModel
- init(album: AlbumModel, selectionParamsHolder: SelectionParamsHolder, filterClosure: MediaPicker.FilterClosure? = nil, massFilterClosure: MediaPicker.MassFilterClosure? = nil, showingLoadingCell: Binding) {
+ init(album: AlbumModel, selectionParamsHolder: SelectionParamsHolder, filterClosure: MediaPicker.FilterClosure? = nil, massFilterClosure: MediaPicker.MassFilterClosure? = nil) {
self.album = album
- super.init(selectionParamsHolder: selectionParamsHolder, filterClosure: filterClosure, massFilterClosure: massFilterClosure, showingLoadingCell: showingLoadingCell)
+ super.init(selectionParamsHolder: selectionParamsHolder, filterClosure: filterClosure, massFilterClosure: massFilterClosure)
}
override func reload() {
- PermissionsService.requestPermission { [ weak self] in
- self?.reloadInternal()
+ PermissionsService.shared.requestPhotoLibraryPermission {
+ DispatchQueue.main.async { [weak self] in
+ self?.reloadInternal()
+ }
}
}
func reloadInternal() {
+ isLoading = true
+ defer {
+ isLoading = false
+ }
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [
NSSortDescriptor(key: "creationDate", ascending: false)
]
let fetchResult = PHAsset.fetchAssets(in: album.source, options: fetchOptions)
if fetchResult.count == 0 {
- assetMediaModelsPublisher.send([])
+ assetMediaModels = []
}
let assets = MediasProvider.map(fetchResult: fetchResult, mediaSelectionType: selectionParamsHolder.mediaType)
diff --git a/Sources/ExyteMediaPicker/Managers/Medias/AllMediasProvider.swift b/Sources/ExyteMediaPicker/Managers/Medias/AllMediasProvider.swift
new file mode 100644
index 0000000..b032b28
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Managers/Medias/AllMediasProvider.swift
@@ -0,0 +1,34 @@
+//
+// Created by Alex.M on 09.06.2022.
+//
+
+import Foundation
+
+import Foundation
+import Photos
+import SwiftUI
+
+final class AllMediasProvider: BaseMediasProvider {
+
+ override func reload() {
+ PermissionsService.shared.requestPhotoLibraryPermission {
+ DispatchQueue.main.async { [weak self] in
+ self?.reloadInternal()
+ }
+ }
+ }
+
+ func reloadInternal() {
+ isLoading = true
+ defer {
+ isLoading = false
+ }
+ let allPhotosOptions = PHFetchOptions()
+ allPhotosOptions.sortDescriptors = [
+ NSSortDescriptor(key: "creationDate", ascending: false)
+ ]
+ let allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
+ let assets = MediasProvider.map(fetchResult: allPhotos, mediaSelectionType: selectionParamsHolder.mediaType)
+ filterAndPublish(assets: assets)
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AllPhotosProvider.swift b/Sources/ExyteMediaPicker/Managers/Medias/AllPhotosProvider.swift
similarity index 77%
rename from Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AllPhotosProvider.swift
rename to Sources/ExyteMediaPicker/Managers/Medias/AllPhotosProvider.swift
index 0c609c1..64e90ba 100644
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/Medias/AllPhotosProvider.swift
+++ b/Sources/ExyteMediaPicker/Managers/Medias/AllPhotosProvider.swift
@@ -6,14 +6,16 @@ import Foundation
import Foundation
import Photos
-import Combine
import SwiftUI
+@MainActor
final class AllPhotosProvider: BaseMediasProvider {
override func reload() {
- PermissionsService.requestPermission { [ weak self] in
- self?.reloadInternal()
+ PermissionsService.shared.requestPhotoLibraryPermission {
+ DispatchQueue.main.async { [weak self] in
+ self?.reloadInternal()
+ }
}
}
diff --git a/Sources/ExyteMediaPicker/Managers/Medias/BaseMediasProvider.swift b/Sources/ExyteMediaPicker/Managers/Medias/BaseMediasProvider.swift
new file mode 100644
index 0000000..cacd9ff
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Managers/Medias/BaseMediasProvider.swift
@@ -0,0 +1,124 @@
+//
+// Created by Alex.M on 09.06.2022.
+//
+
+import Foundation
+import Photos
+import SwiftUI
+
+@MainActor
+class BaseMediasProvider: ObservableObject {
+ var selectionParamsHolder: SelectionParamsHolder
+ var filterClosure: MediaPicker.FilterClosure?
+ var massFilterClosure: MediaPicker.MassFilterClosure?
+
+ @Published var assetMediaModels = [AssetMediaModel]()
+ private var privateAssetMediaModels: [AssetMediaModel] = []
+
+ @Published var isLoading: Bool = false
+
+ private var timerTask: Task?
+ private var cancellableTask: Task?
+
+ init(selectionParamsHolder: SelectionParamsHolder, filterClosure: MediaPicker.FilterClosure?, massFilterClosure: MediaPicker.MassFilterClosure?) {
+ self.selectionParamsHolder = selectionParamsHolder
+ self.filterClosure = filterClosure
+ self.massFilterClosure = massFilterClosure
+ }
+
+ func filterAndPublish(assets: [AssetMediaModel]) {
+ isLoading = true
+ defer {
+ isLoading = false
+ }
+
+ if let filterClosure = filterClosure {
+ startPublishing()
+
+ cancellableTask = Task { [weak self] in
+ let serialQueue = DispatchQueue(label: "filterSerialQueue")
+ self?.privateAssetMediaModels = [AssetMediaModel]()
+
+ await withTaskGroup(of: AssetMediaModel?.self) { group in
+ for asset in assets {
+ group.addTask {
+ if Task.isCancelled { return nil }
+
+ let media = await Task.detached(priority: .userInitiated) {
+ return await filterClosure(Media(source: asset))
+ }.value
+
+ return media?.source as? AssetMediaModel
+ }
+ }
+
+ for await filteredMedia in group {
+ if let model = filteredMedia {
+ serialQueue.sync {
+ self?.privateAssetMediaModels.append(model)
+ }
+ }
+ }
+ }
+
+ self?.stopPublishing()
+ DispatchQueue.main.async {
+ self?.assetMediaModels = self?.privateAssetMediaModels ?? []
+ }
+ }
+ } else if let massFilterClosure = massFilterClosure {
+ cancellableTask = Task { [weak self] in
+ let result = await massFilterClosure(assets.map { Media(source: $0) })
+ self?.assetMediaModels = result.compactMap { $0.source as? AssetMediaModel }
+ }
+ }
+ else {
+ DispatchQueue.main.async { [weak self] in
+ self?.assetMediaModels = assets
+ }
+ }
+ }
+
+ func startPublishing() {
+ // Start a task that runs every second
+ timerTask = Task {
+ while !Task.isCancelled {
+ try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
+
+ await MainActor.run {
+ self.assetMediaModels = self.privateAssetMediaModels
+ }
+ }
+ }
+ }
+
+ func stopPublishing() {
+ timerTask?.cancel()
+ }
+
+ func reload() { }
+
+ func cancel() {
+ cancellableTask?.cancel()
+ stopPublishing()
+ }
+}
+
+class MediasProvider {
+
+ static func map(fetchResult: PHFetchResult, mediaSelectionType: MediaSelectionType) -> [AssetMediaModel] {
+ var assetMediaModels: [AssetMediaModel] = []
+
+ if fetchResult.count == 0 {
+ return assetMediaModels
+ }
+
+ for index in 0...(fetchResult.count - 1) {
+ let asset = fetchResult[index]
+ if (asset.mediaType == .image && mediaSelectionType.allowsPhoto) || (asset.mediaType == .video && mediaSelectionType.allowsVideo) {
+ assetMediaModels.append(AssetMediaModel(asset: asset))
+ }
+ }
+ return assetMediaModels
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Managers/Medias/DefaultAlbumsProvider.swift b/Sources/ExyteMediaPicker/Managers/Medias/DefaultAlbumsProvider.swift
new file mode 100644
index 0000000..1d405b8
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Managers/Medias/DefaultAlbumsProvider.swift
@@ -0,0 +1,88 @@
+//
+// Created by Alex.M on 10.06.2022.
+//
+
+import Foundation
+import Photos
+
+import Photos
+import SwiftUI
+
+@MainActor
+final class DefaultAlbumsProvider: ObservableObject {
+
+ @Published private(set) var albums: [AlbumModel] = []
+ @Published private(set) var isLoading: Bool = false
+
+ var mediaSelectionType: MediaSelectionType = .photoAndVideo
+ private var reloadTask: Task?
+
+ func reload() {
+ cancelReload()
+
+ PermissionsService.shared.requestPhotoLibraryPermission { [weak self] in
+ guard let self = self else { return }
+ DispatchQueue.main.async {
+ self.reloadTask = Task {
+ self.isLoading = true
+ await self.reloadInternal()
+ self.isLoading = false
+ }
+ }
+ }
+ }
+
+ func cancelReload() {
+ reloadTask?.cancel()
+ reloadTask = nil
+ }
+
+ private func reloadInternal() async {
+ let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
+ var allAlbums: [AlbumModel] = []
+
+ for type in albumTypes {
+ if Task.isCancelled { return }
+ let albums = fetchAlbums(type: type)
+ allAlbums.append(contentsOf: albums)
+ }
+
+ if Task.isCancelled { return }
+ self.albums = allAlbums
+ }
+
+ private func fetchAlbums(type: PHAssetCollectionType) -> [AlbumModel] {
+ let options = PHFetchOptions()
+ options.includeAssetSourceTypes = [.typeUserLibrary, .typeiTunesSynced, .typeCloudShared]
+ options.sortDescriptors = [NSSortDescriptor(key: "localizedTitle", ascending: true)]
+
+ let collections = PHAssetCollection.fetchAssetCollections(
+ with: type,
+ subtype: .any,
+ options: options
+ )
+
+ guard collections.count > 0 else { return [] }
+
+ var albums: [AlbumModel] = []
+
+ for index in 0.. 0 else { continue }
+
+ let preview = MediasProvider.map(fetchResult: fetchResult, mediaSelectionType: mediaSelectionType).first
+ let album = AlbumModel(preview: preview, source: collection)
+ albums.append(album)
+ }
+ return albums
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Drivers/MotionManager/MotionManager.swift b/Sources/ExyteMediaPicker/Managers/MotionManager.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Drivers/MotionManager/MotionManager.swift
rename to Sources/ExyteMediaPicker/Managers/MotionManager.swift
diff --git a/Sources/ExyteMediaPicker/Managers/PermissionsService.swift b/Sources/ExyteMediaPicker/Managers/PermissionsService.swift
new file mode 100644
index 0000000..9e35378
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Managers/PermissionsService.swift
@@ -0,0 +1,101 @@
+//
+// Created by Alex.M on 08.06.2022.
+//
+
+import Foundation
+import Combine
+import AVFoundation
+import Photos
+
+@MainActor
+final class PermissionsService: ObservableObject {
+
+ static var shared = PermissionsService()
+
+ @Published var cameraPermissionStatus: CameraPermissionStatus = .unknown
+ @Published var photoLibraryPermissionStatus: PhotoLibraryPermissionStatus = .unknown
+
+ /// photoLibraryChangePermissionPublisher gets called multiple times even when nothing changed in photo library, so just use this one to make sure the closure runs exactly once
+ func requestPhotoLibraryPermission(_ permissionGrantedClosure: @Sendable @escaping ()->()) {
+ Task {
+ let currentStatus = PHPhotoLibrary.authorizationStatus(for: .addOnly)
+ if currentStatus == .authorized || currentStatus == .limited {
+ permissionGrantedClosure()
+ return
+ }
+
+ let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
+ updatePhotoLibraryAuthorizationStatus()
+ if status == .authorized || status == .limited {
+ permissionGrantedClosure()
+ }
+ }
+ }
+
+ func requestCameraPermission() {
+ Task {
+ await AVCaptureDevice.requestAccess(for: .video)
+ updateCameraAuthorizationStatus()
+ }
+ }
+
+ func updatePhotoLibraryAuthorizationStatus() {
+ let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
+
+ let result: PhotoLibraryPermissionStatus
+ switch status {
+ case .authorized:
+ result = .authorized
+ case .limited:
+ result = .limited
+ case .restricted, .denied:
+ result = .unavailable
+ case .notDetermined:
+ result = .unknown
+ default:
+ result = .unknown
+ }
+
+ DispatchQueue.main.async { [weak self] in
+ self?.photoLibraryPermissionStatus = result
+ }
+ }
+
+ func updateCameraAuthorizationStatus() {
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
+
+ let result: CameraPermissionStatus
+#if targetEnvironment(simulator)
+ result = .unavailable
+#else
+ switch status {
+ case .authorized:
+ result = .authorized
+ case .restricted, .denied:
+ result = .unavailable
+ case .notDetermined:
+ result = .unknown
+ default:
+ result = .unknown
+ }
+#endif
+ DispatchQueue.main.async { [weak self] in
+ self?.cameraPermissionStatus = result
+ }
+ }
+}
+
+extension PermissionsService {
+ enum CameraPermissionStatus {
+ case authorized
+ case unavailable
+ case unknown
+ }
+
+ enum PhotoLibraryPermissionStatus {
+ case limited
+ case authorized
+ case unavailable
+ case unknown
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/PHAsset+Utils.swift b/Sources/ExyteMediaPicker/Managers/PhotoKit/PHAsset+Utils.swift
similarity index 83%
rename from Sources/ExyteMediaPicker/Drivers/PhotoKit/PHAsset+Utils.swift
rename to Sources/ExyteMediaPicker/Managers/PhotoKit/PHAsset+Utils.swift
index 031e08e..aad3265 100644
--- a/Sources/ExyteMediaPicker/Drivers/PhotoKit/PHAsset+Utils.swift
+++ b/Sources/ExyteMediaPicker/Managers/PhotoKit/PHAsset+Utils.swift
@@ -41,6 +41,7 @@ extension PHAsset {
if mediaType == .image {
let options = PHContentEditingInputRequestOptions()
+ options.isNetworkAccessAllowed = true
options.canHandleAdjustmentData = { _ -> Bool in
return true
}
@@ -101,28 +102,33 @@ extension PHAsset {
func getThumbnailURL() async -> URL? {
guard let url = await getURL() else { return nil }
- if mediaType == .image {
- return url
- }
-
- let asset: AVAsset = AVAsset(url: url)
- if let thumbnailData = asset.generateThumbnail() {
+ switch mediaType {
+ case .image:
+ guard let image = UIImage.from(url: url),
+ let thumbnailData = image.generateThumbnail()
+ else { return nil }
+ return FileManager.storeToTempDir(data: thumbnailData)
+ case .video:
+ let asset = AVAsset(url: url)
+ guard let thumbnailData = asset.generateThumbnail() else { return nil }
return FileManager.storeToTempDir(data: thumbnailData)
+ default: return nil
}
-
- return nil
}
func getThumbnailData() async -> Data? {
- if mediaType == .image {
- return try? await self.getData()
- }
- else if mediaType == .video {
+ switch mediaType {
+ case .image:
+ guard let data = try? await getData(),
+ let image = UIImage(data: data)
+ else { return nil }
+ return image.generateThumbnail()
+ case .video:
guard let url = await getURL() else { return nil }
let asset: AVAsset = AVAsset(url: url)
return asset.generateThumbnail()
+ default: return nil
}
- return nil
}
}
@@ -139,6 +145,7 @@ extension CGImage {
#if os(iOS)
extension PHAsset {
+ @MainActor
func image(size: CGSize, resultClosure: @escaping (UIImage?)->()) -> PHImageRequestID {
let requestSize = CGSize(width: size.width * UIScreen.main.scale, height: size.height * UIScreen.main.scale)
@@ -217,6 +224,32 @@ extension AVAsset {
}
}
+extension UIImage {
+
+ enum ThumbnailQuality {
+ case low
+ case medium
+ case high
+
+ var compressionValue: CGFloat {
+ switch self {
+ case .low: return 0.3
+ case .medium: return 0.6
+ case .high: return 0.8
+ }
+ }
+ }
+
+ static func from(url: URL) -> UIImage? {
+ guard let data = try? Data(contentsOf: url) else { return nil }
+ return UIImage(data: data)
+ }
+
+ func generateThumbnail(quality: ThumbnailQuality = .medium) -> Data? {
+ jpegData(compressionQuality: quality.compressionValue)
+ }
+}
+
enum AssetFetchError: Error {
case noImageData
case unknownType
diff --git a/Sources/ExyteMediaPicker/Drivers/PhotoKit/URL+Utils.swift b/Sources/ExyteMediaPicker/Managers/PhotoKit/URL+Utils.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Drivers/PhotoKit/URL+Utils.swift
rename to Sources/ExyteMediaPicker/Managers/PhotoKit/URL+Utils.swift
diff --git a/Sources/ExyteMediaPicker/Drivers/Selection/CameraSelectionService.swift b/Sources/ExyteMediaPicker/Managers/Selection/CameraSelectionService.swift
similarity index 57%
rename from Sources/ExyteMediaPicker/Drivers/Selection/CameraSelectionService.swift
rename to Sources/ExyteMediaPicker/Managers/Selection/CameraSelectionService.swift
index 6a46dc2..3ffb547 100644
--- a/Sources/ExyteMediaPicker/Drivers/Selection/CameraSelectionService.swift
+++ b/Sources/ExyteMediaPicker/Managers/Selection/CameraSelectionService.swift
@@ -12,6 +12,7 @@ final class CameraSelectionService: ObservableObject {
var mediaSelectionLimit: Int? // if nill - unlimited
var onChange: MediaPickerCompletionClosure? = nil
+ @Published private(set) var added: [URLMediaModel] = []
@Published private(set) var selected: [URLMediaModel] = []
var hasSelected: Bool {
@@ -30,9 +31,14 @@ final class CameraSelectionService: ObservableObject {
}
func onSelect(media: URLMediaModel) {
- if let index = selected.firstIndex(of: media) {
- selected.remove(at: index)
+ if added.contains(media) {
+ if let index = selected.firstIndex(of: media) {
+ selected.remove(at: index)
+ } else if fitsSelectionLimit {
+ selected.append(media)
+ }
} else {
+ added.append(media)
if fitsSelectionLimit {
selected.append(media)
}
@@ -40,8 +46,26 @@ final class CameraSelectionService: ObservableObject {
onChange?(mapToMedia())
}
- func index(of media: URLMediaModel) -> Int? {
- selected.firstIndex(of: media)
+ func onSelect(index: Int) {
+ guard added.indices.contains(index) else { return }
+ let media = added[index]
+ if let index = selected.firstIndex(of: media) {
+ selected.remove(at: index)
+ } else if fitsSelectionLimit {
+ selected.append(media)
+ }
+ onChange?(mapToMedia())
+ }
+
+ func isSelected(index: Int) -> Bool {
+ guard added.indices.contains(index) else { return false }
+ return selected.contains(added[index])
+ }
+
+ func selectedIndex(fromAddedIndex index: Int) -> Int? {
+ guard added.indices.contains(index) else { return nil }
+ let media = added[index]
+ return selected.firstIndex(of: media)
}
func mapToMedia() -> [Media] {
@@ -56,6 +80,7 @@ final class CameraSelectionService: ObservableObject {
func removeAll() {
selected.removeAll()
+ added.removeAll()
onChange?([])
}
}
diff --git a/Sources/ExyteMediaPicker/Managers/Selection/SelectionParamsHolder.swift b/Sources/ExyteMediaPicker/Managers/Selection/SelectionParamsHolder.swift
new file mode 100644
index 0000000..552cfe5
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Managers/Selection/SelectionParamsHolder.swift
@@ -0,0 +1,42 @@
+//
+// SelectionParamsHolder.swift
+//
+//
+// Created by Alisa Mylnikova on 05.05.2023.
+//
+
+import SwiftUI
+
+final public class SelectionParamsHolder: ObservableObject {
+
+ @Published public var mediaType: MediaSelectionType = .photoAndVideo
+ @Published public var selectionStyle: MediaSelectionStyle = .checkmark
+ @Published public var selectionLimit: Int? // if nil - unlimited
+ @Published public var showFullscreenPreview: Bool = true // if false, tap on image immediately selects this image and closes the picker
+
+ public init(mediaType: MediaSelectionType = .photoAndVideo, selectionStyle: MediaSelectionStyle = .checkmark, selectionLimit: Int? = nil, showFullscreenPreview: Bool = true) {
+ self.mediaType = mediaType
+ self.selectionStyle = selectionStyle
+ self.selectionLimit = selectionLimit
+ self.showFullscreenPreview = showFullscreenPreview
+ }
+}
+
+public enum MediaSelectionStyle {
+ case checkmark
+ case count
+}
+
+public enum MediaSelectionType {
+ case photoAndVideo
+ case photo
+ case video
+
+ var allowsPhoto: Bool {
+ [.photoAndVideo, .photo].contains(self)
+ }
+
+ var allowsVideo: Bool {
+ [.photoAndVideo, .video].contains(self)
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Drivers/Selection/SelectionService.swift b/Sources/ExyteMediaPicker/Managers/Selection/SelectionService.swift
similarity index 73%
rename from Sources/ExyteMediaPicker/Drivers/Selection/SelectionService.swift
rename to Sources/ExyteMediaPicker/Managers/Selection/SelectionService.swift
index cffcead..7a73669 100644
--- a/Sources/ExyteMediaPicker/Drivers/Selection/SelectionService.swift
+++ b/Sources/ExyteMediaPicker/Managers/Selection/SelectionService.swift
@@ -4,7 +4,6 @@
import Foundation
import SwiftUI
-import Combine
import Photos
final class SelectionService: ObservableObject {
@@ -65,23 +64,3 @@ final class SelectionService: ObservableObject {
}
}
}
-
-private extension SelectionService {
-
- func findAsset(identifier: String) -> Future {
- Future { promise in
- let options = PHFetchOptions()
- let photos = PHAsset.fetchAssets(with: options)
-
- var result: PHAsset?
-
- photos.enumerateObjects { (asset, _, stop) in
- if asset.localIdentifier == identifier {
- result = asset
- stop.pointee = true
- }
- }
- promise(.success(result))
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Data/AlbumModel.swift b/Sources/ExyteMediaPicker/Model/AlbumModel.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Data/AlbumModel.swift
rename to Sources/ExyteMediaPicker/Model/AlbumModel.swift
diff --git a/Sources/ExyteMediaPicker/Data/AssetMediaModel.swift b/Sources/ExyteMediaPicker/Model/AssetMediaModel.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Data/AssetMediaModel.swift
rename to Sources/ExyteMediaPicker/Model/AssetMediaModel.swift
diff --git a/Sources/ExyteMediaPicker/Data/Media.swift b/Sources/ExyteMediaPicker/Model/Media.swift
similarity index 74%
rename from Sources/ExyteMediaPicker/Data/Media.swift
rename to Sources/ExyteMediaPicker/Model/Media.swift
index 417ed80..017f09c 100644
--- a/Sources/ExyteMediaPicker/Data/Media.swift
+++ b/Sources/ExyteMediaPicker/Model/Media.swift
@@ -3,16 +3,19 @@
//
import Foundation
-import Combine
public enum MediaType {
case image
case video
}
-public struct Media: Identifiable, Equatable {
+public struct Media: Identifiable, Equatable, Sendable {
public var id = UUID()
- internal let source: MediaModelProtocol
+ public let source: MediaModelProtocol
+
+ public init(source: MediaModelProtocol) {
+ self.source = source
+ }
public static func == (lhs: Media, rhs: Media) -> Bool {
lhs.id == rhs.id
@@ -26,7 +29,9 @@ public extension Media {
}
var duration: CGFloat? {
- source.duration
+ get async {
+ await source.duration
+ }
}
func getURL() async -> URL? {
diff --git a/Sources/ExyteMediaPicker/Data/MediaModelProtocol.swift b/Sources/ExyteMediaPicker/Model/MediaModelProtocol.swift
similarity index 77%
rename from Sources/ExyteMediaPicker/Data/MediaModelProtocol.swift
rename to Sources/ExyteMediaPicker/Model/MediaModelProtocol.swift
index 0e526c9..bcea99f 100644
--- a/Sources/ExyteMediaPicker/Data/MediaModelProtocol.swift
+++ b/Sources/ExyteMediaPicker/Model/MediaModelProtocol.swift
@@ -7,9 +7,9 @@
import SwiftUI
-protocol MediaModelProtocol {
+public protocol MediaModelProtocol: Sendable {
var mediaType: MediaType? { get }
- var duration: CGFloat? { get }
+ var duration: CGFloat? { get async }
func getURL() async -> URL?
func getThumbnailURL() async -> URL?
diff --git a/Sources/ExyteMediaPicker/Data/Types.swift b/Sources/ExyteMediaPicker/Model/Types.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Data/Types.swift
rename to Sources/ExyteMediaPicker/Model/Types.swift
diff --git a/Sources/ExyteMediaPicker/Data/URLMediaModel.swift b/Sources/ExyteMediaPicker/Model/URLMediaModel.swift
similarity index 77%
rename from Sources/ExyteMediaPicker/Data/URLMediaModel.swift
rename to Sources/ExyteMediaPicker/Model/URLMediaModel.swift
index 85aa126..5586ca0 100644
--- a/Sources/ExyteMediaPicker/Data/URLMediaModel.swift
+++ b/Sources/ExyteMediaPicker/Model/URLMediaModel.swift
@@ -26,7 +26,15 @@ extension URLMediaModel: MediaModelProtocol {
}
var duration: CGFloat? {
- CMTimeGetSeconds(AVURLAsset(url: url).duration)
+ get async {
+ let asset = AVURLAsset(url: url)
+ do {
+ let duration = try await asset.load(.duration)
+ return CGFloat(CMTimeGetSeconds(duration))
+ } catch {
+ return nil
+ }
+ }
}
func getURL() async -> URL? {
@@ -36,7 +44,7 @@ extension URLMediaModel: MediaModelProtocol {
func getThumbnailURL() async -> URL? {
switch mediaType {
case .image:
- return url
+ return await url.getThumbnailURL()
case .video:
return await url.getThumbnailURL()
case .none:
@@ -51,7 +59,7 @@ extension URLMediaModel: MediaModelProtocol {
func getThumbnailData() async -> Data? {
switch mediaType {
case .image:
- return try? Data(contentsOf: url)
+ return await url.getThumbnailData()
case .video:
return await url.getThumbnailData()
case .none:
diff --git a/Sources/ExyteMediaPicker/Modifiers/MediaPickerThemeModifier.swift b/Sources/ExyteMediaPicker/Modifiers/MediaPickerThemeModifier.swift
deleted file mode 100644
index 9935246..0000000
--- a/Sources/ExyteMediaPicker/Modifiers/MediaPickerThemeModifier.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// Created by Alex.M on 06.07.2022.
-//
-
-import Foundation
-import SwiftUI
-
-struct MediaPickerThemeKey: EnvironmentKey {
- static var defaultValue: MediaPickerTheme = MediaPickerTheme()
-}
-
-extension EnvironmentValues {
- public var mediaPickerTheme: MediaPickerTheme {
- get { self[MediaPickerThemeKey.self] }
- set { self[MediaPickerThemeKey.self] = newValue }
- }
-}
-
-public extension View {
- func mediaPickerTheme(_ theme: MediaPickerTheme) -> some View {
- self.environment(\.mediaPickerTheme, theme)
- }
-
- func mediaPickerTheme(main: MediaPickerTheme.Main = .init(),
- selection: MediaPickerTheme.Selection = .init(),
- error: MediaPickerTheme.Error = .init()) -> some View {
- self.environment(\.mediaPickerTheme, MediaPickerTheme(main: main, selection: selection, error: error))
- }
-}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraBG.colorset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraBG.colorset/Contents.json
new file mode 100644
index 0000000..be9d677
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraBG.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraText.colorset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraText.colorset/Contents.json
new file mode 100644
index 0000000..951b907
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/cameraText.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFE"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFE"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerBG.colorset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerBG.colorset/Contents.json
new file mode 100644
index 0000000..9c0e331
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerBG.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerText.colorset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerText.colorset/Contents.json
new file mode 100644
index 0000000..3fe9b59
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/pickerText.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x00",
+ "green" : "0x00",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0xFF",
+ "red" : "0xFE"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/selection.colorset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/selection.colorset/Contents.json
new file mode 100644
index 0000000..7e19bd5
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Colors/selection.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0x71",
+ "red" : "0x5A"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xFF",
+ "green" : "0x71",
+ "red" : "0x5A"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOff.imageset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOff.imageset/Contents.json
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOff.imageset/Contents.json
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOff.imageset/Contents.json
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOff.imageset/Flash.pdf b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOff.imageset/Flash.pdf
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOff.imageset/Flash.pdf
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOff.imageset/Flash.pdf
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOn.imageset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOn.imageset/Contents.json
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOn.imageset/Contents.json
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOn.imageset/Contents.json
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOn.imageset/Flash on.pdf b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOn.imageset/Flash on.pdf
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlashOn.imageset/Flash on.pdf
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlashOn.imageset/Flash on.pdf
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlipCamera.imageset/Change Camera.pdf b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlipCamera.imageset/Change Camera.pdf
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlipCamera.imageset/Change Camera.pdf
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlipCamera.imageset/Change Camera.pdf
diff --git a/Sources/ExyteMediaPicker/Resources/Media.xcassets/FlipCamera.imageset/Contents.json b/Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlipCamera.imageset/Contents.json
similarity index 100%
rename from Sources/ExyteMediaPicker/Resources/Media.xcassets/FlipCamera.imageset/Contents.json
rename to Sources/ExyteMediaPicker/Resources/Media.xcassets/Images/FlipCamera.imageset/Contents.json
diff --git a/Sources/ExyteMediaPicker/Screens/AlbumView/AlbumView.swift b/Sources/ExyteMediaPicker/Screens/AlbumView/AlbumView.swift
new file mode 100644
index 0000000..10b910d
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/AlbumView/AlbumView.swift
@@ -0,0 +1,169 @@
+//
+// Created by Alex.M on 27.05.2022.
+//
+
+public enum LiveCameraCellStyle {
+ case none
+ case small // 1 cell in photos grid
+ case prominant // 2 cell height
+}
+
+import SwiftUI
+import AnchoredPopup
+
+struct AlbumView: View {
+
+ @EnvironmentObject private var selectionService: SelectionService
+ @Environment(\.mediaPickerTheme) private var theme
+
+ @ObservedObject var keyboardHeightHelper = KeyboardHeightHelper.shared
+ @ObservedObject var permissionsService = PermissionsService.shared
+
+ @StateObject var viewModel: BaseMediasProvider
+ @Binding var showingCamera: Bool
+ @Binding var currentFullscreenMedia: Media?
+
+ var selectionParamsHolder: SelectionParamsHolder
+ var mediaPickerParamsHolder: MediaPickerParamsHolder
+ var dismiss: ()->()
+
+ @State private var fullscreenItem: AssetMediaModel.ID?
+
+ private var shouldShowLoadingCell: Bool {
+ viewModel.isLoading && viewModel.assetMediaModels.count > 0
+ }
+
+ var body: some View {
+ content
+ .onAppear {
+ viewModel.reload()
+ }
+ .onDisappear {
+ viewModel.cancel()
+ }
+ }
+
+ @ViewBuilder
+ var content: some View {
+ ScrollView {
+ VStack(spacing: 0) {
+ PermissionActionView(type: .library(permissionsService.photoLibraryPermissionStatus))
+
+ if mediaPickerParamsHolder.liveCameraCell != .none {
+ PermissionActionView(type: .camera(permissionsService.cameraPermissionStatus))
+ }
+
+ if viewModel.isLoading, viewModel.assetMediaModels.isEmpty {
+ ProgressView()
+ .padding()
+ } else if !viewModel.isLoading, viewModel.assetMediaModels.isEmpty {
+ Text("Empty data")
+ .font(.title3)
+ .foregroundColor(theme.main.pickerText)
+ } else {
+ mediasGrid
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity)
+ }
+ .background(theme.main.pickerBackground)
+ .onTapGesture {
+ if keyboardHeightHelper.keyboardDisplayed {
+ dismissKeyboard()
+ }
+ }
+ }
+
+ private func getLiveCameraCell() -> LiveCameraCellStyle {
+ #if targetEnvironment(simulator)
+ return .none
+ #else
+ return if permissionsService.cameraPermissionStatus != .authorized {
+ .none
+ } else {
+ mediaPickerParamsHolder.liveCameraCell
+ }
+ #endif
+ }
+
+ var mediasGrid: some View {
+ MediasGrid(viewModel.assetMediaModels, liveCameraCell: getLiveCameraCell()) {
+#if !targetEnvironment(simulator)
+ if getLiveCameraCell() != .none && permissionsService.cameraPermissionStatus == .authorized {
+ LiveCameraCell {
+ showingCamera = true
+ }
+ }
+#endif
+ } content: { assetMediaModel, index, cellSize in
+ cellView(assetMediaModel, index, cellSize)
+ } loadingCell: {
+ if shouldShowLoadingCell {
+ ZStack {
+ Color.white.opacity(0.5)
+ ProgressView()
+ }
+ .aspectRatio(1, contentMode: .fit)
+ }
+ }
+ .onChange(of: viewModel.assetMediaModels) { _ , newValue in
+ selectionService.updateSelection(with: newValue)
+ }
+ }
+
+ @ViewBuilder
+ func cellView(_ assetMediaModel: AssetMediaModel, _ index: Int, _ size: CGFloat) -> some View {
+ let imageButton = Button {
+ if keyboardHeightHelper.keyboardDisplayed {
+ dismissKeyboard()
+ }
+ if !selectionParamsHolder.showFullscreenPreview { // select immediately
+ selectionService.onSelect(assetMediaModel: assetMediaModel)
+ if selectionService.mediaSelectionLimit == 1 {
+ dismiss()
+ }
+ }
+ else if fullscreenItem == nil {
+ fullscreenItem = assetMediaModel.id
+ }
+ } label: {
+ let id = "fullscreen_photo_\(index)"
+ MediaCell(viewModel: MediaViewModel(assetMediaModel: assetMediaModel), size: size)
+ .applyIf(selectionParamsHolder.showFullscreenPreview) {
+ $0.useAsPopupAnchor(id: id) {
+ FullscreenContainer(
+ currentFullscreenMedia: $currentFullscreenMedia,
+ selection: $fullscreenItem,
+ animationID: id,
+ assetMediaModels: viewModel.assetMediaModels,
+ selectionParamsHolder: selectionParamsHolder,
+ dismiss: dismiss
+ )
+ .environmentObject(selectionService)
+ } customize: {
+ $0.closeOnTap(false)
+ .animation(.easeIn(duration: 0.2))
+ }
+ .simultaneousGesture(
+ TapGesture().onEnded {
+ fullscreenItem = assetMediaModel.id
+ }
+ )
+ }
+ }
+ .buttonStyle(MediaButtonStyle())
+ .contentShape(Rectangle())
+
+ if selectionService.mediaSelectionLimit == 1 {
+ imageButton
+ } else {
+ SelectableView(selected: selectionService.index(of: assetMediaModel), isFullscreen: false, canSelect: selectionService.canSelect(assetMediaModel: assetMediaModel), selectionParamsHolder: selectionParamsHolder) {
+ selectionService.onSelect(assetMediaModel: assetMediaModel)
+ } content: {
+ imageButton
+ }
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaCell.swift b/Sources/ExyteMediaPicker/Screens/AlbumView/MediaCell.swift
similarity index 71%
rename from Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaCell.swift
rename to Sources/ExyteMediaPicker/Screens/AlbumView/MediaCell.swift
index 3f4e169..a066de2 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaCell.swift
+++ b/Sources/ExyteMediaPicker/Screens/AlbumView/MediaCell.swift
@@ -6,19 +6,21 @@ import SwiftUI
struct MediaCell: View {
+ @Environment(\.mediaPickerTheme) private var theme
+
@StateObject var viewModel: MediaViewModel
-
+ var size: CGFloat
+
var body: some View {
ZStack {
- GeometryReader { geometry in
- ThumbnailView(preview: viewModel.preview)
- .onAppear {
- viewModel.onStart(size: geometry.size)
- }
- }
- .aspectRatio(1, contentMode: .fill)
- .clipped()
-
+ ThumbnailView(preview: viewModel.preview, size: size)
+ .cornerRadius(theme.cellStyle.cornerRadius)
+ .onAppear {
+ viewModel.onStart(size: size)
+ }
+ .aspectRatio(1, contentMode: .fill)
+ .clipped()
+
if let duration = viewModel.assetMediaModel.asset.formattedDuration {
VStack {
Spacer()
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaViewModel.swift b/Sources/ExyteMediaPicker/Screens/AlbumView/MediaViewModel.swift
similarity index 76%
rename from Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaViewModel.swift
rename to Sources/ExyteMediaPicker/Screens/AlbumView/MediaViewModel.swift
index fb62e9e..3f8c3e4 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/MediaViewModel.swift
+++ b/Sources/ExyteMediaPicker/Screens/AlbumView/MediaViewModel.swift
@@ -22,10 +22,12 @@ class MediaViewModel: ObservableObject {
// FIXME: Create preview for image/video for other platforms
#endif
- func onStart(size: CGSize) {
+ @MainActor func onStart(size: CGFloat) {
requestID = assetMediaModel.asset
- .image(size: size) {
- self.preview = $0
+ .image(size: CGSize(width: size, height: size)) { image in
+ DispatchQueue.main.async {
+ self.preview = image
+ }
}
}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCell.swift b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCell.swift
similarity index 60%
rename from Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCell.swift
rename to Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCell.swift
index 9ec23db..d0acb28 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCell.swift
+++ b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCell.swift
@@ -6,30 +6,29 @@ import SwiftUI
struct AlbumCell: View {
+ @Environment(\.mediaPickerTheme) private var theme
+
@StateObject var viewModel: AlbumCellViewModel
+ var size: CGFloat
- @Environment(\.mediaPickerTheme) private var theme
-
var body: some View {
VStack {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.overlay {
- GeometryReader { geometry in
- ThumbnailView(preview: viewModel.preview)
- .onAppear {
- viewModel.fetchPreview(size: geometry.size)
- }
- }
+ ThumbnailView(preview: viewModel.preview, size: size)
+ .onAppear {
+ viewModel.fetchPreview(size: CGSize(width: size, height: size))
+ }
}
.clipped()
- .foregroundColor(theme.main.albumSelectionBackground)
-
+ .foregroundColor(theme.main.pickerBackground)
+
if let title = viewModel.album.title {
Text(title)
.lineLimit(2)
.multilineTextAlignment(.center)
- .foregroundColor(theme.main.text)
+ .foregroundColor(theme.main.pickerText)
}
}
.onDisappear {
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCellViewModel.swift b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCellViewModel.swift
similarity index 77%
rename from Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCellViewModel.swift
rename to Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCellViewModel.swift
index 16fcfd5..c42102f 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumCellViewModel.swift
+++ b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumCellViewModel.swift
@@ -22,12 +22,14 @@ class AlbumCellViewModel: ObservableObject {
// FIXME: Create preview for image/video for other platforms
#endif
- func fetchPreview(size: CGSize) {
+ @MainActor func fetchPreview(size: CGSize) {
guard preview == nil else { return }
requestID = album.preview?.asset
- .image(size: size) { [weak self] in
- self?.preview = $0
+ .image(size: size) { [weak self] image in
+ DispatchQueue.main.async {
+ self?.preview = image
+ }
}
}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsView.swift b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsView.swift
similarity index 63%
rename from Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsView.swift
rename to Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsView.swift
index 1c6e090..ddd4c0f 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsView.swift
+++ b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsView.swift
@@ -3,28 +3,25 @@
//
import SwiftUI
-import Combine
struct AlbumsView: View {
@EnvironmentObject private var selectionService: SelectionService
- @EnvironmentObject private var permissionsService: PermissionsService
+ @Environment(\.mediaPickerTheme) private var theme
@StateObject var viewModel: AlbumsViewModel
@ObservedObject var mediaPickerViewModel: MediaPickerViewModel
+ @ObservedObject var permissionsService = PermissionsService.shared
@Binding var showingCamera: Bool
@Binding var currentFullscreenMedia: Media?
let selectionParamsHolder: SelectionParamsHolder
+ let mediaPickerParamsHolder: MediaPickerParamsHolder
let filterClosure: MediaPicker.FilterClosure?
let massFilterClosure: MediaPicker.MassFilterClosure?
@State private var showingLoadingCell = false
-
- private var columns: [GridItem] {
- [GridItem(.adaptive(minimum: 100), spacing: 0, alignment: .top)]
- }
private var cellPadding: EdgeInsets {
EdgeInsets(top: 2, leading: 2, bottom: 8, trailing: 2)
@@ -33,24 +30,24 @@ struct AlbumsView: View {
var body: some View {
ScrollView {
VStack {
- if let action = permissionsService.photoLibraryAction {
- PermissionsActionView(action: .library(action))
- }
+ PermissionActionView(type: .library(permissionsService.photoLibraryPermissionStatus))
+
if viewModel.isLoading {
ProgressView()
+ .padding()
} else if viewModel.albums.isEmpty {
Text("Empty data")
.font(.title3)
+ .foregroundColor(theme.main.pickerText)
} else {
+ let (columnWidth, columns) = calculateColumnWidth(spacing: 0)
LazyVGrid(columns: columns, spacing: 0) {
ForEach(viewModel.albums) { album in
- AlbumCell(
- viewModel: AlbumCellViewModel(album: album)
- )
- .padding(cellPadding)
- .onTapGesture {
- mediaPickerViewModel.setPickerMode(.album(album.toAlbum()))
- }
+ AlbumCell(viewModel: AlbumCellViewModel(album: album), size: columnWidth)
+ .padding(cellPadding)
+ .onTapGesture {
+ mediaPickerViewModel.setPickerMode(.album(album.toAlbum()))
+ }
}
}
}
diff --git a/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsViewModel.swift b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsViewModel.swift
new file mode 100644
index 0000000..415973d
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/AlbumsView/AlbumsViewModel.swift
@@ -0,0 +1,31 @@
+//
+// Created by Alex.M on 07.06.2022.
+//
+
+import Foundation
+
+@MainActor
+final class AlbumsViewModel: ObservableObject {
+
+ var albums: [AlbumModel] {
+ albumsProvider.albums
+ }
+
+ var isLoading: Bool {
+ albumsProvider.isLoading
+ }
+
+ private let albumsProvider: DefaultAlbumsProvider
+
+ init(albumsProvider: DefaultAlbumsProvider) {
+ self.albumsProvider = albumsProvider
+ }
+
+ func onStart() {
+ albumsProvider.reload()
+ }
+
+ func onStop() {
+ albumsProvider.cancelReload()
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Screens/Camera/CameraSelectionContainer.swift b/Sources/ExyteMediaPicker/Screens/Camera/CameraSelectionContainer.swift
new file mode 100644
index 0000000..5d9bc4e
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/Camera/CameraSelectionContainer.swift
@@ -0,0 +1,106 @@
+//
+// CameraSelectionContainer.swift
+//
+//
+// Created by Alisa Mylnikova on 12.07.2022.
+//
+
+import SwiftUI
+
+public struct CameraSelectionView: View {
+
+ @EnvironmentObject private var cameraSelectionService: CameraSelectionService
+ @State private var index: Int? = 0
+
+ var selectionParamsHolder: SelectionParamsHolder
+
+ public var body: some View {
+ GeometryReader { g in
+ let size = g.size
+ if #available(iOS 17.0, *) {
+ ScrollView(.horizontal, showsIndicators: false) {
+ LazyHStack(spacing: 0) {
+ ForEach(0..: View {
+ @EnvironmentObject private var cameraSelectionService: CameraSelectionService
+
+ public typealias CameraViewClosure = ((LiveCameraView, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure) -> CameraViewContent)
+
+ // params
@ObservedObject var viewModel: MediaPickerViewModel
let didTakePicture: () -> Void
let didPressCancel: () -> Void
+ var cameraViewBuilder: CameraViewClosure
@StateObject private var cameraViewModel = CameraViewModel()
+
+ var body: some View {
+ cameraViewBuilder(
+ LiveCameraView(
+ session: cameraViewModel.captureSession,
+ videoGravity: .resizeAspectFill,
+ orientation: .portrait
+ ),
+ { // cancel
+ if cameraSelectionService.hasSelected {
+ viewModel.showingExitCameraConfirmation = true
+ } else {
+ didPressCancel()
+ }
+ },
+ { viewModel.setPickerMode(.cameraSelection) }, // show preview of taken photos
+ { Task { await cameraViewModel.takePhoto() } }, // takePhoto
+ { Task { await cameraViewModel.startVideoCapture() } }, // start record video
+ { Task { await cameraViewModel.stopVideoCapture() } }, // stop record video
+ { Task { await cameraViewModel.toggleFlash() } }, // flash off/on
+ { Task { await cameraViewModel.flipCamera() } } // camera back/front
+ )
+ .onChange(of: cameraViewModel.capturedPhoto) { _ , newValue in
+ viewModel.pickedMediaUrl = newValue
+ didTakePicture()
+ }
+ }
+}
+
+struct StandardConrolsCameraView: View {
+
@EnvironmentObject private var cameraSelectionService: CameraSelectionService
- @Environment(\.safeAreaInsets) private var safeAreaInsets
@Environment(\.mediaPickerTheme) private var theme
+ @Environment(\.scenePhase) private var scenePhase
- @State var capturingPhotos = true
- @State var videoCaptureInProgress = false
-
+ @ObservedObject var viewModel: MediaPickerViewModel
+ let didTakePicture: () -> Void
+ let didPressCancel: () -> Void
let selectionParamsHolder: SelectionParamsHolder
+ @StateObject private var cameraViewModel = CameraViewModel()
+
+ @State private var capturingPhotos = true
+ @State private var videoCaptureInProgress = false
+
var body: some View {
VStack(spacing: 0) {
HStack {
@@ -31,13 +73,12 @@ struct CameraView: View {
didPressCancel()
}
}
- .foregroundColor(.white)
- .padding(.top, safeAreaInsets.top)
- .padding(.leading)
- .padding(.bottom)
+ .foregroundColor(theme.main.cameraText)
+ .padding(12, 18)
Spacer()
}
+ .safeAreaPadding(.top, UIApplication.safeArea.top)
LiveCameraView(
session: cameraViewModel.captureSession,
@@ -51,8 +92,16 @@ struct CameraView: View {
}
.gesture(
MagnificationGesture()
- .onChanged(cameraViewModel.zoomChanged(_:))
- .onEnded(cameraViewModel.zoomEnded(_:))
+ .onChanged { value in
+ if cameraViewModel.zoomAllowed {
+ cameraViewModel.zoomChanged(value)
+ }
+ }
+ .onEnded { value in
+ if cameraViewModel.zoomAllowed {
+ cameraViewModel.zoomEnded(value)
+ }
+ }
)
VStack(spacing: 10) {
@@ -70,20 +119,22 @@ struct CameraView: View {
Spacer()
Text("\(cameraSelectionService.selected.count)")
.font(.system(size: 15))
+ .foregroundStyle(theme.main.cameraText)
.padding(8)
.overlay(Circle()
- .stroke(Color.white, lineWidth: 2))
+ .stroke(theme.main.cameraText, lineWidth: 2))
}
- .foregroundColor(.white)
+ .foregroundColor(theme.main.cameraText)
.padding(.horizontal, 12)
}
else if selectionParamsHolder.mediaType.allowsVideo {
photoVideoToggle
+ .padding(.bottom, 8)
}
HStack(spacing: 40) {
- Button {
- cameraViewModel.toggleFlash()
+ AsyncButton {
+ await cameraViewModel.toggleFlash()
} label: {
Image(cameraViewModel.flashEnabled ? "FlashOn" : "FlashOff", bundle: .current)
}
@@ -96,22 +147,31 @@ struct CameraView: View {
stopVideoCaptureButton
}
- Button {
- cameraViewModel.flipCamera()
+ AsyncButton {
+ await cameraViewModel.flipCamera()
} label: {
Image("FlipCamera", bundle: .current)
}
}
}
.padding(.top, 24)
- .padding(.bottom, safeAreaInsets.bottom + 50)
+ .padding(.bottom, 50)
}
.background(theme.main.cameraBackground)
- .onEnteredBackground(perform: cameraViewModel.stopSession)
- .onEnteredForeground(perform: cameraViewModel.startSession)
- .onReceive(cameraViewModel.capturedPhotoPublisher) {
- viewModel.pickedMediaUrl = $0
- didTakePicture()
+ .onChange(of: scenePhase) {
+ Task {
+ if scenePhase == .background {
+ await cameraViewModel.stopSession()
+ } else if scenePhase == .active {
+ await cameraViewModel.startSession()
+ }
+ }
+ }
+ .onChange(of: cameraViewModel.capturedPhoto) { _ , newValue in
+ if let photo = newValue {
+ viewModel.pickedMediaUrl = photo
+ didTakePicture()
+ }
}
}
@@ -136,7 +196,9 @@ struct CameraView: View {
.frame(width: 72, height: 72)
Button {
- cameraViewModel.takePhoto()
+ Task {
+ await cameraViewModel.takePhoto()
+ }
} label: {
Circle()
.foregroundColor(.white)
@@ -151,8 +213,8 @@ struct CameraView: View {
.stroke(Color.white.opacity(0.4), lineWidth: 6)
.frame(width: 72, height: 72)
- Button {
- cameraViewModel.startVideoCapture()
+ AsyncButton {
+ await cameraViewModel.startVideoCapture()
videoCaptureInProgress = true
} label: {
Circle()
@@ -168,8 +230,8 @@ struct CameraView: View {
.stroke(Color.white.opacity(0.4), lineWidth: 6)
.frame(width: 72, height: 72)
- Button {
- cameraViewModel.stopVideoCapture()
+ AsyncButton {
+ await cameraViewModel.stopVideoCapture()
videoCaptureInProgress = false
} label: {
RoundedRectangle(cornerRadius: 10)
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Camera/CameraViewModel.swift b/Sources/ExyteMediaPicker/Screens/Camera/CameraViewModel.swift
similarity index 56%
rename from Sources/ExyteMediaPicker/Views/Pages/Camera/CameraViewModel.swift
rename to Sources/ExyteMediaPicker/Screens/Camera/CameraViewModel.swift
index f71923d..8e21fc9 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/Camera/CameraViewModel.swift
+++ b/Sources/ExyteMediaPicker/Screens/Camera/CameraViewModel.swift
@@ -7,11 +7,17 @@
import Foundation
import AVFoundation
-import Combine
import UIKit
import SwiftUI
+import Combine
+
+#if compiler(>=6.0)
+extension AVCaptureSession: @retroactive @unchecked Sendable { }
+#else
+extension AVCaptureSession: @unchecked Sendable { }
+#endif
-final class CameraViewModel: NSObject, ObservableObject {
+final actor CameraViewModel: NSObject, ObservableObject {
struct CaptureDevice {
let device: AVCaptureDevice
@@ -20,17 +26,16 @@ final class CameraViewModel: NSObject, ObservableObject {
let maxZoom: CGFloat
}
- @Published private(set) var flashEnabled = false
- @Published private(set) var snapOverlay = false
+ @MainActor @Published private(set) var flashEnabled = false
+ @MainActor @Published private(set) var snapOverlay = false
+ @MainActor @Published private(set) var zoomAllowed = false
+ @MainActor @Published private(set) var capturedPhoto: URL?
let captureSession = AVCaptureSession()
- var capturedPhotoPublisher: AnyPublisher { capturedPhotoSubject.eraseToAnyPublisher() }
private let photoOutput = AVCapturePhotoOutput()
private let videoOutput = AVCaptureMovieFileOutput()
private let motionManager = MotionManager()
- private let sessionQueue = DispatchQueue(label: "LiveCameraQueue")
- private let capturedPhotoSubject = PassthroughSubject()
private var captureDevice: CaptureDevice?
private var lastPhotoActualOrientation: UIDeviceOrientation?
@@ -39,44 +44,49 @@ final class CameraViewModel: NSObject, ObservableObject {
private let dualCameraMaxScale: CGFloat = 8
private let tripleCameraMaxScale: CGFloat = 12
private var lastScale: CGFloat = 1
- private var zoomAllowed: Bool { captureDevice?.position == .back }
override init() {
super.init()
- sessionQueue.async { [weak self] in
- self?.configureSession()
- self?.captureSession.startRunning()
+ Task {
+ await configureSession()
+ captureSession.startRunning()
}
}
- deinit {
- captureSession.stopRunning()
- }
-
func startSession() {
- sessionQueue.async { [weak self] in
- self?.captureSession.startRunning()
- }
+ captureSession.startRunning()
}
func stopSession() {
- sessionQueue.async { [weak self] in
- self?.captureSession.stopRunning()
+ captureSession.stopRunning()
+ }
+
+ func setCapturedPhoto(_ photo: URL?) {
+ DispatchQueue.main.async {
+ self.capturedPhoto = photo
}
}
- func takePhoto() {
+ func takePhoto() async {
let settings = AVCapturePhotoSettings()
- settings.flashMode = flashEnabled ? .on : .off
+ settings.flashMode = await flashEnabled ? .on : .off
photoOutput.capturePhoto(with: settings, delegate: self)
lastPhotoActualOrientation = motionManager.orientation
- withAnimation(.linear(duration: 0.1)) { snapOverlay = true }
- withAnimation(.linear(duration: 0.1).delay(0.1)) { snapOverlay = false }
+ withAnimation(.linear(duration: 0.1)) {
+ DispatchQueue.main.async {
+ self.snapOverlay = true
+ }
+ }
+ withAnimation(.linear(duration: 0.1).delay(0.1)) {
+ DispatchQueue.main.async {
+ self.snapOverlay = false
+ }
+ }
}
- func startVideoCapture() {
- setVideoTorchMode(flashEnabled ? .on : .off)
+ func startVideoCapture() async {
+ setVideoTorchMode(await flashEnabled ? .on : .off)
let videoUrl = FileManager.getTempUrl()
videoOutput.startRecording(to: videoUrl, recordingDelegate: self)
@@ -96,34 +106,41 @@ final class CameraViewModel: NSObject, ObservableObject {
}
func flipCamera() {
- sessionQueue.async { [weak self] in
- guard let session = self?.captureSession, let input = session.inputs.first else {
- return
- }
-
- let newPosition: AVCaptureDevice.Position = self?.captureDevice?.position == .back ? .front : .back
-
- session.beginConfiguration()
- session.removeInput(input)
- self?.addInput(to: session, for: newPosition)
- session.commitConfiguration()
+ let session = captureSession
+ guard let input = session.inputs
+ .compactMap({ $0 as? AVCaptureDeviceInput })
+ .first(where: { $0.device.hasMediaType(.video) }) else {
+ return
}
+ let newPosition: AVCaptureDevice.Position = captureDevice?.position == .back ? .front : .back
+
+ session.beginConfiguration()
+ session.removeInput(input)
+ addInput(to: session, for: newPosition)
+ session.commitConfiguration()
}
func toggleFlash() {
- flashEnabled.toggle()
+ DispatchQueue.main.async {
+ self.flashEnabled.toggle()
+ }
}
- func zoomChanged(_ scale: CGFloat) {
- if !zoomAllowed { return }
- zoomCamera(resolveScale(scale))
+ nonisolated func zoomChanged(_ scale: CGFloat) {
+ Task {
+ await zoomCamera(await resolveScale(scale))
+ }
}
- func zoomEnded(_ scale: CGFloat) {
- if !zoomAllowed { return }
+ nonisolated func zoomEnded(_ scale: CGFloat) {
+ Task {
+ await setLastScale(await resolveScale(scale))
+ await zoomCamera(lastScale)
+ }
+ }
- lastScale = resolveScale(scale)
- zoomCamera(lastScale)
+ private func setLastScale(_ scale: CGFloat) {
+ self.lastScale = scale
}
private func resolveScale(_ gestureScale: CGFloat) -> CGFloat {
@@ -150,10 +167,22 @@ final class CameraViewModel: NSObject, ObservableObject {
private func addInput(to session: AVCaptureSession, for position: AVCaptureDevice.Position = .back) {
guard let captureDevice = selectCaptureDevice(for: position) else { return }
+ let zoomAllowed = captureDevice.position == .back
+ Task { @MainActor in
+ self.zoomAllowed = zoomAllowed
+ }
guard let captureDeviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { return }
guard session.canAddInput(captureDeviceInput) else { return }
session.addInput(captureDeviceInput)
+ let hasAudioInput = session.inputs.contains { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) == true }
+ if !hasAudioInput {
+ guard let captureAudioDevice = selectAudioCaptureDevice() else { return }
+ guard let captureAudioDeviceInput = try? AVCaptureDeviceInput(device: captureAudioDevice) else { return }
+ guard session.canAddInput(captureAudioDeviceInput) else { return }
+ session.addInput(captureAudioDeviceInput)
+ }
+
let defaultZoom = CGFloat(truncating: captureDevice.virtualDeviceSwitchOverVideoZoomFactors.first ?? minScale as NSNumber)
let maxZoom: CGFloat
@@ -186,12 +215,15 @@ final class CameraViewModel: NSObject, ObservableObject {
guard session.canAddOutput(videoOutput) else { return }
session.addOutput(videoOutput)
- updateOutputOrientation()
+ updateOutputOrientation(photoOutput)
+ updateOutputOrientation(videoOutput)
}
- private func updateOutputOrientation() {
- guard let connection = photoOutput.connection(with: .video), connection.isVideoOrientationSupported else { return }
- connection.videoOrientation = .portrait
+ private func updateOutputOrientation(_ output: AVCaptureOutput) {
+ guard let connection = output.connection(with: .video) else { return }
+ if connection.isVideoRotationAngleSupported(0) {
+ connection.videoRotationAngle = 0
+ }
}
private func selectCaptureDevice(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
@@ -217,35 +249,47 @@ final class CameraViewModel: NSObject, ObservableObject {
}
}
+ private func selectAudioCaptureDevice() -> AVCaptureDevice? {
+ let session = AVCaptureDevice.DiscoverySession(
+ deviceTypes: [.microphone],
+ mediaType: .audio,
+ position: .unspecified)
+
+ return session.devices.first
+ }
}
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
- func photoOutput(
+ nonisolated func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard let cgImage = photo.cgImageRepresentation() else { return }
- let photoOrientation: UIImage.Orientation
- if let orientation = lastPhotoActualOrientation {
- photoOrientation = UIImage.Orientation(orientation)
- } else {
- photoOrientation = UIImage.Orientation.default
- }
+ Task {
+ let photoOrientation: UIImage.Orientation
+ if let orientation = await lastPhotoActualOrientation {
+ photoOrientation = UIImage.Orientation(orientation)
+ } else {
+ photoOrientation = UIImage.Orientation.default
+ }
- guard let data = UIImage(
- cgImage: cgImage,
- scale: 1,
- orientation: photoOrientation
- ).jpegData(compressionQuality: 0.8) else { return }
+ guard let data = UIImage(
+ cgImage: cgImage,
+ scale: 1,
+ orientation: photoOrientation
+ ).jpegData(compressionQuality: 0.8) else { return }
- capturedPhotoSubject.send(FileManager.storeToTempDir(data: data))
+ await setCapturedPhoto(FileManager.storeToTempDir(data: data))
+ }
}
}
extension CameraViewModel: AVCaptureFileOutputRecordingDelegate {
- func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
- capturedPhotoSubject.send(outputFileURL)
+ nonisolated func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
+ Task {
+ await setCapturedPhoto(outputFileURL)
+ }
}
}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/LiveCameraCell.swift b/Sources/ExyteMediaPicker/Screens/Camera/LiveCameraCell.swift
similarity index 54%
rename from Sources/ExyteMediaPicker/Views/Widgets/LiveCameraCell.swift
rename to Sources/ExyteMediaPicker/Screens/Camera/LiveCameraCell.swift
index f64b4db..7b1c10a 100644
--- a/Sources/ExyteMediaPicker/Views/Widgets/LiveCameraCell.swift
+++ b/Sources/ExyteMediaPicker/Screens/Camera/LiveCameraCell.swift
@@ -5,10 +5,12 @@
import SwiftUI
struct LiveCameraCell: View {
+
+ @Environment(\.scenePhase) private var scenePhase
let action: () -> Void
- @StateObject private var liveCameraViewModel = LiveCameraViewModel()
+ @StateObject private var cameraViewModel = CameraViewModel()
@State private var orientation = UIDevice.current.orientation
var body: some View {
@@ -16,7 +18,7 @@ struct LiveCameraCell: View {
action()
} label: {
LiveCameraView(
- session: liveCameraViewModel.captureSession,
+ session: cameraViewModel.captureSession,
videoGravity: .resizeAspectFill,
orientation: orientation
)
@@ -25,8 +27,15 @@ struct LiveCameraCell: View {
.foregroundColor(.white)
)
}
- .onEnteredBackground(perform: liveCameraViewModel.stopSession)
- .onEnteredForeground(perform: liveCameraViewModel.startSession)
+ .onChange(of: scenePhase) {
+ Task {
+ if scenePhase == .background {
+ await cameraViewModel.stopSession()
+ } else if scenePhase == .active {
+ await cameraViewModel.startSession()
+ }
+ }
+ }
.onRotate { orientation = $0 }
}
}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Camera/LiveCameraView.swift b/Sources/ExyteMediaPicker/Screens/Camera/LiveCameraView.swift
similarity index 55%
rename from Sources/ExyteMediaPicker/Views/Pages/Camera/LiveCameraView.swift
rename to Sources/ExyteMediaPicker/Screens/Camera/LiveCameraView.swift
index c006444..db4daae 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/Camera/LiveCameraView.swift
+++ b/Sources/ExyteMediaPicker/Screens/Camera/LiveCameraView.swift
@@ -8,13 +8,14 @@
import SwiftUI
import AVFoundation
-struct LiveCameraView: UIViewRepresentable {
+@MainActor
+public struct LiveCameraView: UIViewRepresentable {
let session: AVCaptureSession
- var videoGravity: AVLayerVideoGravity = .resizeAspect
- var orientation: UIDeviceOrientation = UIDevice.current.orientation
+ let videoGravity: AVLayerVideoGravity
+ let orientation: UIDeviceOrientation
- func makeUIView(context: Context) -> LiveVideoCaptureView {
+ public func makeUIView(context: Context) -> LiveVideoCaptureView {
LiveVideoCaptureView(
session: session,
videoGravity: videoGravity,
@@ -22,13 +23,10 @@ struct LiveCameraView: UIViewRepresentable {
)
}
- func updateUIView(_ uiView: LiveVideoCaptureView, context: Context) {
- uiView.updateOrientation(orientation)
- }
-
+ public func updateUIView(_ uiView: LiveVideoCaptureView, context: Context) { }
}
-final class LiveVideoCaptureView: UIView {
+public final class LiveVideoCaptureView: UIView {
var session: AVCaptureSession? {
get {
@@ -39,7 +37,7 @@ final class LiveVideoCaptureView: UIView {
}
}
- override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
+ public override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
private var videoLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
@@ -56,12 +54,5 @@ final class LiveVideoCaptureView: UIView {
super.init(frame: frame)
self.session = session
videoLayer.videoGravity = videoGravity
- updateOrientation(orientation)
}
-
- func updateOrientation(_ orientation: UIDeviceOrientation) {
- guard let connection = videoLayer.connection, connection.isVideoOrientationSupported else { return }
- connection.videoOrientation = AVCaptureVideoOrientation(orientation)
- }
-
}
diff --git a/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCell.swift b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCell.swift
new file mode 100644
index 0000000..360cac0
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCell.swift
@@ -0,0 +1,73 @@
+//
+// Created by Alex.M on 09.06.2022.
+//
+
+import Foundation
+import SwiftUI
+import AVKit
+
+struct FullscreenCell: View {
+
+ @Environment(\.mediaPickerTheme) private var theme
+
+ @StateObject var viewModel: FullscreenCellViewModel
+ @ObservedObject var keyboardHeightHelper = KeyboardHeightHelper.shared
+
+ var size: CGSize
+
+ var body: some View {
+ Group {
+ if let image = viewModel.image {
+ ZoomableScrollView {
+ imageView(image: image, useFill: false)
+ }
+ } else if let player = viewModel.player {
+ ZoomableScrollView {
+ videoView(player: player, useFill: false)
+ }
+ } else {
+ ProgressView()
+ .tint(.white)
+ }
+ }
+ .allowsHitTesting(!keyboardHeightHelper.keyboardDisplayed)
+ .task {
+ await viewModel.onStart()
+ }
+ .onDisappear {
+ viewModel.onStop()
+ }
+ }
+
+ @ViewBuilder
+ func imageView(image: UIImage, useFill: Bool) -> some View {
+ Image(uiImage: image)
+ .resizable()
+ .aspectRatio(contentMode: useFill ? .fill : .fit)
+ }
+
+ func videoView(player: AVPlayer, useFill: Bool) -> some View {
+ PlayerView(player: player, bgColor: theme.main.fullscreenPhotoBackground, useFill: useFill)
+ .disabled(true)
+ .overlay {
+ ZStack {
+ Color.clear
+ if !viewModel.isPlaying {
+ Circle().styled(.black.opacity(0.2))
+ .frame(width: 70, height: 70)
+ Image(systemName: "play.fill")
+ .resizable()
+ .foregroundColor(.white.opacity(0.8))
+ .frame(width: 30, height: 30)
+ .padding(.leading, 4)
+ }
+ }
+ .contentShape(Rectangle())
+ .simultaneousGesture(
+ TapGesture().onEnded {
+ viewModel.togglePlay()
+ }
+ )
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCellViewModel.swift b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCellViewModel.swift
similarity index 65%
rename from Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCellViewModel.swift
rename to Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCellViewModel.swift
index 758b795..49fcb15 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCellViewModel.swift
+++ b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenCellViewModel.swift
@@ -3,10 +3,15 @@
//
import Foundation
-import Combine
-import AVKit
+@preconcurrency import AVKit
import UIKit.UIImage
+#if compiler(>=6.0)
+extension AVAssetTrack: @retroactive @unchecked Sendable { }
+#else
+extension AVAssetTrack: @unchecked Sendable { }
+#endif
+
@MainActor
final class FullscreenCellViewModel: ObservableObject {
@@ -15,6 +20,7 @@ final class FullscreenCellViewModel: ObservableObject {
@Published var image: UIImage? = nil
@Published var player: AVPlayer? = nil
@Published var isPlaying = false
+ @Published var videoSize: CGSize = .zero
private var currentTask: Task?
@@ -31,13 +37,15 @@ final class FullscreenCellViewModel: ObservableObject {
case .image:
let data = try? await mediaModel.getData() // url is slow to load in UI, this way photos don't flicker when swiping
guard let data = data else { return }
+ let result = UIImage(data: data)
DispatchQueue.main.async {
- self.image = UIImage(data: data)
+ self.image = result
}
case .video:
let url = await mediaModel.getURL()
guard let url = url else { return }
setupPlayer(url)
+ videoSize = await getVideoSize(url)
case .none:
break
}
@@ -71,4 +79,17 @@ final class FullscreenCellViewModel: ObservableObject {
}
isPlaying = !isPlaying
}
+
+ func getVideoSize(_ url: URL) async -> CGSize {
+ let videoAsset = AVURLAsset(url : url)
+
+ let videoAssetTrack = try? await videoAsset.loadTracks(withMediaType: .video).first
+ let naturalSize = (try? await videoAssetTrack?.load(.naturalSize)) ?? .zero
+ let transform = try? await videoAssetTrack?.load(.preferredTransform)
+ if (transform?.tx == naturalSize.width && transform?.ty == naturalSize.height) || (transform?.tx == 0 && transform?.ty == 0) {
+ return naturalSize
+ } else {
+ return CGSize(width: naturalSize.height, height: naturalSize.width)
+ }
+ }
}
diff --git a/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenContainer.swift b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenContainer.swift
new file mode 100644
index 0000000..69e7057
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/Fullscreen/FullscreenContainer.swift
@@ -0,0 +1,134 @@
+//
+// Created by Alex.M on 09.06.2022.
+//
+
+import Foundation
+import SwiftUI
+import AnchoredPopup
+
+struct FullscreenContainer: View {
+
+ @EnvironmentObject private var selectionService: SelectionService
+ @Environment(\.mediaPickerTheme) private var theme
+
+ @ObservedObject var keyboardHeightHelper = KeyboardHeightHelper.shared
+
+ @Binding var currentFullscreenMedia: Media?
+ @Binding var selection: AssetMediaModel.ID?
+ let animationID: String
+ let assetMediaModels: [AssetMediaModel]
+ var selectionParamsHolder: SelectionParamsHolder
+ var dismiss: ()->()
+
+ private var selectedMediaModel: AssetMediaModel? {
+ assetMediaModels.first { $0.id == selection }
+ }
+
+ private var selectionServiceIndex: Int? {
+ guard let selectedMediaModel = selectedMediaModel else {
+ return nil
+ }
+ return selectionService.index(of: selectedMediaModel)
+ }
+
+ var body: some View {
+ VStack {
+ controlsOverlay
+ GeometryReader { g in
+ contentView(g.size)
+ }
+ }
+ .safeAreaPadding(.top, UIApplication.safeArea.top)
+ .background {
+ theme.main.fullscreenPhotoBackground
+ .ignoresSafeArea()
+ }
+ .onAppear {
+ if let selectedMediaModel {
+ currentFullscreenMedia = Media(source: selectedMediaModel)
+ }
+ }
+ .onDisappear {
+ currentFullscreenMedia = nil
+ }
+ .onChange(of: selection) {
+ if let selectedMediaModel {
+ currentFullscreenMedia = Media(source: selectedMediaModel)
+ }
+ }
+ .onTapGesture {
+ if keyboardHeightHelper.keyboardDisplayed {
+ dismissKeyboard()
+ } else {
+ if let selectedMediaModel = selectedMediaModel, selectedMediaModel.mediaType == .image {
+ selectionService.onSelect(assetMediaModel: selectedMediaModel)
+ }
+ }
+ }
+ }
+
+ @ViewBuilder
+ func contentView(_ size: CGSize) -> some View {
+ if #available(iOS 17.0, *) {
+ ScrollViewReader { scrollReader in
+ ScrollView(.horizontal, showsIndicators: false) {
+ LazyHStack(spacing: 0) {
+ ForEach(assetMediaModels, id: \.id) { assetMediaModel in
+ FullscreenCell(viewModel: FullscreenCellViewModel(mediaModel: assetMediaModel), size: size)
+ .frame(width: size.width, height: size.height)
+ .id(assetMediaModel.id)
+ }
+ }
+ .scrollTargetLayout()
+ }
+ .scrollTargetBehavior(.viewAligned)
+ .scrollPosition(id: $selection)
+ .onAppear {
+ scrollReader.scrollTo(selection)
+ }
+ }
+ } else {
+ TabView(selection: $selection) {
+ ForEach(assetMediaModels, id: \.id) { assetMediaModel in
+ FullscreenCell(viewModel: FullscreenCellViewModel(mediaModel: assetMediaModel), size: size)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .tag(assetMediaModel.id)
+ }
+ }
+ }
+ }
+
+ var controlsOverlay: some View {
+ HStack {
+ Image(systemName: "xmark")
+ .resizable()
+ .frame(width: 20, height: 20)
+ .padding(20, 16)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ selection = nil
+ AnchoredPopup.launchShrinkingAnimation(id: animationID)
+ }
+
+ Spacer()
+
+ if let selectedMediaModel = selectedMediaModel {
+ if selectionParamsHolder.selectionLimit == 1 {
+ Button("Select") {
+ AnchoredPopup.launchShrinkingAnimation(id: animationID)
+ selectionService.onSelect(assetMediaModel: selectedMediaModel)
+ dismiss()
+ }
+ .padding(.horizontal, 20)
+ } else {
+ SelectionIndicatorView(index: selectionServiceIndex, isFullscreen: true, canSelect: selectionService.canSelect(assetMediaModel: selectedMediaModel), selectionParamsHolder: selectionParamsHolder)
+ .padding(.horizontal, 20)
+ .onTapGesture {
+ selectionService.onSelect(assetMediaModel: selectedMediaModel) // for video selection, since tap on video is toggle play
+ }
+ }
+ }
+ }
+ .foregroundStyle(theme.selection.fullscreenSelectedBackground)
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/AlbumSelectionView.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/AlbumSelectionView.swift
similarity index 69%
rename from Sources/ExyteMediaPicker/Views/Pages/MediaPicker/AlbumSelectionView.swift
rename to Sources/ExyteMediaPicker/Screens/MediaPicker/AlbumSelectionView.swift
index a11f217..8034743 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/AlbumSelectionView.swift
+++ b/Sources/ExyteMediaPicker/Screens/MediaPicker/AlbumSelectionView.swift
@@ -13,25 +13,23 @@ public struct AlbumSelectionView: View {
@Binding var showingCamera: Bool
@Binding var currentFullscreenMedia: Media?
- let showingLiveCameraCell: Bool
+
let selectionParamsHolder: SelectionParamsHolder
+ let mediaPickerParamsHolder: MediaPickerParamsHolder
let filterClosure: MediaPicker.FilterClosure?
let massFilterClosure: MediaPicker.MassFilterClosure?
-
- @State private var showingLoadingCell = false
+ var dismiss: ()->()
public var body: some View {
switch viewModel.internalPickerMode {
case .photos:
AlbumView(
- viewModel: AlbumViewModel(
- mediasProvider: AllPhotosProvider(selectionParamsHolder: selectionParamsHolder, filterClosure: filterClosure, massFilterClosure: massFilterClosure, showingLoadingCell: $showingLoadingCell)
- ),
+ viewModel: AllMediasProvider(selectionParamsHolder: selectionParamsHolder, filterClosure: filterClosure, massFilterClosure: massFilterClosure),
showingCamera: $showingCamera,
currentFullscreenMedia: $currentFullscreenMedia,
- shouldShowCamera: showingLiveCameraCell,
- shouldShowLoadingCell: showingLoadingCell,
- selectionParamsHolder: selectionParamsHolder
+ selectionParamsHolder: selectionParamsHolder,
+ mediaPickerParamsHolder: mediaPickerParamsHolder,
+ dismiss: dismiss
)
case .albums:
AlbumsView(
@@ -42,6 +40,7 @@ public struct AlbumSelectionView: View {
showingCamera: $showingCamera,
currentFullscreenMedia: $currentFullscreenMedia,
selectionParamsHolder: selectionParamsHolder,
+ mediaPickerParamsHolder: mediaPickerParamsHolder,
filterClosure: filterClosure,
massFilterClosure: massFilterClosure
)
@@ -51,14 +50,12 @@ public struct AlbumSelectionView: View {
case .album(let album):
if let albumModel = viewModel.getAlbumModel(album) {
AlbumView(
- viewModel: AlbumViewModel(
- mediasProvider: AlbumMediasProvider(album: albumModel, selectionParamsHolder: selectionParamsHolder, filterClosure: filterClosure, massFilterClosure: massFilterClosure, showingLoadingCell: $showingLoadingCell)
- ),
+ viewModel: AlbumMediasProvider(album: albumModel, selectionParamsHolder: SelectionParamsHolder(), filterClosure: filterClosure, massFilterClosure: massFilterClosure),
showingCamera: $showingCamera,
currentFullscreenMedia: $currentFullscreenMedia,
- shouldShowCamera: false,
- shouldShowLoadingCell: showingLoadingCell,
- selectionParamsHolder: selectionParamsHolder
+ selectionParamsHolder: selectionParamsHolder,
+ mediaPickerParamsHolder: MediaPickerParamsHolder(liveCameraCell: .none),
+ dismiss: dismiss
)
.id(album.id)
}
diff --git a/Sources/ExyteMediaPicker/Screens/MediaPicker/GenenricsTrick.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/GenenricsTrick.swift
new file mode 100644
index 0000000..9b93c18
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/MediaPicker/GenenricsTrick.swift
@@ -0,0 +1,110 @@
+//
+// SwiftUIView.swift
+//
+//
+// Created by Alisa Mylnikova on 18.10.2023.
+//
+
+import SwiftUI
+
+// MARK: - Partial genereic specification imitation
+
+public extension MediaPicker where AlbumSelectionContent == EmptyView, CameraSelectionContent == EmptyView, CameraViewContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: nil,
+ cameraSelectionBuilder: nil,
+ cameraViewBuilder: nil)
+ }
+}
+
+public extension MediaPicker where CameraSelectionContent == EmptyView, CameraViewContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ albumSelectionBuilder: @escaping AlbumSelectionClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: albumSelectionBuilder,
+ cameraSelectionBuilder: nil,
+ cameraViewBuilder: nil)
+ }
+}
+
+public extension MediaPicker where AlbumSelectionContent == EmptyView, CameraViewContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ cameraSelectionBuilder: @escaping CameraSelectionClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: nil,
+ cameraSelectionBuilder: cameraSelectionBuilder,
+ cameraViewBuilder: nil)
+ }
+}
+
+public extension MediaPicker where AlbumSelectionContent == EmptyView, CameraSelectionContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ cameraViewBuilder: @escaping CameraViewClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: nil,
+ cameraSelectionBuilder: nil,
+ cameraViewBuilder: cameraViewBuilder)
+ }
+}
+
+public extension MediaPicker where CameraViewContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ albumSelectionBuilder: @escaping AlbumSelectionClosure,
+ cameraSelectionBuilder: @escaping CameraSelectionClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: albumSelectionBuilder,
+ cameraSelectionBuilder: cameraSelectionBuilder,
+ cameraViewBuilder: nil)
+ }
+}
+
+public extension MediaPicker where CameraViewContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ albumSelectionBuilder: @escaping AlbumSelectionClosure,
+ cameraViewBuilder: @escaping CameraViewClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: albumSelectionBuilder,
+ cameraSelectionBuilder: nil,
+ cameraViewBuilder: cameraViewBuilder)
+ }
+}
+
+public extension MediaPicker where AlbumSelectionContent == EmptyView {
+
+ init(isPresented: Binding,
+ onChange: @escaping MediaPickerCompletionClosure,
+ cameraSelectionBuilder: @escaping CameraSelectionClosure,
+ cameraViewBuilder: @escaping CameraViewClosure) {
+
+ self.init(isPresented: isPresented,
+ onChange: onChange,
+ albumSelectionBuilder: nil,
+ cameraSelectionBuilder: cameraSelectionBuilder,
+ cameraViewBuilder: cameraViewBuilder)
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPicker.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPicker.swift
similarity index 54%
rename from Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPicker.swift
rename to Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPicker.swift
index c4d9c02..ebbad49 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPicker.swift
+++ b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPicker.swift
@@ -3,23 +3,34 @@
//
import SwiftUI
-import Combine
-public struct MediaPicker: View {
+public struct MediaPicker: View {
/// To provide custom buttons layout for photos grid view use actions and views provided by this closure:
/// - standard header with photos/albums switcher
/// - selection view you can embed in your view
- public typealias AlbumSelectionClosure = ((ModeSwitcher, AlbumSelectionView) -> AlbumSelectionContent)
+ /// - is in fullscreen photo details mode
+ public typealias AlbumSelectionClosure = ((ModeSwitcher, AlbumSelectionView, Bool) -> AlbumSelectionContent)
- /// To provide custom buttons layout for camera selection view use actions and views by this closure:
+ /// To provide custom buttons layout for camera selection view use actions and views provided by this closure:
/// - add more photos closure
/// - cancel closure
/// - selection view you can embed in your view
public typealias CameraSelectionClosure = ((@escaping SimpleClosure, @escaping SimpleClosure, CameraSelectionView) -> CameraSelectionContent)
- public typealias FilterClosure = (Media) async -> Media?
- public typealias MassFilterClosure = ([Media]) async -> [Media]
+ /// To provide custom buttons layout for camera view use actions and views provided by this closure:
+ /// - live camera capture view
+ /// - cancel closure
+ /// - show preview of taken photos
+ /// - take photo closure
+ /// - start record video closure
+ /// - stop record video closure
+ /// - flash off/on closure
+ /// - camera back/front closure
+ public typealias CameraViewClosure = ((LiveCameraView, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure, @escaping SimpleClosure) -> CameraViewContent)
+
+ public typealias FilterClosure = @Sendable (Media) async -> Media?
+ public typealias MassFilterClosure = @Sendable ([Media]) async -> [Media]
// MARK: - Parameters
@@ -30,19 +41,20 @@ public struct MediaPicker?
- private var showingLiveCameraCell: Bool = false
private var didPressCancelCamera: (() -> Void)?
private var orientationHandler: MediaPickerOrientationHandler = {_ in}
private var filterClosure: FilterClosure?
private var massFilterClosure: MassFilterClosure?
private var selectionParamsHolder = SelectionParamsHolder()
+ private var mediaPickerParamsHolder = MediaPickerParamsHolder()
// MARK: - Inner values
@@ -51,75 +63,58 @@ public struct MediaPicker,
onChange: @escaping MediaPickerCompletionClosure,
- albumSelectionBuilder: @escaping AlbumSelectionClosure,
- cameraSelectionBuilder: @escaping CameraSelectionClosure) {
+ albumSelectionBuilder: AlbumSelectionClosure? = nil,
+ cameraSelectionBuilder: CameraSelectionClosure? = nil,
+ cameraViewBuilder: CameraViewClosure? = nil) {
self._isPresented = isPresented
self._albums = .constant([])
- self._currentFullscreenMedia = .constant(nil)
+ self._currentFullscreenMediaBinding = .constant(nil)
self.onChange = onChange
self.albumSelectionBuilder = albumSelectionBuilder
self.cameraSelectionBuilder = cameraSelectionBuilder
+ self.cameraViewBuilder = cameraViewBuilder
}
public var body: some View {
- NavigationView {
- Group {
- switch viewModel.internalPickerMode {
+ Group {
+ switch internalPickerMode { // please don't use viewModel.internalPickerMode here - it slows down camera dismissal
case .photos, .albums, .album(_):
albumSelectionContainer
case .camera:
- ZStack {
- Color.black
- .ignoresSafeArea(.all)
- .onAppear {
- DispatchQueue.main.async {
- readyToShowCamera = true
- }
- }
- .onDisappear {
- readyToShowCamera = false
- }
- if readyToShowCamera {
- cameraSheet() {
- // did take picture
- if !cameraSelectionService.hasSelected {
- viewModel.setPickerMode(.cameraSelection)
- }
- guard let url = viewModel.pickedMediaUrl else { return }
- cameraSelectionService.onSelect(media: URLMediaModel(url: url))
- viewModel.pickedMediaUrl = nil
- } didPressCancel: {
- if let didPressCancel = didPressCancelCamera {
- didPressCancel()
- } else {
- viewModel.setPickerMode(.photos)
- }
- }
- .confirmationDialog("", isPresented: $viewModel.showingExitCameraConfirmation, titleVisibility: .hidden) {
- deleteAllButton
- }
- }
- }
+ cameraContainer
case .cameraSelection:
cameraSelectionContainer
}
- }
}
- .background(theme.main.albumSelectionBackground.ignoresSafeArea())
+ .background(theme.main.pickerBackground.ignoresSafeArea())
.environmentObject(selectionService)
.environmentObject(cameraSelectionService)
- .environmentObject(permissionService)
.onAppear {
+ PermissionsService.shared.updatePhotoLibraryAuthorizationStatus()
+#if !targetEnvironment(simulator)
+ if mediaPickerParamsHolder.liveCameraCell != .none {
+ PermissionsService.shared.requestCameraPermission()
+ } else {
+ PermissionsService.shared.updateCameraAuthorizationStatus()
+ }
+#endif
+
selectionService.onChange = onChange
selectionService.mediaSelectionLimit = selectionParamsHolder.selectionLimit
@@ -131,17 +126,20 @@ public struct MediaPicker Binding {
@@ -243,12 +294,25 @@ public struct MediaPicker(), didPressCancel: @escaping ()->()) -> some View {
#if targetEnvironment(simulator)
- CameraStubView(isPresented: cameraBinding())
+ CameraStubView {
+ didPressCancel()
+ }
#elseif os(iOS)
- CameraView(viewModel: viewModel, didTakePicture: didTakePicture, didPressCancel: didPressCancel, selectionParamsHolder: selectionParamsHolder)
- .ignoresSafeArea()
+ Group {
+ if let cameraViewBuilder = cameraViewBuilder {
+ CustomCameraView(viewModel: viewModel, didTakePicture: didTakePicture, didPressCancel: didPressCancel, cameraViewBuilder: cameraViewBuilder)
+ .ignoresSafeArea()
+ } else {
+ StandardConrolsCameraView(viewModel: viewModel, didTakePicture: didTakePicture, didPressCancel: didPressCancel, selectionParamsHolder: selectionParamsHolder)
+ .ignoresSafeArea()
+ }
+ }
+ .onAppear {
+ PermissionsService.shared.requestCameraPermission()
+ }
#endif
}
}
@@ -257,10 +321,15 @@ public struct MediaPicker MediaPicker {
- var mediaPicker = self
- mediaPicker.showingLiveCameraCell = true
- return mediaPicker
+ func liveCameraCell(_ style: LiveCameraCellStyle = .small) -> MediaPicker {
+ mediaPickerParamsHolder.liveCameraCell = style
+ return self
+ }
+
+ @available(*, deprecated, message: "use liveCameraCell instead")
+ func showLiveCameraCell(_ show: Bool = true) -> MediaPicker {
+ mediaPickerParamsHolder.liveCameraCell = show ? .small : .none
+ return self
}
func mediaSelectionType(_ type: MediaSelectionType) -> MediaPicker {
@@ -278,6 +347,29 @@ public extension MediaPicker {
return self
}
+ func showFullscreenPreview(_ show: Bool) -> MediaPicker {
+ selectionParamsHolder.showFullscreenPreview = show
+ return self
+ }
+
+ func setMediaPickerParameters(_ params: MediaPickerParamsHolder?) -> MediaPicker {
+ guard let params = params else {
+ return self
+ }
+ var mediaPicker = self
+ mediaPicker.mediaPickerParamsHolder = params
+ return mediaPicker
+ }
+
+ func setSelectionParameters(_ params: SelectionParamsHolder?) -> MediaPicker {
+ guard let params = params else {
+ return self
+ }
+ var mediaPicker = self
+ mediaPicker.selectionParamsHolder = params
+ return mediaPicker
+ }
+
func applyFilter(_ filterClosure: @escaping FilterClosure) -> MediaPicker {
var mediaPicker = self
mediaPicker.filterClosure = filterClosure
@@ -304,7 +396,7 @@ public extension MediaPicker {
func currentFullscreenMedia(_ currentFullscreenMedia: Binding) -> MediaPicker {
var mediaPicker = self
- mediaPicker._currentFullscreenMedia = currentFullscreenMedia
+ mediaPicker._currentFullscreenMediaBinding = currentFullscreenMedia
return mediaPicker
}
@@ -320,48 +412,3 @@ public extension MediaPicker {
return mediaPicker
}
}
-
-// MARK: - Partial genereic specification imitation
-
-public extension MediaPicker where AlbumSelectionContent == EmptyView, CameraSelectionContent == EmptyView {
-
- init(isPresented: Binding,
- onChange: @escaping MediaPickerCompletionClosure) {
-
- self._isPresented = isPresented
- self._albums = .constant([])
- self._currentFullscreenMedia = .constant(nil)
-
- self.onChange = onChange
- }
-}
-
-public extension MediaPicker where AlbumSelectionContent == EmptyView {
-
- init(isPresented: Binding,
- onChange: @escaping MediaPickerCompletionClosure,
- cameraSelectionBuilder: @escaping CameraSelectionClosure) {
-
- self._isPresented = isPresented
- self._albums = .constant([])
- self._currentFullscreenMedia = .constant(nil)
-
- self.onChange = onChange
- self.cameraSelectionBuilder = cameraSelectionBuilder
- }
-}
-
-public extension MediaPicker where CameraSelectionContent == EmptyView {
-
- init(isPresented: Binding,
- onChange: @escaping MediaPickerCompletionClosure,
- albumSelectionBuilder: @escaping AlbumSelectionClosure) {
-
- self._isPresented = isPresented
- self._albums = .constant([])
- self._currentFullscreenMedia = .constant(nil)
-
- self.onChange = onChange
- self.albumSelectionBuilder = albumSelectionBuilder
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPickerMode.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerMode.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPickerMode.swift
rename to Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerMode.swift
diff --git a/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerParamsHolder.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerParamsHolder.swift
new file mode 100644
index 0000000..dda203d
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerParamsHolder.swift
@@ -0,0 +1,8 @@
+public class MediaPickerParamsHolder {
+
+ var liveCameraCell: LiveCameraCellStyle
+
+ public init(liveCameraCell: LiveCameraCellStyle = LiveCameraCellStyle.small) {
+ self.liveCameraCell = liveCameraCell
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPickerViewModel.swift b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerViewModel.swift
similarity index 70%
rename from Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPickerViewModel.swift
rename to Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerViewModel.swift
index ddc0226..fe1be83 100644
--- a/Sources/ExyteMediaPicker/Views/Pages/MediaPicker/MediaPickerViewModel.swift
+++ b/Sources/ExyteMediaPicker/Screens/MediaPicker/MediaPickerViewModel.swift
@@ -4,7 +4,6 @@
import Foundation
import SwiftUI
-import Combine
@MainActor
final class MediaPickerViewModel: ObservableObject {
@@ -14,20 +13,17 @@ final class MediaPickerViewModel: ObservableObject {
@Published var pickedMediaUrl: URL?
#endif
+ @Published private(set) var defaultAlbumsProvider = DefaultAlbumsProvider()
@Published private(set) var internalPickerMode: MediaPickerMode = .photos
- @Published private(set) var albums: [AlbumModel] = []
+
+ var albums: [AlbumModel] {
+ defaultAlbumsProvider.albums
+ }
var shouldUpdatePickerMode: (MediaPickerMode)->() = {_ in}
- let defaultAlbumsProvider = DefaultAlbumsProvider()
- private let watcher = PhotoLibraryChangePermissionWatcher()
- private var albumsCancellable: AnyCancellable?
-
func onStart() {
defaultAlbumsProvider.reload()
- albumsCancellable = defaultAlbumsProvider.albums.sink { [weak self] albums in
- self?.albums = albums
- }
}
func getAlbumModel(_ album: Album) -> AlbumModel? {
diff --git a/Sources/ExyteMediaPicker/Extensions/Bundle+.swift b/Sources/ExyteMediaPicker/Theme/Bundle+.swift
similarity index 88%
rename from Sources/ExyteMediaPicker/Extensions/Bundle+.swift
rename to Sources/ExyteMediaPicker/Theme/Bundle+.swift
index 9d1f8b2..b5f0ae3 100644
--- a/Sources/ExyteMediaPicker/Extensions/Bundle+.swift
+++ b/Sources/ExyteMediaPicker/Theme/Bundle+.swift
@@ -1,5 +1,5 @@
//
-// File.swift
+// Bundle+.swift
//
//
// Created by Alex.M on 07.07.2022.
@@ -19,7 +19,7 @@ private final class BundleToken {
private init() {}
}
-extension Bundle {
+public extension Bundle {
static var current: Bundle {
BundleToken.bundle
}
diff --git a/Sources/ExyteMediaPicker/Theme/MediaPickerTheme.swift b/Sources/ExyteMediaPicker/Theme/MediaPickerTheme.swift
new file mode 100644
index 0000000..e7dc723
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Theme/MediaPickerTheme.swift
@@ -0,0 +1,156 @@
+//
+// Created by Alex.M on 06.07.2022.
+//
+
+import Foundation
+import SwiftUI
+
+public struct MediaPickerTheme: Sendable {
+ public let main: Main
+ public let selection: Selection
+ public let cellStyle: CellStyle
+ public let error: Error
+ public let defaultHeader: DefaultHeader
+
+ public init(main: MediaPickerTheme.Main = .init(),
+ selection: MediaPickerTheme.Selection = .init(),
+ cellStyle: MediaPickerTheme.CellStyle = .init(),
+ error: MediaPickerTheme.Error = .init(),
+ defaultHeader: MediaPickerTheme.DefaultHeader = .init()) {
+ self.main = main
+ self.selection = selection
+ self.cellStyle = cellStyle
+ self.error = error
+ self.defaultHeader = defaultHeader
+ }
+}
+
+extension MediaPickerTheme {
+ public struct Main: Sendable {
+ public let pickerText: Color
+ public let pickerBackground: Color
+ public let fullscreenPhotoBackground: Color
+ public let cameraText: Color
+ public let cameraBackground: Color
+ public let cameraSelectionText: Color
+ public let cameraSelectionBackground: Color
+
+ public init(
+ pickerText: Color = Color("pickerText", bundle: .current),
+ pickerBackground: Color = Color("pickerBG", bundle: .current),
+ fullscreenPhotoBackground: Color = Color("pickerBG", bundle: .current),
+ cameraText: Color = Color("cameraText", bundle: .current),
+ cameraBackground: Color = Color("cameraBG", bundle: .current),
+ cameraSelectionText: Color = Color("cameraText", bundle: .current),
+ cameraSelectionBackground: Color = Color("cameraBG", bundle: .current)
+ ) {
+ self.pickerText = pickerText
+ self.pickerBackground = pickerBackground
+ self.fullscreenPhotoBackground = fullscreenPhotoBackground
+ self.cameraText = cameraText
+ self.cameraBackground = cameraBackground
+ self.cameraSelectionText = cameraSelectionText
+ self.cameraSelectionBackground = cameraSelectionBackground
+ }
+ }
+
+ public struct Selection: Sendable {
+ public let cellEmptyBorder: Color
+ public let cellEmptyBackground: Color
+ public let cellSelectedBorder: Color
+ public let cellSelectedBackground: Color
+ public let cellSelectedCheckmark: Color
+ public let fullscreenEmptyBorder: Color
+ public let fullscreenEmptyBackground: Color
+ public let fullscreenSelectedBorder: Color
+ public let fullscreenSelectedBackground: Color
+ public let fullscreenSelectedCheckmark: Color
+
+ public init(
+ cellEmptyBorder: Color = .white,
+ cellEmptyBackground: Color = .black.opacity(0.25),
+ cellSelectedBorder: Color = .white,
+ cellSelectedBackground: Color = Color("selection", bundle: .current),
+ cellSelectedCheckmark: Color = .white,
+ fullscreenEmptyBorder: Color = Color("selection", bundle: .current),
+ fullscreenEmptyBackground: Color = .clear,
+ fullscreenSelectedBorder: Color = Color("selection", bundle: .current),
+ fullscreenSelectedBackground: Color = Color("selection", bundle: .current),
+ fullscreenSelectedCheckmark: Color = .white
+ ) {
+ self.cellEmptyBorder = cellEmptyBorder
+ self.cellEmptyBackground = cellEmptyBackground
+ self.cellSelectedBorder = cellSelectedBorder
+ self.cellSelectedBackground = cellSelectedBackground
+ self.cellSelectedCheckmark = cellSelectedCheckmark
+ self.fullscreenEmptyBorder = fullscreenEmptyBorder
+ self.fullscreenEmptyBackground = fullscreenEmptyBackground
+ self.fullscreenSelectedBorder = fullscreenSelectedBorder
+ self.fullscreenSelectedBackground = fullscreenSelectedBackground
+ self.fullscreenSelectedCheckmark = fullscreenSelectedCheckmark
+ }
+
+ public init(
+ accent: Color,
+ tint: Color = .white,
+ background: Color = .black.opacity(0.25)
+ ) {
+ self.init(
+ cellEmptyBorder: tint,
+ cellEmptyBackground: background,
+ cellSelectedBorder: tint,
+ cellSelectedBackground: accent,
+ cellSelectedCheckmark: tint,
+ fullscreenEmptyBorder: accent,
+ fullscreenEmptyBackground: .clear,
+ fullscreenSelectedBorder: accent,
+ fullscreenSelectedBackground: accent,
+ fullscreenSelectedCheckmark: tint
+ )
+ }
+ }
+
+ public struct CellStyle: Sendable {
+ public let columnsSpacing: CGFloat
+ public let rowSpacing: CGFloat
+ public let cornerRadius: CGFloat
+
+ public init(columnsSpacing: CGFloat = 1,
+ rowSpacing: CGFloat = 1,
+ cornerRadius: CGFloat = 0) {
+ self.columnsSpacing = columnsSpacing
+ self.rowSpacing = rowSpacing
+ self.cornerRadius = cornerRadius
+ }
+ }
+
+ public struct Error: Sendable {
+ public let background: Color
+ public let tint: Color
+
+ public init(background: Color = .red.opacity(0.7),
+ tint: Color = Color("cameraText", bundle: .current)) {
+ self.background = background
+ self.tint = tint
+ }
+ }
+
+ public struct DefaultHeader: Sendable {
+ public let background: Color
+
+ public init(background: Color = Color("pickerBG", bundle: .current),
+ segmentTintColor: Color = Color("pickerBG", bundle: .current),
+ selectedSegmentTintColor: Color = Color("pickerBG", bundle: .current),
+ selectedText: Color = Color("pickerText", bundle: .current),
+ unselectedText: Color = Color("pickerText", bundle: .current)) {
+ self.background = background
+
+ DispatchQueue.main.async {
+ UISegmentedControl.appearance().backgroundColor = UIColor(segmentTintColor)
+ UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(selectedSegmentTintColor)
+ UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor(selectedText)], for: .selected)
+ UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor(unselectedText)], for: .normal)
+ }
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Errors/PermissionActionView.swift b/Sources/ExyteMediaPicker/Utils/Errors/PermissionActionView.swift
new file mode 100644
index 0000000..2a21bee
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Errors/PermissionActionView.swift
@@ -0,0 +1,79 @@
+//
+// Created by Alex.M on 06.06.2022.
+//
+
+import Foundation
+import SwiftUI
+
+struct PermissionActionView: View {
+
+ enum PermissionType {
+ case library(PermissionsService.PhotoLibraryPermissionStatus)
+ case camera(PermissionsService.CameraPermissionStatus)
+ }
+
+ let type: PermissionType
+
+ @State private var showSheet = false
+
+ var body: some View {
+ ZStack {
+ if showSheet {
+ LimitedLibraryPickerProxyView(isPresented: $showSheet) {
+ DispatchQueue.main.async {
+ PermissionsService.shared.updatePhotoLibraryAuthorizationStatus()
+ }
+ }
+ .frame(width: 1, height: 1)
+ }
+
+ switch type {
+ case .library(let status):
+ buildLibraryActionView(status)
+ case .camera(let status):
+ buildCameraActionView(status)
+ }
+ }
+ }
+}
+
+private extension PermissionActionView {
+
+ @ViewBuilder
+ func buildLibraryActionView(_ status: PermissionsService.PhotoLibraryPermissionStatus) -> some View {
+ switch status {
+ case .authorized, .unknown:
+ EmptyView()
+ case .limited:
+ PermissionsErrorView(text: "Setup Photos access to see more photos here") {
+ showSheet = true
+ }
+ case .unavailable:
+ goToSettingsButton(text: "Allow Photos access in settings to see photos here")
+ }
+ }
+
+ @ViewBuilder
+ func buildCameraActionView(_ status: PermissionsService.CameraPermissionStatus) -> some View {
+ switch status {
+ case .authorized, .unknown:
+ EmptyView()
+ case .unavailable:
+ goToSettingsButton(text: "Allow Camera access in settings to see live preview")
+ }
+ }
+
+ func goToSettingsButton(text: String) -> some View {
+ PermissionsErrorView(
+ text: text,
+ action: {
+ DispatchQueue.main.async {
+ guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
+ if UIApplication.shared.canOpenURL(url) {
+ UIApplication.shared.open(url)
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsErrorView.swift b/Sources/ExyteMediaPicker/Utils/Errors/PermissionsErrorView.swift
similarity index 96%
rename from Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsErrorView.swift
rename to Sources/ExyteMediaPicker/Utils/Errors/PermissionsErrorView.swift
index 8a3f9c0..31b2468 100644
--- a/Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsErrorView.swift
+++ b/Sources/ExyteMediaPicker/Utils/Errors/PermissionsErrorView.swift
@@ -30,5 +30,6 @@ struct PermissionsErrorView: View {
.background(theme.error.background)
.cornerRadius(5)
.padding(.horizontal, 20)
+ .padding(.bottom, 6)
}
}
diff --git a/Sources/ExyteMediaPicker/Utils/Extensions/Collection+.swift b/Sources/ExyteMediaPicker/Utils/Extensions/Collection+.swift
new file mode 100644
index 0000000..104ad59
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Extensions/Collection+.swift
@@ -0,0 +1,12 @@
+//
+// File.swift
+// ExyteMediaPicker
+//
+// Created by Alisa Mylnikova on 25.02.2025.
+//
+
+extension Collection {
+ subscript(safe index: Index) -> Element? {
+ indices.contains(index) ? self[index] : nil
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Extensions/ColumnCalculation.swift b/Sources/ExyteMediaPicker/Utils/Extensions/ColumnCalculation.swift
new file mode 100644
index 0000000..054e111
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Extensions/ColumnCalculation.swift
@@ -0,0 +1,18 @@
+//
+// Untitled.swift
+// ExyteMediaPicker
+//
+// Created by Alisa Mylnikova on 25.03.2025.
+//
+
+import SwiftUI
+
+@MainActor
+func calculateColumnWidth(spacing: CGFloat) -> (CGFloat, [GridItem]) {
+ let gridWidth = UIScreen.main.bounds.width
+ let wholeCount = 3.0
+ let noSpaces = gridWidth - spacing * (wholeCount - 1)
+ let columnWidth = noSpaces / wholeCount
+ let columns = Array(repeating: GridItem(.fixed(columnWidth), spacing: spacing, alignment: .top), count: Int(wholeCount))
+ return (columnWidth, columns)
+}
diff --git a/Sources/ExyteMediaPicker/Extensions/OrientationTransformationExtensions.swift b/Sources/ExyteMediaPicker/Utils/Extensions/OrientationTransformationExtensions.swift
similarity index 64%
rename from Sources/ExyteMediaPicker/Extensions/OrientationTransformationExtensions.swift
rename to Sources/ExyteMediaPicker/Utils/Extensions/OrientationTransformationExtensions.swift
index 9125cc2..ccacf6c 100644
--- a/Sources/ExyteMediaPicker/Extensions/OrientationTransformationExtensions.swift
+++ b/Sources/ExyteMediaPicker/Utils/Extensions/OrientationTransformationExtensions.swift
@@ -21,15 +21,3 @@ extension UIImage.Orientation {
static var `default`: UIImage.Orientation { .right }
}
-
-extension AVCaptureVideoOrientation {
-
- init(_ orientation: UIDeviceOrientation) {
- switch orientation {
- case .landscapeLeft: self = .landscapeRight
- case .landscapeRight: self = .landscapeLeft
- default: self = .portrait
- }
- }
-
-}
diff --git a/Sources/ExyteMediaPicker/Extensions/Sequence+asyncMap.swift b/Sources/ExyteMediaPicker/Utils/Extensions/Sequence+asyncMap.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Extensions/Sequence+asyncMap.swift
rename to Sources/ExyteMediaPicker/Utils/Extensions/Sequence+asyncMap.swift
diff --git a/Sources/ExyteMediaPicker/Extensions/TimeInterval+Duration.swift b/Sources/ExyteMediaPicker/Utils/Extensions/TimeInterval+Duration.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Extensions/TimeInterval+Duration.swift
rename to Sources/ExyteMediaPicker/Utils/Extensions/TimeInterval+Duration.swift
diff --git a/Sources/ExyteMediaPicker/Utils/Extensions/View+.swift b/Sources/ExyteMediaPicker/Utils/Extensions/View+.swift
new file mode 100644
index 0000000..5fc3791
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Extensions/View+.swift
@@ -0,0 +1,42 @@
+//
+// File.swift
+//
+//
+// Created by Alisa Mylnikova on 04.09.2023.
+//
+
+import SwiftUI
+
+extension View {
+ func dismissKeyboard() {
+ DispatchQueue.main.async {
+ UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+ }
+ }
+}
+
+extension Shape {
+ func styled(_ foregroundColor: Color, border borderColor: Color = .clear, _ borderWidth: CGFloat = 0) -> some View {
+ self.foregroundStyle(foregroundColor) // Apply foreground color
+ .overlay {
+ self
+ .stroke(borderColor, lineWidth: borderWidth) // Apply border color and width
+ }
+ }
+}
+
+extension View {
+ func padding(_ horizontal: CGFloat, _ vertical: CGFloat) -> some View {
+ self.padding(.horizontal, horizontal)
+ .padding(.vertical, vertical)
+ }
+
+ @ViewBuilder
+ func applyIf(_ condition: Bool, apply: (Self) -> T) -> some View {
+ if condition {
+ apply(self)
+ } else {
+ self
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Extensions/View+NotificationCenter.swift b/Sources/ExyteMediaPicker/Utils/Extensions/View+NotificationCenter.swift
new file mode 100644
index 0000000..fe38084
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Extensions/View+NotificationCenter.swift
@@ -0,0 +1,15 @@
+//
+// View+NotificationCenter.swift
+//
+//
+// Created by Alexandra Afonasova on 17.10.2022.
+//
+
+import SwiftUI
+
+extension View {
+ @MainActor func onRotate(perform: @escaping (UIDeviceOrientation) -> Void) -> some View {
+ let publisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
+ return onReceive(publisher) { _ in perform(UIDevice.current.orientation) }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Extensions/Zoom+ScrollView.swift b/Sources/ExyteMediaPicker/Utils/Extensions/Zoom+ScrollView.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Extensions/Zoom+ScrollView.swift
rename to Sources/ExyteMediaPicker/Utils/Extensions/Zoom+ScrollView.swift
diff --git a/Sources/ExyteMediaPicker/Utils/Modifiers/KeyboardHeightHelper.swift b/Sources/ExyteMediaPicker/Utils/Modifiers/KeyboardHeightHelper.swift
new file mode 100644
index 0000000..92ca3d6
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Modifiers/KeyboardHeightHelper.swift
@@ -0,0 +1,64 @@
+//
+// KeyboardHeightHelper.swift
+// Example-iOS
+//
+// Created by Alisa Mylnikova on 23.08.2023.
+//
+
+import SwiftUI
+
+#if compiler(>=6.0)
+extension Notification: @retroactive @unchecked Sendable { }
+#else
+extension Notification: @unchecked Sendable { }
+#endif
+
+@MainActor
+class KeyboardHeightHelper: ObservableObject {
+
+ static let shared = KeyboardHeightHelper()
+
+ @Published var keyboardHeight: CGFloat = 0
+ @Published var keyboardDisplayed: Bool = false
+
+ init() {
+ self.listenForKeyboardNotifications()
+ }
+
+ private func listenForKeyboardNotifications() {
+ NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] notification in
+ guard let self = self else { return }
+
+ Task { @MainActor in
+ // Safely extract the data from the notification on the main actor
+ guard let userInfo = notification.userInfo,
+ let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
+
+ // Now update the UI on the main actor
+ self.keyboardHeight = keyboardRect.height
+ self.keyboardDisplayed = true
+ }
+ }
+
+ NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { (notification) in
+ DispatchQueue.main.async {
+ self.keyboardHeight = 0
+ }
+ }
+
+ NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: .main) { (notification) in
+ DispatchQueue.main.async {
+ self.keyboardDisplayed = false
+ }
+ }
+ }
+
+ private func handleKeyboardWillShow(_ notification: Notification) {
+ guard let userInfo = notification.userInfo,
+ let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
+ return
+ }
+ self.keyboardHeight = keyboardRect.height
+ self.keyboardDisplayed = true
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Modifiers/MediaButtonStyle.swift b/Sources/ExyteMediaPicker/Utils/Modifiers/MediaButtonStyle.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Modifiers/MediaButtonStyle.swift
rename to Sources/ExyteMediaPicker/Utils/Modifiers/MediaButtonStyle.swift
diff --git a/Sources/ExyteMediaPicker/Utils/Modifiers/MediaPickerThemeModifier.swift b/Sources/ExyteMediaPicker/Utils/Modifiers/MediaPickerThemeModifier.swift
new file mode 100644
index 0000000..99d3233
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Modifiers/MediaPickerThemeModifier.swift
@@ -0,0 +1,52 @@
+//
+// Created by Alex.M on 06.07.2022.
+//
+
+import Foundation
+import SwiftUI
+
+public extension EnvironmentValues {
+ #if swift(>=6.0)
+ @Entry var mediaPickerTheme = MediaPickerTheme()
+ @Entry var mediaPickerThemeIsOverridden = false
+ #else
+ var mediaPickerTheme: MediaPickerTheme {
+ get { self[MediaPickerThemeKey.self] }
+ set { self[MediaPickerThemeKey.self] = newValue }
+ }
+
+ var mediaPickerThemeIsOverridden: Bool {
+ get { self[MediaPickerThemeIsOverriddenKey.self] }
+ set { self[MediaPickerThemeIsOverriddenKey.self] = newValue }
+ }
+ #endif
+}
+
+// Define keys only for older versions
+#if swift(<6.0)
+@preconcurrency public struct MediaPickerThemeKey: EnvironmentKey {
+ public static let defaultValue = MediaPickerTheme()
+}
+
+public struct MediaPickerThemeIsOverriddenKey: EnvironmentKey {
+ public static let defaultValue = false
+}
+#endif
+
+public extension View {
+ func mediaPickerTheme(_ theme: MediaPickerTheme) -> some View {
+ self.environment(\.mediaPickerTheme, theme)
+ .environment(\.mediaPickerThemeIsOverridden, true)
+ }
+
+ func mediaPickerTheme(
+ main: MediaPickerTheme.Main = .init(),
+ selection: MediaPickerTheme.Selection = .init(),
+ cellStyle: MediaPickerTheme.CellStyle = .init(),
+ error: MediaPickerTheme.Error = .init(),
+ defaultHeader: MediaPickerTheme.DefaultHeader = .init()
+ ) -> some View {
+ self.environment(\.mediaPickerTheme, MediaPickerTheme(main: main, selection: selection, cellStyle: cellStyle, error: error, defaultHeader: defaultHeader))
+ .environment(\.mediaPickerThemeIsOverridden, true)
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Modifiers/SafeAreaEnvironmentValues.swift b/Sources/ExyteMediaPicker/Utils/Modifiers/SafeAreaEnvironmentValues.swift
similarity index 65%
rename from Sources/ExyteMediaPicker/Modifiers/SafeAreaEnvironmentValues.swift
rename to Sources/ExyteMediaPicker/Utils/Modifiers/SafeAreaEnvironmentValues.swift
index 087ad6e..a5a1b61 100644
--- a/Sources/ExyteMediaPicker/Modifiers/SafeAreaEnvironmentValues.swift
+++ b/Sources/ExyteMediaPicker/Utils/Modifiers/SafeAreaEnvironmentValues.swift
@@ -21,28 +21,14 @@ extension UIApplication {
$0.isKeyWindow
}
}
-}
-private struct SafeAreaInsetsKey: EnvironmentKey {
- static var defaultValue: EdgeInsets {
+ static var safeArea: EdgeInsets {
UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
}
}
-extension EnvironmentValues {
- var safeAreaInsets: EdgeInsets {
- self[SafeAreaInsetsKey.self]
- }
-}
-
private extension UIEdgeInsets {
var swiftUiInsets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}
-
-private extension UIEdgeInsets {
- var insets: EdgeInsets {
- EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/Thumbnail/ThumbnailPlaceholder.swift b/Sources/ExyteMediaPicker/Utils/Thumbnail/ThumbnailPlaceholder.swift
similarity index 100%
rename from Sources/ExyteMediaPicker/Views/Widgets/Thumbnail/ThumbnailPlaceholder.swift
rename to Sources/ExyteMediaPicker/Utils/Thumbnail/ThumbnailPlaceholder.swift
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/Thumbnail/ThumbnailView.swift b/Sources/ExyteMediaPicker/Utils/Thumbnail/ThumbnailView.swift
similarity index 55%
rename from Sources/ExyteMediaPicker/Views/Widgets/Thumbnail/ThumbnailView.swift
rename to Sources/ExyteMediaPicker/Utils/Thumbnail/ThumbnailView.swift
index c1b1adb..76566ff 100644
--- a/Sources/ExyteMediaPicker/Views/Widgets/Thumbnail/ThumbnailView.swift
+++ b/Sources/ExyteMediaPicker/Utils/Thumbnail/ThumbnailView.swift
@@ -8,19 +8,18 @@ struct ThumbnailView: View {
#if os(iOS)
let preview: UIImage?
+ let size: CGFloat
#else
// FIXME: Create preview for image/video for other platforms
#endif
var body: some View {
if let preview = preview {
- GeometryReader { proxy in
- Image(uiImage: preview)
- .resizable()
- .scaledToFill()
- .frame(width: proxy.size.width, height: proxy.size.height)
- .clipped()
- }
+ Image(uiImage: preview)
+ .resizable()
+ .scaledToFill()
+ .frame(width: size, height: size)
+ .clipped()
} else {
ThumbnailPlaceholder()
}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/AsyncButton.swift b/Sources/ExyteMediaPicker/Utils/Widgets/AsyncButton.swift
new file mode 100644
index 0000000..58f8fac
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/AsyncButton.swift
@@ -0,0 +1,23 @@
+//
+// SwiftUIView.swift
+//
+//
+// Created by Alisa Mylnikova on 01.04.2025.
+//
+
+import SwiftUI
+
+struct AsyncButton: View {
+ var action: () async -> ()
+ var label: (()->Content)
+
+ var body: some View {
+ Button {
+ Task {
+ await action()
+ }
+ } label: {
+ label()
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/CameraStubView.swift b/Sources/ExyteMediaPicker/Utils/Widgets/CameraStubView.swift
similarity index 81%
rename from Sources/ExyteMediaPicker/Views/Widgets/CameraStubView.swift
rename to Sources/ExyteMediaPicker/Utils/Widgets/CameraStubView.swift
index 76cd613..0285101 100644
--- a/Sources/ExyteMediaPicker/Views/Widgets/CameraStubView.swift
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/CameraStubView.swift
@@ -7,8 +7,8 @@ import SwiftUI
struct CameraStubView: View {
- @Binding var isPresented: Bool
-
+ let didPressCancel: () -> Void
+
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
@@ -22,17 +22,20 @@ struct CameraStubView: View {
.font(.title3)
.multilineTextAlignment(.center)
Button("Close") {
- isPresented = false
+ didPressCancel()
}
.padding()
}
+ .foregroundStyle(.black)
}
}
}
struct CameraStubView_Preview: PreviewProvider {
static var previews: some View {
- CameraStubView(isPresented: .constant(true))
+ CameraStubView {
+ debugPrint("close")
+ }
}
}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/LimitedLibraryPickerProxyView.swift b/Sources/ExyteMediaPicker/Utils/Widgets/LimitedLibraryPickerProxyView.swift
new file mode 100644
index 0000000..c58ae28
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/LimitedLibraryPickerProxyView.swift
@@ -0,0 +1,37 @@
+//
+// Created by Alex.M on 02.06.2022.
+//
+
+import SwiftUI
+import UIKit
+import PhotosUI
+
+@MainActor
+struct LimitedLibraryPickerProxyView: UIViewControllerRepresentable {
+ @Binding var isPresented: Bool
+ var didDismiss: @Sendable ()->()
+
+ func makeUIViewController(context: Context) -> UIViewController {
+ let controller = UIViewController()
+
+ DispatchQueue.main.async { [controller] in
+ PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller)
+ trackCompletion(in: controller)
+ }
+
+ return controller
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+
+ func trackCompletion(in controller: UIViewController) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak controller] in
+ if controller?.presentedViewController == nil {
+ self.$isPresented.wrappedValue = false
+ self.didDismiss()
+ } else if let controller = controller {
+ self.trackCompletion(in: controller)
+ }
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/MediasGrid.swift b/Sources/ExyteMediaPicker/Utils/Widgets/MediasGrid.swift
new file mode 100644
index 0000000..3f21cae
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/MediasGrid.swift
@@ -0,0 +1,90 @@
+//
+// Created by Alex.M on 06.06.2022.
+//
+
+import Foundation
+import SwiftUI
+
+public struct MediasGrid: View
+where Element: Identifiable, Camera: View, Content: View, LoadingCell: View {
+
+ public let data: [Element]
+ public let liveCameraCell: LiveCameraCellStyle
+ public let camera: () -> Camera
+ public let content: (Element, _ index: Int, _ size: CGFloat) -> Content
+ public let loadingCell: () -> LoadingCell
+
+ @Environment(\.mediaPickerTheme) private var theme
+
+ public init(_ data: [Element],
+ liveCameraCell: LiveCameraCellStyle,
+ @ViewBuilder camera: @escaping () -> Camera,
+ @ViewBuilder content: @escaping (Element, Int, CGFloat) -> Content,
+ @ViewBuilder loadingCell: @escaping () -> LoadingCell) {
+ self.data = data
+ self.liveCameraCell = liveCameraCell
+ self.camera = camera
+ self.content = content
+ self.loadingCell = loadingCell
+ }
+
+ public var body: some View {
+ let (columnWidth, columns) = calculateColumnWidth(
+ spacing: theme.cellStyle.columnsSpacing
+ )
+ let spacing = theme.cellStyle.rowSpacing
+
+ switch liveCameraCell {
+ case .prominant:
+ let columnCount = columns.count
+ let indexedData = Array(data.enumerated())
+ let itemsToTake = (columnCount - 1) * 2
+ let topData = indexedData.prefix(itemsToTake)
+ let remainingData = indexedData.dropFirst(itemsToTake)
+
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(alignment: .top, spacing: spacing) {
+ camera()
+ .frame(width: columnWidth, height: columnWidth * 2 + spacing)
+ .clipped()
+
+ let topColumns = Array(
+ repeating: GridItem(.fixed(columnWidth), spacing: spacing,alignment: .top),
+ count: columnCount - 1
+ )
+
+ LazyVGrid(columns: topColumns, spacing: spacing) {
+ ForEach(topData, id: \.element.id) { index, element in
+ content(element, index, columnWidth)
+ }
+ }
+ }
+ .padding(.bottom, spacing)
+
+ LazyVGrid(columns: columns, spacing: spacing) {
+ ForEach(remainingData, id: \.element.id) { index, element in
+ content(element, index, columnWidth)
+ }
+ loadingCell()
+ }
+ }
+
+ case .small:
+ LazyVGrid(columns: columns, spacing: theme.cellStyle.rowSpacing) {
+ camera()
+ ForEach(data.indices, id: \.self) { index in
+ content(data[index], index, columnWidth)
+ }
+ loadingCell()
+ }
+
+ case .none:
+ LazyVGrid(columns: columns, spacing: spacing) {
+ ForEach(Array(data.enumerated()), id: \.element.id) { index, element in
+ content(element, index, columnWidth)
+ }
+ loadingCell()
+ }
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/PlayerUIView.swift b/Sources/ExyteMediaPicker/Utils/Widgets/PlayerUIView.swift
new file mode 100644
index 0000000..a2d1556
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/PlayerUIView.swift
@@ -0,0 +1,68 @@
+//
+// File.swift
+//
+//
+// Created by Alisa Mylnikova on 04.09.2023.
+//
+
+import SwiftUI
+import AVFoundation
+
+struct PlayerView: UIViewRepresentable {
+
+ var player: AVPlayer
+ var bgColor: Color
+ var useFill: Bool
+
+ func makeUIView(context: Context) -> PlayerUIView {
+ PlayerUIView(player: player, bgColor: bgColor, useFill: useFill)
+ }
+
+ func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext) {
+ uiView.playerLayer.player = player
+ uiView.playerLayer.videoGravity = useFill ? .resizeAspectFill : .resizeAspect
+ }
+}
+
+class PlayerUIView: UIView {
+
+ // MARK: Class Property
+
+ let playerLayer = AVPlayerLayer()
+
+ // MARK: Init
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ init(player: AVPlayer, bgColor: Color, useFill: Bool) {
+ super.init(frame: .zero)
+ self.playerSetup(player: player, bgColor: bgColor, useFill: useFill)
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ // MARK: Life-Cycle
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ playerLayer.frame = bounds
+ }
+
+ // MARK: Class Methods
+
+ private func playerSetup(player: AVPlayer, bgColor: Color, useFill: Bool) {
+ playerLayer.player = player
+ playerLayer.videoGravity = useFill ? .resizeAspectFill : .resizeAspect
+ player.actionAtItemEnd = .none
+ layer.addSublayer(playerLayer)
+ playerLayer.backgroundColor = bgColor.cgColor
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/SelectableView.swift b/Sources/ExyteMediaPicker/Utils/Widgets/SelectableView.swift
new file mode 100644
index 0000000..ba1b795
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/SelectableView.swift
@@ -0,0 +1,28 @@
+//
+// Created by Alex.M on 27.05.2022.
+//
+
+import SwiftUI
+
+struct SelectableView: View where Content: View {
+
+ var selected: Int?
+ var isFullscreen: Bool
+ var canSelect: Bool
+ var selectionParamsHolder: SelectionParamsHolder
+ var onSelect: () -> Void
+ @ViewBuilder var content: () -> Content
+
+ var body: some View {
+ content()
+ .overlay(alignment: .topTrailing) {
+ SelectionIndicatorView(index: selected, isFullscreen: isFullscreen, canSelect: canSelect, selectionParamsHolder: selectionParamsHolder)
+ .padding([.bottom, .leading], 10) // extend tappable area where possible
+ .contentShape(Rectangle())
+ .onTapGesture {
+ onSelect()
+ }
+ .padding(2)
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Utils/Widgets/SelectionIndicatorView.swift b/Sources/ExyteMediaPicker/Utils/Widgets/SelectionIndicatorView.swift
new file mode 100644
index 0000000..8945bd0
--- /dev/null
+++ b/Sources/ExyteMediaPicker/Utils/Widgets/SelectionIndicatorView.swift
@@ -0,0 +1,77 @@
+//
+// Created by Alex.M on 27.05.2022.
+//
+
+import SwiftUI
+
+struct SelectionIndicatorView: View {
+
+ @EnvironmentObject private var selectionService: SelectionService
+
+ @Environment(\.mediaPickerTheme) var theme
+
+ var index: Int?
+ var isFullscreen: Bool
+ var canSelect: Bool
+ var selectionParamsHolder: SelectionParamsHolder
+
+ var size: CGFloat { isFullscreen ? 26 : 24 }
+
+ var emptyBorder: Color { isFullscreen ? theme.selection.fullscreenEmptyBorder : theme.selection.cellEmptyBorder }
+ var emptyBackground: Color { isFullscreen ? theme.selection.fullscreenEmptyBackground : theme.selection.cellEmptyBackground }
+ var selectedBorder: Color { isFullscreen ? theme.selection.fullscreenSelectedBorder : theme.selection.cellSelectedBorder }
+ var selectedBackground: Color { isFullscreen ? theme.selection.fullscreenSelectedBackground : theme.selection.cellSelectedBackground }
+ var selectedCheckmark: Color { isFullscreen ? theme.selection.fullscreenSelectedCheckmark : theme.selection.cellSelectedCheckmark }
+
+ var body: some View {
+ Group {
+ switch selectionParamsHolder.selectionStyle {
+ case .checkmark:
+ checkView
+ case .count:
+ countView
+ }
+ }
+ .frame(width: size, height: size)
+ }
+
+ @ViewBuilder
+ var checkView: some View {
+ if canSelect {
+ let selected = index != nil
+ ZStack {
+ Circle().styled(
+ selected ? selectedBackground : emptyBackground,
+ border: selected ? selectedBorder : emptyBorder, 2
+ )
+ if index != nil {
+ Image(systemName: "checkmark")
+ .resizable()
+ .foregroundColor(selectedCheckmark)
+ .font(.system(size: 14, weight: .bold))
+ .padding(7)
+ }
+ }
+ .animation(.easeOut(duration: 0.2), value: selected)
+ }
+ }
+
+ @ViewBuilder
+ var countView: some View {
+ if canSelect {
+ let selected = index != nil
+ ZStack {
+ Circle().styled(
+ selected ? selectedBackground : emptyBackground,
+ border: selected ? selectedBorder : emptyBorder, 2
+ )
+ if let index {
+ Text("\(index + 1)")
+ .foregroundColor(selectedCheckmark)
+ .font(.system(size: 14, weight: .bold))
+ }
+ }
+ .animation(.easeOut(duration: 0.2), value: selected)
+ }
+ }
+}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumView.swift b/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumView.swift
deleted file mode 100644
index d47564d..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumView.swift
+++ /dev/null
@@ -1,119 +0,0 @@
-//
-// Created by Alex.M on 27.05.2022.
-//
-
-import SwiftUI
-
-struct AlbumView: View {
-
- @EnvironmentObject private var selectionService: SelectionService
- @EnvironmentObject private var permissionsService: PermissionsService
- @Environment(\.mediaPickerTheme) private var theme
-
- @StateObject var viewModel: AlbumViewModel
- @Binding var showingCamera: Bool
- @Binding var currentFullscreenMedia: Media?
- var shouldShowCamera: Bool
- var shouldShowLoadingCell: Bool
- var selectionParamsHolder: SelectionParamsHolder
-
- @State private var fullscreenItem: AssetMediaModel? {
- didSet {
- if let item = fullscreenItem {
- currentFullscreenMedia = Media(source: item)
- } else {
- currentFullscreenMedia = nil
- }
- }
- }
-
- var body: some View {
- if let title = viewModel.title {
- content.navigationTitle(title)
- } else {
- content
- }
- }
-}
-
-private extension AlbumView {
-
- @ViewBuilder
- var content: some View {
- ScrollView {
- VStack {
- if let action = permissionsService.photoLibraryAction {
- PermissionsActionView(action: .library(action))
- }
- if shouldShowCamera, let action = permissionsService.cameraAction {
- PermissionsActionView(action: .camera(action))
- }
- if viewModel.isLoading {
- ProgressView()
- } else if viewModel.assetMediaModels.isEmpty, !shouldShowLoadingCell {
- Text("Empty data")
- .font(.title3)
- } else {
- MediasGrid(viewModel.assetMediaModels) {
- if shouldShowCamera && permissionsService.cameraAction == nil {
- LiveCameraCell {
- showingCamera = true
- }
- }
- } content: { assetMediaModel in
- let index = selectionService.index(of: assetMediaModel)
- SelectableView(selected: index, isFullscreen: false, canSelect: selectionService.canSelect(assetMediaModel: assetMediaModel), selectionParamsHolder: selectionParamsHolder) {
- selectionService.onSelect(assetMediaModel: assetMediaModel)
- } content: {
- Button {
- if fullscreenItem == nil {
- fullscreenItem = assetMediaModel
- }
- } label: {
- MediaCell(viewModel: MediaViewModel(assetMediaModel: assetMediaModel))
- }
- .buttonStyle(MediaButtonStyle())
- .contentShape(Rectangle())
- }
- } loadingCell: {
- if shouldShowLoadingCell {
- ZStack {
- Color.white.opacity(0.5)
- ProgressView()
- }
- .aspectRatio(1, contentMode: .fit)
- }
- }
- .onChange(of: viewModel.assetMediaModels) { newValue in
- selectionService.updateSelection(with: newValue)
- }
- }
-
- Spacer()
- }
- .frame(maxWidth: .infinity)
- }
- .background(theme.main.albumSelectionBackground)
- .overlay {
- if let item = fullscreenItem {
- FullscreenContainer(
- isPresented: fullscreenPresentedBinding(),
- assetMediaModels: viewModel.assetMediaModels,
- selection: item.id,
- selectionParamsHolder: selectionParamsHolder
- )
- }
- }
- }
-
- func fullscreenPresentedBinding() -> Binding {
- Binding(
- get: { fullscreenItem != nil },
- set: { value in
- if value == false {
- fullscreenItem = nil
- }
- }
- )
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumViewModel.swift b/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumViewModel.swift
deleted file mode 100644
index 01b1c9b..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumView/AlbumViewModel.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-//
-// Created by Alex.M on 09.06.2022.
-//
-
-import Foundation
-import Combine
-
-final class AlbumViewModel: ObservableObject {
-
- @Published var title: String? = nil
- @Published var assetMediaModels: [AssetMediaModel] = []
- @Published var isLoading: Bool = false
-
- let mediasProvider: MediasProviderProtocol
-
- private var mediaCancellable: AnyCancellable?
-
- init(mediasProvider: MediasProviderProtocol) {
- self.mediasProvider = mediasProvider
- onStart()
- }
-
- func onStart() {
- isLoading = true
- mediaCancellable = mediasProvider.assetMediaModelsPublisher
- .receive(on: RunLoop.main)
- .sink { [weak self] in
- self?.assetMediaModels = $0
- self?.isLoading = false
- }
-
- mediasProvider.reload()
- }
-
- deinit {
- mediasProvider.cancel()
- mediaCancellable = nil
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsViewModel.swift b/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsViewModel.swift
deleted file mode 100644
index 3ac2be8..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/AlbumsView/AlbumsViewModel.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-// Created by Alex.M on 07.06.2022.
-//
-
-import Foundation
-import Combine
-
-final class AlbumsViewModel: ObservableObject {
- // MARK: - Values
- // MARK: Public
- @Published var albums: [AlbumModel] = []
- @Published var isLoading: Bool = false
-
- let albumsProvider: AlbumsProviderProtocol
-
- // MARK: Private
- private var albumsCancellable: AnyCancellable?
-
- // MARK: - Object life cycle
- init(albumsProvider: AlbumsProviderProtocol) {
- self.albumsProvider = albumsProvider
- }
-
- // MARK: - Public methods
- func onStart() {
- isLoading = true
- albumsCancellable = albumsProvider.albums
- .receive(on: DispatchQueue.main)
- .sink { [weak self] in
- self?.albums = $0
- self?.isLoading = false
- }
-
- albumsProvider.reload()
- }
-
- func onStop() {
- albumsCancellable = nil
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Camera/CameraSelectionContainer.swift b/Sources/ExyteMediaPicker/Views/Pages/Camera/CameraSelectionContainer.swift
deleted file mode 100644
index b78db92..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/Camera/CameraSelectionContainer.swift
+++ /dev/null
@@ -1,70 +0,0 @@
-//
-// CameraSelectionContainer.swift
-//
-//
-// Created by Alisa Mylnikova on 12.07.2022.
-//
-
-import SwiftUI
-
-public struct CameraSelectionView: View {
-
- @EnvironmentObject private var cameraSelectionService: CameraSelectionService
-
- @State private var index: Int = 0
-
- public var body: some View {
- TabView(selection: $index) {
- ForEach(cameraSelectionService.selected.enumerated().map({ $0 }), id: \.offset) { (index, mediaModel) in
- FullscreenCell(viewModel: FullscreenCellViewModel(mediaModel: mediaModel))
- .tag(index)
- .frame(maxHeight: .infinity)
- .padding(.vertical)
- }
- }
- .tabViewStyle(.page(indexDisplayMode: .never))
- }
-}
-
-struct DefaultCameraSelectionContainer: View {
-
- @EnvironmentObject private var cameraSelectionService: CameraSelectionService
- @Environment(\.mediaPickerTheme) private var theme
-
- @ObservedObject var viewModel: MediaPickerViewModel
-
- @Binding var showingPicker: Bool
-
- var body: some View {
- VStack {
- HStack {
- Button("Cancel") {
- viewModel.onCancelCameraSelection(cameraSelectionService.hasSelected)
- }
- .foregroundColor(.white)
- Spacer()
- }
- .padding()
-
- CameraSelectionView()
-
- HStack {
- Button("Done") {
- showingPicker = false
- }
- Spacer()
- Button {
- viewModel.setPickerMode(.camera)
- } label: {
- Image(systemName: "plus.app")
- .resizable()
- .frame(width: 30, height: 30)
- }
- }
- .foregroundColor(.white)
- .font(.system(size: 16))
- .padding()
- }
- .background(theme.main.cameraSelectionBackground)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCell.swift b/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCell.swift
deleted file mode 100644
index 644982e..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenCell.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// Created by Alex.M on 09.06.2022.
-//
-
-import Foundation
-import SwiftUI
-import AVKit
-
-struct FullscreenCell: View {
-
- @StateObject var viewModel: FullscreenCellViewModel
-
- var body: some View {
- VStack {
- if let image = viewModel.image {
- ZoomableScrollView {
- Image(uiImage: image)
- .resizable()
- .scaledToFit()
- }
- } else if let player = viewModel.player {
- ZoomableScrollView {
- VideoPlayer(player: player)
- .disabled(true)
- .overlay {
- ZStack {
- Color.clear
- if !viewModel.isPlaying {
- Image(systemName: "play.fill")
- .resizable()
- .frame(width: 50, height: 50)
- .foregroundColor(.white.opacity(0.8))
- }
- }
- .contentShape(Rectangle())
- .onTapGesture {
- viewModel.togglePlay()
- }
- }
- }
- } else {
- Spacer()
- ProgressView()
- .tint(.white)
- Spacer()
- }
- }
- .task {
- await viewModel.onStart()
- }
- .onDisappear {
- viewModel.onStop()
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenContainer.swift b/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenContainer.swift
deleted file mode 100644
index 83654a1..0000000
--- a/Sources/ExyteMediaPicker/Views/Pages/Fullscreen/FullscreenContainer.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// Created by Alex.M on 09.06.2022.
-//
-
-import Foundation
-import SwiftUI
-
-struct FullscreenContainer: View {
-
- @EnvironmentObject private var selectionService: SelectionService
- @Environment(\.mediaPickerTheme) private var theme
-
- @Binding var isPresented: Bool
- let assetMediaModels: [AssetMediaModel]
- @State var selection: AssetMediaModel.ID
- var selectionParamsHolder: SelectionParamsHolder
-
- private var selectedMediaModel: AssetMediaModel? {
- assetMediaModels.first { $0.id == selection }
- }
-
- private var selectionServiceIndex: Int? {
- guard let selectedMediaModel = selectedMediaModel else {
- return nil
- }
- return selectionService.index(of: selectedMediaModel)
- }
-
- var body: some View {
- TabView(selection: $selection) {
- ForEach(assetMediaModels, id: \.id) { assetMediaModel in
- FullscreenCell(viewModel: FullscreenCellViewModel(mediaModel: assetMediaModel))
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .tag(assetMediaModel.id)
- }
- }
- .overlay {
- if let selectedMediaModel = selectedMediaModel {
- SelectIndicatorView(index: selectionServiceIndex, isFullscreen: true, canSelect: selectionService.canSelect(assetMediaModel: selectedMediaModel), selectionParamsHolder: selectionParamsHolder)
- .padding([.horizontal, .bottom], 20)
- .contentShape(Rectangle())
- .onTapGesture {
- selectionService.onSelect(assetMediaModel: selectedMediaModel)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
- }
- }
- .onTapGesture {
- if let selectedMediaModel = selectedMediaModel, selectedMediaModel.mediaType == .image {
- selectionService.onSelect(assetMediaModel: selectedMediaModel)
- }
- }
- .overlay(closeButton)
- .tabViewStyle(.page(indexDisplayMode: .never))
- .background(
- theme.main.fullscreenPhotoBackground
- .ignoresSafeArea()
- )
- }
-
- var closeButton: some View {
- Button {
- isPresented = false
- } label: {
- Image(systemName: "xmark")
- .resizable()
- .tint(theme.selection.fullscreenTint)
- .frame(width: 20, height: 20)
- }
- .padding([.horizontal, .bottom], 20)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsActionView.swift b/Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsActionView.swift
deleted file mode 100644
index 65f585a..0000000
--- a/Sources/ExyteMediaPicker/Views/Widgets/Errors/PermissionsActionView.swift
+++ /dev/null
@@ -1,84 +0,0 @@
-//
-// Created by Alex.M on 06.06.2022.
-//
-
-import Foundation
-import SwiftUI
-
-struct PermissionsActionView: View {
-
- let action: Action
-
- @State private var showSheet = false
-
- var body: some View {
- ZStack {
- if showSheet {
- LimitedLibraryPickerProxyView(isPresented: $showSheet) {
- NotificationCenter.default.post(
- name: photoLibraryChangeLimitedPhotosNotification,
- object: nil)
- }
- .frame(width: 1, height: 1)
- }
-
- switch action {
- case .library(let assetsLibraryAction):
- buildLibraryAction(assetsLibraryAction)
- case .camera(let cameraAction):
- buildCameraAction(cameraAction)
- }
- }
- }
-}
-
-private extension PermissionsActionView {
-
- @ViewBuilder
- func buildLibraryAction(_ action: PermissionsService.PhotoLibraryAction) -> some View {
- switch action {
- case .selectMore:
- PermissionsErrorView(text: "Setup Photos access to see more photos here") {
- showSheet = true
- }
- case .authorize:
- goToSettingsButton(text: "Allow Photos access in settings to see photos here")
- case .unavailable:
- PermissionsErrorView(text: "Sorry, Photos are not available.", action: nil)
- case .unknown:
- fatalError("Unknown permission status.")
- }
- }
-
- @ViewBuilder
- func buildCameraAction(_ action: PermissionsService.CameraAction) -> some View {
- switch action {
- case .authorize:
- goToSettingsButton(text: "Allow Camera access in settings to see live preview")
- case .unavailable:
- PermissionsErrorView(text: "Sorry, Camera is not available.", action: nil)
- case .unknown:
- fatalError("Unknown permission status.")
- }
- }
-
- func goToSettingsButton(text: String) -> some View {
- PermissionsErrorView(
- text: text,
- action: {
- guard let url = URL(string: UIApplication.openSettingsURLString)
- else { return }
- if UIApplication.shared.canOpenURL(url) {
- UIApplication.shared.open(url)
- }
- }
- )
- }
-}
-
-extension PermissionsActionView {
- enum Action {
- case library(PermissionsService.PhotoLibraryAction)
- case camera(PermissionsService.CameraAction)
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/LimitedLibraryPickerProxyView.swift b/Sources/ExyteMediaPicker/Views/Widgets/LimitedLibraryPickerProxyView.swift
deleted file mode 100644
index 3d1aa28..0000000
--- a/Sources/ExyteMediaPicker/Views/Widgets/LimitedLibraryPickerProxyView.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// Created by Alex.M on 02.06.2022.
-//
-
-import SwiftUI
-import UIKit
-import PhotosUI
-
-struct LimitedLibraryPickerProxyView: UIViewControllerRepresentable {
- @Binding var isPresented: Bool
- var didDismiss: ()->()
-
- func makeUIViewController(context: Context) -> UIViewController {
- let controller = UIViewController()
-
- DispatchQueue.main.async { [controller] in
- PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller)
- context.coordinator.trackCompletion(in: controller)
- }
-
- return controller
- }
-
- func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
-
- func makeCoordinator() -> Coordinator {
- Coordinator(isPresented: $isPresented, didDismiss: didDismiss)
- }
-
- class Coordinator: NSObject {
- private var isPresented: Binding
- var didDismiss: ()->()
-
- init(isPresented: Binding, didDismiss: @escaping ()->()) {
- self.isPresented = isPresented
- self.didDismiss = didDismiss
- }
-
- func trackCompletion(in controller: UIViewController) {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak controller] in
- if controller?.presentedViewController == nil {
- self?.isPresented.wrappedValue = false
- self?.didDismiss()
- } else if let controller = controller {
- self?.trackCompletion(in: controller)
- }
- }
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/MediasGrid.swift b/Sources/ExyteMediaPicker/Views/Widgets/MediasGrid.swift
deleted file mode 100644
index e21304d..0000000
--- a/Sources/ExyteMediaPicker/Views/Widgets/MediasGrid.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// Created by Alex.M on 06.06.2022.
-//
-
-import Foundation
-import SwiftUI
-
-public struct MediasGrid: View where Data: RandomAccessCollection, Data.Element: Identifiable, Camera: View, Content: View, LoadingCell: View {
-
- public let data: Data
- public let camera: () -> Camera
- public let content: (Data.Element) -> Content
- public let loadingCell: () -> LoadingCell
-
- private var columns: [GridItem] {
- [GridItem(.adaptive(minimum: 100), spacing: 1, alignment: .top)]
- }
-
- public init(_ data: Data, @ViewBuilder camera: @escaping () -> Camera, @ViewBuilder content: @escaping (Data.Element) -> Content, @ViewBuilder loadingCell: @escaping () -> LoadingCell) {
- self.data = data
- self.camera = camera
- self.content = content
- self.loadingCell = loadingCell
- }
-
- public var body: some View {
- LazyVGrid(columns: columns, spacing: 1) {
- camera()
- ForEach(data) { item in
- content(item)
- }
- loadingCell()
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/SelectIndicatorView.swift b/Sources/ExyteMediaPicker/Views/Widgets/SelectIndicatorView.swift
deleted file mode 100644
index 93cf4fe..0000000
--- a/Sources/ExyteMediaPicker/Views/Widgets/SelectIndicatorView.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-//
-// Created by Alex.M on 27.05.2022.
-//
-
-import SwiftUI
-
-struct SelectIndicatorView: View {
-
- @EnvironmentObject private var selectionService: SelectionService
-
- @Environment(\.mediaPickerTheme) var theme
-
- var index: Int?
- var isFullscreen: Bool
- var canSelect: Bool
- var selectionParamsHolder: SelectionParamsHolder
-
- var body: some View {
- Group {
- switch selectionParamsHolder.selectionStyle {
- case .checkmark:
- checkView
- case .count:
- countView
- }
- }
- .frame(width: 24, height: 24)
- }
-
- var checkView: some View {
- Group {
- if index != nil {
- Image(systemName: "checkmark.circle.fill")
- .resizable()
- .foregroundColor(theme.selection.selectedTint)
- .padding(2)
- .background {
- Circle()
- .fill(theme.selection.selectedBackground)
- }
- } else if canSelect {
- Image(systemName: "circle")
- .resizable()
- .foregroundColor(isFullscreen ? theme.selection.fullscreenTint : theme.selection.emptyTint)
- .background {
- Circle()
- .fill(theme.selection.emptyBackground)
- }
- }
- }
- }
-
- var countView: some View {
- Group {
- if let index = index {
- Image(systemName: "\(index + 1).circle.fill")
- .resizable()
- .foregroundColor(theme.selection.selectedTint)
- .background {
- Circle()
- .fill(theme.selection.selectedBackground)
- }
- } else if canSelect {
- Image(systemName: "circle")
- .resizable()
- .foregroundColor(isFullscreen ? theme.selection.fullscreenTint : theme.selection.emptyTint)
- .background {
- Circle()
- .fill(theme.selection.emptyBackground)
- }
- }
- }
- }
-}
diff --git a/Sources/ExyteMediaPicker/Views/Widgets/SelectableView.swift b/Sources/ExyteMediaPicker/Views/Widgets/SelectableView.swift
deleted file mode 100644
index 893b9eb..0000000
--- a/Sources/ExyteMediaPicker/Views/Widgets/SelectableView.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//
-// Created by Alex.M on 27.05.2022.
-//
-
-import SwiftUI
-
-struct SelectableView: View where Content: View {
-
- var selected: Int?
- var paddings: CGFloat = 2
- var isFullscreen: Bool
- var canSelect: Bool
- var selectionParamsHolder: SelectionParamsHolder
- var onSelect: () -> Void
- @ViewBuilder var content: () -> Content
-
- var body: some View {
- content().overlay {
- Button {
- onSelect()
- } label: {
- SelectIndicatorView(index: selected, isFullscreen: isFullscreen, canSelect: canSelect, selectionParamsHolder: selectionParamsHolder)
- .padding([.bottom, .leading], 10)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
- .padding(paddings)
- }
- }
-}