├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── FYPhoto.xcscheme ├── .travis.yml ├── Example ├── FYPhotoExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── FYPhotoExample.xcscheme ├── FYPhotoExample │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── StarrySky.imageset │ │ │ ├── Contents.json │ │ │ └── StarrySky.png │ ├── Info.plist │ ├── NamSpaceTest.swift │ ├── ViewController.swift │ └── zh-Hans.lproj │ │ ├── LaunchScreen.strings │ │ └── Main.strings ├── fyphoto-custom-strings-template.stencil ├── fyphoto-custom-xcassets-template.stencil └── swiftgen.yml ├── FYPhoto.podspec ├── Images ├── CropImage.png ├── CropVideo.gif ├── CustomCamera.png └── PickPhotos.gif ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── FYPhoto │ ├── Assets │ ├── FYPhoto.xcassets │ │ ├── Browser-ErrorLoading.imageset │ │ │ ├── Browser-ErrorLoading.png │ │ │ ├── Browser-ErrorLoading@2x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Crop │ │ │ ├── Contents.json │ │ │ ├── aspectratio.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── aspectratio.pdf │ │ │ ├── icons8-edit-image.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── icons8-edit-image.png │ │ │ └── rotate.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── rotate.pdf │ │ ├── FlipCamera.imageset │ │ │ ├── Contents.json │ │ │ └── FlipCamera@2x.png │ │ ├── ImageError.imageset │ │ │ ├── Contents.json │ │ │ ├── ImageError.png │ │ │ ├── ImageError@2x.png │ │ │ └── ImageError@3x.png │ │ ├── ImageSelectedOff.imageset │ │ │ ├── Contents.json │ │ │ ├── ImageSelectedOff.png │ │ │ ├── ImageSelectedOff@2x.png │ │ │ └── ImageSelectedOff@3x.png │ │ ├── ImageSelectedOn.imageset │ │ │ ├── Contents.json │ │ │ ├── ImageSelectedOn.png │ │ │ ├── ImageSelectedOn@2x.png │ │ │ └── ImageSelectedOn@3x.png │ │ ├── ImageSelectedSmallOff.imageset │ │ │ ├── Contents.json │ │ │ ├── ImageSelectedSmallOff.png │ │ │ ├── ImageSelectedSmallOff@2x.png │ │ │ └── ImageSelectedSmallOff@3x.png │ │ ├── ImageSelectedSmallOn.imageset │ │ │ ├── Contents.json │ │ │ ├── ImageSelectedSmallOn.png │ │ │ ├── ImageSelectedSmallOn@2x.png │ │ │ └── ImageSelectedSmallOn@3x.png │ │ ├── PlayButtonOverlayLarge.imageset │ │ │ ├── Contents.json │ │ │ ├── PlayButtonOverlayLarge.png │ │ │ ├── PlayButtonOverlayLarge@2x.png │ │ │ └── PlayButtonOverlayLarge@3x.png │ │ ├── PlayButtonOverlayLargeTap.imageset │ │ │ ├── Contents.json │ │ │ ├── PlayButtonOverlayLargeTap.png │ │ │ ├── PlayButtonOverlayLargeTap@2x.png │ │ │ └── PlayButtonOverlayLargeTap@3x.png │ │ ├── UIBarButtonItemArrowLeft.imageset │ │ │ ├── Contents.json │ │ │ ├── UIBarButtonItemArrowLeft.png │ │ │ ├── UIBarButtonItemArrowLeft@2x.png │ │ │ └── UIBarButtonItemArrowLeft@3x.png │ │ ├── UIBarButtonItemArrowRight.imageset │ │ │ ├── Contents.json │ │ │ ├── UIBarButtonItemArrowRight.png │ │ │ ├── UIBarButtonItemArrowRight@2x.png │ │ │ └── UIBarButtonItemArrowRight@3x.png │ │ ├── albumArrow.imageset │ │ │ ├── Contents.json │ │ │ ├── albumArrow@2x.png │ │ │ └── albumArrow@3x.png │ │ ├── back.imageset │ │ │ ├── Contents.json │ │ │ └── back.png │ │ ├── cover_placeholder.imageset │ │ │ ├── Contents.json │ │ │ └── cover_placeholder.png │ │ ├── icons8-flash-off.imageset │ │ │ ├── Contents.json │ │ │ ├── icons8-flash-off@2x.png │ │ │ └── icons8-flash-off@3x.png │ │ ├── icons8-flash-on.imageset │ │ │ ├── Contents.json │ │ │ ├── icons8-flash-on@2x.png │ │ │ └── icons8-flash-on@3x.png │ │ ├── icons8-pause.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-pause.png │ │ ├── icons8-play.imageset │ │ │ ├── Contents.json │ │ │ └── icons8-play.png │ │ ├── photo_image_camera.imageset │ │ │ ├── Contents.json │ │ │ └── photo_image_camera@2x.png │ │ ├── photo_video_camera.imageset │ │ │ ├── Contents.json │ │ │ ├── photo_video_camera@2x.png │ │ │ └── photo_video_camera@3x.png │ │ └── play_button.imageset │ │ │ ├── Contents.json │ │ │ └── play-button.png │ ├── en.lproj │ │ └── FYPhoto.strings │ └── zh-Hans.lproj │ │ └── FYPhoto.strings │ ├── Classes │ ├── Camera │ │ ├── CameraExtensions.swift │ │ ├── CameraViewController+InfoKey.swift │ │ ├── CameraViewController+Tool.swift │ │ ├── CameraViewController+Watermark.swift │ │ ├── CameraViewController.swift │ │ ├── CameraViewControllerDelegate.swift │ │ ├── CircularProgressView.swift │ │ ├── PhotoCaptureDelegate.swift │ │ ├── SaveMediaTool.swift │ │ ├── VideoCaptureOverlay.swift │ │ ├── VideoPreviewView.swift │ │ └── Watermark.swift │ ├── Configuration │ │ ├── FYColorConfiguration.swift │ │ └── FYPhotoPickerConfiguration.swift │ ├── Editor │ │ ├── Photo │ │ │ └── Crop │ │ │ │ ├── AspectRatioControl │ │ │ │ ├── AspectRatioBar.swift │ │ │ │ ├── AspectRatioButton.swift │ │ │ │ ├── AspectRatioButtonItem.swift │ │ │ │ └── PhotoAspectRatio.swift │ │ │ │ ├── CropImageViewController.swift │ │ │ │ ├── CropOverlayHandlesView.swift │ │ │ │ ├── CropScrollView.swift │ │ │ │ ├── CropView+ImageView.swift │ │ │ │ ├── CropView+UIScrollViewDelegate.swift │ │ │ │ ├── CropView.swift │ │ │ │ ├── CropViewModel.swift │ │ │ │ ├── CropViewStatus.swift │ │ │ │ ├── CroppedRestoreData.swift │ │ │ │ ├── GeometryHelper.swift │ │ │ │ ├── InteractiveCropGuideView.swift │ │ │ │ ├── Mask │ │ │ │ ├── CropDimmingView.swift │ │ │ │ ├── CropMaskProtocol.swift │ │ │ │ ├── CropViewMaskManager.swift │ │ │ │ └── CropVisualEffectView.swift │ │ │ │ ├── PhotoRotation.swift │ │ │ │ ├── TapExpandedView.swift │ │ │ │ └── UIImage+Crop.swift │ │ └── Video │ │ │ ├── RangeSlider.swift │ │ │ ├── VideoTrimmerToolView.swift │ │ │ └── VideoTrimmerViewController.swift │ ├── FYPhotoCacheCleaner.swift │ ├── Helper │ │ ├── Extensions │ │ │ ├── AVAsset+VideoSize.swift │ │ │ ├── AVFileType.swift │ │ │ ├── CG+extensions.swift │ │ │ ├── FileManager+TempDirectory.swift │ │ │ ├── PHAsset+GetImage.swift │ │ │ ├── TimeInterval+VideoFormat.swift │ │ │ ├── UICollectionView+IndexPathsInRect.swift │ │ │ ├── UIImagePickerController+Tool.swift │ │ │ ├── UIResponder+routerEvent.swift │ │ │ ├── UIStackView+Remove.swift │ │ │ ├── UIViewController+ShowMessage.swift │ │ │ ├── URL+FileSize.swift │ │ │ ├── URL+Thumbnail.swift │ │ │ └── URL+mediaType.swift │ │ ├── FYPhotoNameSpace.swift │ │ ├── PhotoPickerError.swift │ │ ├── PhotoPickerResource.swift │ │ └── PhotosAuthority.swift │ ├── PhotoBrowser │ │ ├── PhotoBrowserViewController+Builder.swift │ │ ├── PhotoBrowserViewController+PlayVideo.swift │ │ ├── PhotoBrowserViewController.swift │ │ ├── PhotoBrowserViewControllerDelegate.swift │ │ ├── PhotoModel │ │ │ ├── Photo.swift │ │ │ ├── PhotoAsset.swift │ │ │ ├── PhotoImage.swift │ │ │ ├── PhotoMetaData.swift │ │ │ ├── PhotoProtocol.swift │ │ │ └── PhotoURL.swift │ │ └── Views │ │ │ ├── CaptionView.swift │ │ │ ├── CellWithPhotoProtocol.swift │ │ │ ├── PBSelectedPhotosThumbnailCell.swift │ │ │ ├── PhotoAnimatedImageView.swift │ │ │ ├── PhotoBrowserBottomToolView.swift │ │ │ ├── PhotoDetailCell.swift │ │ │ └── ZoomingScrollView.swift │ ├── PhotoLauncher.swift │ ├── PhotoPicker │ │ ├── AlbumsTableViewController.swift │ │ ├── PhotoPickerViewController+AssetTransition.swift │ │ ├── PhotoPickerViewController.swift │ │ ├── PlayVideoForSelectionViewController.swift │ │ ├── SelectedModel │ │ │ ├── SelectedImage.swift │ │ │ └── SelectedVideo.swift │ │ ├── VideoPreviewController.swift │ │ └── Views │ │ │ ├── AlbumCell.swift │ │ │ ├── GridCameraCell.swift │ │ │ ├── GridViewCell.swift │ │ │ ├── PhotoPickerBottomToolView.swift │ │ │ ├── PhotoPickerTopBar.swift │ │ │ ├── PickerAlbulmTitleView.swift │ │ │ └── SelectionButton.swift │ ├── Strings+Generated.swift │ ├── Transition │ │ ├── AssetTransitioningMath.swift │ │ ├── PhotoAnimators.swift │ │ ├── PhotoBrowserCurrentPage.swift │ │ ├── PhotoInteractiveDismissTransitionDriver.swift │ │ ├── PhotoPresentTransitionController.swift │ │ ├── PhotoPushTransitionController.swift │ │ ├── PhotoTransitionDriver.swift │ │ ├── PhotoTransitioning.swift │ │ ├── TransitionDriver.swift │ │ └── TransitionEssential.swift │ ├── UIViewController+Present.swift │ ├── Video │ │ ├── PlayerView.swift │ │ ├── VideoCache.swift │ │ ├── VideoDetailCell.swift │ │ ├── VideoTrimmer.swift │ │ └── VideoValidator.swift │ └── XCAssets+Generated.swift │ └── FYPhoto.h └── Tests └── FYPhotoTests ├── TestAuthority.swift ├── TestHelperExtensions.swift └── TestVideoCache.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | Pods/ 38 | 39 | Example/Pods/ 40 | fastlane/report.xml 41 | 42 | # Swift Package Manager 43 | .build/ 44 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FYPhoto.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 88 | 89 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/FYPhoto.xcworkspace -scheme FYPhoto-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/FYPhotoExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/FYPhotoExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/FYPhotoExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "FYVideoCompressor", 6 | "repositoryURL": "https://github.com/T2Je/FYVideoCompressor.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "de6682c33825ffb1248bc1e817bfc27e7490797e", 10 | "version": "0.0.8" 11 | } 12 | }, 13 | { 14 | "package": "SDWebImage", 15 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc", 19 | "version": "5.11.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Example/FYPhotoExample.xcodeproj/xcshareddata/xcschemes/FYPhotoExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FYPhoto 4 | // 5 | // Created by t2je on 08/27/2020. 6 | // Copyright (c) 2020 t2je. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Images.xcassets/StarrySky.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StarrySky.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/Images.xcassets/StarrySky.imageset/StarrySky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Example/FYPhotoExample/Images.xcassets/StarrySky.imageset/StarrySky.png -------------------------------------------------------------------------------- /Example/FYPhotoExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSCameraUsageDescription 31 | 需要您的同意来访问您的相机 32 | NSMicrophoneUsageDescription 33 | 录制视频需要访问您的麦克风 34 | NSPhotoLibraryAddUsageDescription 35 | 需要您的同意来存储刚刚拍摄的照片或者视频 36 | NSPhotoLibraryUsageDescription 37 | 点击允许来浏览您的相册 38 | PHPhotoLibraryPreventAutomaticLimitedAccessAlert 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | Main 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/NamSpaceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NamSpaceTest.swift 3 | // FYPhoto_Example 4 | // 5 | // Created by xiaoyang on 2021/1/8. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import FYPhoto 12 | 13 | extension UICollectionView: FYNameSpaceProtocol {} 14 | 15 | extension TypeWrapperProtocol where WrappedType == UICollectionView { 16 | func registerCell(_ cell: T.Type) { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/zh-Hans.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UILabel"; text = " Copyright (c) 2015 CocoaPods. All rights reserved."; ObjectID = "8ie-xW-0ye"; */ 3 | "8ie-xW-0ye.text" = " Copyright (c) 2015 CocoaPods. All rights reserved."; 4 | 5 | /* Class = "UILabel"; text = "FYPhoto"; ObjectID = "kId-c2-rCX"; */ 6 | "kId-c2-rCX.text" = "FYPhoto"; 7 | -------------------------------------------------------------------------------- /Example/FYPhotoExample/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Example/fyphoto-custom-strings-template.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | {% if tables.count > 0 %} 5 | {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} 6 | import Foundation 7 | 8 | // swiftlint:disable superfluous_disable_command file_length implicit_return 9 | 10 | // MARK: - Strings 11 | 12 | {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} 13 | {% for type in types %} 14 | {% if type == "String" %} 15 | _ p{{forloop.counter}}: Any 16 | {% else %} 17 | _ p{{forloop.counter}}: {{type}} 18 | {% endif %} 19 | {{ ", " if not forloop.last }} 20 | {% endfor %} 21 | {% endfilter %}{% endmacro %} 22 | {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} 23 | {% for type in types %} 24 | {% if type == "String" %} 25 | String(describing: p{{forloop.counter}}) 26 | {% elif type == "UnsafeRawPointer" %} 27 | Int(bitPattern: p{{forloop.counter}}) 28 | {% else %} 29 | p{{forloop.counter}} 30 | {% endif %} 31 | {{ ", " if not forloop.last }} 32 | {% endfor %} 33 | {% endfilter %}{% endmacro %} 34 | {% macro recursiveBlock table item %} 35 | {% for string in item.strings %} 36 | {% if not param.noComments %} 37 | /// {{string.translation}} 38 | {% endif %} 39 | {% if string.types %} 40 | {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { 41 | return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %}) 42 | } 43 | {% elif param.lookupFunction %} 44 | {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #} 45 | {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") } 46 | {% else %} 47 | {{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}") 48 | {% endif %} 49 | {% endfor %} 50 | {% for child in item.children %} 51 | 52 | {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 53 | {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %} 54 | } 55 | {% endfor %} 56 | {% endmacro %} 57 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 58 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces 59 | {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} 60 | {{accessModifier}} enum {{enumName}} { 61 | {% if tables.count > 1 or param.forceFileNameEnum %} 62 | {% for table in tables %} 63 | {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 64 | {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %} 65 | } 66 | {% endfor %} 67 | {% else %} 68 | {% call recursiveBlock tables.first.name tables.first.levels %} 69 | {% endif %} 70 | } 71 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 72 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces 73 | 74 | // MARK: - Implementation Details 75 | 76 | extension {{enumName}} { 77 | private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { 78 | {% if param.lookupFunction %} 79 | let format = {{ param.lookupFunction }}(key, table) 80 | {% else %} 81 | let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table) 82 | {% endif %} 83 | return String(format: format, locale: Locale.current, arguments: args) 84 | } 85 | } 86 | {% if not param.bundle and not param.lookupFunction %} 87 | 88 | // swiftlint:disable convenience_type 89 | private final class BundleToken { 90 | static let bundle: Bundle = { 91 | #if SWIFT_PACKAGE 92 | return Bundle.module 93 | #else 94 | guard let url = Bundle(for: BundleToken.self).url(forResource: "FYPhoto", withExtension: "bundle") else { 95 | return .main 96 | } 97 | return Bundle(url: url) ?? .main 98 | #endif 99 | }() 100 | } 101 | // swiftlint:enable convenience_type 102 | {% endif %} 103 | {% else %} 104 | // No string found 105 | {% endif %} 106 | 107 | -------------------------------------------------------------------------------- /Example/swiftgen.yml: -------------------------------------------------------------------------------- 1 | ## Note: all of the config entries below are just examples with placeholders. Be sure to edit and adjust to your needs when uncommenting. 2 | 3 | ## In case your config entries all use a common input/output parent directory, you can specify those here. 4 | ## Every input/output paths in the rest of the config will then be expressed relative to these. 5 | ## Those two top-level keys are optional and default to "." (the directory of the config file). 6 | # input_dir: MyLib/Sources/ 7 | # output_dir: MyLib/Generated/ 8 | 9 | 10 | ## Generate constants for your localized strings. 11 | ## Be sure that SwiftGen only parses ONE locale (typically Base.lproj, or en.lproj, or whichever your development region is); otherwise it will generate the same keys multiple times. 12 | ## SwiftGen will parse all `.strings` files found in that folder. 13 | strings: 14 | inputs: 15 | - ../Sources/FYPhoto/Assets/zh-Hans.lproj 16 | outputs: 17 | - templatePath: fyphoto-custom-strings-template.stencil 18 | output: ../Sources/FYPhoto/Classes/Strings+Generated.swift 19 | 20 | 21 | ## Generate constants for your Assets Catalogs, including constants for images, colors, ARKit resources, etc. 22 | ## This example also shows how to provide additional parameters to your template to customize the output. 23 | ## - Especially the `forceProvidesNamespaces: true` param forces to create sub-namespace for each folder/group used in your Asset Catalogs, even the ones without "Provides Namespace". Without this param, SwiftGen only generates sub-namespaces for folders/groups which have the "Provides Namespace" box checked in the Inspector pane. 24 | ## - To know which params are supported for a template, use `swiftgen template doc xcassets swift5` to open the template documentation on GitHub. 25 | xcassets: 26 | inputs: 27 | - ../Sources/FYPhoto/Assets/FYPhoto.xcassets 28 | outputs: 29 | - templatePath: fyphoto-custom-xcassets-template.stencil 30 | params: 31 | forceProvidesNamespaces: true 32 | output: ../Sources/FYPhoto/Classes/XCAssets+Generated.swift 33 | 34 | 35 | ## Generate constants for your storyboards and XIBs. 36 | ## This one generates 2 output files, one containing the storyboard scenes, and another for the segues. 37 | ## (You can remove the segues entry if you don't use segues in your IB files). 38 | ## For `inputs` we can use "." here (aka "current directory", at least relative to `input_dir` = "MyLib/Sources"), 39 | ## and SwiftGen will recursively find all `*.storyboard` and `*.xib` files in there. 40 | # ib: 41 | # inputs: 42 | # - . 43 | # outputs: 44 | # - templateName: scenes-swift5 45 | # output: IB-Scenes+Generated.swift 46 | # - templateName: segues-swift5 47 | # output: IB-Segues+Generated.swift 48 | 49 | 50 | ## There are other parsers available for you to use depending on your needs, for example: 51 | ## - `fonts` (if you have custom ttf/ttc font files) 52 | ## - `coredata` (for CoreData models) 53 | ## - `json`, `yaml` and `plist` (to parse custom JSON/YAML/Plist files and generate code from their content) 54 | ## … 55 | ## 56 | ## For more info, use `swiftgen config doc` to open the full documentation on GitHub. 57 | ## https://github.com/SwiftGen/SwiftGen/tree/6.4.0/Documentation/ 58 | -------------------------------------------------------------------------------- /FYPhoto.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint FYPhoto.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'FYPhoto' 11 | s.version = '2.3.2' 12 | s.summary = 'FYPhoto is a photo/video picker and image browser library for iOS written in pure Swift' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | FYPhoto is a photo/video picker and image browser library for iOS written in pure Swift. It is feature-rich and highly customizable to match your App's requirements. 22 | DESC 23 | 24 | s.homepage = 'https://github.com/T2Je' 25 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 't2je' => 't2je@icloud.com' } 28 | s.source = { :git => 'https://github.com/T2Je/FYPhoto.git', :tag => s.version.to_s } 29 | # s.social_media_url = 'https://twitter.com/' 30 | 31 | s.ios.deployment_target = '11' 32 | s.swift_version = '5' 33 | 34 | s.source_files = 'Sources/FYPhoto/Classes/**/*' 35 | 36 | s.resource_bundles = { 37 | 'FYPhoto' => ['Sources/FYPhoto/Assets/*.{xcassets}', 'Sources/FYPhoto/Assets/*.lproj/*.strings'] 38 | } 39 | 40 | s.frameworks = 'UIKit', 'Photos' 41 | 42 | s.dependency 'SDWebImage/Core' 43 | s.dependency 'FYVideoCompressor' 44 | 45 | end 46 | -------------------------------------------------------------------------------- /Images/CropImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Images/CropImage.png -------------------------------------------------------------------------------- /Images/CropVideo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Images/CropVideo.gif -------------------------------------------------------------------------------- /Images/CustomCamera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Images/CustomCamera.png -------------------------------------------------------------------------------- /Images/PickPhotos.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Images/PickPhotos.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 t2je 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SDWebImage", 6 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc", 10 | "version": "5.11.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FYPhoto", 8 | defaultLocalization: "en", 9 | platforms: [ 10 | .iOS(.v11) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "FYPhoto", 16 | targets: ["FYPhoto"]) 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.1.0"), 21 | .package(url: "https://github.com/T2Je/FYVideoCompressor.git", from: "0.0.8") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "FYPhoto", 28 | dependencies: [ 29 | "SDWebImage", 30 | "FYVideoCompressor" 31 | ], 32 | path: "Sources"), 33 | .testTarget( 34 | name: "FYPhotoTests", 35 | dependencies: ["FYPhoto"], 36 | path: "Tests") 37 | ], 38 | swiftLanguageVersions: [.v5] 39 | ) 40 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Browser-ErrorLoading.imageset/Browser-ErrorLoading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/Browser-ErrorLoading.imageset/Browser-ErrorLoading.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Browser-ErrorLoading.imageset/Browser-ErrorLoading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/Browser-ErrorLoading.imageset/Browser-ErrorLoading@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Browser-ErrorLoading.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Browser-ErrorLoading.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Browser-ErrorLoading@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/aspectratio.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "aspectratio.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/aspectratio.imageset/aspectratio.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 3.000000 5.414215 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 20.104931 11.584381 m 16 | 20.104931 -0.000013 l 17 | 22.855730 -0.000013 l 18 | 25.203939 -0.000013 26.366823 1.162899 26.366823 3.466360 c 19 | 26.366823 17.119392 l 20 | 26.366823 19.422855 25.203939 20.585785 22.855730 20.585785 c 21 | 3.511094 20.585785 l 22 | 1.174082 20.585785 0.000000 19.434053 0.000000 17.119392 c 23 | 0.000000 14.323936 l 24 | 17.320673 14.323936 l 25 | 19.143318 14.323936 20.104931 13.373478 20.104931 11.584381 c 26 | h 27 | 18.505955 11.405483 m 28 | 18.505955 12.232922 18.025137 12.724937 17.208874 12.724937 c 29 | 13.720129 12.724937 l 30 | 13.720129 -0.000013 l 31 | 18.505955 -0.000013 l 32 | 18.505955 11.405483 l 33 | h 34 | 3.511094 -0.000013 m 35 | 12.121130 -0.000013 l 36 | 12.121130 12.724937 l 37 | 0.000000 12.724937 l 38 | 0.000000 3.466360 l 39 | 0.000000 1.151716 1.174082 -0.000013 3.511094 -0.000013 c 40 | h 41 | f 42 | n 43 | Q 44 | 45 | endstream 46 | endobj 47 | 48 | 3 0 obj 49 | 862 50 | endobj 51 | 52 | 4 0 obj 53 | << /Annots [] 54 | /Type /Page 55 | /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] 56 | /Resources 1 0 R 57 | /Contents 2 0 R 58 | /Parent 5 0 R 59 | >> 60 | endobj 61 | 62 | 5 0 obj 63 | << /Kids [ 4 0 R ] 64 | /Count 1 65 | /Type /Pages 66 | >> 67 | endobj 68 | 69 | 6 0 obj 70 | << /Type /Catalog 71 | /Pages 5 0 R 72 | >> 73 | endobj 74 | 75 | xref 76 | 0 7 77 | 0000000000 65535 f 78 | 0000000010 00000 n 79 | 0000000034 00000 n 80 | 0000000952 00000 n 81 | 0000000974 00000 n 82 | 0000001147 00000 n 83 | 0000001221 00000 n 84 | trailer 85 | << /ID [ (some) (id) ] 86 | /Root 6 0 R 87 | /Size 7 88 | >> 89 | startxref 90 | 1280 91 | %%EOF -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/icons8-edit-image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icons8-edit-image.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/icons8-edit-image.imageset/icons8-edit-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/icons8-edit-image.imageset/icons8-edit-image.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/rotate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rotate.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/Crop/rotate.imageset/rotate.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 4.000000 3.753052 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 16.774809 20.063398 m 16 | 17.493732 19.538219 18.236631 19.748287 18.236631 20.670248 c 17 | 18.236631 22.280777 l 18 | 18.260580 22.280777 l 19 | 20.860678 22.280777 22.502129 20.518547 22.502129 18.021049 c 20 | 22.502129 16.468887 22.010855 15.885367 21.987053 15.465234 c 21 | 21.975029 15.138458 22.094778 14.940055 22.370354 14.811682 c 22 | 22.741877 14.636630 23.173275 14.800017 23.376949 15.138457 c 23 | 23.748472 15.756993 24.000000 16.784000 24.000000 18.032738 c 24 | 24.000000 21.370480 21.723501 23.622869 18.272581 23.622869 c 25 | 18.236631 23.622869 l 26 | 18.236631 25.338444 l 27 | 18.236631 26.283756 17.505732 26.493895 16.774809 25.956980 c 28 | 13.503736 23.622869 l 29 | 12.952562 23.226088 12.952562 22.770939 13.503736 22.385822 c 30 | 16.774809 20.063398 l 31 | h 32 | 3.079372 0.246948 m 33 | 14.953561 0.246948 l 34 | 17.002483 0.246948 18.032932 1.203926 18.032932 3.246256 c 35 | 18.032932 14.788331 l 36 | 18.032932 16.830681 17.002483 17.787655 14.953561 17.787655 c 37 | 3.079372 17.787655 l 38 | 1.030449 17.787655 0.000000 16.830681 0.000000 14.788331 c 39 | 0.000000 3.246256 l 40 | 0.000000 1.203926 1.030449 0.246948 3.079372 0.246948 c 41 | h 42 | f 43 | n 44 | Q 45 | 46 | endstream 47 | endobj 48 | 49 | 3 0 obj 50 | 1181 51 | endobj 52 | 53 | 4 0 obj 54 | << /Annots [] 55 | /Type /Page 56 | /MediaBox [ 0.000000 0.000000 32.000000 32.000000 ] 57 | /Resources 1 0 R 58 | /Contents 2 0 R 59 | /Parent 5 0 R 60 | >> 61 | endobj 62 | 63 | 5 0 obj 64 | << /Kids [ 4 0 R ] 65 | /Count 1 66 | /Type /Pages 67 | >> 68 | endobj 69 | 70 | 6 0 obj 71 | << /Type /Catalog 72 | /Pages 5 0 R 73 | >> 74 | endobj 75 | 76 | xref 77 | 0 7 78 | 0000000000 65535 f 79 | 0000000010 00000 n 80 | 0000000034 00000 n 81 | 0000001271 00000 n 82 | 0000001294 00000 n 83 | 0000001467 00000 n 84 | 0000001541 00000 n 85 | trailer 86 | << /ID [ (some) (id) ] 87 | /Root 6 0 R 88 | /Size 7 89 | >> 90 | startxref 91 | 1600 92 | %%EOF -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/FlipCamera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "FlipCamera@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/FlipCamera.imageset/FlipCamera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/FlipCamera.imageset/FlipCamera@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "ImageError.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "ImageError@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "ImageError@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageError.imageset/ImageError@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "ImageSelectedOff.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "ImageSelectedOff@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "ImageSelectedOff@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOff.imageset/ImageSelectedOff@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "ImageSelectedOn.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "ImageSelectedOn@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "ImageSelectedOn@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedOn.imageset/ImageSelectedOn@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "ImageSelectedSmallOff.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "ImageSelectedSmallOff@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "ImageSelectedSmallOff@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOff.imageset/ImageSelectedSmallOff@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "ImageSelectedSmallOn.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "ImageSelectedSmallOn@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "ImageSelectedSmallOn@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/ImageSelectedSmallOn.imageset/ImageSelectedSmallOn@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "PlayButtonOverlayLarge.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "PlayButtonOverlayLarge@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "PlayButtonOverlayLarge@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLarge.imageset/PlayButtonOverlayLarge@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "PlayButtonOverlayLargeTap.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "PlayButtonOverlayLargeTap@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "PlayButtonOverlayLargeTap@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/PlayButtonOverlayLargeTap.imageset/PlayButtonOverlayLargeTap@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "UIBarButtonItemArrowLeft.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "UIBarButtonItemArrowLeft@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "UIBarButtonItemArrowLeft@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowLeft.imageset/UIBarButtonItemArrowLeft@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "UIBarButtonItemArrowRight.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "UIBarButtonItemArrowRight@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "UIBarButtonItemArrowRight@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/UIBarButtonItemArrowRight.imageset/UIBarButtonItemArrowRight@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/albumArrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "albumArrow@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "albumArrow@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/albumArrow.imageset/albumArrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/albumArrow.imageset/albumArrow@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/albumArrow.imageset/albumArrow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/albumArrow.imageset/albumArrow@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "back.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/back.imageset/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/back.imageset/back.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/cover_placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "cover_placeholder.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/cover_placeholder.imageset/cover_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/cover_placeholder.imageset/cover_placeholder.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icons8-flash-off@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "icons8-flash-off@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-off.imageset/icons8-flash-off@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-off.imageset/icons8-flash-off@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-off.imageset/icons8-flash-off@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-off.imageset/icons8-flash-off@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-on.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icons8-flash-on@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "icons8-flash-on@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-on.imageset/icons8-flash-on@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-on.imageset/icons8-flash-on@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-on.imageset/icons8-flash-on@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-flash-on.imageset/icons8-flash-on@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icons8-pause.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-pause.imageset/icons8-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-pause.imageset/icons8-pause.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icons8-play.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-play.imageset/icons8-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/icons8-play.imageset/icons8-play.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_image_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "photo_image_camera@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_image_camera.imageset/photo_image_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_image_camera.imageset/photo_image_camera@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_video_camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "photo_video_camera@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "photo_video_camera@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_video_camera.imageset/photo_video_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_video_camera.imageset/photo_video_camera@2x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_video_camera.imageset/photo_video_camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/photo_video_camera.imageset/photo_video_camera@3x.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/play_button.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "play-button.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/FYPhoto.xcassets/play_button.imageset/play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/T2Je/FYPhoto/ff6a430b324b0a1822ec328ee5c9cb73a87056ae/Sources/FYPhoto/Assets/FYPhoto.xcassets/play_button.imageset/play-button.png -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/en.lproj/FYPhoto.strings: -------------------------------------------------------------------------------- 1 | /* 2 | FYPhotoPicker.strings 3 | Pods 4 | 5 | Created by xiaoyang on 2020/7/29. 6 | 7 | */ 8 | 9 | "add" = "Add"; 10 | "Confirm" = "Confirm"; 11 | 12 | "AccessPhotosFailed" = "Unable to access the photos in the album"; 13 | "AccessPhotosFailedMessage" = "No photo access right at present, it is recommended to go to the system Settings"; 14 | "GoToSettings" = "Go to Settings"; 15 | 16 | 17 | "AllPhotos" = "Recent Items"; 18 | "Smart Albums" = "Smart Albums"; 19 | "User Albums" = "User Albums"; 20 | 21 | "hour" = "hour"; 22 | 23 | "Cancel" = "Cancel"; 24 | "Save" = "Save"; 25 | 26 | "Photo" = "Photo"; 27 | "Camera" = "Camera"; 28 | "Front/Rear" = "Front/Rear"; 29 | 30 | "AccessPhotoLibraryTitle" = "Wants to access your photos"; 31 | "SelectMorePhotos" = "Select more photos..."; 32 | "KeepCurrent" = "Keep current selection"; 33 | 34 | "VideoDurationTooLong" = "Selected video duration larger than expected"; 35 | "VideoMemoryOutOfSize" = "Selected video out of size"; 36 | "UnspportedVideoFormat" = "Unspported video format"; 37 | 38 | "WithoutCameraPermission" = "doesn't have permission to use the camera, please change privacy settings"; 39 | 40 | "OK" = "OK"; 41 | "Settings" = "Settings"; 42 | 43 | "CameraConfigurationFailed" = "Unable to capture media"; 44 | 45 | "NoPermissionToSave" = "No permission to save"; 46 | 47 | "Preview" = "Preview"; 48 | 49 | "SavePhoto" = "Save picture"; 50 | "SaveVideo" = "Save video"; 51 | 52 | "FailedToSaveMedia" = "Save failed"; 53 | "SuccessfullySavedMedia" = "Successfully Saved to Photo Library"; 54 | 55 | "GotIt" = "Got it"; 56 | 57 | "Resume" = "Resume"; 58 | "Unable to resume" = "Unable to resume"; 59 | 60 | "NoVideo" = "URL isn't a video"; 61 | 62 | "DataNotFound" = "data is not found"; 63 | 64 | "Saved" = "Successfully Saved"; 65 | 66 | // PhotoEdit 67 | "Orinial" = "Original"; 68 | "Square" = "Square"; 69 | "ResetPhoto" = "Reset"; 70 | "DiscardChanges" = "Discard changes"; 71 | "CropPhoto" = "Crop"; 72 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Assets/zh-Hans.lproj/FYPhoto.strings: -------------------------------------------------------------------------------- 1 | /* 2 | FYPhotoPicker.strings 3 | Pods 4 | 5 | Created by xiaoyang on 2020/7/29. 6 | 7 | */ 8 | 9 | "add" = "添加"; 10 | "Done" = "完成"; 11 | 12 | "Confirm" = "确定"; 13 | 14 | "AccessPhotosFailed" = "无法访问相册中照片"; 15 | "AccessPhotosFailedMessage" = "当前无照片访问权限,建议前往系统设置"; 16 | "GoToSettings" = "前往设置"; 17 | 18 | "AllPhotos" = "最近项目"; 19 | "Smart Albums" = "分类相簿"; 20 | "User Albums" = "自定义相簿"; 21 | 22 | "hour" = "小时"; 23 | "Cancel" = "取消"; 24 | 25 | "Save" = "保存"; 26 | 27 | "Select" = "选择"; 28 | 29 | "photo" = "照片"; 30 | "Camera" = "相机"; 31 | 32 | "Front/Rear" = "前置/后置"; 33 | 34 | "AccessPhotoLibraryTitle" = "想访问您的照片"; 35 | 36 | "SelectMorePhotos" = "选择更多照片..."; 37 | "KeepCurrent" = "保留当前所选内容"; 38 | 39 | "VideoDurationTooLong" = "视频时间过长,请重新选择"; 40 | "UnspportedVideoFormat" = "不支持的文件格式"; 41 | "VideoMemoryOutOfSize" = "文件过大,请重新选择"; 42 | 43 | "WithoutCameraPermission" = "没有使用相机的权限,请修改权限设置"; 44 | 45 | "OK" = "好的"; 46 | "Settings" = "设置"; 47 | 48 | "CameraConfigurationFailed" = "无法拍摄视频"; 49 | 50 | "NoPermissionToSave" = "没有权限将照片存储到相册中"; 51 | 52 | "Preview" = "预览"; 53 | 54 | "SavePhoto" = "保存图片"; 55 | "SaveVideo" = "保存视频"; 56 | 57 | "FailedToSaveMedia" = "保存失败"; 58 | "SuccessfullySavedMedia" = "已保存到相册"; 59 | 60 | "GotIt" = "知道了"; 61 | 62 | "Resume" = "恢复"; 63 | "Unable to resume" = "无法恢复"; 64 | 65 | "NoVideo" = "URL不是一个视频"; 66 | 67 | "DataNotFound" = "没有需要的数据"; 68 | 69 | "Saved" = "已保存"; 70 | 71 | // PhotoEdit 72 | "Orinial" = "原始尺寸"; 73 | "Square" = "正方形"; 74 | "ResetPhoto" = "还原"; 75 | "DiscardChanges" = "放弃修改"; 76 | "CropPhoto" = "裁剪"; 77 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/CameraExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraExtensions.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/24. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | import UIKit 11 | 12 | extension AVCaptureDevice.DiscoverySession { 13 | var uniqueDevicePositionsCount: Int { 14 | 15 | var uniqueDevicePositions = [AVCaptureDevice.Position]() 16 | 17 | for device in devices where !uniqueDevicePositions.contains(device.position) { 18 | uniqueDevicePositions.append(device.position) 19 | } 20 | 21 | return uniqueDevicePositions.count 22 | } 23 | } 24 | 25 | extension AVCaptureVideoOrientation { 26 | init?(deviceOrientation: UIDeviceOrientation) { 27 | switch deviceOrientation { 28 | case .portrait: self = .portrait 29 | case .portraitUpsideDown: self = .portraitUpsideDown 30 | case .landscapeLeft: self = .landscapeRight 31 | case .landscapeRight: self = .landscapeLeft 32 | default: return nil 33 | } 34 | } 35 | 36 | init?(interfaceOrientation: UIInterfaceOrientation) { 37 | switch interfaceOrientation { 38 | case .portrait: self = .portrait 39 | case .portraitUpsideDown: self = .portraitUpsideDown 40 | case .landscapeLeft: self = .landscapeLeft 41 | case .landscapeRight: self = .landscapeRight 42 | default: return nil 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/CameraViewController+InfoKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraViewController+InfoKey.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/3/8. 6 | // 7 | 8 | import Foundation 9 | 10 | extension CameraViewController { 11 | public struct InfoKey: Hashable, Equatable, RawRepresentable { 12 | public let rawValue: String 13 | public init(rawValue: String) { 14 | self.rawValue = rawValue 15 | } 16 | 17 | public static let mediaType: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "mediaType") 18 | 19 | public static let originalImage: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "originalImage") // a UIImage 20 | 21 | public static let editedImage: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "editedImage")// a UIImage 22 | 23 | public static let cropRect: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "cropRect")// an NSValue (CGRect) 24 | 25 | public static let mediaURL: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "mediaURL") // an URL 26 | 27 | public static let mediaMetadata: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "mediaMetadata") // an NSDictionary containing metadata from a captured photo 28 | public static let livePhoto: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "livePhoto") // a PHLivePhoto 29 | 30 | public static let imageURL: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "imageURL") // a URL 31 | 32 | public static let watermarkImage: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "watermarkImage") // a UIImage 33 | public static let watermarkVideoURL: CameraViewController.InfoKey = CameraViewController.InfoKey(rawValue: "watermarkVideoURL") // a URL 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/CameraViewController+Tool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraViewController+Tool.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/24. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | public extension CameraViewController { 13 | static func saveImageDataToAlbums(_ photoData: Data, completion: @escaping ((Result) -> Void)) { 14 | SaveMediaTool.saveImageDataToAlbums(photoData, completion: completion) 15 | } 16 | 17 | static func saveImageToAlbums(_ image: UIImage, completion: @escaping ((Result) -> Void)) { 18 | SaveMediaTool.saveImageToAlbums(image, completion: completion) 19 | } 20 | 21 | static func saveVideoDataToAlbums(_ videoPath: URL, completion: @escaping ((Result) -> Void)) { 22 | SaveMediaTool.saveVideoDataToAlbums(videoPath, completion: completion) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/CameraViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by xiaoyang on 2023/2/9. 6 | // 7 | 8 | import Foundation 9 | import MobileCoreServices 10 | import UIKit 11 | import Photos 12 | 13 | public protocol CameraViewControllerDelegate: AnyObject { 14 | func camera(_ cameraViewController: CameraViewController, didFinishCapturingMediaInfo info: [CameraViewController.InfoKey: Any]) 15 | func cameraDidCancel(_ cameraViewController: CameraViewController) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/SaveMediaTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveMediaTool.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/20. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | class SaveMediaTool { 13 | enum SaveMediaError: Error, LocalizedError { 14 | public var errorDescription: String? { 15 | switch self { 16 | case .withoutAuthourity: 17 | return L10n.noPermissionToSave 18 | } 19 | } 20 | 21 | case withoutAuthourity 22 | } 23 | 24 | static func saveImageDataToAlbums(_ photoData: Data, completion: @escaping ((Result) -> Void)) { 25 | PHPhotoLibrary.requestAuthorization { status in 26 | if status == .authorized { 27 | PHPhotoLibrary.shared().performChanges({ 28 | let options = PHAssetResourceCreationOptions() 29 | let creationRequest = PHAssetCreationRequest.forAsset() 30 | creationRequest.addResource(with: .photo, data: photoData, options: options) 31 | }, completionHandler: { _, error in 32 | DispatchQueue.main.async { 33 | if let error = error { 34 | completion(.failure(error)) 35 | print("Error occurred while saving photo to photo library: \(error)") 36 | } else { 37 | completion(.success(())) 38 | } 39 | } 40 | }) 41 | } else { 42 | DispatchQueue.main.async { 43 | completion(.failure(SaveMediaError.withoutAuthourity)) 44 | } 45 | } 46 | } 47 | } 48 | 49 | static func saveImageToAlbums(_ image: UIImage, completion: @escaping ((Result) -> Void)) { 50 | PHPhotoLibrary.requestAuthorization { status in 51 | if status == .authorized { 52 | PHPhotoLibrary.shared().performChanges({ 53 | let options = PHAssetResourceCreationOptions() 54 | let creationRequest = PHAssetCreationRequest.forAsset() 55 | // options.uniformTypeIdentifier = self.requestedPhotoSettings.processedFileType.map { $0.rawValue } 56 | if let data = image.jpegData(compressionQuality: 1) { 57 | creationRequest.addResource(with: .photo, data: data, options: options) 58 | } 59 | }, completionHandler: { _, error in 60 | if let error = error { 61 | print("Error occurred while saving photo to photo library: \(error)") 62 | DispatchQueue.main.async { 63 | completion(.failure(error)) 64 | } 65 | } 66 | DispatchQueue.main.async { 67 | completion(.success(())) 68 | } 69 | }) 70 | } else { 71 | DispatchQueue.main.async { 72 | completion(.failure(SaveMediaError.withoutAuthourity)) 73 | } 74 | 75 | } 76 | } 77 | } 78 | 79 | static func saveVideoDataToAlbums(_ videoPath: URL, completion: @escaping ((Result) -> Void)) { 80 | // Check the authorization status. 81 | PHPhotoLibrary.requestAuthorization { status in 82 | if status == .authorized { 83 | // Save the movie file to the photo library and cleanup. 84 | PHPhotoLibrary.shared().performChanges({ 85 | let options = PHAssetResourceCreationOptions() 86 | options.shouldMoveFile = true 87 | let creationRequest = PHAssetCreationRequest.forAsset() 88 | creationRequest.addResource(with: .video, fileURL: videoPath, options: options) 89 | }, completionHandler: { success, error in 90 | DispatchQueue.main.async { 91 | if let error = error { 92 | completion(.failure(error)) 93 | } else { 94 | completion(.success(())) 95 | } 96 | } 97 | if !success { 98 | print("FYPhoto couldn't save the movie to your photo library: \(String(describing: error))") 99 | } 100 | 101 | }) 102 | } else { 103 | DispatchQueue.main.async { 104 | completion(.failure(SaveMediaError.withoutAuthourity)) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/VideoPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPreviewView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/24. 6 | // 7 | // Abstract: 8 | // The camera preview view that displays the capture output. 9 | 10 | import UIKit 11 | import AVFoundation 12 | 13 | class VideoPreviewView: UIView { 14 | 15 | var videoPreviewLayer: AVCaptureVideoPreviewLayer { 16 | guard let layer = layer as? AVCaptureVideoPreviewLayer else { 17 | fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.") 18 | } 19 | layer.videoGravity = .resizeAspect 20 | return layer 21 | } 22 | 23 | var session: AVCaptureSession? { 24 | get { 25 | return videoPreviewLayer.session 26 | } 27 | set { 28 | videoPreviewLayer.session = newValue 29 | } 30 | } 31 | 32 | override class var layerClass: AnyClass { 33 | return AVCaptureVideoPreviewLayer.self 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Camera/Watermark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by xiaoyang on 2023/2/9. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public protocol WatermarkDataSource: AnyObject { 12 | func watermarkImage() -> WatermarkImage? 13 | } 14 | 15 | public protocol WatermarkDelegate: AnyObject { 16 | func cameraViewControllerStartAddingWatermark(_ cameraViewController: CameraViewController) 17 | 18 | @available(swift, deprecated, message: "use `didFinishAddingWatermarkToVideo` instead") 19 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkAt path: URL) 20 | 21 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkToVideo path: URL) 22 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkToImage image: UIImage) 23 | } 24 | 25 | public extension WatermarkDataSource { 26 | func watermarkImage() -> WatermarkImage? { return nil } 27 | } 28 | 29 | public extension WatermarkDelegate { 30 | func cameraViewControllerStartAddingWatermark(_ cameraViewController: CameraViewController) {} 31 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkAt path: URL) {} 32 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkToVideo path: URL) {} 33 | func camera(_ cameraViewController: CameraViewController, didFinishAddingWatermarkToImage image: UIImage) {} 34 | } 35 | 36 | 37 | public struct WatermarkImage { 38 | let image: UIImage 39 | let frame: CGRect 40 | 41 | public init(image: UIImage, frame: CGRect) { 42 | self.image = image 43 | self.frame = frame 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Configuration/FYColorConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FYColorConfiguration.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/18. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// FYPhoto color configuration. 12 | public class FYColorConfiguration { 13 | public class BarColor { 14 | public let itemTintColor: UIColor 15 | public let itemDisableColor: UIColor 16 | public let itemBackgroundColor: UIColor 17 | // bar backgroundColor 18 | public let backgroundColor: UIColor 19 | 20 | public init(itemTintColor: UIColor, 21 | itemDisableColor: UIColor, 22 | itemBackgroundColor: UIColor, 23 | backgroundColor: UIColor) { 24 | self.itemTintColor = itemTintColor 25 | self.itemDisableColor = itemDisableColor 26 | self.itemBackgroundColor = itemBackgroundColor 27 | self.backgroundColor = backgroundColor 28 | } 29 | } 30 | 31 | public init() {} 32 | 33 | // picker cell selection color 34 | public var selectionTitleColor: UIColor = .white 35 | public var selectionBackgroudColor: UIColor = .fyBlueTintColor 36 | 37 | public var topBarColor = 38 | BarColor(itemTintColor: UIColor.fyBlueTintColor, 39 | itemDisableColor: .systemGray, 40 | itemBackgroundColor: .clear, 41 | backgroundColor: .white) 42 | 43 | public var pickerBottomBarColor = 44 | BarColor(itemTintColor: UIColor.fyBlueTintColor, 45 | itemDisableColor: .fyItemDisableColor, 46 | itemBackgroundColor: .fyGrayBackgroundColor, 47 | backgroundColor: .fyGrayBackgroundColor) 48 | 49 | public var browserBottomBarColor = 50 | BarColor(itemTintColor: UIColor.fyBlueTintColor, 51 | itemDisableColor: .fyItemDisableColor, 52 | itemBackgroundColor: .white, 53 | backgroundColor: UIColor(white: 0.1, alpha: 0.9)) 54 | 55 | } 56 | 57 | extension UIColor { 58 | static let fyBlueTintColor = #colorLiteral(red: 0.09411764706, green: 0.5294117647, blue: 0.9843137255, alpha: 1) 59 | 60 | static let fyItemDisableColor = #colorLiteral(red: 0.6549019608, green: 0.6705882353, blue: 0.6941176471, alpha: 1) 61 | 62 | static let fyGrayBackgroundColor = UIColor.color(light: #colorLiteral(red: 0.9764705882, green: 0.9764705882, blue: 0.9764705882, alpha: 1), 63 | dark: #colorLiteral(red: 0.1843137255, green: 0.1843137255, blue: 0.1843137255, alpha: 1)) 64 | 65 | static func color(light: UIColor, dark: UIColor) -> UIColor { 66 | if #available(iOS 13.0, *) { 67 | return UIColor { traits -> UIColor in 68 | if traits.userInterfaceStyle == .dark { 69 | return dark 70 | } else { 71 | return light 72 | } 73 | } 74 | } else { 75 | return light 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Configuration/FYPhotoPickerConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FYPhotoPickerConfiguration.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/3. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import FYVideoCompressor 11 | 12 | /// A configuration for `FYPhoto.PickerViewController`. 13 | public struct FYPhotoPickerConfiguration { 14 | /// Maximum number of assets that can be selected. Default is 1. 15 | /// 16 | /// Setting `selectionLimit` to 0 means maximum supported by the system. 17 | public var selectionLimit: Int = 1 18 | 19 | @available(swift, deprecated: 1.2.0, message: "use mediaFilter instead") 20 | public var filterdMedia: MediaOptions = .all 21 | 22 | /// Filter the media types PhotoPickerController can display. Default are video and image. 23 | public var mediaFilter: MediaOptions = .all 24 | 25 | /// Maximum video duration can pick. Default is 15 seconds. 26 | public var maximumVideoDuration: Double = 15 27 | 28 | /// Maximum video size can pick. Default is 0, not limit. 29 | public var maximumVideoMemorySize: Double = 0 30 | 31 | /// If true, compress video which is larger than `compressVideoLimitSize` MB before giving it to user. Default is true. 32 | public var compressVideoBeforeSelected: Bool = true 33 | public var compressVideoLimitSize: Double = 1 // MB 34 | 35 | /// Video compressed quality. Default is 640x480. 36 | public var compressedQuality: FYVideoCompressor.VideoQuality = .mediumQuality 37 | 38 | /// Captured movie path extension. Default is `mp4`. 39 | public var moviePathExtension: String = "mp4" 40 | 41 | /// whether first cell is camera cell or not. Default is true. 42 | public var supportCamera: Bool = true 43 | 44 | @available(swift, deprecated: 1.2.0, message: "custom color with colorConfiguration") 45 | public var uiConfiguration = FYUIConfiguration() 46 | 47 | public var colorConfiguration = FYColorConfiguration() 48 | 49 | public init() { 50 | 51 | } 52 | } 53 | 54 | @available(swift, deprecated: 1.2.0, message: "Use FYColorConfiguration instead") 55 | public class FYUIConfiguration { 56 | public class BarColorSytle { 57 | 58 | public let itemTintColor: UIColor 59 | public let itemDisableColor: UIColor 60 | public let itemBackgroundColor: UIColor 61 | // bar backgroundColor 62 | public let backgroundColor: UIColor 63 | 64 | public init(itemTintColor: UIColor, 65 | itemDisableColor: UIColor, 66 | itemBackgroundColor: UIColor, 67 | backgroundColor: UIColor) { 68 | self.itemTintColor = itemTintColor 69 | self.itemDisableColor = itemDisableColor 70 | self.itemBackgroundColor = itemBackgroundColor 71 | self.backgroundColor = backgroundColor 72 | } 73 | } 74 | 75 | public init() {} 76 | 77 | public var selectionTitleColor: UIColor = .white 78 | public var selectionBackgroudColor: UIColor = .fyBlueTintColor 79 | 80 | public var topBarColorStyle = 81 | BarColorSytle(itemTintColor: UIColor.fyBlueTintColor, 82 | itemDisableColor: .systemGray, 83 | itemBackgroundColor: .white, 84 | backgroundColor: .white) 85 | 86 | public var pickerBottomBarColorStyle = 87 | BarColorSytle(itemTintColor: UIColor.fyBlueTintColor, 88 | itemDisableColor: .fyItemDisableColor, 89 | itemBackgroundColor: .fyGrayBackgroundColor, 90 | backgroundColor: .fyGrayBackgroundColor) 91 | 92 | public var browserBottomBarColorStyle = 93 | BarColorSytle(itemTintColor: UIColor.fyBlueTintColor, 94 | itemDisableColor: .fyItemDisableColor, 95 | itemBackgroundColor: .white, 96 | backgroundColor: UIColor(white: 0.1, alpha: 0.9)) 97 | } 98 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/AspectRatioControl/AspectRatioBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AspectRatioBar.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/5/6. 6 | // 7 | 8 | import UIKit 9 | 10 | class AspectRatioBar: UIScrollView { 11 | private struct Constants { 12 | static let maxButtonsSpacing: CGFloat = 10.0 13 | static let minButtonsSpacing: CGFloat = 8.0 14 | static let minButtonVisibleWidth: CGFloat = 20.0 15 | static let minButtonWidth: CGFloat = 56.0 16 | static let minHeight: CGFloat = 28.0 17 | static let sideInset: CGFloat = 16.0 18 | } 19 | 20 | var didSelectedRatio: ((Double?) -> Void)? 21 | 22 | let items: [AspectRatioButtonItem] 23 | 24 | private var selectedButton: AspectRatioButton? 25 | 26 | private lazy var stackView: UIStackView = { 27 | let view = UIStackView() 28 | view.axis = isPortrait ? .horizontal : .vertical 29 | view.alignment = .center 30 | view.spacing = Constants.minButtonsSpacing 31 | return view 32 | }() 33 | 34 | let isPortrait: Bool 35 | init(items: [AspectRatioButtonItem], isPortrait: Bool) { 36 | self.items = items 37 | self.isPortrait = isPortrait 38 | super.init(frame: .zero) 39 | showsHorizontalScrollIndicator = false 40 | showsVerticalScrollIndicator = false 41 | setupStackView() 42 | addButtonsWithItems(items) 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | var stackFrameLayoutGuides: [NSLayoutConstraint] = [] 50 | private func setupStackView() { 51 | addSubview(stackView) 52 | stackView.translatesAutoresizingMaskIntoConstraints = false 53 | NSLayoutConstraint.activate([ 54 | stackView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor), 55 | stackView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor), 56 | stackView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor), 57 | stackView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor) 58 | ]) 59 | if isPortrait { 60 | stackFrameLayoutGuides = [ 61 | stackView.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor) 62 | ] 63 | } else { 64 | stackFrameLayoutGuides = [ 65 | stackView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor) 66 | ] 67 | } 68 | 69 | NSLayoutConstraint.activate(stackFrameLayoutGuides) 70 | } 71 | 72 | func addButtonsWithItems(_ items: [AspectRatioButtonItem]) { 73 | for item in items { 74 | let button = AspectRatioButton(item: item) 75 | button.translatesAutoresizingMaskIntoConstraints = false 76 | button.addTarget(self, action: #selector(buttonClicked(_:)), for: .touchUpInside) 77 | if item.isSelected { 78 | selectedButton = button 79 | } 80 | stackView.addArrangedSubview(button) 81 | } 82 | 83 | } 84 | 85 | func flip() { 86 | stackView.axis = (stackView.axis == .horizontal) ? .vertical : .horizontal 87 | 88 | NSLayoutConstraint.deactivate(stackFrameLayoutGuides) 89 | if stackView.axis == .horizontal { 90 | stackFrameLayoutGuides = [ 91 | stackView.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor) 92 | ] 93 | } else { 94 | stackFrameLayoutGuides = [ 95 | stackView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor) 96 | ] 97 | } 98 | NSLayoutConstraint.activate(stackFrameLayoutGuides) 99 | 100 | stackView.layoutIfNeeded() // fix stackview autolayout warnings after changing axis 101 | } 102 | 103 | func reloadItems(_ items: [AspectRatioButtonItem]) { 104 | stackView.removeFullyAllArrangedSubviews() 105 | addButtonsWithItems(items) 106 | } 107 | 108 | @objc private func buttonClicked(_ sender: AspectRatioButton) { 109 | handleButtonsState(sender) 110 | didSelectedRatio?(sender.item.ratio) 111 | } 112 | 113 | private func handleButtonsState(_ new: AspectRatioButton) { 114 | if let old = selectedButton { 115 | if old === new { 116 | return 117 | } else { 118 | old.isSelected = false 119 | new.isSelected = true 120 | } 121 | } else { 122 | new.isSelected = true 123 | } 124 | selectedButton = new 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/AspectRatioControl/AspectRatioButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AspectRatioButton.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/5/6. 6 | // 7 | 8 | import UIKit 9 | 10 | class AspectRatioButton: UIButton { 11 | let item: AspectRatioButtonItem 12 | 13 | init(item: AspectRatioButtonItem) { 14 | self.item = item 15 | super.init(frame: .zero) 16 | setTitleColor(UIColor(white: 1, alpha: 0.8), for: .normal) 17 | setTitleColor(.white, for: .selected) 18 | backgroundColor = .clear 19 | titleLabel?.font = UIFont.systemFont(ofSize: 14) 20 | 21 | layer.masksToBounds = true 22 | 23 | if #available(iOS 13.0, *) { 24 | layer.cornerCurve = .continuous 25 | } else { 26 | // Fallback on earlier versions 27 | } 28 | 29 | contentEdgeInsets = UIEdgeInsets(top: 6, 30 | left: 16, 31 | bottom: 6, 32 | right: 16) 33 | 34 | isSelected = item.isSelected 35 | setTitle(item.title, for: .normal) 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override var isSelected: Bool { 43 | didSet { 44 | item.isSelected = isSelected 45 | updateAppearance() 46 | } 47 | } 48 | 49 | func updateAppearance() { 50 | if isSelected { 51 | backgroundColor = .init(white: 1, alpha: 0.5) 52 | // setTitleColor(.white, for: .normal) 53 | } else { 54 | backgroundColor = .clear 55 | // setTitleColor(UIColor(white: 1, alpha: 0.8), for: .normal) 56 | } 57 | } 58 | 59 | override func layoutSubviews() { 60 | super.layoutSubviews() 61 | layer.cornerRadius = bounds.height / 2 62 | } 63 | 64 | // override var intrinsicContentSize: CGSize { 65 | // let superSize = super.intrinsicContentSize 66 | // return superSize 67 | // } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/AspectRatioControl/AspectRatioButtonItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AspectRatioButtonItem.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/5/6. 6 | // 7 | 8 | import Foundation 9 | 10 | public class AspectRatioButtonItem { 11 | let title: String 12 | var isSelected: Bool 13 | var ratio: Double? 14 | let isFreeForm: Bool 15 | 16 | public init(title: String, ratio: Double?) { 17 | self.title = title 18 | 19 | self.ratio = ratio 20 | self.isFreeForm = ratio == nil 21 | self.isSelected = isFreeForm 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/AspectRatioControl/PhotoAspectRatio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoAspectRatio.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct RatioItem { 11 | let title: String 12 | let value: Double? 13 | 14 | /// Initialize RatioItem 15 | /// - Parameters: 16 | /// - title: title 17 | /// - value: ratio value, nil means without apsect ratio limit. 18 | public init(title: String, value: Double?) { 19 | self.title = title 20 | self.value = value 21 | } 22 | } 23 | 24 | public struct RatioOptions: OptionSet { 25 | public let rawValue: Int 26 | public init(rawValue: Int) { 27 | self.rawValue = rawValue 28 | } 29 | 30 | static public let original = RatioOptions(rawValue: 1 << 0) 31 | static public let freeform = RatioOptions(rawValue: 1 << 1) 32 | static public let square = RatioOptions(rawValue: 1 << 2) 33 | static public let extraDefaultRatios = RatioOptions(rawValue: 1 << 3) 34 | static public let custom = RatioOptions(rawValue: 1 << 4) 35 | 36 | static public let all: RatioOptions = [original, freeform, square, extraDefaultRatios, custom] 37 | } 38 | 39 | class RatioManager { 40 | 41 | private(set) var items: [RatioItem] = [] 42 | 43 | init(ratioOptions: RatioOptions, custom: [RatioItem], original: Double, isHorizontal: Bool) { 44 | if ratioOptions.contains(.original) { 45 | items.append(RatioItem(title: L10n.orinial, value: original)) 46 | } 47 | if ratioOptions.contains(.freeform) { 48 | items.append(RatioItem(title: "FreeForm", value: nil)) 49 | } 50 | if ratioOptions.contains(.square) { 51 | items.append(RatioItem(title: L10n.square, value: 1)) 52 | } 53 | if ratioOptions.contains(.extraDefaultRatios) { 54 | if isHorizontal { 55 | items += horizontalExtraDefault() 56 | } else { 57 | items += verticalExtraDefault() 58 | } 59 | } 60 | if ratioOptions.contains(.custom) { 61 | items += custom 62 | } 63 | } 64 | 65 | private func horizontalExtraDefault() -> [RatioItem] { 66 | [RatioItem(title: "16:9", value: 16.0 / 9.0), 67 | RatioItem(title: "10:8", value: 10.0 / 8.0), 68 | RatioItem(title: "7:5", value: 7.0 / 5.0), 69 | RatioItem(title: "4:3", value: 4.0 / 3.0), 70 | RatioItem(title: "5:3", value: 5.0 / 3.0), 71 | RatioItem(title: "3:2", value: 3.0 / 2.0) 72 | ] 73 | } 74 | 75 | private func verticalExtraDefault() -> [RatioItem] { 76 | [RatioItem(title: "9:16", value: 9.0 / 16.0), 77 | RatioItem(title: "8:10", value: 8.0 / 10.0), 78 | RatioItem(title: "5:7", value: 5.0 / 7.0), 79 | RatioItem(title: "3:4", value: 3.0 / 4.0), 80 | RatioItem(title: "3:5", value: 3.0 / 5.0), 81 | RatioItem(title: "2:3", value: 2.0 / 3.0) 82 | ] 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/CropScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropScrollView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | class CropScrollView: UIScrollView { 11 | 12 | weak var imageContainer: CropView.ImageView? 13 | 14 | var touchesBegan = {} 15 | var touchesCancelled = {} 16 | var touchesEnd = {} 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | alwaysBounceHorizontal = true 21 | alwaysBounceVertical = true 22 | showsHorizontalScrollIndicator = false 23 | showsVerticalScrollIndicator = false 24 | contentInsetAdjustmentBehavior = .never 25 | minimumZoomScale = 1.0 26 | maximumZoomScale = 20.0 27 | clipsToBounds = false 28 | contentSize = bounds.size 29 | layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | } 35 | 36 | func reset(rect: CGRect, isPortrait: Bool) { 37 | // Reseting zoom need to be before resetting frame and contentsize 38 | let minimum = getBoundZoomScale() 39 | minimumZoomScale = minimum 40 | zoomScale = minimum 41 | 42 | frame = rect 43 | 44 | contentSize = isPortrait ? CGSize(width: rect.width, height: rect.height) : CGSize(width: rect.height, height: rect.width) 45 | } 46 | 47 | // Update bound size, re-center with old center, then scrollView's frame will be changed. 48 | func updateBounds(with newSize: CGSize) { 49 | let oldCenter = center 50 | let oldOffsetCenter = CGPoint(x: contentOffset.x + bounds.width/2, y: contentOffset.y + bounds.height/2) 51 | 52 | bounds.size = newSize 53 | let newOffset = CGPoint(x: oldOffsetCenter.x - newSize.width/2, y: oldOffsetCenter.y - newSize.height/2) 54 | contentOffset = newOffset 55 | center = oldCenter 56 | } 57 | 58 | func updateMinimumScacle() { 59 | minimumZoomScale = getBoundZoomScale() 60 | } 61 | 62 | private func getBoundZoomScale() -> CGFloat { 63 | guard let imageContainer = imageContainer, bounds.size != .zero else { 64 | return 1.0 65 | } 66 | let scaleW = bounds.width / imageContainer.bounds.width 67 | let scaleH = bounds.height / imageContainer.bounds.height 68 | 69 | return max(scaleW, scaleH) 70 | } 71 | 72 | func checkContentOffset() { 73 | contentOffset.x = max(contentOffset.x, 0) 74 | contentOffset.y = max(contentOffset.y, 0) 75 | 76 | if contentSize.height - contentOffset.y <= bounds.size.height { 77 | contentOffset.y = contentSize.height - bounds.size.height 78 | } 79 | 80 | if contentSize.width - contentOffset.x <= bounds.size.width { 81 | contentOffset.x = contentSize.width - bounds.size.width 82 | } 83 | } 84 | 85 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 86 | super.touchesBegan(touches, with: event) 87 | touchesBegan() 88 | } 89 | 90 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 91 | super.touchesCancelled(touches, with: event) 92 | touchesCancelled() 93 | } 94 | 95 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 96 | super.touchesEnded(touches, with: event) 97 | touchesEnd() 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/CropView+ImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropView+ImageView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension CropView { 12 | 13 | /// Partially constrained view size, adapting to image aspect ratio 14 | class ImageView: UIImageView { 15 | override init(image: UIImage?) { 16 | super.init(image: image) 17 | setup() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | /// constraint to maintain same aspect ratio as the image 25 | private var aspectRatioConstraint: NSLayoutConstraint? 26 | 27 | private func setup() { 28 | self.contentMode = .scaleAspectFit 29 | // self.updateAspectRatioConstraint() 30 | } 31 | 32 | /// Removes any pre-existing aspect ratio constraint, and adds a new one based on the current image 33 | // private func updateAspectRatioConstraint() { 34 | // // remove any existing aspect ratio constraint 35 | // if let cons = self.aspectRatioConstraint { 36 | // self.removeConstraint(cons) 37 | // } 38 | // self.aspectRatioConstraint = nil 39 | // 40 | // if let imageSize = image?.size, imageSize.height != 0 41 | // { 42 | // let aspectRatio = imageSize.width / imageSize.height 43 | // let cons = NSLayoutConstraint(item: self, attribute: .width, 44 | // relatedBy: .equal, 45 | // toItem: self, attribute: .height, 46 | // multiplier: aspectRatio, constant: 0) 47 | // self.addConstraint(cons) 48 | // self.aspectRatioConstraint = cons 49 | // } 50 | // } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/CropView+UIScrollViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropView+UIScrollViewDelegate.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension CropView: UIScrollViewDelegate { 12 | // pinches imageView 13 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 14 | return imageView 15 | } 16 | 17 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 18 | scrollViewWillBeginDragging() 19 | } 20 | 21 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 22 | if !decelerate { 23 | scrollViewDidEndDragging() 24 | } 25 | } 26 | 27 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 28 | scrollViewDidEndDecelerating() 29 | } 30 | 31 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 32 | scrollViewDidZoom() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/CropViewStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropViewStatus.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CropViewHandle { 11 | case top 12 | case leftTop 13 | case left 14 | case leftBottom 15 | case bottom 16 | case rightBottom 17 | case right 18 | case rightTop 19 | } 20 | 21 | enum CropViewStatus { 22 | case initial 23 | case touchImage 24 | case touchHandle(_ handle: CropViewHandle) 25 | case imageRotation 26 | case endTouch 27 | } 28 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/CroppedRestoreData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CroppedRestoreData.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/6/23. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// data for restore previous cropped data 12 | public struct CroppedRestoreData { 13 | let initialFrame: CGRect 14 | let initialZoomScale: CGFloat 15 | let cropFrame: CGRect 16 | let zoomScale: CGFloat 17 | var zoomRect: CGRect? 18 | var contentOffset: CGPoint? 19 | let rotation: PhotoRotation 20 | let originImage: UIImage 21 | let editedImage: UIImage 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/GeometryHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeometryHelper.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/20. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | 11 | struct GeometryHelper { 12 | 13 | /// Calculate appropriate rect within the outside coordinator for cropView from two rects. 14 | /// - Parameters: 15 | /// - outside: outside view 16 | /// - inside: inside view (guide view) 17 | /// - Returns: rect 18 | static func getAppropriateRect(fromOutside outside: CGRect, inside: CGRect) -> CGRect { 19 | var rect = CGRect(origin: .zero, size: inside.size) 20 | let outsideRatio = outside.width / outside.height 21 | let insideRatio = inside.width / inside.height 22 | 23 | if outsideRatio >= insideRatio { 24 | rect.size.width *= (outside.height / inside.height) 25 | rect.size.height = outside.height 26 | } else if outsideRatio < insideRatio { 27 | rect.size.height *= (outside.width / inside.width) 28 | rect.size.width = outside.width 29 | } 30 | 31 | // reset precision 32 | let tempX = ((outside.midX - rect.width / 2) * 100).rounded(.toNearestOrAwayFromZero) / 100 33 | let tempY = ((outside.midY - rect.height / 2) * 100).rounded(.toNearestOrAwayFromZero) / 100 34 | rect.origin.x = tempX 35 | rect.origin.y = tempY 36 | 37 | return rect 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/Mask/CropDimmingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropDimmingView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class CropDimmingView: UIView, CropMaskProtocol { 11 | var transparentLayer: CAShapeLayer? 12 | 13 | func setMask(_ insideRect: CGRect, animated: Bool) { 14 | guard self.bounds.size != .zero else { 15 | return 16 | } 17 | 18 | let layer = createTransparentRect(withOutside: bounds, insideRect: insideRect, opacity: 0.5) 19 | 20 | if let shapeLayer = transparentLayer { 21 | if animated { 22 | animateTransparentLayer(shapeLayer, withOutside: bounds, insideRect: insideRect, opacity: 0.5) 23 | } else { 24 | shapeLayer.path = layer.path 25 | } 26 | } else { 27 | self.layer.addSublayer(layer) 28 | transparentLayer = layer 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/Mask/CropMaskProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropMaskProtocol.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/22. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | protocol CropMaskProtocol where Self: UIView { 12 | var transparentLayer: CAShapeLayer? { get set } 13 | func setMask(_ insideRect: CGRect, animated: Bool) 14 | } 15 | 16 | extension CropMaskProtocol { 17 | func createTransparentRect(withOutside outsideRect: CGRect, insideRect: CGRect, opacity: Float) -> CAShapeLayer { 18 | let path = UIBezierPath(rect: outsideRect) 19 | 20 | let innerPath = UIBezierPath(rect: insideRect) 21 | 22 | path.append(innerPath) 23 | path.usesEvenOddFillRule = true 24 | 25 | let fillLayer = CAShapeLayer() 26 | fillLayer.path = path.cgPath 27 | fillLayer.fillRule = .evenOdd 28 | fillLayer.fillColor = UIColor.black.cgColor 29 | fillLayer.opacity = opacity 30 | return fillLayer 31 | } 32 | 33 | func animateTransparentLayer(_ shapeLayer: CAShapeLayer, withOutside outsideRect: CGRect, insideRect: CGRect, opacity: Float) { 34 | let animation = CABasicAnimation(keyPath: "path") 35 | animation.fromValue = shapeLayer.path 36 | addTransparentRect(on: shapeLayer, withOutside: outsideRect, insideRect: insideRect, opacity: opacity) 37 | animation.toValue = shapeLayer.path 38 | animation.duration = 0.2 39 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) // Avoid animation vibration, but still not smooth enough. 40 | shapeLayer.add(animation, forKey: "pathAnimation") 41 | } 42 | 43 | func addTransparentRect(on fillLayer: CAShapeLayer, withOutside outsideRect: CGRect, insideRect: CGRect, opacity: Float) { 44 | let path = UIBezierPath(rect: outsideRect) 45 | let innerPath = UIBezierPath(rect: insideRect) 46 | 47 | path.append(innerPath) 48 | path.usesEvenOddFillRule = true 49 | 50 | fillLayer.path = path.cgPath 51 | fillLayer.fillRule = .evenOdd 52 | fillLayer.fillColor = UIColor.black.cgColor 53 | fillLayer.opacity = opacity 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/Mask/CropViewMaskManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropViewBackBlurredView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class CropViewMaskManager { 11 | 12 | private let effectView: CropVisualEffectView 13 | private let dimmingView: CropDimmingView 14 | 15 | let dimmingOpacity: Float 16 | init(effect: UIBlurEffect = UIBlurEffect(style: .dark), 17 | dimmingOpacity: Float = 0.5) { 18 | self.dimmingOpacity = dimmingOpacity 19 | 20 | effectView = CropVisualEffectView(effect: effect) 21 | dimmingView = CropDimmingView() 22 | dimmingView.alpha = 0 23 | 24 | effectView.isUserInteractionEnabled = false 25 | dimmingView.isUserInteractionEnabled = false 26 | } 27 | 28 | private var effectFilledLayer: CALayer? 29 | private var dimmingFilledLayer: CALayer? 30 | 31 | func showIn(_ view: UIView) { 32 | view.addSubview(effectView) 33 | view.addSubview(dimmingView) 34 | 35 | effectView.translatesAutoresizingMaskIntoConstraints = false 36 | dimmingView.translatesAutoresizingMaskIntoConstraints = false 37 | NSLayoutConstraint.activate([ 38 | effectView.topAnchor.constraint(equalTo: view.topAnchor), 39 | effectView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 40 | effectView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 41 | effectView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 42 | ]) 43 | 44 | NSLayoutConstraint.activate([ 45 | dimmingView.topAnchor.constraint(equalTo: view.topAnchor), 46 | dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 47 | dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 48 | dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 49 | ]) 50 | 51 | } 52 | 53 | required init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | func reset() { 58 | dimmingView.removeFromSuperview() 59 | effectView.removeFromSuperview() 60 | } 61 | 62 | func showVisualEffectBackground() { 63 | UIView.animate(withDuration: 0.5) { 64 | self.dimmingView.alpha = 0 65 | self.effectView.alpha = 1 66 | } 67 | } 68 | 69 | func showDimmingBackground() { 70 | UIView.animate(withDuration: 0.1) { 71 | self.effectView.alpha = 0 72 | self.dimmingView.alpha = 1 73 | } 74 | } 75 | 76 | func rotateMask(_ rect: CGRect) { 77 | effectView.createBrandNewMask(rect) 78 | dimmingView.setMask(rect, animated: false) 79 | } 80 | 81 | func recreateTransparentRect(_ rect: CGRect, animated: Bool) { 82 | createTransparentRect(with: rect, animated: animated) 83 | } 84 | 85 | func createTransparentRect(with insideRect: CGRect, animated: Bool) { 86 | effectView.setMask(insideRect, animated: animated) 87 | dimmingView.setMask(insideRect, animated: animated) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/Mask/CropVisualEffectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropVisualEffectView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class CropVisualEffectView: UIVisualEffectView, CropMaskProtocol { 11 | 12 | var transparentLayer: CAShapeLayer? 13 | 14 | override init(effect: UIVisualEffect?) { 15 | super.init(effect: effect) 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | func setMask(_ insideRect: CGRect, animated: Bool) { 23 | guard self.bounds.size != .zero else { return } 24 | let layer = createTransparentRect(withOutside: bounds, insideRect: insideRect, opacity: 0.98) 25 | 26 | if let shapeLayer = transparentLayer { 27 | if animated { 28 | animateTransparentLayer(shapeLayer, withOutside: bounds, insideRect: insideRect, opacity: 0.98) 29 | } else { 30 | shapeLayer.path = layer.path 31 | } 32 | } else { 33 | let maskView = UIView(frame: bounds) 34 | maskView.clipsToBounds = true 35 | maskView.layer.addSublayer(layer) 36 | transparentLayer = layer 37 | self.mask = maskView 38 | } 39 | } 40 | 41 | /// Create a brand new mask layer without using the exsisting shapeLayer. 42 | /// - Parameter insideRect: transparent rect 43 | func createBrandNewMask(_ insideRect: CGRect) { 44 | guard self.bounds.size != .zero else { return } 45 | let layer = createTransparentRect(withOutside: bounds, insideRect: insideRect, opacity: 0.98) 46 | let maskView = UIView(frame: bounds) 47 | maskView.clipsToBounds = true 48 | maskView.layer.addSublayer(layer) 49 | transparentLayer = layer 50 | self.mask = maskView 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/PhotoRotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoRotationDegree.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/4/25. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | 11 | enum PhotoRotation: Int { 12 | case zero 13 | case counterclockwise90 14 | case counterclockwise180 15 | case counterclockwise270 16 | // case custom(radians: Double) 17 | 18 | var radians: CGFloat { 19 | switch self { 20 | case .zero: 21 | return 0 22 | case .counterclockwise90: 23 | return -CGFloat.pi/2 24 | case .counterclockwise180: 25 | return -CGFloat.pi 26 | case .counterclockwise270: 27 | return -CGFloat.pi*1.5 28 | // case .custom(radians: let value): 29 | // return value 30 | } 31 | } 32 | 33 | var degree: CGFloat { 34 | get { 35 | return radians / CGFloat.pi * 180.0 36 | } 37 | } 38 | 39 | mutating func counterclockwiseRotate90Degree() { 40 | switch self { 41 | case .zero: 42 | self = .counterclockwise90 43 | case .counterclockwise90: 44 | self = .counterclockwise180 45 | case .counterclockwise180: 46 | self = .counterclockwise270 47 | case .counterclockwise270: 48 | self = .zero 49 | // case .custom(radians: let radians): 50 | // self = .custom(radians: radians + Double.pi / 2) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Editor/Photo/Crop/TapExpandedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapExpandedView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/3/26. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class TapExpandedView: UIView { 12 | 13 | let horizontal: CGFloat 14 | let vertical: CGFloat 15 | 16 | init(horizontal: CGFloat, vertical: CGFloat) { 17 | self.horizontal = horizontal 18 | self.vertical = vertical 19 | super.init(frame: .zero) 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 27 | bounds.insetBy(dx: -horizontal, dy: -vertical).contains(point) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/FYPhotoCacheCleaner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FYPhotoCacheCleaner.swift 3 | // 4 | // 5 | // Created by xiaoyang on 2021/11/8. 6 | // 7 | 8 | import Foundation 9 | import SDWebImage 10 | 11 | public class FYPhotoCacheCleaner { 12 | public static func clearMemory() { 13 | SDImageCache.shared.clearMemory() 14 | } 15 | 16 | public static func clearDisk() { 17 | SDImageCache.shared.clearDisk {} 18 | VideoCache.shared?.clearAll() 19 | VideoTrimmer.shared.clear() 20 | PhotoPickerResource.shared.clearCache() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/AVAsset+VideoSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVAsset+VideoSize.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/21. 6 | // 7 | 8 | import Foundation 9 | import AVKit 10 | import Photos 11 | 12 | extension AVAsset { 13 | func dataSize() -> Float? { 14 | guard let lastTrackTotal = tracks(withMediaType: .video).last?.totalSampleDataLength else { 15 | return nil 16 | } 17 | return Float(lastTrackTotal) / 1024 / 1024 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/AVFileType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVFileType.swift 3 | // 4 | // 5 | // Created by xiaoyang on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | import MobileCoreServices 11 | 12 | extension AVFileType { 13 | /// Fetch and extension for a file from UTI string 14 | var fileExtension: String { 15 | if #available(iOS 14.0, *) { 16 | if let utType = UTType(self.rawValue) { 17 | return utType.preferredFilenameExtension ?? "None" 18 | } 19 | return "None" 20 | } else { 21 | if let ext = UTTypeCopyPreferredTagWithClass(self as CFString, 22 | kUTTagClassFilenameExtension)?.takeRetainedValue() { 23 | return ext as String 24 | } 25 | return "None" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/CG+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CG+extensions.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/24. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | 11 | public extension CGRect { 12 | /// Kinda like AVFoundation.AVMakeRect, but handles tall-skinny aspect ratios differently. 13 | /// Returns a rectangle of the same aspect ratio, but scaleAspectFit inside the other rectangle. 14 | static func makeRect(aspectRatio: CGSize, insideRect rect: CGRect) -> CGRect { 15 | let viewRatio = rect.width / rect.height 16 | let imageRatio = aspectRatio.width / aspectRatio.height 17 | let touchesHorizontalSides = (imageRatio > viewRatio) 18 | 19 | let result: CGRect 20 | if touchesHorizontalSides { 21 | let height = rect.width / imageRatio 22 | let yPoint = rect.minY + (rect.height - height) / 2 23 | result = CGRect(x: 0, y: yPoint, width: rect.width, height: height) 24 | } else { 25 | let width = rect.height * imageRatio 26 | let xPoint = rect.minX + (rect.width - width) / 2 27 | result = CGRect(x: xPoint, y: 0, width: width, height: rect.height) 28 | } 29 | return result 30 | } 31 | } 32 | 33 | public extension CGFloat { 34 | /// Returns the value, scaled-and-shifted to the targetRange. 35 | /// If no target range is provided, we assume the unit range (0, 1) 36 | static func scaleAndShift(value: CGFloat, 37 | inRange: (min: CGFloat, max: CGFloat), 38 | toRange: (min: CGFloat, max: CGFloat) = (min: 0.0, max: 1.0)) -> CGFloat { 39 | assert(inRange.max > inRange.min) 40 | assert(toRange.max > toRange.min) 41 | 42 | if value < inRange.min { 43 | return toRange.min 44 | } else if value > inRange.max { 45 | return toRange.max 46 | } else { 47 | let ratio = (value - inRange.min) / (inRange.max - inRange.min) 48 | return toRange.min + ratio * (toRange.max - toRange.min) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/FileManager+TempDirectory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager+TempDirectory.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FileManager { 11 | enum CreateTempDirectoryError: Error, LocalizedError { 12 | case fileExsisted 13 | 14 | var errorDescription: String? { 15 | switch self { 16 | case .fileExsisted: 17 | return "File exsisted" 18 | } 19 | } 20 | } 21 | 22 | /// Get temp directory. If it exsists, return it, else create it. 23 | /// - Parameter pathComponent: path to append to temp directory. 24 | /// - Returns: temp directory location. 25 | /// - Warning: Every time you call this function will return a different directory. 26 | static func tempDirectory(with pathComponent: String = ProcessInfo.processInfo.globallyUniqueString) -> URL { 27 | var tempURL: URL 28 | 29 | // Only the volume(卷) of cache url is used. 30 | let cacheURL = FileManager.default.temporaryDirectory 31 | if let url = try? FileManager.default.url(for: .itemReplacementDirectory, 32 | in: .userDomainMask, 33 | appropriateFor: cacheURL, 34 | create: true) { 35 | tempURL = url 36 | } else { 37 | tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) 38 | } 39 | 40 | tempURL.appendPathComponent(pathComponent) 41 | 42 | if !FileManager.default.fileExists(atPath: tempURL.path) { 43 | do { 44 | try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil) 45 | } catch { 46 | tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(pathComponent, isDirectory: true) 47 | } 48 | } 49 | #if DEBUG 50 | print("temp directory path \(tempURL)") 51 | #endif 52 | return tempURL 53 | } 54 | } 55 | 56 | extension FileManager { 57 | static let trimmedVideoDirName = "TrimmedVideo" 58 | static let cachedWebVideoDirName = "FYPhotoVideoCache" 59 | static let avCompositionDirName = "AVCompositionDir" 60 | } 61 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/PHAsset+GetImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PHAsset+GetImage.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/22. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | extension PHAsset { 13 | func getHightQualityImageSynchorously() -> UIImage? { 14 | let options = PHImageRequestOptions() 15 | options.isNetworkAccessAllowed = true 16 | options.resizeMode = .fast 17 | options.deliveryMode = .highQualityFormat 18 | options.isSynchronous = true 19 | let targetSize = CGSize(width: pixelWidth, height: pixelHeight) 20 | var temp: UIImage? 21 | PHImageManager.default().requestImage(for: self, targetSize: targetSize, contentMode: .aspectFit, options: options) { (image, _) in 22 | temp = image 23 | } 24 | return temp 25 | } 26 | 27 | func getThumbnailImageSynchorously() -> UIImage? { 28 | // FIXME: Synchronous image requests are incompatible with fast delivery mode, changing delivery mode to high 29 | let options = PHImageRequestOptions() 30 | options.isNetworkAccessAllowed = true 31 | options.resizeMode = .fast 32 | options.deliveryMode = .fastFormat 33 | options.isSynchronous = true 34 | let targetSize = CGSize(width: 50, height: 50) 35 | var temp: UIImage? 36 | PHImageManager.default().requestImage(for: self, targetSize: targetSize, contentMode: .aspectFit, options: options) { (image, _) in 37 | temp = image 38 | } 39 | return temp 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/TimeInterval+VideoFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+VideoFormat.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | /// Get time string from timeInterval, timeInterval > 3600, retrun '> 1 hour'. TimeInterval in (60, 3600), return 'xx:yy'. 12 | /// TimeInterval less than 10, return '00:xx'. 13 | /// 14 | /// - Returns: e.g. 00:00 15 | func videoDurationFormat() -> String { 16 | guard self != 0 else { return "00:00" } 17 | guard self / Double(3600) < 1 else { 18 | return String(format: "> 1 %@", L10n.hour) 19 | } 20 | let minutes = Int(ceil(self)) / 60 21 | let seconds = Int(ceil(self)) % 60 22 | 23 | let fixedSeconds = seconds < 10 ? "0\(seconds)" : "\(seconds)" 24 | if minutes == 0 { 25 | return "00:\(fixedSeconds)" 26 | } else { 27 | return String(format: "%d:%@", minutes, fixedSeconds) 28 | } 29 | } 30 | 31 | static func zeroDurationFormat() -> String { 32 | return 0.videoDurationFormat() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/UICollectionView+IndexPathsInRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+IndexPathsInRect.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/7/30. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | func indexPathsForElements(in rect: CGRect) -> [IndexPath] { 13 | let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)! 14 | return allLayoutAttributes.map { $0.indexPath } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/UIImagePickerController+Tool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImagePickerController+Tool.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/1. 6 | // 7 | 8 | import Foundation 9 | import MobileCoreServices 10 | import Photos 11 | import UIKit 12 | 13 | public extension UIImagePickerController { 14 | @objc func fg_pickerFinished(withInfo info: [UIImagePickerController.InfoKey: Any]) -> UIImage? { 15 | guard let type = info[.mediaType] as? String else { return nil } 16 | guard type == (kUTTypeImage as String) else { return nil } 17 | 18 | guard let _image: UIImage = info[.originalImage] as? UIImage else { return nil } 19 | var image: UIImage! 20 | image = _image 21 | 22 | if #available(iOS 11, *) { 23 | if let asset = info[.phAsset] as? PHAsset { 24 | let options = PHImageRequestOptions() 25 | options.resizeMode = .exact 26 | options.deliveryMode = .highQualityFormat 27 | options.isSynchronous = true 28 | PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { (_image, _) in 29 | image = _image 30 | } 31 | } 32 | } 33 | 34 | if image.imageOrientation != .up { 35 | // 原始图片可以根据照相时的角度来显示,但UIImage无法判定,于是出现获取的图片会向左转90度的现象。 36 | // 以下为调整图片角度的部分 37 | UIGraphicsBeginImageContext(image.size) 38 | image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) 39 | image = UIGraphicsGetImageFromCurrentImageContext() 40 | UIGraphicsEndImageContext() 41 | } 42 | if sourceType == .camera { 43 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(fg_image(_:didFinishSavingWithError:contextInfo:)), nil) 44 | } 45 | 46 | return image 47 | } 48 | 49 | @objc func fg_image(_ image: UIImage, didFinishSavingWithError error: Error, contextInfo: UnsafeMutableRawPointer) { 50 | print("photo saved") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/UIResponder+routerEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIResponder+routerEvent.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/22. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIResponder { 12 | @objc func routerEvent(name: String, userInfo: [AnyHashable: Any]?) { 13 | next?.routerEvent(name: name, userInfo: userInfo) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/UIStackView+Remove.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Remove.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/5/8. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIStackView { 12 | 13 | func removeFully(view: UIView) { 14 | removeArrangedSubview(view) 15 | view.removeFromSuperview() 16 | } 17 | 18 | func removeFullyAllArrangedSubviews() { 19 | arrangedSubviews.forEach { (view) in 20 | removeFully(view: view) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/UIViewController+ShowMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+ShowMessage.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/3/10. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIViewController { 12 | func showMessage(_ message: String, autoDismiss: Bool = true, completion: (() -> Void)? = nil) { 13 | let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) 14 | present(alert, animated: true) 15 | 16 | if autoDismiss { 17 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 18 | self.dismiss(animated: true) 19 | } 20 | } else { 21 | let okAction = UIAlertAction(title: L10n.ok, style: .default) { _ in 22 | self.dismiss(animated: true, completion: completion) 23 | } 24 | let cancelAction = UIAlertAction(title: L10n.cancel, style: .cancel) 25 | alert.addAction(okAction) 26 | alert.addAction(cancelAction) 27 | } 28 | } 29 | 30 | func showError(_ error: Error, autoDismiss: Bool = true, completion: (() -> Void)? = nil) { 31 | let alert = UIAlertController(title: "✕", message: error.localizedDescription, preferredStyle: .alert) 32 | present(alert, animated: true) 33 | 34 | if autoDismiss { 35 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 36 | self.dismiss(animated: true) 37 | } 38 | } else { 39 | let okAction = UIAlertAction(title: L10n.ok, style: .default) { _ in 40 | self.dismiss(animated: true, completion: completion) 41 | } 42 | alert.addAction(okAction) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/URL+FileSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+FileSize.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | /// File url video memory footprint. 12 | /// Remote url will return 0. 13 | /// - Returns: memory size 14 | func sizePerMB() -> Double { 15 | guard isFileURL else { return 0 } 16 | do { 17 | let attribute = try FileManager.default.attributesOfItem(atPath: path) 18 | if let size = attribute[FileAttributeKey.size] as? NSNumber { 19 | return size.doubleValue / (1024 * 1024) 20 | } 21 | } catch { 22 | print("Error: \(error)") 23 | } 24 | return 0.0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/URL+Thumbnail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Thumbnail.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/14. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | import UIKit 11 | 12 | extension URL { 13 | 14 | public func generateThumbnail(maximumSize: CGSize = .zero, completion: @escaping ((Result) -> Void)) { 15 | let cache = URLCache.shared 16 | let urlRequest = URLRequest(url: self) 17 | if let response = cache.cachedResponse(for: urlRequest), let image = UIImage(data: response.data) { 18 | completion(.success(image)) 19 | return 20 | } 21 | 22 | DispatchQueue.global().async { // 1 23 | let url = URL(string: absoluteString) 24 | let asset = AVURLAsset(url: url!) // 2 25 | 26 | let avAssetImageGenerator = AVAssetImageGenerator(asset: asset) // 3 27 | avAssetImageGenerator.maximumSize = maximumSize 28 | avAssetImageGenerator.appliesPreferredTrackTransform = true // 4 29 | let thumnailTime = CMTimeMake(value: 0, timescale: 1) // 5 30 | do { 31 | let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil) // 6 32 | let thumbImage = UIImage(cgImage: cgThumbImage) // 7 33 | // cache 34 | if let response = HTTPURLResponse(url: self, statusCode: 200, httpVersion: nil, headerFields: nil), 35 | let data = thumbImage.pngData() { 36 | let cachedResponse = CachedURLResponse(response: response, data: data) 37 | cache.storeCachedResponse(cachedResponse, for: urlRequest) 38 | } 39 | DispatchQueue.main.async { // 8 40 | completion(.success(thumbImage)) // 9 41 | } 42 | } catch { 43 | print("video thumbnail generated error: \(error.localizedDescription)") // 10 44 | DispatchQueue.main.async { 45 | completion(.failure(error)) // 11 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/Extensions/URL+mediaType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+mediaType.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/16. 6 | // 7 | 8 | import Foundation 9 | import MobileCoreServices 10 | import AVFoundation 11 | 12 | extension URL { 13 | func isImage() -> Bool { 14 | let filePathURL = URL(fileURLWithPath: absoluteString) 15 | let pathExtension = filePathURL.pathExtension 16 | guard !pathExtension.isEmpty else { 17 | return false 18 | } 19 | guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil) else { 20 | return false 21 | } 22 | return UTTypeConformsTo(uti.takeRetainedValue(), kUTTypeImage) 23 | } 24 | 25 | func isVideo() -> Bool { 26 | let filePathURL = URL(fileURLWithPath: absoluteString) 27 | let pathExtension = filePathURL.pathExtension 28 | guard !pathExtension.isEmpty else { 29 | return false 30 | } 31 | guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil) else { 32 | return false 33 | } 34 | return UTTypeConformsTo(uti.takeRetainedValue(), kUTTypeMovie) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/FYPhotoNameSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FYPhotoNameSpace.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/7. 6 | // 7 | 8 | import Foundation 9 | 10 | // Type wrapper 11 | public protocol TypeWrapperProtocol { 12 | associatedtype WrappedType 13 | var wrappedValue: WrappedType { get } 14 | init(value: WrappedType) 15 | } 16 | 17 | public struct TypeWrapper: TypeWrapperProtocol { 18 | public let wrappedValue: T 19 | 20 | public init(value: T) { 21 | self.wrappedValue = value 22 | } 23 | } 24 | 25 | // namespace 26 | public protocol FYNameSpaceProtocol { 27 | associatedtype WrappedType 28 | var fyphoto: WrappedType { get } 29 | /// FYPhoto namespace for present or push animation 30 | static var fyphoto: WrappedType.Type { get } 31 | } 32 | 33 | public extension FYNameSpaceProtocol { 34 | var fyphoto: TypeWrapper { 35 | TypeWrapper(value: self) 36 | } 37 | 38 | static var fyphoto: TypeWrapper.Type { 39 | return TypeWrapper.self 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/PhotoPickerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPickerError.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum PhotoPickerError: Error { 11 | case VideoDurationTooLong 12 | case VideoMemoryOutOfSize 13 | case UnspportedVideoFormat 14 | case DataNotFound 15 | } 16 | 17 | extension PhotoPickerError: LocalizedError { 18 | public var errorDescription: String? { 19 | switch self { 20 | case .VideoDurationTooLong: 21 | return L10n.videoDurationTooLong 22 | case .VideoMemoryOutOfSize: 23 | return L10n.videoMemoryOutOfSize 24 | case .UnspportedVideoFormat: 25 | return L10n.unspportedVideoFormat 26 | case .DataNotFound: 27 | return L10n.dataNotFound 28 | } 29 | } 30 | } 31 | 32 | public enum AVAssetExportSessionError: Error { 33 | case exportSessionCreationFailed 34 | case exportStatuUnknown 35 | } 36 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Helper/PhotosAuthority.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosAuthority.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/14. 6 | // 7 | 8 | import Foundation 9 | import MobileCoreServices 10 | import Photos 11 | import UIKit 12 | 13 | @objc public class PhotosAuthority: NSObject { 14 | 15 | @objc public static func isCameraAvailable() -> Bool { 16 | return UIImagePickerController.isSourceTypeAvailable(.camera) 17 | } 18 | 19 | @objc public static func isPhotoLibraryAvailable() -> Bool { 20 | return UIImagePickerController.isSourceTypeAvailable(.photoLibrary) 21 | } 22 | 23 | @objc public static func isRearCameraAvailable() -> Bool { 24 | return UIImagePickerController.isCameraDeviceAvailable(.rear) 25 | } 26 | 27 | @objc static public func doesCameraSupportTakingPhotos() -> Bool { 28 | let media = kUTTypeImage as String 29 | return PhotosAuthority.cameraSupportsMedia(media, sourceType: .camera) 30 | } 31 | 32 | @objc static public func canUserPickPhotosFromPhotoLibrary() -> Bool { 33 | let media = kUTTypeMovie as String 34 | return PhotosAuthority.cameraSupportsMedia(media, sourceType: .photoLibrary) 35 | } 36 | 37 | @objc public static func cameraSupportsMedia(_ paramMediaType: String, sourceType: UIImagePickerController.SourceType) -> Bool { 38 | guard !paramMediaType.isEmpty else { 39 | return false 40 | } 41 | 42 | guard let sources = UIImagePickerController.availableMediaTypes(for: sourceType) else { return false } 43 | return sources.contains(paramMediaType) 44 | } 45 | 46 | static func requestPhotoAuthority(_ completion: @escaping (_ isSuccess: Bool) -> Void) { 47 | let status = PHPhotoLibrary.authorizationStatus() 48 | switch status { 49 | case .authorized, .limited: 50 | completion(true) 51 | case .denied, .restricted: 52 | completion(false) 53 | case .notDetermined: 54 | PHPhotoLibrary.requestAuthorization { (status) in 55 | DispatchQueue.main.async { 56 | switch status { 57 | case .authorized, .limited: 58 | completion(true) 59 | case .denied, .restricted, .notDetermined: 60 | completion(false) 61 | print("⚠️ without authorization! ⚠️") 62 | @unknown default: 63 | fatalError() 64 | } 65 | } 66 | } 67 | default: 68 | completion(false) 69 | } 70 | } 71 | 72 | @available(iOS 14, *) 73 | static func presentLimitedLibraryPicker(title: String, message: String?, from viewController: UIViewController) { 74 | guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited else { 75 | return 76 | } 77 | let alert = UIAlertController.init(title: title, message: message, preferredStyle: .alert) 78 | alert.addAction(UIAlertAction.init(title: L10n.selectMorePhotos, style: .default, handler: { (_) in 79 | PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: viewController) 80 | })) 81 | alert.addAction(UIAlertAction.init(title: L10n.keepCurrent, style: .default)) 82 | viewController.present(alert, animated: true, completion: nil) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoBrowserViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoDetailCollectionViewControllerDelegate.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/21. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | public protocol PhotoBrowserViewControllerDelegate: AnyObject { 13 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, scrollAt item: Int) 14 | 15 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, selectedAssets identifiers: [String]) 16 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, didCompleteSelected photos: [PhotoProtocol]) 17 | 18 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, deletePhotoAtIndexWhenBrowsing index: Int) 19 | 20 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, longPressedOnPhoto photo: PhotoProtocol, in location: CGPoint) 21 | 22 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, saveMediaCompletedWith error: Error?) 23 | 24 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, editedPhotos: [String: CroppedRestoreData]) 25 | } 26 | 27 | public extension PhotoBrowserViewControllerDelegate { 28 | 29 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, scrollAt item: Int) { } 30 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, selectedAssets identifiers: [String]) { } 31 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, didCompleteSelected photos: [PhotoProtocol]) { } 32 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, deletePhotoAtIndexWhenBrowsing index: Int) { } 33 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, editedPhotos: [String: CroppedRestoreData]) { } 34 | } 35 | 36 | public extension PhotoBrowserViewControllerDelegate { 37 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, longPressedOnPhoto photo: PhotoProtocol, in location: CGPoint) { 38 | alertSavePhoto(photo, on: photoBrowser, in: location) 39 | } 40 | 41 | func photoBrowser(_ photoBrowser: PhotoBrowserViewController, saveMediaCompletedWith error: Error?) { 42 | if let error = error { 43 | photoBrowser.showError(error) 44 | } else { 45 | photoBrowser.showMessage(L10n.successfullySavedMedia) 46 | } 47 | } 48 | 49 | func alertSavePhoto(_ photo: PhotoProtocol, on viewController: PhotoBrowserViewController, in location: CGPoint) { 50 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 51 | let actionTitle = photo.isVideo ? L10n.saveVideo : L10n.savePhoto 52 | let saveAction = UIAlertAction(title: actionTitle, style: .default) { (_) in 53 | self.savePhotoToLibrary(photo, with: viewController) 54 | } 55 | let cancelAction = UIAlertAction(title: L10n.cancel, style: .cancel, handler: nil) 56 | alertController.addAction(saveAction) 57 | alertController.addAction(cancelAction) 58 | 59 | if UIDevice.current.userInterfaceIdiom == UIUserInterfaceIdiom.pad { 60 | if let popoverController = alertController.popoverPresentationController { 61 | popoverController.sourceView = viewController.view 62 | popoverController.sourceRect = CGRect(origin: location, size: CGSize.zero) 63 | } 64 | } 65 | viewController.present(alertController, animated: true, completion: nil) 66 | } 67 | 68 | func savePhotoToLibrary(_ photo: PhotoProtocol, with viewController: PhotoBrowserViewController) { 69 | if photo.isVideo, let url = photo.url { 70 | VideoCache.shared?.fetchFilePathWith(key: url, completion: { [weak self] (result) in 71 | guard let self = self else { return } 72 | switch result { 73 | case .success(let filePath): 74 | SaveMediaTool.saveVideoDataToAlbums(filePath) { (result) in 75 | switch result { 76 | case .failure(let error): 77 | self.photoBrowser(viewController, saveMediaCompletedWith: error) 78 | case .success(_): 79 | self.photoBrowser(viewController, saveMediaCompletedWith: nil) 80 | } 81 | } 82 | case .failure(let error): 83 | self.photoBrowser(viewController, saveMediaCompletedWith: error) 84 | } 85 | }) 86 | } else if let image = photo.image { 87 | SaveMediaTool.saveImageToAlbums(image) { (result) in 88 | switch result { 89 | case .failure(let error): 90 | self.photoBrowser(viewController, saveMediaCompletedWith: error) 91 | case .success(_): 92 | self.photoBrowser(viewController, saveMediaCompletedWith: nil) 93 | } 94 | } 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FYPhoto.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/22. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | public enum PhotoResourceType { 13 | case pureImage 14 | case asset 15 | case url 16 | case data 17 | } 18 | 19 | // MARK: Factory function 20 | public protocol ImagePhotoFactoryFunction { 21 | static func photoWithUIImage(_ image: UIImage) -> PhotoProtocol 22 | static func photoWithCGImage(_ cgImage: CGImage) -> PhotoProtocol 23 | static func photoWithImageNamed(_ named: String) -> PhotoProtocol 24 | static func photoWithContentsOfFile(_ path: String) -> PhotoProtocol 25 | } 26 | 27 | public protocol MetaDataPhotoFactoryFunction { 28 | static func photoWithData(_ data: Data) -> PhotoProtocol 29 | } 30 | 31 | public protocol AssetPhotoFactoryFunction { 32 | static func photoWithPHAsset(_ asset: PHAsset) -> PhotoProtocol 33 | } 34 | 35 | public protocol URLPhotoFactoryFunction { 36 | static func photoWithURL(_ url: URL) -> PhotoProtocol 37 | } 38 | 39 | /// Photo factory 40 | public class Photo: AssetPhotoFactoryFunction, ImagePhotoFactoryFunction, URLPhotoFactoryFunction, MetaDataPhotoFactoryFunction { 41 | 42 | public static func photoWithPHAsset(_ asset: PHAsset) -> PhotoProtocol { 43 | return PhotoAsset(asset: asset) 44 | } 45 | 46 | public static func photoWithUIImage(_ image: UIImage) -> PhotoProtocol { 47 | return PhotoImage(image: image) 48 | } 49 | 50 | public static func photoWithCGImage(_ cgImage: CGImage) -> PhotoProtocol { 51 | return PhotoImage(cgImage: cgImage) 52 | } 53 | 54 | public static func photoWithImageNamed(_ named: String) -> PhotoProtocol { 55 | PhotoImage(imageNamed: named) 56 | } 57 | 58 | public static func photoWithContentsOfFile(_ path: String) -> PhotoProtocol { 59 | PhotoImage(contentsOfFile: path) 60 | } 61 | 62 | public static func photoWithURL(_ url: URL) -> PhotoProtocol { 63 | PhotoURL(url: url) 64 | } 65 | 66 | public static func photoWithData(_ data: Data) -> PhotoProtocol { 67 | PhotoMetaData(data: data) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/PhotoAsset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoAsset.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | class PhotoAsset: PhotoProtocol { 13 | var image: UIImage? 14 | 15 | var metaData: Data? 16 | 17 | var url: URL? 18 | 19 | var asset: PHAsset? 20 | var targetSize: CGSize? 21 | 22 | var isVideo: Bool { 23 | guard let asset = asset else { return false } 24 | return asset.mediaType == .video 25 | } 26 | 27 | var restoreData: CroppedRestoreData? { 28 | didSet { 29 | if restoreData != nil { 30 | image = restoreData?.editedImage 31 | } 32 | } 33 | } 34 | 35 | func storeImage(_ image: UIImage?) { 36 | self.image = image 37 | } 38 | 39 | init(asset: PHAsset) { 40 | self.asset = asset 41 | } 42 | 43 | func isEqualTo(_ photo: PhotoProtocol) -> Bool { 44 | guard let photoAsset = photo.asset else { return false } 45 | return photoAsset.localIdentifier == asset!.localIdentifier 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/PhotoImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoImage.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | class PhotoImage: PhotoProtocol { 13 | var image: UIImage? 14 | 15 | var metaData: Data? 16 | 17 | var url: URL? 18 | 19 | var asset: PHAsset? 20 | var targetSize: CGSize? 21 | 22 | var restoreData: CroppedRestoreData? 23 | 24 | private init() { } 25 | 26 | convenience init(image: UIImage) { 27 | self.init() 28 | self.image = image 29 | } 30 | 31 | convenience init(cgImage: CGImage) { 32 | self.init() 33 | let image = UIImage(cgImage: cgImage) 34 | self.image = image 35 | } 36 | 37 | convenience init(contentsOfFile path: String) { 38 | self.init() 39 | let image = UIImage(contentsOfFile: path) 40 | self.image = image 41 | } 42 | 43 | convenience init(imageNamed named: String) { 44 | self.init() 45 | let image = UIImage(named: named) 46 | self.image = image 47 | } 48 | 49 | func isEqualTo(_ photo: PhotoProtocol) -> Bool { 50 | guard let photoImage = photo.image else { return false } 51 | return photoImage == image! 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/PhotoMetaData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoMetaData.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import SDWebImage 11 | 12 | class PhotoMetaData: PhotoProtocol { 13 | // let resourceType: PhotoResourceType 14 | 15 | var url: URL? 16 | 17 | var asset: PHAsset? 18 | var targetSize: CGSize? 19 | 20 | var image: UIImage? 21 | var metaData: Data? 22 | 23 | var isVideo: Bool { 24 | // FIXME: How do I get this value from data 🤔? 25 | return false 26 | } 27 | 28 | var restoreData: CroppedRestoreData? 29 | 30 | init(data: Data) { 31 | self.metaData = data 32 | } 33 | 34 | func storeImage(_ image: UIImage?) { 35 | self.image = image 36 | } 37 | 38 | func isEqualTo(_ photo: PhotoProtocol) -> Bool { 39 | guard let data = metaData else { return false } 40 | return data == metaData! 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/PhotoProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoProtocol.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | public protocol PhotoProtocol: URLPhotoProtocol, AssetPhotoProtocol, PhotoCaption { 13 | var image: UIImage? { get } 14 | var metaData: Data? { get } 15 | var isVideo: Bool { get } 16 | 17 | /// use this data to restore cropping photo scene 18 | var restoreData: CroppedRestoreData? { get set } 19 | 20 | func storeImage(_ image: UIImage?) 21 | func isEqualTo(_ photo: PhotoProtocol) -> Bool 22 | } 23 | 24 | public extension PhotoProtocol { 25 | var isVideo: Bool { return false } 26 | func storeImage(_ image: UIImage?) { } 27 | } 28 | 29 | public protocol PhotoCaption { 30 | var captionContent: String? { get } 31 | var captionSignature: String? { get } 32 | } 33 | 34 | public extension PhotoCaption { 35 | var captionContent: String? { return nil } 36 | var captionSignature: String? { return nil } 37 | } 38 | 39 | public protocol AssetPhotoProtocol { 40 | var asset: PHAsset? { get set } 41 | var targetSize: CGSize? { get set } 42 | } 43 | 44 | public protocol URLPhotoProtocol { 45 | var url: URL? { get set } 46 | 47 | func generateThumbnail(_ url: URL, size: CGSize, completion: @escaping ((Result) -> Void)) 48 | func clearThumbnail() 49 | func setCaptionContent(_ content: String) 50 | func setCaptionSignature(_ signature: String) 51 | } 52 | 53 | public extension URLPhotoProtocol { 54 | 55 | func generateThumbnail(_ url: URL, size: CGSize, completion: @escaping ((Result) -> Void)) {} 56 | func clearThumbnail() {} 57 | func setCaptionContent(_ content: String) {} 58 | func setCaptionSignature(_ signature: String) {} 59 | } 60 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/PhotoModel/PhotoURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoURL.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | class PhotoURL: PhotoProtocol { 13 | func isEqualTo(_ photo: PhotoProtocol) -> Bool { 14 | guard let photoUrl = photo.url else { 15 | return false 16 | } 17 | return photoUrl == url! 18 | } 19 | 20 | private(set) var image: UIImage? 21 | var metaData: Data? 22 | 23 | var url: URL? 24 | 25 | var asset: PHAsset? 26 | var targetSize: CGSize? 27 | 28 | private(set) var captionContent: String? 29 | private(set) var captionSignature: String? 30 | 31 | private let videoTypes = ["mp4", "m4a", "mov"] 32 | 33 | var restoreData: CroppedRestoreData? 34 | 35 | var isVideo: Bool { 36 | guard let url = url else { return false } 37 | 38 | if url.isImage() { 39 | return false 40 | } 41 | if url.isVideo() { 42 | return true 43 | } 44 | // last chance to set the value. For example: http://client.gsup.sichuanair.com/file.php?70c1dafd4eaccb9a722ac3fcd8459cfc.jpg 45 | if let suffix = url.absoluteString.components(separatedBy: ".").last { 46 | return videoTypes.contains(suffix) 47 | } else { 48 | return false 49 | } 50 | } 51 | 52 | init(url: URL) { 53 | self.url = url 54 | } 55 | 56 | func storeImage(_ image: UIImage?) { 57 | self.image = image 58 | } 59 | 60 | func generateThumbnail(_ url: URL, size: CGSize, completion: @escaping ((Result) -> Void)) { 61 | url.generateThumbnail { (result) in 62 | if let image = try? result.get() { 63 | self.storeImage(image) 64 | } 65 | completion(result) 66 | } 67 | } 68 | 69 | func clearThumbnail() { 70 | image = nil 71 | guard let url = url else { 72 | return 73 | } 74 | let cache = URLCache.shared 75 | let urlRequest = URLRequest(url: url) 76 | cache.removeCachedResponse(for: urlRequest) 77 | } 78 | 79 | func clearAsset() { 80 | self.asset = nil 81 | self.image = nil 82 | } 83 | 84 | func setCaptionContent(_ content: String) { 85 | self.captionContent = content 86 | } 87 | 88 | func setCaptionSignature(_ signature: String) { 89 | self.captionSignature = signature 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/Views/CaptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptionView.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/8/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class CaptionView: UIStackView { 11 | 12 | let contentLabel = UILabel() 13 | let signatureLabel = UILabel() 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | axis = .vertical 18 | spacing = 5 19 | distribution = .fillProportionally 20 | contentLabel.backgroundColor = .clear 21 | contentLabel.textAlignment = .left 22 | contentLabel.lineBreakMode = .byWordWrapping 23 | contentLabel.font = UIFont.systemFont(ofSize: 17) 24 | contentLabel.textColor = .white 25 | contentLabel.numberOfLines = 0 26 | 27 | signatureLabel.backgroundColor = .clear 28 | signatureLabel.textAlignment = .right 29 | signatureLabel.textColor = .white 30 | signatureLabel.font = UIFont.systemFont(ofSize: 14) 31 | 32 | addArrangedSubview(contentLabel) 33 | addArrangedSubview(signatureLabel) 34 | } 35 | 36 | required init(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | func setup(content: String?, signature: String?) { 41 | contentLabel.text = content 42 | signatureLabel.text = signature 43 | } 44 | 45 | override var intrinsicContentSize: CGSize { 46 | return CGSize(width: UIView.noIntrinsicMetric, height: contentLabel.intrinsicContentSize.height + signatureLabel.intrinsicContentSize.height + 5) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/Views/CellWithPhotoProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellWithPhotoProtocol.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/13. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol CellWithPhotoProtocol { 11 | var photo: PhotoProtocol? { get set } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/Views/PBSelectedPhotosThumbnailCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoBrowserThumbnailCollectionViewCell.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/15. 6 | // 7 | 8 | import UIKit 9 | import Photos 10 | 11 | class PBSelectedPhotosThumbnailCell: UICollectionViewCell { 12 | static let reuseIdentifier = "PBSelectedPhotosThumbnailCell" 13 | 14 | let imageView = UIImageView() 15 | var photo: PhotoProtocol? { 16 | willSet { 17 | guard let photo = newValue else { return } 18 | if let image = photo.image { 19 | if image != imageView.image { 20 | imageView.image = image 21 | } 22 | } else if let asset = photo.asset { 23 | PHImageManager.default().requestImage(for: asset, 24 | targetSize: photo.targetSize ?? bounds.size, 25 | contentMode: .aspectFill, 26 | options: nil) { (image, _) in 27 | if let image = image { 28 | self.imageView.image = image 29 | photo.storeImage(image) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | var cellBorderColor: UIColor = UIColor.systemBlue 37 | 38 | var thumbnailIsSelected: Bool = false { 39 | willSet { 40 | if newValue { 41 | contentView.layer.borderColor = cellBorderColor.cgColor 42 | contentView.layer.borderWidth = 2 43 | } else { 44 | contentView.layer.borderColor = UIColor.clear.cgColor 45 | contentView.layer.borderWidth = 0 46 | } 47 | } 48 | } 49 | 50 | override init(frame: CGRect) { 51 | super.init(frame: frame) 52 | contentView.layer.cornerRadius = 4 53 | contentView.layer.masksToBounds = true 54 | contentView.addSubview(imageView) 55 | imageView.frame = contentView.frame 56 | imageView.contentMode = .scaleAspectFill 57 | imageView.layer.masksToBounds = true 58 | } 59 | 60 | required init?(coder: NSCoder) { 61 | fatalError("init(coder:) has not been implemented") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoBrowser/Views/PhotoDetailCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoDetailCell.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/27. 6 | // 7 | 8 | import UIKit 9 | import Photos 10 | 11 | class PhotoDetailCell: UICollectionViewCell, CellWithPhotoProtocol { 12 | static let reuseIdentifier = "PhotoDetailCell" 13 | 14 | let zoomingView = ZoomingScrollView(frame: .zero) 15 | 16 | var image: UIImage? { 17 | get { 18 | zoomingView.imageView.image ?? Asset.coverPlaceholder.image 19 | } 20 | set { 21 | zoomingView.imageView.image = newValue 22 | } 23 | } 24 | 25 | var photo: PhotoProtocol? { 26 | didSet { 27 | zoomingView.photo = photo 28 | } 29 | } 30 | 31 | // Fixed a bug that could not display long images 32 | var maximumZoomScale: CGFloat = 15 { 33 | willSet { 34 | zoomingView.maximumZoomScale = newValue 35 | } 36 | } 37 | 38 | var minimumZoomScale: CGFloat = 1 { 39 | willSet { 40 | zoomingView.minimumZoomScale = newValue 41 | } 42 | } 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | contentView.addSubview(zoomingView) 47 | contentView.backgroundColor = .black 48 | zoomingView.maximumZoomScale = 15 49 | zoomingView.delegate = self 50 | zoomingView.translatesAutoresizingMaskIntoConstraints = false 51 | NSLayoutConstraint.activate([ 52 | zoomingView.topAnchor.constraint(equalTo: contentView.topAnchor), 53 | zoomingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 54 | zoomingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 55 | zoomingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) 56 | ]) 57 | } 58 | 59 | required init?(coder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | override func prepareForReuse() { 64 | super.prepareForReuse() 65 | zoomingView.imageView.image = nil 66 | } 67 | 68 | } 69 | 70 | extension PhotoDetailCell: UIScrollViewDelegate { 71 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 72 | return zoomingView.imageView 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/PhotoPickerViewController+AssetTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPickerViewController+AssetTransition.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/8/27. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | extension PhotoPickerViewController { 13 | 14 | public func transitionWillStart() { 15 | guard let indexPath = lastSelectedIndexPath else { return } 16 | collectionView.cellForItem(at: indexPath)?.isHidden = true 17 | } 18 | 19 | public func transitionDidEnd() { 20 | guard let indexPath = lastSelectedIndexPath else { return } 21 | collectionView.cellForItem(at: indexPath)?.isHidden = false 22 | } 23 | 24 | public func referenceImage() -> UIImage? { 25 | guard let indexPath = lastSelectedIndexPath else { return nil } 26 | guard let cell = collectionView.cellForItem(at: indexPath) as? GridViewCell else { 27 | return nil 28 | } 29 | return cell.imageView.image 30 | } 31 | 32 | public func imageFrame() -> CGRect? { 33 | guard 34 | let lastSelected = lastSelectedIndexPath, 35 | let cell = self.collectionView.cellForItem(at: lastSelected) 36 | else { 37 | return nil 38 | } 39 | return collectionView.convert(cell.frame, to: self.view) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/SelectedModel/SelectedImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedImage.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/30. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | public class SelectedImage { 13 | public init(asset: PHAsset?, image: UIImage) { 14 | self.asset = asset 15 | self.image = image 16 | } 17 | 18 | public let asset: PHAsset? 19 | public let image: UIImage 20 | 21 | public lazy var data: Data? = { 22 | var _data: Data? 23 | if let asset = asset { 24 | let options = PHImageRequestOptions() 25 | options.deliveryMode = .highQualityFormat 26 | options.isNetworkAccessAllowed = true 27 | options.isSynchronous = true 28 | 29 | PHImageManager.default().requestImageData(for: asset, options: options) { (data, _, _, _) in 30 | _data = data 31 | } 32 | return _data 33 | } else { 34 | return image.pngData() 35 | } 36 | }() 37 | } 38 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/SelectedModel/SelectedVideo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedVideo.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/22. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | import UIKit 11 | 12 | @objc(VideoModel) 13 | public class SelectedVideo: NSObject { 14 | 15 | @available(iOS, deprecated: 0.3, message: "PHPickerViewController results can only load video urls from PhotoLibrary, use url instead") 16 | public var asset: PHAsset? 17 | 18 | public var briefImage: UIImage? 19 | 20 | @available(iOS, deprecated: 0.3, message: "High quality image of video is useless, use briefImage instead!") 21 | public var fullImage: UIImage? 22 | 23 | public var url: URL 24 | 25 | @available(iOS, deprecated: 0.3, message: "use init(url: URL) instead") 26 | public init(asset: PHAsset?, fullImage: UIImage?, url: URL) { 27 | self.asset = asset 28 | self.fullImage = fullImage 29 | self.url = url 30 | super.init() 31 | } 32 | 33 | public init(url: URL) { 34 | self.url = url 35 | super.init() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/Views/AlbumCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumCell.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/30. 6 | // 7 | 8 | import UIKit 9 | 10 | class AlbumCell: UITableViewCell { 11 | 12 | fileprivate let coverImage = UIImageView() 13 | fileprivate let nameLabel = UILabel() 14 | 15 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 16 | super.init(style: style, reuseIdentifier: reuseIdentifier) 17 | 18 | coverImage.contentMode = .scaleAspectFill 19 | coverImage.clipsToBounds = true 20 | 21 | contentView.addSubview(coverImage) 22 | contentView.addSubview(nameLabel) 23 | 24 | coverImage.translatesAutoresizingMaskIntoConstraints = false 25 | nameLabel.translatesAutoresizingMaskIntoConstraints = false 26 | 27 | NSLayoutConstraint.activate([ 28 | coverImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), 29 | coverImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 30 | coverImage.widthAnchor.constraint(equalToConstant: 50), 31 | coverImage.heightAnchor.constraint(equalToConstant: 50) 32 | ]) 33 | 34 | NSLayoutConstraint.activate([ 35 | nameLabel.leadingAnchor.constraint(equalTo: coverImage.trailingAnchor, constant: 10), 36 | nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 37 | nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10) 38 | ]) 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | func configure(image: UIImage, title: String) { 46 | coverImage.image = image 47 | nameLabel.text = title 48 | } 49 | 50 | var cover: UIImage? { 51 | willSet { 52 | coverImage.image = newValue 53 | setNeedsDisplay() 54 | } 55 | } 56 | 57 | var name: String? { 58 | willSet { 59 | nameLabel.text = newValue 60 | setNeedsDisplay() 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/Views/GridCameraCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridCameraCell.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/12/8. 6 | // 7 | 8 | import UIKit 9 | 10 | class GridCameraCell: UICollectionViewCell { 11 | static let reuseIdentifier = "GridCameraCell" 12 | let imageView = UIImageView() 13 | 14 | override init(frame: CGRect = .zero) { 15 | super.init(frame: frame) 16 | contentView.backgroundColor = UIColor.color(light: #colorLiteral(red: 0.9294117647, green: 0.937254902, blue: 0.9450980392, alpha: 1), dark: #colorLiteral(red: 0.1843137255, green: 0.1843137255, blue: 0.1843137255, alpha: 1)) 17 | imageView.contentMode = .center 18 | imageView.image = Asset.photoImageCamera.image.withRenderingMode(.alwaysTemplate) 19 | imageView.tintColor = UIColor.color(light: #colorLiteral(red: 0.1843137255, green: 0.1843137255, blue: 0.1843137255, alpha: 1), dark: #colorLiteral(red: 0.9294117647, green: 0.937254902, blue: 0.9450980392, alpha: 1)) 20 | contentView.addSubview(imageView) 21 | imageView.translatesAutoresizingMaskIntoConstraints = false 22 | 23 | NSLayoutConstraint.activate([ 24 | imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 25 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 26 | imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 27 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor) 28 | ]) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/Views/PhotoPickerTopBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPickerTopBar.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/4. 6 | // 7 | 8 | import UIKit 9 | 10 | class PhotoPickerTopBar: UIView { 11 | let cancelButton = UIButton() 12 | let titleView = PickerAlbulmTitleView() 13 | 14 | var dismiss: (() -> Void)? 15 | 16 | var albulmTitleTapped: (() -> Void)? { 17 | didSet { 18 | titleView.tapped = albulmTitleTapped 19 | } 20 | } 21 | 22 | init(colorStyle: FYColorConfiguration.BarColor, safeAreaInsetsTop: CGFloat) { 23 | super.init(frame: .zero) 24 | backgroundColor = colorStyle.backgroundColor 25 | cancelButton.layer.cornerRadius = 4 26 | cancelButton.layer.masksToBounds = true 27 | cancelButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 28 | cancelButton.backgroundColor = colorStyle.itemBackgroundColor 29 | cancelButton.setTitle(L10n.cancel, for: .normal) 30 | cancelButton.setTitleColor(colorStyle.itemTintColor, for: .normal) 31 | cancelButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) 32 | cancelButton.addTarget(self, action: #selector(cancelButtonClicked(_:)), for: .touchUpInside) 33 | 34 | addSubview(cancelButton) 35 | addSubview(titleView) 36 | titleView.titleColor = colorStyle.itemTintColor 37 | titleView.imageColor = colorStyle.itemTintColor 38 | 39 | makeConstraints(safeAreaInsetsTop: safeAreaInsetsTop) 40 | } 41 | 42 | required init?(coder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | func setTitle(_ title: String) { 47 | titleView.title = title 48 | } 49 | 50 | fileprivate func makeConstraints(safeAreaInsetsTop: CGFloat) { 51 | cancelButton.translatesAutoresizingMaskIntoConstraints = false 52 | NSLayoutConstraint.activate([ 53 | cancelButton.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: safeAreaInsetsTop/2), 54 | cancelButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15) 55 | ]) 56 | 57 | titleView.translatesAutoresizingMaskIntoConstraints = false 58 | NSLayoutConstraint.activate([ 59 | titleView.centerYAnchor.constraint(equalTo: cancelButton.centerYAnchor), 60 | titleView.centerXAnchor.constraint(equalTo: self.centerXAnchor), 61 | titleView.widthAnchor.constraint(equalToConstant: 100), 62 | titleView.heightAnchor.constraint(equalToConstant: 40) 63 | ]) 64 | } 65 | 66 | @objc 67 | fileprivate func cancelButtonClicked(_ sender: UIButton) { 68 | dismiss?() 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/Views/PickerAlbulmTitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerNavigationTitleView.swift 3 | // FYPhotoPicker 4 | // 5 | // Created by xiaoyang on 2020/7/30. 6 | // 7 | 8 | import UIKit 9 | 10 | class PickerAlbulmTitleView: UIView { 11 | 12 | var title: String = "" { 13 | willSet { 14 | titleLabel.text = newValue 15 | setNeedsDisplay() 16 | } 17 | } 18 | 19 | var titleColor: UIColor = .black { 20 | willSet { 21 | titleLabel.textColor = newValue 22 | setNeedsDisplay() 23 | } 24 | } 25 | 26 | var titleFont: UIFont = UIFont.boldSystemFont(ofSize: 14) { 27 | willSet { 28 | titleLabel.font = newValue 29 | setNeedsDisplay() 30 | } 31 | } 32 | 33 | var imageColor: UIColor = .systemBlue { 34 | didSet { 35 | imageView.tintColor = imageColor 36 | imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) 37 | } 38 | } 39 | 40 | fileprivate let titleLabel = UILabel() 41 | 42 | let imageView = UIImageView(image: Asset.albumArrow.image) 43 | 44 | var tapped: (() -> Void)? 45 | 46 | override init(frame: CGRect = .zero) { 47 | super.init(frame: frame) 48 | titleLabel.textColor = .black 49 | titleLabel.font = UIFont.systemFont(ofSize: 17) 50 | titleLabel.adjustsFontSizeToFitWidth = true 51 | titleLabel.textAlignment = .center 52 | 53 | imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi) 54 | imageView.isUserInteractionEnabled = true 55 | imageView.contentMode = .scaleAspectFit 56 | addSubview(titleLabel) 57 | addSubview(imageView) 58 | 59 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 60 | NSLayoutConstraint.activate([ 61 | titleLabel.topAnchor.constraint(equalTo: self.topAnchor), 62 | titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor), 63 | titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), 64 | titleLabel.trailingAnchor.constraint(equalTo: self.imageView.leadingAnchor, constant: -2) 65 | ]) 66 | 67 | imageView.translatesAutoresizingMaskIntoConstraints = false 68 | NSLayoutConstraint.activate([ 69 | imageView.centerYAnchor.constraint(equalTo: self.centerYAnchor), 70 | imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 71 | imageView.widthAnchor.constraint(equalToConstant: 15), 72 | imageView.heightAnchor.constraint(equalToConstant: 15) 73 | ]) 74 | 75 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PickerAlbulmTitleView.tap(_:))) 76 | addGestureRecognizer(tapGesture) 77 | } 78 | 79 | required init?(coder: NSCoder) { 80 | fatalError("init(coder:) has not been implemented") 81 | } 82 | 83 | @objc fileprivate func tap(_ gesture: UITapGestureRecognizer) { 84 | tapped?() 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/PhotoPicker/Views/SelectionButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionButton.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class SelectionButton: UIButton { 11 | 12 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 13 | if CGRect(x: -20, y: -10, width: self.frame.width + 32, height: self.frame.height + 32).contains(point) { 14 | if self.isHidden { // hidden button can still handle touch event 15 | return super.hitTest(point, with: event) 16 | } else { 17 | return self 18 | } 19 | } 20 | return super.hitTest(point, with: event) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/AssetTransitioningMath.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | Convenience math operators 7 | */ 8 | 9 | import QuartzCore 10 | 11 | // func clip(_ x0: T, _ x1: T, _ v: T) -> T { 12 | // return max(x0, min(x1, v)) 13 | // } 14 | // 15 | // func lerp(_ v0: T, _ v1: T, _ t: T) -> T { 16 | // return v0 + (v1 - v0) * t 17 | // } 18 | // 19 | // 20 | // func -(lhs: CGPoint, rhs: CGPoint) -> CGVector { 21 | // return CGVector(dx: lhs.x - rhs.x, dy: lhs.y - rhs.y) 22 | // } 23 | // 24 | // func -(lhs: CGPoint, rhs: CGVector) -> CGPoint { 25 | // return CGPoint(x: lhs.x - rhs.dx, y: lhs.y - rhs.dy) 26 | // } 27 | // 28 | // func -(lhs: CGVector, rhs: CGVector) -> CGVector { 29 | // return CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy) 30 | // } 31 | // 32 | // func +(lhs: CGPoint, rhs: CGPoint) -> CGVector { 33 | // return CGVector(dx: lhs.x + rhs.x, dy: lhs.y + rhs.y) 34 | // } 35 | // 36 | // func +(lhs: CGPoint, rhs: CGVector) -> CGPoint { 37 | // return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy) 38 | // } 39 | // 40 | // func +(lhs: CGVector, rhs: CGVector) -> CGVector { 41 | // return CGVector(dx: lhs.dx + rhs.dx, dy: lhs.dy + rhs.dy) 42 | // } 43 | // 44 | // func *(left: CGVector, right:CGFloat) -> CGVector { 45 | // return CGVector(dx: left.dx * right, dy: left.dy * right) 46 | // } 47 | // 48 | extension CGPoint { 49 | var vector: CGVector { 50 | return CGVector(dx: x, dy: y) 51 | } 52 | } 53 | 54 | extension CGVector { 55 | var magnitude: CGFloat { 56 | return sqrt(dx*dx + dy*dy) 57 | } 58 | 59 | var point: CGPoint { 60 | return CGPoint(x: dx, y: dy) 61 | } 62 | 63 | func apply(transform t: CGAffineTransform) -> CGVector { 64 | return point.applying(t).vector 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/PhotoAnimators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoAnimators.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/7. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | // MARK: - Show / Hide Transitioning 12 | class PhotoHideShowAnimator: NSObject, UIViewControllerAnimatedTransitioning { 13 | var transitionDriver: TransitionDriver? 14 | 15 | let isPresenting: Bool 16 | let isNavigationAnimation: Bool 17 | let transitionEssential: TransitionEssentialClosure? 18 | let completion: (() -> Void)? 19 | 20 | init(isPresenting: Bool, isNavigationAnimation: Bool, transitionEssential: TransitionEssentialClosure?, completion: (() -> Void)?) { 21 | self.isPresenting = isPresenting 22 | self.isNavigationAnimation = isNavigationAnimation 23 | self.transitionEssential = transitionEssential 24 | self.completion = completion 25 | super.init() 26 | } 27 | 28 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 29 | if isPresenting { 30 | return 0.48 31 | } else { 32 | return 0.38 33 | } 34 | } 35 | 36 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 37 | transitionDriver = PhotoTransitionDriver(isPresenting: isPresenting, 38 | isNavigationAnimation: isNavigationAnimation, 39 | context: transitionContext, 40 | duration: transitionDuration(using: transitionContext), 41 | transitionEssential: transitionEssential) 42 | } 43 | 44 | func animationEnded(_ transitionCompleted: Bool) { 45 | transitionDriver = nil 46 | completion?() 47 | } 48 | } 49 | 50 | // MARK: - InteractiveTransitioning 51 | class PhotoInteractiveAnimator: NSObject, UIViewControllerInteractiveTransitioning { 52 | var transitionDriver: TransitionDriver? 53 | let panGestureRecognizer: UIPanGestureRecognizer 54 | let isNavigationDismiss: Bool 55 | let transitionEssential: TransitionEssentialClosure? 56 | let completion: ((_ isCancelled: Bool, _ isNavigation: Bool) -> Void)? 57 | 58 | init(panGestureRecognizer: UIPanGestureRecognizer, isNavigationDismiss: Bool, transitionEssential: TransitionEssentialClosure?, completion: ((_ isCancelled: Bool, _ isNavigation: Bool) -> Void)?) { 59 | self.panGestureRecognizer = panGestureRecognizer 60 | self.isNavigationDismiss = isNavigationDismiss 61 | self.transitionEssential = transitionEssential 62 | self.completion = completion 63 | super.init() 64 | } 65 | 66 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 67 | // Create our helper object to manage the transition for the given transitionContext. 68 | if transitionContext.isInteractive { 69 | transitionDriver = PhotoInteractiveDismissTransitionDriver(context: transitionContext, 70 | panGestureRecognizer: panGestureRecognizer, 71 | isNavigationDismiss: isNavigationDismiss, 72 | transitionEssential: transitionEssential, 73 | completion: completion) 74 | } 75 | } 76 | } 77 | 78 | extension PhotoInteractiveAnimator: UIViewControllerAnimatedTransitioning { 79 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 80 | fatalError("never called") 81 | } 82 | 83 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 84 | 0.38 85 | } 86 | 87 | func animationEnded(_ transitionCompleted: Bool) { 88 | transitionDriver = nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/PhotoBrowserCurrentPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoBrowserCurrentPage.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/5. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol PhotoBrowserCurrentPage { 11 | var currentPage: Int { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/PhotoPresentTransitionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPresentTransitionController.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/1/7. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class PhotoPresentTransitionController: NSObject, UIViewControllerTransitioningDelegate { 12 | let panGesture = UIPanGestureRecognizer() 13 | let transitionEssential: TransitionEssentialClosure? 14 | 15 | var isInteractive: Bool = false 16 | 17 | weak var viewController: UIViewController? 18 | init(viewController: UIViewController?, transitionEssential: TransitionEssentialClosure?) { 19 | self.viewController = viewController 20 | self.transitionEssential = transitionEssential 21 | super.init() 22 | configurePanGestureRecognizer() 23 | } 24 | 25 | deinit { 26 | // UIViewController.TransitionHolder.clearViewControllerTransition() 27 | // self.viewController?.view.removeGestureRecognizer(panGesture) 28 | } 29 | 30 | var interactiveAnimator: PhotoInteractiveAnimator? 31 | var normalAnimator: UIViewControllerAnimatedTransitioning? 32 | 33 | func configurePanGestureRecognizer() { 34 | panGesture.delegate = self 35 | panGesture.maximumNumberOfTouches = 1 36 | panGesture.addTarget(self, action: #selector(initiateTransitionInteractively(_:))) 37 | viewController?.view.addGestureRecognizer(panGesture) 38 | } 39 | 40 | @objc func initiateTransitionInteractively(_ panGesture: UIPanGestureRecognizer) { 41 | if panGesture.state == .began && interactiveAnimator?.transitionDriver == nil { 42 | isInteractive = true 43 | viewController?.dismiss(animated: true) {} 44 | } 45 | } 46 | 47 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 48 | let animator = PhotoHideShowAnimator(isPresenting: true, isNavigationAnimation: false, transitionEssential: transitionEssential, completion: nil) 49 | normalAnimator = animator 50 | return animator 51 | } 52 | 53 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 54 | return interactiveAnimator 55 | } 56 | 57 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 58 | let animator: UIViewControllerAnimatedTransitioning 59 | if isInteractive { 60 | let interactiveAnimator = PhotoInteractiveAnimator(panGestureRecognizer: panGesture, isNavigationDismiss: false, transitionEssential: transitionEssential, completion: { [weak self] (isCancelled, _) in 61 | self?.interactiveAnimator = nil 62 | self?.isInteractive = false 63 | if !isCancelled { 64 | self?.completePresentationTransition() 65 | } 66 | }) 67 | self.interactiveAnimator = interactiveAnimator 68 | animator = interactiveAnimator 69 | } else { 70 | animator = PhotoHideShowAnimator(isPresenting: false, isNavigationAnimation: false, transitionEssential: transitionEssential, completion: { [weak self] in 71 | self?.completePresentationTransition() 72 | }) 73 | normalAnimator = animator 74 | } 75 | 76 | return animator 77 | } 78 | 79 | func completePresentationTransition() { 80 | UIViewController.TransitionHolder.clearViewControllerTransition() 81 | viewController?.view.removeGestureRecognizer(panGesture) 82 | } 83 | } 84 | 85 | extension PhotoPresentTransitionController: UIGestureRecognizerDelegate { 86 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 87 | return false 88 | } 89 | 90 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 91 | guard let interactiveAnimator = self.interactiveAnimator else { 92 | let translation = panGesture.translation(in: panGesture.view) 93 | let translationIsVertical = (translation.y > 0) && (abs(translation.y) > abs(translation.x)) 94 | // print(#function, translationIsVertical && (navigationController?.viewControllers.count ?? 0 > 1)) 95 | return translationIsVertical 96 | } 97 | return interactiveAnimator.transitionDriver?.isInteractive ?? true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/PhotoTransitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssetTransitioning.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/8/27. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Photos 11 | 12 | public protocol PhotoTransitioning { 13 | /// Called just-before the transition animation begins. 14 | /// Use this to prepare for the transition. 15 | func transitionWillStart() 16 | 17 | /// Called right-after the transition animation ends. 18 | /// Use this to clean up after the transition. 19 | func transitionDidEnd() 20 | 21 | /// The animator needs a UIImageView for the transition; 22 | /// eg the Photo Detail screen should provide a snapshotView of its image, 23 | /// and a collectionView should do the same for its image views. 24 | func referenceImage() -> UIImage? 25 | 26 | /// The location onscreen for the imageView provided in `referenceImageView(for:)`. 27 | /// If image frame is right, but image shows in wrong origin, consider set edgesForExtendedLayout to .all. 28 | func imageFrame() -> CGRect? 29 | 30 | /// if true, self is pushed by navigation controller using Photo transition. 31 | func enablePhotoTransitionPush() -> Bool 32 | } 33 | 34 | extension PhotoTransitioning { 35 | public func transitionWillStart() {} 36 | public func transitionDidEnd() {} 37 | public func enablePhotoTransitionPush() -> Bool { 38 | true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/TransitionDriver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionDriver.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/1. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | typealias TransitionEssentialClosure = ((Int) -> TransitionEssential?) 12 | 13 | enum TransitionType { 14 | case photoTransitionProtocol(from: PhotoTransitioning, to: PhotoTransitioning) 15 | case transitionBlock(essential: TransitionEssential) 16 | case noTransitionAnimation // lack of transition info 17 | } 18 | 19 | protocol TransitionDriver { 20 | var transitionAnimator: UIViewPropertyAnimator! { get set } 21 | var isInteractive: Bool { get } 22 | } 23 | 24 | extension TransitionDriver { 25 | internal static func calculateZoomInImageFrame(image: UIImage, forView view: UIView) -> CGRect { 26 | CGRect.makeRect(aspectRatio: image.size, insideRect: view.bounds) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Transition/TransitionEssential.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionEssential.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/5. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /// Transition animation needs these infos to find out which image to show and where is it. 12 | public struct TransitionEssential { 13 | let transitionImage: UIImage? 14 | /// frame coverted to viewController view 15 | let convertedFrame: CGRect 16 | 17 | /// Initial essentials 18 | /// - Parameters: 19 | /// - transitionImage: Transition uses the image for animation 20 | /// - convertedFrame: Location of the image in the ViewController. e.g., imageView.convert(imageView.bounds, to: viewControllerView) 21 | public init(transitionImage: UIImage?, convertedFrame: CGRect) { 22 | self.transitionImage = transitionImage 23 | self.convertedFrame = convertedFrame 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Video/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2020/9/21. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | import UIKit 11 | 12 | class PlayerView: UIView { 13 | var player: AVPlayer? { 14 | get { 15 | return playerLayer.player 16 | } 17 | set { 18 | playerLayer.player = newValue 19 | // .resizeAspectFill -> fullScreen 20 | // playerLayer.videoGravity = .resizeAspectFill 21 | } 22 | } 23 | 24 | var playerLayer: AVPlayerLayer { 25 | return layer as! AVPlayerLayer 26 | } 27 | 28 | // Override UIView property 29 | override static var layerClass: AnyClass { 30 | return AVPlayerLayer.self 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Video/VideoTrimmer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoTrimmer.swift 3 | // 4 | // 5 | // Created by xiaoyang on 2021/11/9. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | class VideoTrimmer { 12 | static let shared = VideoTrimmer() 13 | 14 | let tempDirectory: URL 15 | 16 | private init() { 17 | tempDirectory = FileManager.tempDirectory(with: FileManager.trimmedVideoDirName) 18 | } 19 | 20 | func trimVideo(_ asset: AVAsset, from startTime: Double, to endTime: Double, completion: @escaping((Result) -> Void)) { 21 | var tempFile = tempDirectory 22 | let videoName = UUID().uuidString + ".mp4" 23 | tempFile.appendPathComponent("\(videoName)") 24 | 25 | guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { 26 | completion(.failure(AVAssetExportSessionError.exportSessionCreationFailed)) 27 | return 28 | } 29 | 30 | let start = CMTime(seconds: startTime, preferredTimescale: 600) 31 | let end = CMTime(seconds: endTime, preferredTimescale: 600) 32 | exporter.timeRange = CMTimeRange(start: start, end: end) 33 | exporter.outputURL = tempFile 34 | exporter.outputFileType = .mp4 35 | exporter.exportAsynchronously { 36 | DispatchQueue.main.async { 37 | switch exporter.status { 38 | case .waiting: 39 | #if DEBUG 40 | print("waiting to be exported") 41 | #endif 42 | case .exporting: 43 | #if DEBUG 44 | print("exporting video") 45 | #endif 46 | case .cancelled, .failed: 47 | completion(.failure(exporter.error!)) 48 | case .completed: 49 | #if DEBUG 50 | print("finish exporting, video size: \(tempFile.sizePerMB()) MB") 51 | #endif 52 | completion(.success(tempFile)) 53 | case .unknown: 54 | completion(.failure(AVAssetExportSessionError.exportStatuUnknown)) 55 | @unknown default: 56 | completion(.failure(AVAssetExportSessionError.exportStatuUnknown)) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func clear() { 63 | try? FileManager.default.removeItem(at: tempDirectory) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/Video/VideoValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoValidator.swift 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/2/24. 6 | // 7 | 8 | import Foundation 9 | import Photos 10 | 11 | protocol VideoValidatorProtocol { 12 | func validVideoDuration(_ asset: PHAsset, limit: Double) -> Bool 13 | func validVideoSize(_ url: URL, limit: Double) -> Bool 14 | } 15 | 16 | class FYVideoValidator: VideoValidatorProtocol { 17 | /// Validate the video asset's duration is within the time limit. 18 | /// - Parameters: 19 | /// - asset: selected asset 20 | /// - limit: time limit 21 | /// - Returns: Bool value 22 | func validVideoDuration(_ asset: PHAsset, limit: Double) -> Bool { 23 | if limit <= 0 { 24 | return true 25 | } else { 26 | return asset.duration <= limit 27 | } 28 | } 29 | 30 | /// Validate the asset's memory footprint is within the limit. 31 | /// - Parameters: 32 | /// - asset: selected video asset url 33 | /// - limit: memory footprint limit 34 | /// - Returns: Bool value 35 | func validVideoSize(_ url: URL, limit: Double) -> Bool { 36 | guard url.isFileURL else { 37 | return false 38 | } 39 | return url.sizePerMB() <= limit 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/FYPhoto/Classes/XCAssets+Generated.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | #if os(macOS) 5 | import AppKit 6 | #elseif os(iOS) 7 | import UIKit 8 | #elseif os(tvOS) || os(watchOS) 9 | import UIKit 10 | #endif 11 | 12 | // Deprecated typealiases 13 | @available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") 14 | internal typealias AssetImageTypeAlias = ImageAsset.Image 15 | 16 | // swiftlint:disable superfluous_disable_command file_length implicit_return 17 | 18 | // MARK: - Asset Catalogs 19 | 20 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 21 | internal enum Asset { 22 | internal static let browserErrorLoading = ImageAsset(name: "Browser-ErrorLoading") 23 | internal enum Crop { 24 | internal static let aspectratio = ImageAsset(name: "aspectratio") 25 | internal static let icons8EditImage = ImageAsset(name: "icons8-edit-image") 26 | internal static let rotate = ImageAsset(name: "rotate") 27 | } 28 | internal static let flipCamera = ImageAsset(name: "FlipCamera") 29 | internal static let imageError = ImageAsset(name: "ImageError") 30 | internal static let imageSelectedOff = ImageAsset(name: "ImageSelectedOff") 31 | internal static let imageSelectedOn = ImageAsset(name: "ImageSelectedOn") 32 | internal static let imageSelectedSmallOff = ImageAsset(name: "ImageSelectedSmallOff") 33 | internal static let imageSelectedSmallOn = ImageAsset(name: "ImageSelectedSmallOn") 34 | internal static let playButtonOverlayLarge = ImageAsset(name: "PlayButtonOverlayLarge") 35 | internal static let playButtonOverlayLargeTap = ImageAsset(name: "PlayButtonOverlayLargeTap") 36 | internal static let uiBarButtonItemArrowLeft = ImageAsset(name: "UIBarButtonItemArrowLeft") 37 | internal static let uiBarButtonItemArrowRight = ImageAsset(name: "UIBarButtonItemArrowRight") 38 | internal static let albumArrow = ImageAsset(name: "albumArrow") 39 | internal static let back = ImageAsset(name: "back") 40 | internal static let coverPlaceholder = ImageAsset(name: "cover_placeholder") 41 | internal static let icons8FlashOff = ImageAsset(name: "icons8-flash-off") 42 | internal static let icons8FlashOn = ImageAsset(name: "icons8-flash-on") 43 | internal static let icons8Pause = ImageAsset(name: "icons8-pause") 44 | internal static let icons8Play = ImageAsset(name: "icons8-play") 45 | internal static let photoImageCamera = ImageAsset(name: "photo_image_camera") 46 | internal static let photoVideoCamera = ImageAsset(name: "photo_video_camera") 47 | internal static let playButton = ImageAsset(name: "play_button") 48 | } 49 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 50 | 51 | // MARK: - Implementation Details 52 | 53 | internal struct ImageAsset { 54 | internal fileprivate(set) var name: String 55 | 56 | #if os(macOS) 57 | internal typealias Image = NSImage 58 | #elseif os(iOS) || os(tvOS) || os(watchOS) 59 | internal typealias Image = UIImage 60 | #endif 61 | 62 | internal var image: Image { 63 | let bundle = BundleToken.bundle 64 | #if os(iOS) || os(tvOS) 65 | let image = Image(named: name, in: bundle, compatibleWith: nil) 66 | #elseif os(macOS) 67 | let name = NSImage.Name(self.name) 68 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) 69 | #elseif os(watchOS) 70 | let image = Image(named: name) 71 | #endif 72 | guard let result = image else { 73 | fatalError("Unable to load image asset named \(name).") 74 | } 75 | return result 76 | } 77 | } 78 | 79 | internal extension ImageAsset.Image { 80 | @available(macOS, deprecated, 81 | message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") 82 | convenience init?(asset: ImageAsset) { 83 | #if os(iOS) || os(tvOS) 84 | let bundle = BundleToken.bundle 85 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 86 | #elseif os(macOS) 87 | self.init(named: NSImage.Name(asset.name)) 88 | #elseif os(watchOS) 89 | self.init(named: asset.name) 90 | #endif 91 | } 92 | } 93 | 94 | // swiftlint:disable convenience_type 95 | private final class BundleToken { 96 | static let bundle: Bundle = { 97 | #if SWIFT_PACKAGE 98 | return Bundle.module 99 | #else 100 | guard let url = Bundle(for: BundleToken.self).url(forResource: "FYPhoto", withExtension: "bundle") else { 101 | return .main 102 | } 103 | return Bundle(url: url) ?? .main 104 | #endif 105 | }() 106 | } 107 | // swiftlint:enable convenience_type 108 | 109 | -------------------------------------------------------------------------------- /Sources/FYPhoto/FYPhoto.h: -------------------------------------------------------------------------------- 1 | // 2 | // FYPhoto.h 3 | // FYPhoto 4 | // 5 | // Created by xiaoyang on 2021/9/24. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for FYPhoto. 11 | FOUNDATION_EXPORT double FYPhotoVersionNumber; 12 | 13 | //! Project version string for FYPhoto. 14 | FOUNDATION_EXPORT const unsigned char FYPhotoVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /Tests/FYPhotoTests/TestAuthority.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAuthority.swift 3 | // FYPhoto_Tests 4 | // 5 | // Created by xiaoyang on 2020/9/14. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import FYPhoto 11 | 12 | class TestAuthority: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testDeviceSupportsTakePhoto() throws { 23 | // PhotosAuthority.doesCameraSupportTakingPhotos() 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | self.measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Tests/FYPhotoTests/TestHelperExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelperExtensions.swift 3 | // FYPhoto_Tests 4 | // 5 | // Created by xiaoyang on 2021/3/8. 6 | // Copyright © 2021 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import FYPhoto 11 | 12 | class TestHelperExtensions: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testVideoSize() throws { 23 | // guard let videoSample = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4") else { 24 | // return 25 | // } 26 | // let size = videoSample.sizePerMB() 27 | // XCTAssertEqual(size, Double(12.9), accuracy: 1.0) 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | func testPerformanceExample() throws { 33 | // This is an example of a performance test case. 34 | self.measure { 35 | // Put the code you want to measure the time of here. 36 | } 37 | } 38 | 39 | } 40 | --------------------------------------------------------------------------------