├── .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 |
--------------------------------------------------------------------------------