├── .swift-version ├── Screenshots ├── Banner.png └── Icon.png ├── .slather.yml ├── Resources └── Gallery.bundle │ ├── gallery_close@2x.png │ ├── gallery_close@3x.png │ ├── gallery_camera_focus@3x.png │ ├── gallery_placeholder@2x.png │ ├── gallery_title_arrow@2x.png │ ├── gallery_title_arrow@3x.png │ ├── gallery_camera_rotate@3x.png │ ├── gallery_page_indicator@2x.png │ ├── gallery_page_indicator@3x.png │ ├── gallery_camera_flash_auto@3x.png │ ├── gallery_camera_flash_off@3x.png │ ├── gallery_camera_flash_on@3x.png │ ├── gallery_empty_view_image@2x.png │ ├── gallery_empty_view_image@3x.png │ ├── gallery_video_cell_camera@2x.png │ ├── gallery_video_view_camera@2x.png │ └── gallery_permission_view_camera@2x.png ├── Playground-iOS.playground ├── Contents.swift ├── timeline.xctimeline └── contents.xcplayground ├── Gallery.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── Gallery-Mac.xcscheme │ └── Gallery-iOS.xcscheme ├── Sources ├── Utils │ ├── Once.swift │ ├── Extensions │ │ ├── String+Extensions.swift │ │ ├── Array+Extensions.swift │ │ ├── UIScrollView+Extensions.swift │ │ ├── UIViewController+Extensions.swift │ │ ├── UIView+Extensions.swift │ │ ├── UIImageView+Extensions.swift │ │ └── AVAsset+Extensions.swift │ ├── Bundle.swift │ ├── EventHub.swift │ ├── Fetcher.swift │ ├── VideoEditor │ │ ├── VideoEditor.swift │ │ ├── VideoEditing.swift │ │ ├── EditInfo.swift │ │ └── AdvancedVideoEditor.swift │ ├── View │ │ ├── EmptyView.swift │ │ ├── FrameView.swift │ │ ├── AlbumCell.swift │ │ ├── ArrowButton.swift │ │ └── GridView.swift │ ├── LocationManager.swift │ ├── Permission │ │ ├── Permission.swift │ │ ├── PermissionController.swift │ │ └── PermissionView.swift │ ├── Utils.swift │ ├── Constraints.swift │ ├── Dropdown │ │ └── DropdownController.swift │ ├── Pages │ │ ├── PageIndicator.swift │ │ └── PagesController.swift │ ├── ClosuredAVCaptureMovieFileOutput.swift │ └── Config.swift ├── Images │ ├── Image.swift │ ├── Album.swift │ ├── ImagesLibrary.swift │ ├── ImageCell.swift │ ├── Cart.swift │ └── ImagesController.swift ├── Videos │ ├── VideosLibrary.swift │ ├── VideoBox.swift │ ├── VideoCell.swift │ ├── Video.swift │ └── VideosController.swift ├── Camera │ ├── TripleButton.swift │ ├── ShutterButton.swift │ ├── StackView.swift │ ├── CameraController.swift │ ├── CameraMan.swift │ └── CameraView.swift └── Gallery │ └── GalleryController.swift ├── Example └── GalleryDemo │ ├── GalleryDemo.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── GalleryDemo.xcscheme │ └── project.pbxproj │ ├── GalleryDemo.xcworkspace │ └── contents.xcworkspacedata │ ├── Podfile │ ├── GalleryDemo │ ├── Resources │ │ └── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Sources │ │ ├── AppDelegate.swift │ │ └── ViewController.swift │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Podfile.lock ├── .travis.yml ├── CONTRIBUTING.md ├── .gitignore ├── GalleryTests ├── Tests.swift └── Info-iOS.plist ├── AbraGallery.podspec ├── Gallery ├── Info-iOS.plist └── Info-Mac.plist ├── LICENSE.md └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /Screenshots/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Screenshots/Banner.png -------------------------------------------------------------------------------- /Screenshots/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Screenshots/Icon.png -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | ci_service: travis_ci 2 | coverage_service: coveralls 3 | xcodeproj: Gallery.xcodeproj 4 | source_directory: Sources 5 | -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_close@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_close@3x.png -------------------------------------------------------------------------------- /Playground-iOS.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | // Gallery iOS Playground 2 | 3 | import UIKit 4 | import Gallery 5 | 6 | var str = "Hello, playground" 7 | -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_camera_focus@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_camera_focus@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_placeholder@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_placeholder@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_title_arrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_title_arrow@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_title_arrow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_title_arrow@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_camera_rotate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_camera_rotate@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_page_indicator@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_page_indicator@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_page_indicator@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_page_indicator@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_camera_flash_auto@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_camera_flash_auto@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_camera_flash_off@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_camera_flash_off@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_camera_flash_on@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_camera_flash_on@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_empty_view_image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_empty_view_image@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_empty_view_image@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_empty_view_image@3x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_video_cell_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_video_cell_camera@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_video_view_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_video_view_camera@2x.png -------------------------------------------------------------------------------- /Resources/Gallery.bundle/gallery_permission_view_camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSooq/Abra/HEAD/Resources/Gallery.bundle/gallery_permission_view_camera@2x.png -------------------------------------------------------------------------------- /Playground-iOS.playground/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Playground-iOS.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Gallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Utils/Once.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Once { 4 | 5 | var already: Bool = false 6 | 7 | func run(_ block: () -> Void) { 8 | guard !already else { return } 9 | 10 | block() 11 | already = true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | func g_localize(fallback: String) -> String { 6 | let string = NSLocalizedString(self, comment: "") 7 | return string == self ? fallback : string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Utils/Bundle.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Bundle { 4 | 5 | static func image(_ named: String) -> UIImage? { 6 | let bundle = Foundation.Bundle(for: Bundle.self) 7 | return UIImage(named: "Gallery.bundle/\(named)", in: bundle, compatibleWith: nil) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array { 4 | 5 | mutating func g_moveToFirst(_ index: Int) { 6 | guard index != 0 && index < count else { return } 7 | 8 | let item = self[index] 9 | remove(at: index) 10 | insert(item, at: 0) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/Utils/EventHub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class EventHub { 4 | 5 | typealias Action = () -> Void 6 | 7 | static let shared = EventHub() 8 | 9 | // MARK: Initialization 10 | 11 | init() {} 12 | 13 | var close: Action? 14 | var doneWithImages: Action? 15 | var doneWithVideos: Action? 16 | var stackViewTouched: Action? 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Images/Image.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | public class Image: Equatable { 5 | 6 | let asset: PHAsset 7 | 8 | // MARK: - Initialization 9 | 10 | init(asset: PHAsset) { 11 | self.asset = asset 12 | } 13 | } 14 | 15 | // MARK: - Equatable 16 | 17 | public func ==(lhs: Image, rhs: Image) -> Bool { 18 | return lhs.asset == rhs.asset 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Example/GalleryDemo/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '8.0' 4 | 5 | target 'GalleryDemo' do 6 | pod 'Gallery', path: '../../' 7 | pod 'Lightbox', git: 'https://github.com/hyperoslo/Lightbox.git', branch: 'swift-3' 8 | pod 'Sugar', git: 'https://github.com/hyperoslo/Sugar.git', branch: 'master' 9 | pod 'Hue', git: 'https://github.com/hyperoslo/Hue.git', branch: 'master' 10 | end 11 | 12 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/UIScrollView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIScrollView { 4 | 5 | func g_scrollToTop() { 6 | setContentOffset(CGPoint.zero, animated: false) 7 | } 8 | 9 | func g_updateBottomInset(_ value: CGFloat) { 10 | var inset = contentInset 11 | inset.bottom = value 12 | 13 | contentInset = inset 14 | scrollIndicatorInsets = inset 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8 2 | language: objective-c 3 | 4 | before_install: 5 | - brew update 6 | - if brew outdated | grep -qx carthage; then brew upgrade carthage; fi 7 | - travis_wait 35 carthage bootstrap --platform iOS,Mac 8 | 9 | script: 10 | - xcodebuild clean build -project Gallery.xcodeproj -scheme Gallery-iOS -sdk iphonesimulator 11 | - xcodebuild test -project Gallery.xcodeproj -scheme Gallery-iOS -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=10.0' 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | GitHub Issues is for reporting bugs, discussing features and general feedback in **Gallery**. Be sure to check our [documentation](http://cocoadocs.org/docsets/Gallery), [FAQ](https://github.com/hyperoslo/Gallery/wiki/FAQ) and [past issues](https://github.com/hyperoslo/Gallery/issues?state=closed) before opening any new issues. 2 | 3 | If you are posting about a crash in your application, a stack trace is helpful, but additional context, in the form of code and explanation, is necessary to be of any use. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | ._* 7 | .Spotlight-V100 8 | .Trashes 9 | 10 | # Xcode 11 | # 12 | build/ 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | *.xccheckout 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | *.xcuserstate 28 | 29 | # CocoaPods 30 | Pods 31 | 32 | # Carthage 33 | Carthage 34 | 35 | # SPM 36 | .build/ 37 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | 5 | func g_addChildController(_ controller: UIViewController) { 6 | addChildViewController(controller) 7 | view.addSubview(controller.view) 8 | controller.didMove(toParentViewController: self) 9 | 10 | controller.view.g_pinEdges() 11 | } 12 | 13 | func g_removeFromParentController() { 14 | willMove(toParentViewController: nil) 15 | view.removeFromSuperview() 16 | removeFromParentViewController() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Images/Album.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class Album { 5 | 6 | let collection: PHAssetCollection 7 | var items: [Image] = [] 8 | 9 | // MARK: - Initialization 10 | 11 | init(collection: PHAssetCollection) { 12 | self.collection = collection 13 | } 14 | 15 | func reload() { 16 | items = [] 17 | 18 | let itemsFetchResult = PHAsset.fetchAssets(in: collection, options: Utils.fetchOptions()) 19 | itemsFetchResult.enumerateObjects({ (asset, count, stop) in 20 | if asset.mediaType == .image { 21 | self.items.append(Image(asset: asset)) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Videos/VideosLibrary.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class VideosLibrary { 5 | 6 | var items: [Video] = [] 7 | var fetchResults: PHFetchResult? 8 | 9 | // MARK: - Initialization 10 | 11 | init() { 12 | 13 | } 14 | 15 | // MARK: - Logic 16 | 17 | func reload(_ completion: @escaping () -> Void) { 18 | DispatchQueue.global().async { 19 | self.reloadSync() 20 | DispatchQueue.main.async { 21 | completion() 22 | } 23 | } 24 | } 25 | 26 | fileprivate func reloadSync() { 27 | fetchResults = PHAsset.fetchAssets(with: .video, options: Utils.fetchOptions()) 28 | 29 | items = [] 30 | fetchResults?.enumerateObjects({ (asset, _, _) in 31 | self.items.append(Video(asset: asset)) 32 | }) 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | func g_addShadow() { 6 | layer.shadowColor = UIColor.black.cgColor 7 | layer.shadowOpacity = 0.5 8 | layer.shadowOffset = CGSize(width: 0, height: 1) 9 | layer.shadowRadius = 1 10 | } 11 | 12 | func g_addRoundBorder() { 13 | layer.borderWidth = 1 14 | layer.borderColor = Config.Grid.FrameView.borderColor.cgColor 15 | layer.cornerRadius = 3 16 | clipsToBounds = true 17 | } 18 | 19 | func g_quickFade(visible: Bool = true) { 20 | UIView.animate(withDuration: 0.1, animations: { 21 | self.alpha = visible ? 1 : 0 22 | }) 23 | } 24 | 25 | func g_fade(visible: Bool) { 26 | UIView.animate(withDuration: 0.25, animations: { 27 | self.alpha = visible ? 1 : 0 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GalleryTests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class Tests: XCTestCase { 4 | 5 | override func setUp() { 6 | super.setUp() 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDown() { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | super.tearDown() 13 | } 14 | 15 | func testExample() { 16 | // This is an example of a functional test case. 17 | // Use XCTAssert and related functions to verify your tests produce the correct results. 18 | } 19 | 20 | func testPerformanceExample() { 21 | // This is an example of a performance test case. 22 | self.measure { 23 | // Put the code you want to measure the time of here. 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /GalleryTests/Info-iOS.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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Utils/Fetcher.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | struct Fetcher { 5 | 6 | // TODO: Why not use screen size? 7 | static func fetchImages(_ assets: [PHAsset], size: CGSize = CGSize(width: 720, height: 1280)) -> [UIImage] { 8 | let options = PHImageRequestOptions() 9 | options.isSynchronous = true 10 | 11 | var images = [UIImage]() 12 | for asset in assets { 13 | PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { image, _ in 14 | if let image = image { 15 | images.append(image) 16 | } 17 | } 18 | } 19 | 20 | return images 21 | } 22 | 23 | static func fetchAsset(_ localIdentifer: String) -> PHAsset? { 24 | return PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifer], options: nil).firstObject 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AbraGallery.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "AbraGallery" 3 | s.summary = "Something good about gallery" 4 | s.version = "1.0.0" 5 | s.homepage = "https://github.com/OpenSooq/Abra.git" 6 | s.license = 'MIT' 7 | s.author = { "OpenSooq" => "ramzi.q@opensooq.com" } 8 | s.source = { 9 | :git => "https://github.com/OpenSooq/Abra.git", 10 | :tag => s.version.to_s 11 | } 12 | s.social_media_url = 'https://www.facebook.com/opesnooq.engineering/' 13 | 14 | s.ios.deployment_target = '8.0' 15 | 16 | s.requires_arc = true 17 | s.source_files = 'Sources/**/*' 18 | s.resource = 'Resources/Gallery.bundle' 19 | s.frameworks = 'UIKit', 'Foundation', 'AVFoundation', 'Photos', 'PhotosUI', 'CoreLocation', 'AVKit' 20 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '3.0' } 21 | 22 | end 23 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/UIImageView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | extension UIImageView { 5 | 6 | func g_loadImage(_ asset: PHAsset) { 7 | guard frame.size != CGSize.zero 8 | else { 9 | image = Bundle.image("gallery_placeholder") 10 | return 11 | } 12 | 13 | if tag == 0 { 14 | image = Bundle.image("gallery_placeholder") 15 | } else { 16 | PHImageManager.default().cancelImageRequest(PHImageRequestID(tag)) 17 | } 18 | 19 | let options = PHImageRequestOptions() 20 | 21 | let id = PHImageManager.default().requestImage(for: asset, targetSize: frame.size, 22 | contentMode: .aspectFill, options: options) 23 | { [weak self] image, _ in 24 | 25 | self?.image = image 26 | } 27 | 28 | tag = Int(id) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gallery 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | 7 | var window: UIWindow? 8 | 9 | lazy var navigationController: UINavigationController = { [unowned self] in 10 | let controller = UINavigationController(rootViewController: self.viewController) 11 | return controller 12 | }() 13 | 14 | lazy var viewController: ViewController = { 15 | let controller = ViewController() 16 | return controller 17 | }() 18 | 19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 20 | window = UIWindow(frame: UIScreen.main.bounds) 21 | window?.rootViewController = navigationController 22 | window?.makeKeyAndVisible() 23 | 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Gallery/Info-iOS.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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Gallery/Info-Mac.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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016 Hyper Interaktiv AS. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the **MIT** license 2 | 3 | > Copyright (c) 2015 Hyper Interaktiv AS 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining 6 | > a copy of this software and associated documentation files (the 7 | > "Software"), to deal in the Software without restriction, including 8 | > without limitation the rights to use, copy, modify, merge, publish, 9 | > distribute, sublicense, and/or sell copies of the Software, and to 10 | > permit persons to whom the Software is furnished to do so, subject to 11 | > the following conditions: 12 | > 13 | > The above copyright notice and this permission notice shall be 14 | > included in all copies or substantial portions of the Software. 15 | > 16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Sources/Utils/VideoEditor/VideoEditor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import Photos 4 | 5 | public class VideoEditor: VideoEditing { 6 | 7 | // MARK: - Initialization 8 | 9 | public init() { 10 | 11 | } 12 | 13 | // MARK: - Edit 14 | 15 | public func edit(video: Video, completion: @escaping (_ video: Video?, _ tempPath: URL?) -> Void) { 16 | process(video: video, completion: completion) 17 | } 18 | 19 | public func crop(avAsset: AVAsset, completion: @escaping (URL?) -> Void) { 20 | guard let outputURL = EditInfo.outputURL else { 21 | completion(nil) 22 | return 23 | } 24 | 25 | let export = AVAssetExportSession(asset: avAsset, presetName: EditInfo.presetName(avAsset)) 26 | export?.timeRange = EditInfo.timeRange(avAsset) 27 | export?.outputURL = outputURL 28 | export?.outputFileType = EditInfo.file.type 29 | export?.videoComposition = EditInfo.composition(avAsset) 30 | export?.shouldOptimizeForNetworkUse = true 31 | 32 | export?.exportAsynchronously { 33 | if export?.status == AVAssetExportSessionStatus.completed { 34 | completion(outputURL) 35 | } else { 36 | completion(nil) 37 | } 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sources/Utils/View/EmptyView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class EmptyView: UIView { 4 | 5 | lazy var imageView: UIImageView = self.makeImageView() 6 | lazy var label: UILabel = self.makeLabel() 7 | 8 | // MARK: - Initialization 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | 13 | setup() 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | // MARK: - Setup 21 | 22 | func setup() { 23 | [label, imageView].forEach { 24 | addSubview($0 as! UIView) 25 | } 26 | 27 | label.g_pinCenter() 28 | imageView.g_pin(on: .centerX) 29 | imageView.g_pin(on: .bottom, view: label, on: .top, constant: -12) 30 | } 31 | 32 | // MARK: - Controls 33 | 34 | func makeLabel() -> UILabel { 35 | let label = UILabel() 36 | label.textColor = Config.EmptyView.textColor 37 | label.font = Config.Font.Text.regular.withSize(14) 38 | label.text = "Gallery.EmptyView.Text".g_localize(fallback: "Nothing to show") 39 | 40 | return label 41 | } 42 | 43 | func makeImageView() -> UIImageView { 44 | let view = UIImageView() 45 | view.image = Config.EmptyView.image 46 | 47 | return view 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Utils/LocationManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreLocation 3 | 4 | class LocationManager: NSObject, CLLocationManagerDelegate { 5 | var locationManager = CLLocationManager() 6 | var latestLocation: CLLocation? 7 | 8 | override init() { 9 | super.init() 10 | locationManager.delegate = self 11 | locationManager.desiredAccuracy = kCLLocationAccuracyBest 12 | locationManager.requestWhenInUseAuthorization() 13 | } 14 | 15 | func start() { 16 | locationManager.startUpdatingLocation() 17 | } 18 | 19 | func stop() { 20 | locationManager.stopUpdatingLocation() 21 | } 22 | 23 | // MARK: - CLLocationManagerDelegate 24 | 25 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 26 | // Pick the location with best (= smallest value) horizontal accuracy 27 | latestLocation = locations.sorted { $0.horizontalAccuracy < $1.horizontalAccuracy }.first 28 | } 29 | 30 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 31 | if status == .authorizedAlways || status == .authorizedWhenInUse { 32 | locationManager.startUpdatingLocation() 33 | } else { 34 | locationManager.stopUpdatingLocation() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Camera/TripleButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TripleButton: UIButton { 4 | 5 | struct State { 6 | let title: String 7 | let image: UIImage 8 | } 9 | 10 | let states: [State] 11 | var selectedIndex: Int = 0 12 | 13 | // MARK: - Initialization 14 | 15 | init(states: [State]) { 16 | self.states = states 17 | super.init(frame: .zero) 18 | setup() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | // MARK: - Setup 26 | 27 | func setup() { 28 | titleLabel?.font = Config.Font.Text.semibold.withSize(12) 29 | imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0) 30 | setTitleColor(UIColor.gray, for: .highlighted) 31 | 32 | select(index: selectedIndex) 33 | } 34 | 35 | // MARK: - Logic 36 | 37 | @discardableResult func toggle() -> Int { 38 | selectedIndex = (selectedIndex + 1) % states.count 39 | select(index: selectedIndex) 40 | 41 | return selectedIndex 42 | } 43 | 44 | func select(index: Int) { 45 | guard index < states.count else { return } 46 | 47 | let state = states[index] 48 | 49 | setTitle(state.title, for: UIControlState()) 50 | setImage(state.image, for: UIControlState()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Images/ImagesLibrary.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class ImagesLibrary { 5 | 6 | var albums: [Album] = [] 7 | var albumsFetchResults = [PHFetchResult]() 8 | 9 | // MARK: - Initialization 10 | 11 | init() { 12 | 13 | } 14 | 15 | // MARK: - Logic 16 | 17 | func reload(_ completion: @escaping () -> Void) { 18 | DispatchQueue.global().async { 19 | self.reloadSync() 20 | DispatchQueue.main.async { 21 | completion() 22 | } 23 | } 24 | } 25 | 26 | fileprivate func reloadSync() { 27 | let types: [PHAssetCollectionType] = [.smartAlbum, .album] 28 | 29 | albumsFetchResults = types.map { 30 | return PHAssetCollection.fetchAssetCollections(with: $0, subtype: .any, options: nil) 31 | } 32 | 33 | albums = [] 34 | 35 | for result in albumsFetchResults { 36 | result.enumerateObjects({ (collection, _, _) in 37 | let album = Album(collection: collection) 38 | album.reload() 39 | 40 | if !album.items.isEmpty { 41 | self.albums.append(album) 42 | } 43 | }) 44 | } 45 | 46 | // Move Camera Roll first 47 | if let index = albums.index(where: { $0.collection.assetCollectionSubtype == . smartAlbumUserLibrary }) { 48 | albums.g_moveToFirst(index) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Utils/View/FrameView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class FrameView: UIView { 4 | 5 | lazy var label: UILabel = self.makeLabel() 6 | lazy var gradientLayer: CAGradientLayer = self.makeGradientLayer() 7 | 8 | // MARK: - Initialization 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | 13 | setup() 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | // MARK: - Setup 21 | 22 | func setup() { 23 | layer.addSublayer(gradientLayer) 24 | layer.borderColor = Config.Grid.FrameView.borderColor.cgColor 25 | layer.borderWidth = 3 26 | 27 | addSubview(label) 28 | label.g_pinCenter() 29 | } 30 | 31 | // MARK: - Layout 32 | 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | 36 | gradientLayer.frame = bounds 37 | } 38 | 39 | // MARK: - Controls 40 | 41 | func makeLabel() -> UILabel { 42 | let label = UILabel() 43 | label.font = Config.Font.Main.regular.withSize(40) 44 | label.textColor = UIColor.white 45 | 46 | return label 47 | } 48 | 49 | func makeGradientLayer() -> CAGradientLayer { 50 | let layer = CAGradientLayer() 51 | layer.colors = [ 52 | Config.Grid.FrameView.fillColor.withAlphaComponent(0.25).cgColor, 53 | Config.Grid.FrameView.fillColor.withAlphaComponent(0.4).cgColor 54 | ] 55 | 56 | return layer 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo/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 | NSCameraUsageDescription 26 | This app requires access to camera 27 | NSPhotoLibraryUsageDescription 28 | This app requires access to photo library 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UIViewControllerBasedStatusBarAppearance 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/GalleryDemo/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Gallery (1.0.0) 3 | - Hue (2.0.1) 4 | - Lightbox (1.0.0): 5 | - Hue 6 | - Sugar 7 | - Sugar (3.0.0) 8 | 9 | DEPENDENCIES: 10 | - Gallery (from `../../`) 11 | - Hue (from `https://github.com/hyperoslo/Hue.git`, branch `master`) 12 | - Lightbox (from `https://github.com/hyperoslo/Lightbox.git`, branch `swift-3`) 13 | - Sugar (from `https://github.com/hyperoslo/Sugar.git`, branch `master`) 14 | 15 | EXTERNAL SOURCES: 16 | Gallery: 17 | :path: ../../ 18 | Hue: 19 | :branch: master 20 | :git: https://github.com/hyperoslo/Hue.git 21 | Lightbox: 22 | :branch: swift-3 23 | :git: https://github.com/hyperoslo/Lightbox.git 24 | Sugar: 25 | :branch: master 26 | :git: https://github.com/hyperoslo/Sugar.git 27 | 28 | CHECKOUT OPTIONS: 29 | Hue: 30 | :commit: ef39c988eb6c1606454359c808e5373edb257c57 31 | :git: https://github.com/hyperoslo/Hue.git 32 | Lightbox: 33 | :commit: 98e3f62ebe14e187cef505b8fc7aa860363ca45b 34 | :git: https://github.com/hyperoslo/Lightbox.git 35 | Sugar: 36 | :commit: 343a4409f0339f601fdb39c8db000b2481545726 37 | :git: https://github.com/hyperoslo/Sugar.git 38 | 39 | SPEC CHECKSUMS: 40 | Gallery: ac50f749bb5b0f4409bd5596504617d9bf97e767 41 | Hue: ab7efb15270e0bc764f2f468aa6f3f2728d52f2b 42 | Lightbox: 6deb5fc2c51f353212631470c4c689bc5120598c 43 | Sugar: 013db92ee417299586c93007ca2d91f09a29dae3 44 | 45 | PODFILE CHECKSUM: 0d6c925ae667b86ff448c7cc8926b2dfc7c121cc 46 | 47 | COCOAPODS: 1.2.0 48 | -------------------------------------------------------------------------------- /Sources/Utils/Permission/Permission.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Photos 3 | import AVFoundation 4 | 5 | struct Permission { 6 | 7 | static var hasPermissions: Bool { 8 | return Photos.hasPermission && Camera.hasPermission && Microphone.hasPermissionOrAlreadyAsked 9 | } 10 | 11 | struct Photos { 12 | static var hasPermission: Bool { 13 | return PHPhotoLibrary.authorizationStatus() == .authorized 14 | } 15 | 16 | static func request(_ completion: @escaping () -> Void) { 17 | PHPhotoLibrary.requestAuthorization { status in 18 | completion() 19 | } 20 | } 21 | } 22 | 23 | struct Camera { 24 | static var hasPermission: Bool { 25 | return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) == .authorized 26 | } 27 | 28 | static func request(_ completion: @escaping () -> Void) { 29 | AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { granted in 30 | completion() 31 | } 32 | } 33 | } 34 | 35 | struct Microphone { 36 | 37 | static var didAsk = false 38 | 39 | static var hasPermission: Bool { 40 | return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeAudio) == .authorized 41 | } 42 | 43 | static var hasPermissionOrAlreadyAsked: Bool { 44 | if didAsk { 45 | return true 46 | } 47 | return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeAudio) == .authorized 48 | } 49 | 50 | static func request(_ completion: @escaping () -> Void) { 51 | AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeAudio) { granted in 52 | didAsk = true 53 | completion() 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Utils/VideoEditor/VideoEditing.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | import Photos 4 | 5 | public protocol VideoEditing: class { 6 | 7 | func crop(avAsset: AVAsset, completion: @escaping (URL?) -> Void) 8 | func edit(video: Video, completion: @escaping (_ video: Video?, _ tempPath: URL?) -> Void) 9 | } 10 | 11 | extension VideoEditing { 12 | 13 | public func process(video: Video, completion: @escaping (_ video: Video?, _ tempPath: URL?) -> Void) { 14 | video.fetchAVAsset { avAsset in 15 | guard let avAsset = avAsset else { 16 | completion(nil, nil) 17 | return 18 | } 19 | 20 | self.crop(avAsset: avAsset) { (outputURL: URL?) in 21 | guard let outputURL = outputURL else { 22 | completion(nil, nil) 23 | return 24 | } 25 | 26 | self.handle(outputURL: outputURL, completion: completion) 27 | } 28 | } 29 | } 30 | 31 | func handle(outputURL: URL, completion: @escaping (_ video: Video?, _ tempPath: URL?) -> Void) { 32 | guard Config.VideoEditor.savesEditedVideoToLibrary else { 33 | completion(nil, outputURL) 34 | return 35 | } 36 | 37 | var localIdentifier: String? 38 | PHPhotoLibrary.shared().performChanges({ 39 | let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL) 40 | localIdentifier = request?.placeholderForCreatedAsset?.localIdentifier 41 | }, completionHandler: { succeeded, info in 42 | if let localIdentifier = localIdentifier, let asset = Fetcher.fetchAsset(localIdentifier) { 43 | completion(Video(asset: asset), outputURL) 44 | } else { 45 | completion(nil, outputURL) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Videos/VideoBox.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol VideoBoxDelegate: class { 4 | func videoBoxDidTap(_ videoBox: VideoBox) 5 | } 6 | 7 | class VideoBox: UIView { 8 | 9 | lazy var imageView: UIImageView = self.makeImageView() 10 | lazy var cameraImageView: UIImageView = self.makeCameraImageView() 11 | 12 | weak var delegate: VideoBoxDelegate? 13 | 14 | // MARK: - Initialization 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | 19 | setup() 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | // MARK: - Action 27 | 28 | func viewTapped(_ gr: UITapGestureRecognizer) { 29 | delegate?.videoBoxDidTap(self) 30 | } 31 | 32 | // MARK: - Setup 33 | 34 | func setup() { 35 | backgroundColor = UIColor.clear 36 | imageView.g_addRoundBorder() 37 | 38 | let gr = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:))) 39 | addGestureRecognizer(gr) 40 | 41 | [imageView, cameraImageView].forEach { 42 | self.addSubview($0) 43 | } 44 | 45 | imageView.g_pinEdges() 46 | cameraImageView.g_pin(on: .left, constant: 5) 47 | cameraImageView.g_pin(on: .bottom, constant: -5) 48 | cameraImageView.g_pin(size: CGSize(width: 12, height: 6)) 49 | } 50 | 51 | // MARK: - Controls 52 | 53 | func makeImageView() -> UIImageView { 54 | let imageView = UIImageView() 55 | imageView.clipsToBounds = true 56 | 57 | return imageView 58 | } 59 | 60 | func makeCameraImageView() -> UIImageView { 61 | let imageView = UIImageView() 62 | imageView.image = Bundle.image("gallery_video_cell_camera") 63 | 64 | return imageView 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Videos/VideoCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class VideoCell: ImageCell { 5 | 6 | lazy var cameraImageView: UIImageView = self.makeCameraImageView() 7 | lazy var durationLabel: UILabel = self.makeDurationLabel() 8 | lazy var bottomOverlay: UIView = self.makeBottomOverlay() 9 | 10 | // MARK: - Config 11 | 12 | func configure(_ video: Video) { 13 | super.configure(video.asset) 14 | 15 | video.fetchDuration { duration in 16 | DispatchQueue.main.async { 17 | self.durationLabel.text = "\(Utils.format(duration))" 18 | } 19 | } 20 | } 21 | 22 | // MARK: - Setup 23 | 24 | override func setup() { 25 | super.setup() 26 | 27 | [bottomOverlay, cameraImageView, durationLabel].forEach { 28 | self.insertSubview($0, belowSubview: self.highlightOverlay) 29 | } 30 | 31 | bottomOverlay.g_pinDownward() 32 | bottomOverlay.g_pin(height: 16) 33 | 34 | cameraImageView.g_pinHorizontally(padding: 5) 35 | cameraImageView.g_pin(size: CGSize(width: 12, height: 6)) 36 | 37 | durationLabel.g_pin(on: .right, constant: -4) 38 | durationLabel.g_pin(on: .bottom, constant: -2) 39 | } 40 | 41 | // MARK: - Controls 42 | 43 | func makeCameraImageView() -> UIImageView { 44 | let imageView = UIImageView() 45 | imageView.image = Bundle.image("gallery_video_cell_camera") 46 | 47 | return imageView 48 | } 49 | 50 | func makeDurationLabel() -> UILabel { 51 | let label = UILabel() 52 | label.font = Config.Font.Text.bold.withSize(9) 53 | label.textColor = UIColor.white 54 | label.textAlignment = .right 55 | 56 | return label 57 | } 58 | 59 | func makeBottomOverlay() -> UIView { 60 | let view = UIView() 61 | view.backgroundColor = UIColor.black.withAlphaComponent(0.5) 62 | 63 | return view 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Utils/Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | import Photos 4 | 5 | struct Utils { 6 | 7 | static func rotationTransform() -> CGAffineTransform { 8 | switch UIDevice.current.orientation { 9 | case .landscapeLeft: 10 | return CGAffineTransform(rotationAngle: CGFloat(M_PI_2)) 11 | case .landscapeRight: 12 | return CGAffineTransform(rotationAngle: CGFloat(-M_PI_2)) 13 | case .portraitUpsideDown: 14 | return CGAffineTransform(rotationAngle: CGFloat(M_PI)) 15 | default: 16 | return CGAffineTransform.identity 17 | } 18 | } 19 | 20 | static func videoOrientation() -> AVCaptureVideoOrientation { 21 | switch UIDevice.current.orientation { 22 | case .portrait: 23 | return .portrait 24 | case .landscapeLeft: 25 | return .landscapeRight 26 | case .landscapeRight: 27 | return .landscapeLeft 28 | case .portraitUpsideDown: 29 | return .portraitUpsideDown 30 | default: 31 | return .portrait 32 | } 33 | } 34 | 35 | static func fetchOptions() -> PHFetchOptions { 36 | let options = PHFetchOptions() 37 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 38 | if let fetchLimit = Config.Fetch.limit { 39 | if #available(iOS 9.0, *) { 40 | options.fetchLimit = fetchLimit 41 | } else { 42 | // Fallback on earlier versions 43 | } 44 | } 45 | return options 46 | } 47 | 48 | static func format(_ duration: TimeInterval) -> String { 49 | let formatter = DateComponentsFormatter() 50 | formatter.zeroFormattingBehavior = .pad 51 | 52 | if duration >= 3600 { 53 | formatter.allowedUnits = [.hour, .minute, .second] 54 | } else { 55 | formatter.allowedUnits = [.minute, .second] 56 | } 57 | 58 | return formatter.string(from: duration) ?? "" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Videos/Video.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | public class Video: Equatable { 5 | 6 | let asset: PHAsset 7 | 8 | var durationRequestID: Int = 0 9 | var duration: Double = 0 10 | 11 | // MARK: - Initialization 12 | 13 | init(asset: PHAsset) { 14 | self.asset = asset 15 | } 16 | 17 | func fetchDuration(_ completion: @escaping (Double) -> Void) { 18 | guard duration == 0 19 | else { 20 | completion(duration) 21 | return 22 | } 23 | 24 | if durationRequestID != 0 { 25 | PHImageManager.default().cancelImageRequest(PHImageRequestID(durationRequestID)) 26 | } 27 | 28 | let id = PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { 29 | asset, mix, _ in 30 | 31 | self.duration = asset?.duration.seconds ?? 0 32 | completion(self.duration) 33 | } 34 | 35 | durationRequestID = Int(id) 36 | } 37 | 38 | public func fetchPlayerItem(_ completion: @escaping (AVPlayerItem?) -> Void) { 39 | PHImageManager.default().requestPlayerItem(forVideo: asset, options: nil) { 40 | item, _ in 41 | 42 | completion(item) 43 | } 44 | } 45 | 46 | public func fetchAVAsset(_ completion: @escaping (AVAsset?) -> Void){ 47 | PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in 48 | completion(avAsset) 49 | } 50 | } 51 | 52 | public func fetchThumbnail(_ size: CGSize = CGSize(width: 100, height: 100), completion: @escaping (UIImage?) -> Void) { 53 | PHImageManager.default().requestImage(for: asset, targetSize: size, 54 | contentMode: .aspectFill, options: nil) 55 | { image, _ in 56 | completion(image) 57 | } 58 | } 59 | } 60 | 61 | // MARK: - Equatable 62 | 63 | public func ==(lhs: Video, rhs: Video) -> Bool { 64 | return lhs.asset == rhs.asset 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Camera/ShutterButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ShutterButton: UIButton { 4 | 5 | lazy var overlayView: UIView = self.makeOverlayView() 6 | lazy var roundLayer: CAShapeLayer = self.makeRoundLayer() 7 | 8 | // MARK: - Initialization 9 | 10 | override init(frame: CGRect) { 11 | super.init(frame: frame) 12 | 13 | setup() 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | // MARK: - Layout 21 | 22 | override func layoutSubviews() { 23 | super.layoutSubviews() 24 | 25 | overlayView.frame = bounds.insetBy(dx: 3, dy: 3) 26 | overlayView.layer.cornerRadius = overlayView.frame.size.width/2 27 | 28 | roundLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3, dy: 3)).cgPath 29 | layer.cornerRadius = bounds.size.width/2 30 | } 31 | 32 | // MARK: - Setup 33 | 34 | func setup() { 35 | backgroundColor = UIColor.white 36 | 37 | addSubview(overlayView) 38 | layer.addSublayer(roundLayer) 39 | } 40 | 41 | // MARK: - Controls 42 | 43 | func makeOverlayView() -> UIView { 44 | let view = UIView() 45 | view.backgroundColor = UIColor.white 46 | view.isUserInteractionEnabled = false 47 | 48 | return view 49 | } 50 | 51 | func makeRoundLayer() -> CAShapeLayer { 52 | let layer = CAShapeLayer() 53 | layer.strokeColor = Config.Camera.ShutterButton.numberColor.cgColor 54 | layer.lineWidth = 2 55 | layer.fillColor = nil 56 | 57 | return layer 58 | } 59 | 60 | // MARK: - Highlight 61 | 62 | override var isHighlighted: Bool { 63 | didSet { 64 | switch Config.Camera.recordMode { 65 | case .photo: 66 | overlayView.backgroundColor = isHighlighted ? UIColor.gray : UIColor.white 67 | case .video: 68 | overlayView.backgroundColor = isHighlighted ? UIColor.brown : UIColor.red 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Images/ImageCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class ImageCell: UICollectionViewCell { 5 | 6 | lazy var imageView: UIImageView = self.makeImageView() 7 | lazy var highlightOverlay: UIView = self.makeHighlightOverlay() 8 | lazy var frameView: FrameView = self.makeFrameView() 9 | 10 | // MARK: - Initialization 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | 15 | setup() 16 | } 17 | 18 | required init?(coder aDecoder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | // MARK: - Highlight 23 | 24 | override var isHighlighted: Bool { 25 | didSet { 26 | highlightOverlay.isHidden = !isHighlighted 27 | } 28 | } 29 | 30 | // MARK: - Config 31 | 32 | func configure(_ asset: PHAsset) { 33 | imageView.layoutIfNeeded() 34 | imageView.g_loadImage(asset) 35 | } 36 | 37 | func configure(_ image: Image) { 38 | configure(image.asset) 39 | } 40 | 41 | // MARK: - Setup 42 | 43 | func setup() { 44 | [imageView, frameView, highlightOverlay].forEach { 45 | self.contentView.addSubview($0) 46 | } 47 | 48 | imageView.g_pinEdges() 49 | frameView.g_pinEdges() 50 | highlightOverlay.g_pinEdges() 51 | } 52 | 53 | // MARK: - Controls 54 | 55 | func makeImageView() -> UIImageView { 56 | let imageView = UIImageView() 57 | imageView.clipsToBounds = true 58 | imageView.contentMode = .scaleAspectFill 59 | 60 | return imageView 61 | } 62 | 63 | func makeHighlightOverlay() -> UIView { 64 | let view = UIView() 65 | view.isUserInteractionEnabled = false 66 | view.backgroundColor = Config.Grid.FrameView.borderColor.withAlphaComponent(0.3) 67 | view.isHidden = true 68 | 69 | return view 70 | } 71 | 72 | func makeFrameView() -> FrameView { 73 | let frameView = FrameView(frame: .zero) 74 | frameView.alpha = 0 75 | 76 | return frameView 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Sources/Utils/Permission/PermissionController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol PermissionControllerDelegate: class { 4 | func permissionControllerDidFinish(_ controller: PermissionController) 5 | } 6 | 7 | class PermissionController: UIViewController { 8 | 9 | lazy var permissionView: PermissionView = self.makePermissionView() 10 | 11 | weak var delegate: PermissionControllerDelegate? 12 | 13 | // MARK: - Life cycle 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | setup() 19 | } 20 | 21 | override func viewDidAppear(_ animated: Bool) { 22 | super.viewDidAppear(animated) 23 | 24 | requestPermission() 25 | } 26 | 27 | // MARK: - Setup 28 | 29 | func setup() { 30 | view.addSubview(permissionView) 31 | permissionView.closeButton.addTarget(self, action: #selector(closeButtonTouched(_:)), 32 | for: .touchUpInside) 33 | permissionView.settingButton.addTarget(self, action: #selector(settingButtonTouched(_:)), 34 | for: .touchUpInside) 35 | permissionView.g_pinEdges() 36 | } 37 | 38 | // MARK: - Logic 39 | 40 | func requestPermission() { 41 | Permission.Photos.request { 42 | self.check() 43 | } 44 | 45 | Permission.Camera.request { 46 | self.check() 47 | } 48 | 49 | Permission.Microphone.request { 50 | self.check() 51 | } 52 | } 53 | 54 | func check() { 55 | if Permission.hasPermissions { 56 | DispatchQueue.main.async { 57 | self.delegate?.permissionControllerDidFinish(self) 58 | } 59 | } 60 | } 61 | 62 | // MARK: - Action 63 | 64 | func settingButtonTouched(_ button: UIButton) { 65 | DispatchQueue.main.async { 66 | if let settingsURL = URL(string: UIApplicationOpenSettingsURLString) { 67 | UIApplication.shared.openURL(settingsURL) 68 | } 69 | } 70 | } 71 | 72 | func closeButtonTouched(_ button: UIButton) { 73 | EventHub.shared.close?() 74 | } 75 | 76 | // MARK: - Controls 77 | 78 | func makePermissionView() -> PermissionView { 79 | let view = PermissionView() 80 | 81 | return view 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Utils/Extensions/AVAsset+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | 4 | extension AVAsset { 5 | 6 | fileprivate var g_naturalSize: CGSize { 7 | return tracks(withMediaType: AVMediaTypeVideo).first?.naturalSize ?? .zero 8 | } 9 | 10 | var g_correctSize: CGSize { 11 | return g_isPortrait ? CGSize(width: g_naturalSize.height, height: g_naturalSize.width) : g_naturalSize 12 | } 13 | 14 | var g_isPortrait: Bool { 15 | let portraits: [UIInterfaceOrientation] = [.portrait, .portraitUpsideDown] 16 | return portraits.contains(g_orientation) 17 | } 18 | 19 | var g_fileSize: Double { 20 | guard let avURLAsset = self as? AVURLAsset else { return 0 } 21 | 22 | var result: AnyObject? 23 | try? (avURLAsset.url as NSURL).getResourceValue(&result, forKey: URLResourceKey.fileSizeKey) 24 | 25 | if let result = result as? NSNumber { 26 | return result.doubleValue 27 | } else { 28 | return 0 29 | } 30 | } 31 | 32 | var g_frameRate: Float { 33 | return tracks(withMediaType: AVMediaTypeVideo).first?.nominalFrameRate ?? 30 34 | } 35 | 36 | // Same as UIImageOrientation 37 | var g_orientation: UIInterfaceOrientation { 38 | guard let transform = tracks(withMediaType: AVMediaTypeVideo).first?.preferredTransform else { 39 | return .portrait 40 | } 41 | 42 | switch (transform.tx, transform.ty) { 43 | case (0, 0): 44 | return .landscapeRight 45 | case (g_naturalSize.width, g_naturalSize.height): 46 | return .landscapeLeft 47 | case (0, g_naturalSize.width): 48 | return .portraitUpsideDown 49 | default: 50 | return .portrait 51 | } 52 | } 53 | 54 | // MARK: - Description 55 | 56 | var g_videoDescription: CMFormatDescription? { 57 | if let object = tracks(withMediaType: AVMediaTypeVideo).first?.formatDescriptions.first { 58 | return object as! CMFormatDescription 59 | } 60 | 61 | return nil 62 | } 63 | 64 | var g_audioDescription: CMFormatDescription? { 65 | if let object = tracks(withMediaType: AVMediaTypeAudio).first?.formatDescriptions.first { 66 | return object as! CMFormatDescription 67 | } 68 | 69 | return nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Utils/View/AlbumCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class AlbumCell: UITableViewCell { 4 | 5 | lazy var albumImageView: UIImageView = self.makeAlbumImageView() 6 | lazy var albumTitleLabel: UILabel = self.makeAlbumTitleLabel() 7 | lazy var itemCountLabel: UILabel = self.makeItemCountLabel() 8 | 9 | // MARK: - Initialization 10 | 11 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 12 | super.init(style: style, reuseIdentifier: reuseIdentifier) 13 | 14 | setup() 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | // MARK: - Config 22 | 23 | func configure(_ album: Album) { 24 | albumTitleLabel.text = album.collection.localizedTitle 25 | itemCountLabel.text = "\(album.items.count)" 26 | 27 | if let item = album.items.first { 28 | albumImageView.layoutIfNeeded() 29 | albumImageView.g_loadImage(item.asset) 30 | } 31 | } 32 | 33 | // MARK: - Setup 34 | 35 | func setup() { 36 | [albumImageView, albumTitleLabel, itemCountLabel].forEach { 37 | addSubview($0 as! UIView) 38 | } 39 | 40 | albumImageView.g_pin(on: .left, constant: 12) 41 | albumImageView.g_pin(on: .top, constant: 5) 42 | albumImageView.g_pin(on: .bottom, constant: -5) 43 | albumImageView.g_pin(on: .width, view: albumImageView, on: .height) 44 | 45 | albumTitleLabel.g_pin(on: .left, view: albumImageView, on: .right, constant: 10) 46 | albumTitleLabel.g_pin(on: .top, constant: 24) 47 | albumTitleLabel.g_pin(on: .right, constant: -10) 48 | 49 | itemCountLabel.g_pin(on: .left, view: albumImageView, on: .right, constant: 10) 50 | itemCountLabel.g_pin(on: .top, view: albumTitleLabel, on: .bottom, constant: 6) 51 | } 52 | 53 | // MARK: - Controls 54 | 55 | func makeAlbumImageView() -> UIImageView { 56 | let imageView = UIImageView() 57 | imageView.image = Bundle.image("gallery_placeholder") 58 | 59 | return imageView 60 | } 61 | 62 | func makeAlbumTitleLabel() -> UILabel { 63 | let label = UILabel() 64 | label.numberOfLines = 1 65 | label.font = Config.Font.Text.regular.withSize(14) 66 | 67 | return label 68 | } 69 | 70 | func makeItemCountLabel() -> UILabel { 71 | let label = UILabel() 72 | label.numberOfLines = 1 73 | label.font = Config.Font.Text.regular.withSize(10) 74 | 75 | return label 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Utils/View/ArrowButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ArrowButton: UIButton { 4 | 5 | lazy var label: UILabel = self.makeLabel() 6 | lazy var arrow: UIImageView = self.makeArrow() 7 | 8 | let padding: CGFloat = 10 9 | let arrowSize: CGFloat = 8 10 | 11 | // MARK: - Initialization 12 | 13 | init() { 14 | super.init(frame: CGRect.zero) 15 | 16 | addSubview(label) 17 | addSubview(arrow) 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: - Layout 25 | 26 | override func layoutSubviews() { 27 | super.layoutSubviews() 28 | 29 | label.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2) 30 | 31 | arrow.frame.size = CGSize(width: arrowSize, height: arrowSize) 32 | arrow.center = CGPoint(x: label.frame.maxX + padding, y: bounds.size.height / 2) 33 | } 34 | 35 | 36 | override var intrinsicContentSize : CGSize { 37 | let size = super.intrinsicContentSize 38 | label.sizeToFit() 39 | 40 | return CGSize(width: label.frame.size.width + arrowSize*2 + padding, 41 | height: size.height) 42 | } 43 | 44 | // MARK: - Logic 45 | 46 | func updateText(_ text: String) { 47 | label.text = text.uppercased() 48 | arrow.alpha = text.isEmpty ? 0 : 1 49 | invalidateIntrinsicContentSize() 50 | } 51 | 52 | func toggle(_ expanding: Bool) { 53 | let transform = expanding 54 | ? CGAffineTransform(rotationAngle: CGFloat(M_PI)) : CGAffineTransform.identity 55 | 56 | UIView.animate(withDuration: 0.25, animations: { 57 | self.arrow.transform = transform 58 | }) 59 | } 60 | 61 | // MARK: - Controls 62 | 63 | func makeLabel() -> UILabel { 64 | let label = UILabel() 65 | label.textColor = Config.Grid.ArrowButton.tintColor 66 | label.font = Config.Font.Main.regular.withSize(16) 67 | label.textAlignment = .center 68 | 69 | return label 70 | } 71 | 72 | func makeArrow() -> UIImageView { 73 | let arrow = UIImageView() 74 | arrow.image = Bundle.image("gallery_title_arrow")?.withRenderingMode(.alwaysTemplate) 75 | arrow.tintColor = Config.Grid.ArrowButton.tintColor 76 | arrow.alpha = 0 77 | 78 | return arrow 79 | } 80 | 81 | // MARK: - Touch 82 | 83 | override var isHighlighted: Bool { 84 | didSet { 85 | label.textColor = isHighlighted ? UIColor.lightGray : Config.Grid.ArrowButton.tintColor 86 | arrow.tintColor = isHighlighted ? UIColor.lightGray : Config.Grid.ArrowButton.tintColor 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Utils/Permission/PermissionView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class PermissionView: UIView { 4 | 5 | lazy var imageView: UIImageView = self.makeImageView() 6 | lazy var label: UILabel = self.makeLabel() 7 | lazy var settingButton: UIButton = self.makeSettingButton() 8 | lazy var closeButton: UIButton = self.makeCloseButton() 9 | 10 | // MARK: - Initialization 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | 15 | backgroundColor = UIColor.white 16 | setup() 17 | 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: - Setup 25 | 26 | func setup() { 27 | [label, settingButton, closeButton, imageView].forEach { 28 | addSubview($0) 29 | } 30 | 31 | closeButton.g_pin(on: .top) 32 | closeButton.g_pin(on: .left) 33 | closeButton.g_pin(size: CGSize(width: 44, height: 44)) 34 | 35 | settingButton.g_pinCenter() 36 | settingButton.g_pin(height: 44) 37 | 38 | label.g_pin(on: .bottom, view: settingButton, on: .top, constant: -33) 39 | label.g_pinHorizontally(padding: 50) 40 | label.g_pin(greaterThanHeight: 20) 41 | 42 | imageView.g_pinCenter() 43 | imageView.g_pin(on: .bottom, view: label, on: .top, constant: -12) 44 | } 45 | 46 | // MARK: - Controls 47 | 48 | func makeLabel() -> UILabel { 49 | let label = UILabel() 50 | label.textColor = Config.Permission.textColor 51 | label.font = Config.Font.Text.regular.withSize(14) 52 | label.text = "Gallery.Permission.Info".g_localize(fallback: "Please enable photos and camera") 53 | label.textAlignment = .center 54 | label.numberOfLines = 0 55 | 56 | return label 57 | } 58 | 59 | func makeSettingButton() -> UIButton { 60 | let button = UIButton(type: .custom) 61 | button.setTitle("Gallery.Permission.Button".g_localize(fallback: "Go to Settings").uppercased(), 62 | for: UIControlState()) 63 | button.backgroundColor = Config.Permission.Button.backgroundColor 64 | button.titleLabel?.font = Config.Font.Main.medium.withSize(16) 65 | button.setTitleColor(Config.Permission.Button.textColor, for: UIControlState()) 66 | button.setTitleColor(Config.Permission.Button.highlightedTextColor, for: .highlighted) 67 | button.layer.cornerRadius = 22 68 | button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) 69 | 70 | return button 71 | } 72 | 73 | func makeCloseButton() -> UIButton { 74 | let button = UIButton(type: .custom) 75 | button.setImage(Bundle.image("gallery_close")?.withRenderingMode(.alwaysTemplate), for: UIControlState()) 76 | button.tintColor = Config.Grid.CloseButton.tintColor 77 | 78 | return button 79 | } 80 | 81 | func makeImageView() -> UIImageView { 82 | let view = UIImageView() 83 | view.image = Config.Permission.image 84 | 85 | return view 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Images/Cart.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | protocol CartDelegate: class { 5 | func cart(_ cart: Cart, didSet video: Video) 6 | func cart(_ cart: Cart, didAdd image: Image, newlyTaken: Bool) 7 | func cart(_ cart: Cart, didRemove image: Image) 8 | func cartDidReload(_ cart: Cart) 9 | } 10 | 11 | public class Cart { 12 | 13 | public static let shared = Cart() 14 | 15 | public var images: [Image] = [] 16 | fileprivate var lightBoxUIImages: [UIImage] = [] 17 | public var video: Video? 18 | var delegates: NSHashTable = NSHashTable.weakObjects() 19 | 20 | // MARK: - Initialization 21 | 22 | fileprivate init() { 23 | 24 | } 25 | 26 | // MARK: - Delegate 27 | 28 | func add(delegate: CartDelegate) { 29 | delegates.add(delegate) 30 | } 31 | 32 | // MARK: - Logic 33 | 34 | func setVideo(_ video: Video) { 35 | self.video = video 36 | for case let delegate as CartDelegate in delegates.allObjects { 37 | delegate.cart(self, didSet: video) 38 | } 39 | } 40 | 41 | func add(_ image: Image, newlyTaken: Bool = false) { 42 | guard !images.contains(image) else { return } 43 | 44 | images.append(image) 45 | 46 | for case let delegate as CartDelegate in delegates.allObjects { 47 | delegate.cart(self, didAdd: image, newlyTaken: newlyTaken) 48 | } 49 | } 50 | 51 | func remove(_ image: Image) { 52 | guard let index = images.index(of: image) else { return } 53 | 54 | images.remove(at: index) 55 | 56 | for case let delegate as CartDelegate in delegates.allObjects { 57 | delegate.cart(self, didRemove: image) 58 | } 59 | } 60 | 61 | func reload(_ images: [Image]) { 62 | self.images = images 63 | 64 | for case let delegate as CartDelegate in delegates.allObjects { 65 | delegate.cartDidReload(self) 66 | } 67 | } 68 | 69 | // MARK: - Reset 70 | 71 | func reset() { 72 | video = nil 73 | images.removeAll() 74 | delegates.removeAllObjects() 75 | } 76 | 77 | // MARK: - UIImages 78 | 79 | func assets() -> [PHAsset] { 80 | var assets = [PHAsset]() 81 | if let videoAsset = Cart.shared.video?.asset { 82 | assets.append(videoAsset) 83 | } 84 | assets.append(contentsOf: Cart.shared.images.map({ $0.asset })) 85 | return assets 86 | } 87 | 88 | func UIImages() -> [UIImage] { 89 | lightBoxUIImages = Fetcher.fetchImages(images.map({ $0.asset })) 90 | return lightBoxUIImages 91 | } 92 | 93 | func reload(_ UIImages: [UIImage]) { 94 | var changedImages: [Image] = [] 95 | 96 | lightBoxUIImages.filter { 97 | return UIImages.contains($0) 98 | }.flatMap { 99 | return lightBoxUIImages.index(of: $0) 100 | }.forEach { index in 101 | if index < images.count { 102 | changedImages.append(images[index]) 103 | } 104 | } 105 | 106 | lightBoxUIImages = [] 107 | reload(changedImages) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Gallery Banner 3 | 4 | `AbraGallery` is a small library for images & videos picking. It provides video recording too. 5 | 6 | ### Usage 7 | 8 | `GalleryController` is the main entry point, just instantiate and set the delegate: 9 | 10 | ```swift 11 | let gallery = GalleryController() 12 | gallery.delegate2 = self 13 | present(gallery, animated: true, completion: nil) 14 | ``` 15 | 16 | ### Delegate 17 | 18 | The `GalleryControllerDelegate2` requires you to implement some delegate methods in order to interact with the `GalleryController` 19 | 20 | ```swift 21 | public protocol GalleryControllerDelegate2: class { 22 | func galleryController(_ controller: GalleryController, requestLightbox images: [UIImage]) 23 | func galleryControllerDidCancel(_ controller: GalleryController) 24 | func galleryController(_ controller: GalleryController, didSelectAssets assets: [PHAsset]) 25 | } 26 | ``` 27 | 28 | ### Permission 29 | 30 | `Gallery` handles permissions for you. It checks and askes for photo and camera usage permissions at first launch. As of iOS 10, we need to explicitly declare usage descriptions in plist files 31 | 32 | ```xml 33 | NSCameraUsageDescription 34 | This app requires access to camera 35 | NSPhotoLibraryUsageDescription 36 | This app requires access to photo library 37 | ``` 38 | You may disable permissions flow by the config `Gallery.Config.Permission.shouldCheckPermission = false` 39 | 40 | ### Configuration 41 | 42 | There are lots of customization points in `Config` structs. For example 43 | 44 | ```swift 45 | Config.Permission.image = UIImage(named: ImageList.Gallery.cameraIcon) 46 | Config.Font.Text.bold = UIFont(name: FontList.OpenSans.bold, size: 14)! 47 | Config.Camera.recordLocation = true 48 | Config.Camera.recordMode = .video // to enable video recording. 49 | Config.VideoRecording.maxBytesCount = 1024 // to set the maximum size of video. 50 | Config.VideoRecording.maxLengthInSeconds = .video // to set the max length of video. 51 | Config.Selection.mode = [.photo, .camera, .video] // to enable/disable Photo, Camera and Video tabs. 52 | Config.SessionPreset.quality = AVCaptureSessionPresetHigh // to define the quality of recorded video. 53 | 54 | ... and many many more at Config file. 55 | ``` 56 | 57 | ## Installation 58 | 59 | **AbraGallery** is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: 60 | 61 | ```ruby 62 | pod 'AbraGallery' 63 | ``` 64 | 65 | **AbraGallery** can also be installed manually. Just download and drop `Sources` folders in your project. 66 | 67 | ## Author & Contact 68 | 69 | OpenSooq, ramzi.q@opensooq.com, damian.k@opensooq.com 70 | 71 | Abra Gallery is build at the top of [Gallery](https://github.com/blueimp/Gallery) project. 72 | 73 | ## Contributing 74 | 75 | We would love you to contribute to **AbraGallery**, check the [CONTRIBUTING](https://github.com/hyperoslo/Gallery/blob/master/CONTRIBUTING.md) file for more info. 76 | 77 | ## License 78 | 79 | **AbraGallery** is available under the MIT license. See the [LICENSE](https://github.com/hyperoslo/Gallery/blob/master/LICENSE.md) file for more info. 80 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Gallery 3 | import Lightbox 4 | import AVFoundation 5 | import AVKit 6 | import Photos 7 | 8 | class ViewController: UIViewController, LightboxControllerDismissalDelegate, GalleryControllerDelegate, GalleryControllerDelegate2 { 9 | 10 | func galleryController(_ controller: GalleryController, didSelectAssets assets: [PHAsset]) { 11 | controller.dismiss(animated: true, completion: nil) 12 | gallery = nil 13 | } 14 | 15 | 16 | var button: UIButton! 17 | var gallery: GalleryController! 18 | let editor: VideoEditing = VideoEditor() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | view.backgroundColor = UIColor.white 23 | 24 | Gallery.Config.VideoEditor.savesEditedVideoToLibrary = true 25 | Gallery.Config.Camera.recordMode = .video 26 | 27 | button = UIButton(type: .system) 28 | button.frame.size = CGSize(width: 200, height: 50) 29 | button.setTitle("Open Gallery", for: UIControlState()) 30 | button.addTarget(self, action: #selector(buttonTouched(_:)), for: .touchUpInside) 31 | 32 | view.addSubview(button) 33 | } 34 | 35 | override func viewDidLayoutSubviews() { 36 | super.viewDidLayoutSubviews() 37 | 38 | button.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2) 39 | } 40 | 41 | func buttonTouched(_ button: UIButton) { 42 | gallery = GalleryController() 43 | //gallery.delegate = self 44 | gallery.delegate2 = self 45 | present(gallery, animated: true, completion: nil) 46 | } 47 | 48 | // MARK: - LightboxControllerDismissalDelegate 49 | 50 | func lightboxControllerWillDismiss(_ controller: LightboxController) { 51 | gallery.reload(controller.images.flatMap({ $0.image })) 52 | } 53 | 54 | // MARK: - GalleryControllerDelegate 55 | 56 | func galleryControllerDidCancel(_ controller: GalleryController) { 57 | controller.dismiss(animated: true, completion: nil) 58 | gallery = nil 59 | } 60 | 61 | func galleryController(_ controller: GalleryController, didSelectVideo video: Video) { 62 | controller.dismiss(animated: true, completion: nil) 63 | gallery = nil 64 | 65 | 66 | editor.edit(video: video) { (editedVideo: Video?, tempPath: URL?) in 67 | DispatchQueue.main.async { 68 | print(editedVideo) 69 | if let tempPath = tempPath { 70 | let data = NSData(contentsOf: tempPath) 71 | print(data?.length) 72 | let controller = AVPlayerViewController() 73 | controller.player = AVPlayer(url: tempPath) 74 | 75 | self.present(controller, animated: true, completion: nil) 76 | } 77 | } 78 | } 79 | } 80 | 81 | func galleryController(_ controller: GalleryController, didSelectImages images: [UIImage]) { 82 | controller.dismiss(animated: true, completion: nil) 83 | gallery = nil 84 | } 85 | 86 | func galleryController(_ controller: GalleryController, requestLightbox images: [UIImage]) { 87 | LightboxConfig.DeleteButton.enabled = true 88 | 89 | let lightbox = LightboxController(images: images.map({ LightboxImage(image: $0) }), startIndex: 0) 90 | lightbox.dismissalDelegate = self 91 | 92 | controller.dismiss(animated: true, completion: nil) 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Sources/Utils/Constraints.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | func g_ancestors() -> [UIView] { 6 | var current = self 7 | var views = [current] 8 | while let superview = current.superview { 9 | views.append(superview) 10 | current = superview 11 | } 12 | 13 | return views 14 | } 15 | 16 | func g_commonAncestor(view: UIView) -> UIView? { 17 | let viewAncestors = view.g_ancestors() 18 | 19 | return g_ancestors() 20 | .filter { 21 | return viewAncestors.contains($0) 22 | }.first 23 | } 24 | 25 | @discardableResult func g_pin(on type1: NSLayoutAttribute, 26 | view: UIView? = nil, on type2: NSLayoutAttribute? = nil, 27 | constant: CGFloat = 0, 28 | priority: Float? = nil) -> NSLayoutConstraint? { 29 | guard let view = view ?? superview, 30 | let commonAncestor = g_commonAncestor(view: view) 31 | else { return nil } 32 | 33 | translatesAutoresizingMaskIntoConstraints = false 34 | let type2 = type2 ?? type1 35 | let constraint = NSLayoutConstraint(item: self, attribute: type1, 36 | relatedBy: .equal, 37 | toItem: view, attribute: type2, 38 | multiplier: 1, constant: constant) 39 | if let priority = priority { 40 | constraint.priority = priority 41 | } 42 | 43 | commonAncestor.addConstraint(constraint) 44 | 45 | return constraint 46 | } 47 | 48 | func g_pinEdges(view: UIView? = nil) { 49 | g_pin(on: .top, view: view) 50 | g_pin(on: .bottom, view: view) 51 | g_pin(on: .left, view: view) 52 | g_pin(on: .right, view: view) 53 | } 54 | 55 | func g_pin(size: CGSize) { 56 | g_pin(width: size.width) 57 | g_pin(height: size.height) 58 | } 59 | 60 | func g_pin(width: CGFloat) { 61 | translatesAutoresizingMaskIntoConstraints = false 62 | addConstraint(NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: width)) 63 | } 64 | 65 | func g_pin(height: CGFloat) { 66 | translatesAutoresizingMaskIntoConstraints = false 67 | addConstraint(NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height)) 68 | } 69 | 70 | func g_pin(greaterThanHeight height: CGFloat) { 71 | translatesAutoresizingMaskIntoConstraints = false 72 | addConstraint(NSLayoutConstraint(item: self, attribute: .height, relatedBy: .greaterThanOrEqual, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height)) 73 | } 74 | 75 | func g_pinHorizontally(view: UIView? = nil, padding: CGFloat) { 76 | g_pin(on: .left, view: view, constant: padding) 77 | g_pin(on: .right, view: view, constant: -padding) 78 | } 79 | 80 | func g_pinUpward(view: UIView? = nil) { 81 | g_pin(on: .top, view: view) 82 | g_pin(on: .left, view: view) 83 | g_pin(on: .right, view: view) 84 | } 85 | 86 | func g_pinDownward(view: UIView? = nil) { 87 | g_pin(on: .bottom, view: view) 88 | g_pin(on: .left, view: view) 89 | g_pin(on: .right, view: view) 90 | } 91 | 92 | func g_pinCenter(view: UIView? = nil) { 93 | g_pin(on: .centerX, view: view) 94 | g_pin(on: .centerY, view: view) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Utils/Dropdown/DropdownController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | protocol DropdownControllerDelegate: class { 5 | func dropdownController(_ controller: DropdownController, didSelect album: Album) 6 | } 7 | 8 | class DropdownController: UIViewController { 9 | 10 | lazy var tableView: UITableView = self.makeTableView() 11 | lazy var blurView: UIVisualEffectView = self.makeBlurView() 12 | 13 | var animating: Bool = false 14 | var expanding: Bool = false 15 | var selectedIndex: Int = 0 16 | 17 | var albums: [Album] = [] { 18 | didSet { 19 | selectedIndex = 0 20 | } 21 | } 22 | 23 | var topConstraint: NSLayoutConstraint? 24 | weak var delegate: DropdownControllerDelegate? 25 | 26 | // MARK: - Initialization 27 | 28 | // MARK: - Life cycle 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | setup() 34 | } 35 | 36 | // MARK: - Setup 37 | 38 | func setup() { 39 | view.backgroundColor = UIColor.clear 40 | tableView.backgroundColor = UIColor.clear 41 | tableView.backgroundView = blurView 42 | 43 | view.addSubview(tableView) 44 | tableView.register(AlbumCell.self, forCellReuseIdentifier: String(describing: AlbumCell.self)) 45 | 46 | tableView.g_pinEdges() 47 | } 48 | 49 | // MARK: - Logic 50 | 51 | func toggle() { 52 | guard !animating else { return } 53 | 54 | animating = true 55 | expanding = !expanding 56 | 57 | self.topConstraint?.constant = expanding ? 1 : view.bounds.size.height 58 | 59 | UIView.animate(withDuration: 0.25, delay: 0, options: UIViewAnimationOptions(), animations: { 60 | self.view.superview?.layoutIfNeeded() 61 | }, completion: { finished in 62 | self.animating = false 63 | }) 64 | } 65 | 66 | // MARK: - Controls 67 | 68 | func makeTableView() -> UITableView { 69 | let tableView = UITableView() 70 | tableView.tableFooterView = UIView() 71 | tableView.separatorStyle = .none 72 | tableView.rowHeight = 84 73 | 74 | tableView.dataSource = self 75 | tableView.delegate = self 76 | 77 | return tableView 78 | } 79 | 80 | func makeBlurView() -> UIVisualEffectView { 81 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight)) 82 | 83 | return view 84 | } 85 | } 86 | 87 | extension DropdownController: UITableViewDataSource, UITableViewDelegate { 88 | 89 | // MARK: - UITableViewDataSource 90 | 91 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 92 | return albums.count 93 | } 94 | 95 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 96 | let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AlbumCell.self), for: indexPath) 97 | as! AlbumCell 98 | 99 | let album = albums[(indexPath as NSIndexPath).row] 100 | cell.configure(album) 101 | cell.backgroundColor = UIColor.clear 102 | 103 | return cell 104 | } 105 | 106 | // MARK: - UITableViewDelegate 107 | 108 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 109 | tableView.deselectRow(at: indexPath, animated: true) 110 | 111 | let album = albums[(indexPath as NSIndexPath).row] 112 | delegate?.dropdownController(self, didSelect: album) 113 | 114 | selectedIndex = (indexPath as NSIndexPath).row 115 | tableView.reloadData() 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /Sources/Utils/Pages/PageIndicator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol PageIndicatorDelegate: class { 4 | func pageIndicator(_ pageIndicator: PageIndicator, didSelect index: Int) 5 | } 6 | 7 | class PageIndicator: UIView { 8 | 9 | let items: [String] 10 | var buttons: [UIButton]! 11 | lazy var indicator: UIImageView = self.makeIndicator() 12 | weak var delegate: PageIndicatorDelegate? 13 | 14 | // MARK: - Initialization 15 | 16 | required init(items: [String]) { 17 | self.items = items 18 | 19 | super.init(frame: .zero) 20 | 21 | setup() 22 | } 23 | 24 | required init?(coder aDecoder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | // MARK: - Layout 29 | 30 | override func layoutSubviews() { 31 | super.layoutSubviews() 32 | 33 | let width = bounds.size.width / CGFloat(buttons.count) 34 | 35 | for (i, button) in buttons.enumerated() { 36 | 37 | button.frame = CGRect(x: width * CGFloat(i), 38 | y: 0, 39 | width: width, 40 | height: bounds.size.height) 41 | } 42 | 43 | indicator.frame.size = CGSize(width: width / 1.5, height: 4) 44 | indicator.frame.origin.y = bounds.size.height - indicator.frame.size.height 45 | 46 | if indicator.frame.origin.x == 0 { 47 | select(index: 0) 48 | } 49 | } 50 | 51 | override func didMoveToSuperview() { 52 | super.didMoveToSuperview() 53 | } 54 | 55 | // MARK: - Setup 56 | 57 | func setup() { 58 | buttons = items.map { 59 | let button = self.makeButton($0) 60 | addSubview(button) 61 | 62 | return button 63 | } 64 | 65 | addSubview(indicator) 66 | } 67 | 68 | // MARK: - Controls 69 | 70 | func makeButton(_ title: String) -> UIButton { 71 | let button = UIButton(type: .custom) 72 | button.setTitle(title, for: UIControlState()) 73 | button.setTitleColor(Config.PageIndicator.textColor, for: UIControlState()) 74 | button.setTitleColor(UIColor.gray, for: .highlighted) 75 | button.backgroundColor = Config.PageIndicator.backgroundColor 76 | button.addTarget(self, action: #selector(buttonTouched(_:)), for: .touchUpInside) 77 | button.titleLabel?.font = buttonFont(false) 78 | 79 | return button 80 | } 81 | 82 | func makeIndicator() -> UIImageView { 83 | let imageView = UIImageView(image: Bundle.image("gallery_page_indicator")) 84 | 85 | return imageView 86 | } 87 | 88 | // MARK: - Action 89 | 90 | func buttonTouched(_ button: UIButton) { 91 | let index = buttons.index(of: button) ?? 0 92 | delegate?.pageIndicator(self, didSelect: index) 93 | select(index: index) 94 | } 95 | 96 | // MARK: - Logic 97 | 98 | func select(index: Int) { 99 | for (i, b) in buttons.enumerated() { 100 | b.titleLabel?.font = buttonFont(i == index) 101 | } 102 | 103 | UIView.animate(withDuration: 0.25, delay: 0, 104 | usingSpringWithDamping: 0.7, 105 | initialSpringVelocity: 0.5, 106 | options: [], 107 | animations: 108 | { 109 | self.indicator.center.x = self.buttons[index].center.x 110 | }, completion: nil) 111 | } 112 | 113 | // MARK: - Helper 114 | 115 | func buttonFont(_ selected: Bool) -> UIFont { 116 | return selected ? Config.Font.Main.bold.withSize(14) : Config.Font.Main.regular.withSize(14) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo.xcodeproj/xcshareddata/xcschemes/GalleryDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Sources/Utils/ClosuredAVCaptureMovieFileOutput.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import Photos 4 | 5 | public class ClosuredAVCaptureMovieFileOutput: NSObject, AVCaptureFileOutputRecordingDelegate { 6 | 7 | private let output: AVCaptureMovieFileOutput 8 | private let queue: DispatchQueue 9 | 10 | private var videoRecordStartedCompletion: ((Bool) -> Void)? 11 | private var videoRecordCompletion: ((URL?) -> Void)? 12 | 13 | public init(sessionQueue: DispatchQueue) { 14 | self.queue = sessionQueue 15 | self.output = AVCaptureMovieFileOutput() 16 | self.output.minFreeDiskSpaceLimit = 1024 * 1024 17 | self.output.movieFragmentInterval = kCMTimeInvalid 18 | 19 | if let maxLengthInSecondsFound = Config.VideoRecording.maxLengthInSeconds { 20 | self.output.maxRecordedDuration = CMTimeMakeWithSeconds(Float64(maxLengthInSecondsFound), Int32(30)) 21 | } 22 | 23 | if let maxBytesCountFound = Config.VideoRecording.maxBytesCount { 24 | self.output.maxRecordedFileSize = maxBytesCountFound 25 | } 26 | } 27 | 28 | public func addToSession(_ session: AVCaptureSession) { 29 | if session.canAddOutput(output) { 30 | session.addOutput(output) 31 | } 32 | 33 | if Permission.Microphone.hasPermission { 34 | if let audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio) { 35 | if let audioInput = try? AVCaptureDeviceInput(device: audioDevice) { 36 | if session.canAddInput(audioInput){ 37 | session.addInput(audioInput) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | public func isRecording() -> Bool { 45 | return output.isRecording 46 | } 47 | 48 | public func startRecording(startCompletion: ((Bool) -> Void)?, stopCompletion: ((URL?) -> Void)?) { 49 | 50 | guard let connection = output.connection(withMediaType: AVMediaTypeVideo) else { 51 | startCompletion?(false) 52 | return 53 | } 54 | 55 | connection.videoOrientation = Utils.videoOrientation() 56 | 57 | self.videoRecordCompletion = stopCompletion 58 | 59 | queue.async { 60 | if let url = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("movie.mov") { 61 | if FileManager.default.fileExists(atPath: url.absoluteString) { 62 | try? FileManager.default.removeItem(at: url) 63 | } 64 | self.videoRecordStartedCompletion = startCompletion 65 | self.output.startRecording(toOutputFileURL: url, recordingDelegate: self) 66 | } else { 67 | DispatchQueue.main.async { startCompletion?(false) } 68 | } 69 | } 70 | } 71 | 72 | public func stopVideoRecording() { 73 | queue.async { 74 | self.output.stopRecording() 75 | } 76 | } 77 | 78 | public func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) { 79 | self.videoRecordStartedCompletion?(false) 80 | self.videoRecordStartedCompletion = nil 81 | } 82 | 83 | public func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) { 84 | if error == nil { 85 | DispatchQueue.main.async { 86 | self.videoRecordCompletion?(outputFileURL) 87 | self.videoRecordCompletion = nil 88 | } 89 | } else { 90 | let finishedSuccesfully = recodringFinishedWithSuccess(error) 91 | DispatchQueue.main.async { 92 | self.videoRecordCompletion?(finishedSuccesfully ? outputFileURL : nil) 93 | self.videoRecordCompletion = nil 94 | } 95 | } 96 | } 97 | 98 | private func recodringFinishedWithSuccess(_ error: Error) -> Bool { 99 | let nserror = error as NSError 100 | let success = nserror.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool 101 | if nserror.domain == AVFoundationErrorDomain, let successFound = success, successFound { 102 | return true 103 | } 104 | return false 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Gallery.xcodeproj/xcshareddata/xcschemes/Gallery-Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Gallery.xcodeproj/xcshareddata/xcschemes/Gallery-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Sources/Utils/View/GridView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class GridView: UIView { 5 | 6 | // MARK: - Initialization 7 | 8 | lazy var topView: UIView = self.makeTopView() 9 | lazy var bottomView: UIView = self.makeBottomView() 10 | lazy var bottomBlurView: UIVisualEffectView = self.makeBottomBlurView() 11 | lazy var arrowButton: ArrowButton = self.makeArrowButton() 12 | lazy var collectionView: UICollectionView = self.makeCollectionView() 13 | lazy var closeButton: UIButton = self.makeCloseButton() 14 | lazy var doneButton: UIButton = self.makeDoneButton() 15 | lazy var emptyView: UIView = self.makeEmptyView() 16 | 17 | // MARK: - Initialization 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | 22 | setup() 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | // MARK: - Setup 30 | 31 | func setup() { 32 | backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) 33 | 34 | [collectionView, bottomView, topView, emptyView].forEach { 35 | addSubview($0) 36 | } 37 | 38 | [closeButton, arrowButton].forEach { 39 | topView.addSubview($0) 40 | } 41 | 42 | [bottomBlurView, doneButton].forEach { 43 | bottomView.addSubview($0 as! UIView) 44 | } 45 | 46 | topView.g_pinUpward() 47 | topView.g_pin(height: 40) 48 | bottomView.g_pinDownward() 49 | bottomView.g_pin(height: 80) 50 | 51 | emptyView.g_pinEdges(view: collectionView) 52 | collectionView.g_pin(on: .left) 53 | collectionView.g_pin(on: .right) 54 | collectionView.g_pin(on: .bottom) 55 | collectionView.g_pin(on: .top, view: topView, on: .bottom, constant: 1) 56 | 57 | bottomBlurView.g_pinEdges() 58 | 59 | closeButton.g_pin(on: .top) 60 | closeButton.g_pin(on: .left) 61 | closeButton.g_pin(size: CGSize(width: 40, height: 40)) 62 | 63 | arrowButton.g_pinCenter() 64 | arrowButton.g_pin(height: 40) 65 | 66 | doneButton.g_pin(on: .centerY) 67 | doneButton.g_pin(on: .right, constant: -38) 68 | } 69 | 70 | // MARK: - Controls 71 | 72 | func makeTopView() -> UIView { 73 | let view = UIView() 74 | view.backgroundColor = UIColor.white 75 | 76 | return view 77 | } 78 | 79 | func makeBottomView() -> UIView { 80 | let view = UIView() 81 | 82 | return view 83 | } 84 | 85 | func makeBottomBlurView() -> UIVisualEffectView { 86 | let view = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) 87 | 88 | return view 89 | } 90 | 91 | func makeArrowButton() -> ArrowButton { 92 | let button = ArrowButton() 93 | button.layoutSubviews() 94 | 95 | return button 96 | } 97 | 98 | func makeGridView() -> GridView { 99 | let view = GridView() 100 | 101 | return view 102 | } 103 | 104 | func makeCloseButton() -> UIButton { 105 | let button = UIButton(type: .custom) 106 | button.setImage(Bundle.image("gallery_close")?.withRenderingMode(.alwaysTemplate), for: UIControlState()) 107 | button.tintColor = Config.Grid.CloseButton.tintColor 108 | 109 | return button 110 | } 111 | 112 | func makeDoneButton() -> UIButton { 113 | let button = UIButton(type: .system) 114 | button.setTitleColor(UIColor.white, for: UIControlState()) 115 | button.setTitleColor(UIColor.lightGray, for: .disabled) 116 | button.titleLabel?.font = Config.Font.Text.regular.withSize(16) 117 | button.setTitle("Gallery.Done".g_localize(fallback: "Done"), for: UIControlState()) 118 | 119 | return button 120 | } 121 | 122 | func makeCollectionView() -> UICollectionView { 123 | let layout = UICollectionViewFlowLayout() 124 | layout.minimumInteritemSpacing = 2 125 | layout.minimumLineSpacing = 2 126 | 127 | let view = UICollectionView(frame: .zero, collectionViewLayout: layout) 128 | view.backgroundColor = UIColor.white 129 | 130 | return view 131 | } 132 | 133 | func makeEmptyView() -> EmptyView { 134 | let view = EmptyView() 135 | view.isHidden = true 136 | 137 | return view 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Utils/Config.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | 4 | public struct Config { 5 | 6 | public struct PageIndicator { 7 | public static var backgroundColor: UIColor = UIColor(red: 0, green: 3/255, blue: 10/255, alpha: 1) 8 | public static var textColor: UIColor = UIColor.white 9 | public static var customCameraLabel: String? 10 | } 11 | 12 | public struct Selection { 13 | 14 | public enum Mode { case photo, video, camera } 15 | 16 | public static var mode: Set = [.photo, .camera, .video] 17 | } 18 | 19 | public struct Camera { 20 | 21 | public static var recordLocation: Bool = false 22 | 23 | public enum RecordMode { case photo, video } 24 | 25 | public static var recordMode = RecordMode.photo 26 | 27 | public struct ShutterButton { 28 | public static var numberColor: UIColor = UIColor(red: 54/255, green: 56/255, blue: 62/255, alpha: 1) 29 | } 30 | 31 | public struct BottomContainer { 32 | public static var backgroundColor: UIColor = UIColor(red: 23/255, green: 25/255, blue: 28/255, alpha: 0.8) 33 | } 34 | 35 | public struct StackView { 36 | public static let imageCount: Int = 4 37 | } 38 | } 39 | 40 | public struct Grid { 41 | 42 | public struct CloseButton { 43 | public static var tintColor: UIColor = UIColor(red: 109/255, green: 107/255, blue: 132/255, alpha: 1) 44 | } 45 | 46 | public struct ArrowButton { 47 | public static var tintColor: UIColor = UIColor(red: 110/255, green: 117/255, blue: 131/255, alpha: 1) 48 | } 49 | 50 | public struct FrameView { 51 | public static var fillColor: UIColor = UIColor(red: 50/255, green: 51/255, blue: 59/255, alpha: 1) 52 | public static var borderColor: UIColor = UIColor(red: 0, green: 239/255, blue: 155/255, alpha: 1) 53 | } 54 | 55 | struct Dimension { 56 | static let columnCount: CGFloat = 4 57 | static let cellSpacing: CGFloat = 2 58 | } 59 | } 60 | 61 | public struct EmptyView { 62 | public static var image: UIImage? = Bundle.image("gallery_empty_view_image") 63 | public static var textColor: UIColor = UIColor(red: 102/255, green: 118/255, blue: 138/255, alpha: 1) 64 | } 65 | 66 | public struct Permission { 67 | public static var shouldCheckPermission = true 68 | public static var image: UIImage? = Bundle.image("gallery_permission_view_camera") 69 | public static var textColor: UIColor = UIColor(red: 102/255, green: 118/255, blue: 138/255, alpha: 1) 70 | 71 | public struct Button { 72 | public static var textColor: UIColor = UIColor.white 73 | public static var highlightedTextColor: UIColor = UIColor.lightGray 74 | public static var backgroundColor = UIColor(red: 40/255, green: 170/255, blue: 236/255, alpha: 1) 75 | } 76 | } 77 | 78 | public struct Font { 79 | 80 | public struct Main { 81 | public static var light: UIFont = UIFont.systemFont(ofSize: 1) 82 | public static var regular: UIFont = UIFont.systemFont(ofSize: 1) 83 | public static var bold: UIFont = UIFont.boldSystemFont(ofSize: 1) 84 | public static var medium: UIFont = UIFont.boldSystemFont(ofSize: 1) 85 | } 86 | 87 | public struct Text { 88 | public static var regular: UIFont = UIFont.systemFont(ofSize: 1) 89 | public static var bold: UIFont = UIFont.boldSystemFont(ofSize: 1) 90 | public static var semibold: UIFont = UIFont.boldSystemFont(ofSize: 1) 91 | } 92 | } 93 | 94 | public struct Fetch { 95 | public static var limit: Int? = nil 96 | } 97 | 98 | public struct SessionPreset { 99 | public static var quality: String = AVCaptureSessionPresetHigh 100 | } 101 | 102 | public struct VideoRecording { 103 | 104 | public static var maxBytesCount: Int64? 105 | public static var maxLengthInSeconds: Int? 106 | } 107 | 108 | public struct VideoEditor { 109 | 110 | public static var quality: String = AVAssetExportPresetHighestQuality 111 | public static var savesEditedVideoToLibrary: Bool = false 112 | public static var maximumDuration: TimeInterval = 15 113 | public static var portraitSize: CGSize = CGSize(width: 360, height: 640) 114 | public static var landscapeSize: CGSize = CGSize(width: 640, height: 360) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Utils/VideoEditor/EditInfo.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | 4 | struct EditInfo { 5 | 6 | // MARK: - Basic 7 | 8 | static func composition(_ avAsset: AVAsset) -> AVVideoComposition? { 9 | guard let track = avAsset.tracks(withMediaType: AVMediaTypeVideo).first else { return nil } 10 | 11 | let cropInfo = EditInfo.cropInfo(avAsset) 12 | 13 | let layer = AVMutableVideoCompositionLayerInstruction(assetTrack: track) 14 | layer.setTransform(EditInfo.transform(avAsset, scale: cropInfo.scale), at: kCMTimeZero) 15 | 16 | let instruction = AVMutableVideoCompositionInstruction() 17 | instruction.layerInstructions = [layer] 18 | instruction.timeRange = timeRange(avAsset) 19 | 20 | let composition = AVMutableVideoComposition(propertiesOf: avAsset) 21 | composition.instructions = [instruction] 22 | composition.renderSize = cropInfo.size 23 | 24 | return composition 25 | } 26 | 27 | static func cropInfo(_ avAsset: AVAsset) -> (size: CGSize, scale: CGFloat) { 28 | let desiredSize = avAsset.g_isPortrait ? Config.VideoEditor.portraitSize : Config.VideoEditor.landscapeSize 29 | let avAssetSize = avAsset.g_correctSize 30 | 31 | let scale = min(desiredSize.width / avAssetSize.width, desiredSize.height / avAssetSize.height) 32 | let size = CGSize(width: avAssetSize.width*scale, height: avAssetSize.height*scale) 33 | 34 | return (size: size, scale: scale) 35 | } 36 | 37 | static func transform(_ avAsset: AVAsset, scale: CGFloat) -> CGAffineTransform { 38 | let offset: CGPoint 39 | let angle: Double 40 | 41 | switch avAsset.g_orientation { 42 | case .landscapeLeft: 43 | offset = CGPoint(x: avAsset.g_correctSize.width, y: avAsset.g_correctSize.height) 44 | angle = M_PI 45 | case .landscapeRight: 46 | offset = CGPoint.zero 47 | angle = 0 48 | case .portraitUpsideDown: 49 | offset = CGPoint(x: 0, y: avAsset.g_correctSize.height) 50 | angle = -M_PI_2 51 | default: 52 | offset = CGPoint(x: avAsset.g_correctSize.width, y: 0) 53 | angle = M_PI_2 54 | } 55 | 56 | let scaleTransform = CGAffineTransform(scaleX: scale, y: scale) 57 | let translationTransform = scaleTransform.translatedBy(x: offset.x, y: offset.y) 58 | let rotationTransform = translationTransform.rotated(by: CGFloat(angle)) 59 | 60 | return rotationTransform 61 | } 62 | 63 | static func presetName(_ avAsset: AVAsset) -> String { 64 | let availablePresets = AVAssetExportSession.exportPresets(compatibleWith: avAsset) 65 | 66 | if availablePresets.contains(preferredPresetName) { 67 | return preferredPresetName 68 | } else { 69 | return availablePresets.first ?? AVAssetExportPresetHighestQuality 70 | } 71 | } 72 | 73 | static var preferredPresetName: String { 74 | return Config.VideoEditor.quality 75 | } 76 | 77 | static func timeRange(_ avAsset: AVAsset) -> CMTimeRange { 78 | var end = avAsset.duration 79 | 80 | if Config.VideoEditor.maximumDuration < avAsset.duration.seconds { 81 | end = CMTime(seconds: Config.VideoEditor.maximumDuration, preferredTimescale: 1000) 82 | } 83 | 84 | return CMTimeRange(start: kCMTimeZero, duration: end) 85 | } 86 | 87 | static var file: (type: String, pathExtension: String) { 88 | return (type: AVFileTypeMPEG4, pathExtension: "mp4") 89 | } 90 | 91 | static var outputURL: URL? { 92 | return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 93 | .appendingPathComponent(UUID().uuidString) 94 | .appendingPathExtension(file.pathExtension) 95 | } 96 | 97 | // MARK: - Advanced 98 | 99 | static var audioSettings: [String: AnyObject] { 100 | return [ 101 | AVFormatIDKey: NSNumber(value: Int(kAudioFormatMPEG4AAC) as Int), 102 | AVNumberOfChannelsKey: NSNumber(value: 2 as Int), 103 | AVSampleRateKey: NSNumber(value: 44100 as Int), 104 | AVEncoderBitRateKey: NSNumber(value: 128000 as Int) 105 | ] 106 | } 107 | 108 | static var videoSettings: [String: AnyObject] { 109 | let compression: [String: Any] = [ 110 | AVVideoAverageBitRateKey: NSNumber(value: 6000000), 111 | AVVideoProfileLevelKey: AVVideoProfileLevelH264High40 112 | ] 113 | 114 | return [ 115 | AVVideoCodecKey: AVVideoCodecH264 as AnyObject, 116 | AVVideoWidthKey: NSNumber(value: 1920 as Int), 117 | AVVideoHeightKey: NSNumber(value: 1080 as Int), 118 | AVVideoCompressionPropertiesKey: compression as AnyObject 119 | ] 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /Sources/Utils/Pages/PagesController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol PageAware: class { 4 | func pageDidShow() 5 | func pageDidHide() 6 | } 7 | 8 | class PagesController: UIViewController { 9 | 10 | let controllers: [UIViewController] 11 | 12 | lazy var scrollView: UIScrollView = self.makeScrollView() 13 | lazy var scrollViewContentView: UIView = UIView() 14 | lazy var pageIndicator: PageIndicator = self.makePageIndicator() 15 | 16 | var selectedIndex: Int = 0 17 | let once = Once() 18 | 19 | // MARK: - Initialization 20 | 21 | required init(controllers: [UIViewController]) { 22 | self.controllers = controllers 23 | 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | // MARK: - Life cycle 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | setup() 37 | } 38 | 39 | override func viewDidLayoutSubviews() { 40 | super.viewDidLayoutSubviews() 41 | 42 | if scrollView.frame.size.width > 0 { 43 | once.run { 44 | goAndNotify() 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Controls 50 | 51 | func makeScrollView() -> UIScrollView { 52 | let scrollView = UIScrollView() 53 | scrollView.isPagingEnabled = true 54 | scrollView.showsHorizontalScrollIndicator = false 55 | scrollView.alwaysBounceHorizontal = false 56 | scrollView.bounces = false 57 | scrollView.delegate = self 58 | 59 | return scrollView 60 | } 61 | 62 | func makePageIndicator() -> PageIndicator { 63 | let items = controllers.flatMap { $0.title } 64 | let indicator = PageIndicator(items: items) 65 | indicator.delegate = self 66 | 67 | return indicator 68 | } 69 | 70 | // MARK: - Setup 71 | 72 | func setup() { 73 | view.addSubview(pageIndicator) 74 | view.addSubview(scrollView) 75 | scrollView.addSubview(scrollViewContentView) 76 | 77 | pageIndicator.g_pinDownward() 78 | pageIndicator.g_pin(height: 40) 79 | 80 | scrollView.g_pinUpward() 81 | scrollView.g_pin(on: .bottom, view: pageIndicator, on: .top) 82 | 83 | scrollViewContentView.g_pinEdges() 84 | 85 | for (i, controller) in controllers.enumerated() { 86 | addChildViewController(controller) 87 | scrollViewContentView.addSubview(controller.view) 88 | controller.didMove(toParentViewController: self) 89 | 90 | controller.view.g_pin(on: .top) 91 | controller.view.g_pin(on: .bottom) 92 | controller.view.g_pin(on: .width, view: scrollView) 93 | controller.view.g_pin(on: .height, view: scrollView) 94 | 95 | if i == 0 { 96 | controller.view.g_pin(on: .left) 97 | } else { 98 | controller.view.g_pin(on: .left, view: self.controllers[i-1].view, on: .right) 99 | } 100 | 101 | if i == self.controllers.count - 1 { 102 | controller.view.g_pin(on: .right) 103 | } 104 | } 105 | } 106 | 107 | // MARK: - Index 108 | 109 | func goAndNotify() { 110 | let point = CGPoint(x: scrollView.frame.size.width * CGFloat(selectedIndex), y: scrollView.contentOffset.y) 111 | 112 | DispatchQueue.main.async { 113 | self.scrollView.setContentOffset(point, animated: false) 114 | } 115 | 116 | notifyShow() 117 | } 118 | 119 | func updateAndNotify(_ index: Int) { 120 | guard selectedIndex != index else { return } 121 | notifyHide() 122 | selectedIndex = index 123 | notifyShow() 124 | } 125 | 126 | func notifyShow() { 127 | if let controller = controllers[selectedIndex] as? PageAware { 128 | controller.pageDidShow() 129 | } 130 | } 131 | 132 | func notifyHide() { 133 | if let controller = controllers[selectedIndex] as? PageAware { 134 | controller.pageDidHide() 135 | } 136 | } 137 | } 138 | 139 | extension PagesController: PageIndicatorDelegate { 140 | 141 | func pageIndicator(_ pageIndicator: PageIndicator, didSelect index: Int) { 142 | let point = CGPoint(x: scrollView.frame.size.width * CGFloat(index), y: scrollView.contentOffset.y) 143 | scrollView.setContentOffset(point, animated: false) 144 | updateAndNotify(index) 145 | } 146 | 147 | } 148 | 149 | extension PagesController: UIScrollViewDelegate { 150 | 151 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 152 | let index = Int(round(scrollView.contentOffset.x / scrollView.frame.size.width)) 153 | pageIndicator.select(index: index) 154 | updateAndNotify(index) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/Camera/StackView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class StackView: UIControl{ 5 | 6 | lazy var indicator: UIActivityIndicatorView = self.makeIndicator() 7 | lazy var imageViews: [UIImageView] = self.makeImageViews() 8 | lazy var countLabel: UILabel = self.makeCountLabel() 9 | lazy var tapGR: UITapGestureRecognizer = self.makeTapGR() 10 | 11 | // MARK: - Initialization 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | setup() 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | // MARK: - Setup 24 | 25 | func setup() { 26 | addGestureRecognizer(tapGR) 27 | imageViews.forEach { 28 | addSubview($0) 29 | } 30 | 31 | [countLabel, indicator].forEach { 32 | self.addSubview($0) 33 | } 34 | } 35 | 36 | // MARK: - Layout 37 | 38 | override func layoutSubviews() { 39 | super.layoutSubviews() 40 | 41 | let step: CGFloat = 3.0 42 | let scale: CGFloat = 0.8 43 | let imageViewSize = CGSize(width: frame.width * scale, 44 | height: frame.height * scale) 45 | 46 | for (index, imageView) in imageViews.enumerated() { 47 | let origin = CGPoint(x: CGFloat(index) * step, 48 | y: CGFloat(imageViews.count - index) * step) 49 | imageView.frame = CGRect(origin: origin, size: imageViewSize) 50 | } 51 | } 52 | 53 | // MARK: - Action 54 | 55 | func viewTapped(_ gr: UITapGestureRecognizer) { 56 | sendActions(for: .touchUpInside) 57 | } 58 | 59 | // MARK: - Logic 60 | 61 | func startLoading() { 62 | if let topVisibleView = imageViews.filter({ $0.alpha == 1.0 }).last { 63 | indicator.center = topVisibleView.center 64 | } else if let first = imageViews.first { 65 | indicator.center = first.center 66 | } 67 | 68 | indicator.startAnimating() 69 | UIView.animate(withDuration: 0.3, animations: { 70 | self.indicator.alpha = 1.0 71 | }) 72 | } 73 | 74 | func stopLoading() { 75 | indicator.stopAnimating() 76 | indicator.alpha = 0 77 | } 78 | 79 | func renderViews(_ assets: [PHAsset]) { 80 | let photos = Array(assets.suffix(Config.Camera.StackView.imageCount)) 81 | 82 | for (index, view) in imageViews.enumerated() { 83 | if index < photos.count { 84 | view.g_loadImage(photos[index]) 85 | view.alpha = 1 86 | } else { 87 | view.image = nil 88 | view.alpha = 0 89 | } 90 | } 91 | } 92 | 93 | fileprivate func animate(imageView: UIImageView) { 94 | imageView.transform = CGAffineTransform(scaleX: 0, y: 0) 95 | 96 | UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: [], animations: { 97 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.6) { 98 | imageView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) 99 | } 100 | 101 | UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.4) { 102 | imageView.transform = CGAffineTransform.identity 103 | } 104 | 105 | }, completion: { finished in 106 | 107 | }) 108 | } 109 | 110 | // MARK: - Reload 111 | 112 | func reload(_ images: [Image], added: Bool = false) { 113 | // Animate empty view 114 | if added { 115 | if let emptyView = imageViews.filter({ $0.image == nil }).first { 116 | animate(imageView: emptyView) 117 | } 118 | } 119 | 120 | // Update images into views 121 | renderViews(images.map { $0.asset }) 122 | 123 | // Update count label 124 | if let topVisibleView = imageViews.filter({ $0.alpha == 1.0 }).last , images.count > 1 { 125 | countLabel.text = "\(images.count)" 126 | countLabel.sizeToFit() 127 | countLabel.center = topVisibleView.center 128 | countLabel.g_quickFade() 129 | } else { 130 | countLabel.alpha = 0 131 | } 132 | } 133 | 134 | // MARK: - Controls 135 | 136 | func makeIndicator() -> UIActivityIndicatorView { 137 | let indicator = UIActivityIndicatorView() 138 | indicator.alpha = 0 139 | 140 | return indicator 141 | } 142 | 143 | func makeImageViews() -> [UIImageView] { 144 | return Array(0.. UILabel { 156 | let label = UILabel() 157 | label.textColor = UIColor.white 158 | label.font = Config.Font.Main.regular.withSize(20) 159 | label.textAlignment = .center 160 | label.g_addShadow() 161 | label.alpha = 0 162 | 163 | return label 164 | } 165 | 166 | func makeTapGR() -> UITapGestureRecognizer { 167 | let gr = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:))) 168 | 169 | return gr 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/Gallery/GalleryController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | import Photos 4 | 5 | public protocol GalleryControllerDelegate: class { 6 | 7 | func galleryController(_ controller: GalleryController, didSelectImages images: [UIImage]) 8 | func galleryController(_ controller: GalleryController, didSelectVideo video: Video) 9 | func galleryController(_ controller: GalleryController, requestLightbox images: [UIImage]) 10 | func galleryControllerDidCancel(_ controller: GalleryController) 11 | } 12 | 13 | public protocol GalleryControllerDelegate2: class { 14 | 15 | func galleryController(_ controller: GalleryController, requestLightbox images: [UIImage]) 16 | func galleryControllerDidCancel(_ controller: GalleryController) 17 | 18 | func galleryController(_ controller: GalleryController, didSelectAssets assets: [PHAsset]) 19 | } 20 | 21 | public class GalleryController: UIViewController, PermissionControllerDelegate { 22 | 23 | lazy var imagesController: ImagesController = self.makeImagesController() 24 | lazy var cameraController: CameraController = self.makeCameraController() 25 | lazy var videosController: VideosController = self.makeVideosController() 26 | 27 | enum Page: Int { 28 | case images, camera, videos 29 | } 30 | 31 | lazy var pagesController: PagesController = self.makePagesController() 32 | lazy var permissionController: PermissionController = self.makePermissionController() 33 | public weak var delegate: GalleryControllerDelegate? 34 | public weak var delegate2: GalleryControllerDelegate2? 35 | 36 | // MARK: - Life cycle 37 | 38 | public override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | setup() 42 | 43 | Permission.Microphone.didAsk = false 44 | 45 | if !Config.Permission.shouldCheckPermission || Permission.hasPermissions { 46 | showMain() 47 | } else { 48 | showPermissionView() 49 | } 50 | } 51 | 52 | deinit { 53 | Cart.shared.reset() 54 | } 55 | 56 | public override var prefersStatusBarHidden : Bool { 57 | return true 58 | } 59 | 60 | // MARK: - Logic 61 | 62 | public func reload(_ images: [UIImage]) { 63 | Cart.shared.reload(images) 64 | } 65 | 66 | func showMain() { 67 | g_addChildController(pagesController) 68 | } 69 | 70 | func showPermissionView() { 71 | g_addChildController(permissionController) 72 | } 73 | 74 | // MARK: - Child view controller 75 | 76 | func makeImagesController() -> ImagesController { 77 | let controller = ImagesController() 78 | controller.title = "Gallery.Images.Title".g_localize(fallback: "PHOTOS") 79 | Cart.shared.add(delegate: controller) 80 | 81 | return controller 82 | } 83 | 84 | func makeCameraController() -> CameraController { 85 | let controller = CameraController() 86 | if let customCameraTitleFound = Config.PageIndicator.customCameraLabel { 87 | controller.title = customCameraTitleFound 88 | } else { 89 | controller.title = "Gallery.Camera.Title".g_localize(fallback: "CAMERA") 90 | } 91 | Cart.shared.add(delegate: controller) 92 | 93 | return controller 94 | } 95 | 96 | func makeVideosController() -> VideosController { 97 | let controller = VideosController() 98 | controller.title = "Gallery.Videos.Title".g_localize(fallback: "VIDEOS") 99 | Cart.shared.add(delegate: controller) 100 | return controller 101 | } 102 | 103 | func makePagesController() -> PagesController { 104 | 105 | var controllers = [UIViewController]() 106 | 107 | if Config.Selection.mode.contains(.photo) { 108 | controllers.append(imagesController) 109 | } 110 | 111 | if Config.Selection.mode.contains(.camera) { 112 | controllers.append(cameraController) 113 | } 114 | 115 | if Config.Selection.mode.contains(.video) { 116 | controllers.append(videosController) 117 | } 118 | 119 | let controller = PagesController(controllers: controllers) 120 | 121 | if Config.Selection.mode.contains(.camera) { 122 | controller.selectedIndex = controllers.index(of: cameraController) ?? 0 123 | } 124 | 125 | return controller 126 | } 127 | 128 | func makePermissionController() -> PermissionController { 129 | let controller = PermissionController() 130 | controller.delegate = self 131 | 132 | return controller 133 | } 134 | 135 | // MARK: - Setup 136 | 137 | func setup() { 138 | EventHub.shared.close = { [weak self] in 139 | if let strongSelf = self { 140 | strongSelf.cameraController.stopVideoRecordingIfStarted() 141 | strongSelf.delegate?.galleryControllerDidCancel(strongSelf) 142 | strongSelf.delegate2?.galleryControllerDidCancel(strongSelf) 143 | } 144 | } 145 | 146 | EventHub.shared.doneWithImages = { [weak self] in 147 | if let strongSelf = self { 148 | strongSelf.delegate?.galleryController(strongSelf, didSelectImages: Cart.shared.UIImages()) 149 | strongSelf.delegate2?.galleryController(strongSelf, didSelectAssets: Cart.shared.assets()) 150 | } 151 | } 152 | 153 | EventHub.shared.doneWithVideos = { [weak self] in 154 | if let strongSelf = self, let video = Cart.shared.video { 155 | strongSelf.delegate?.galleryController(strongSelf, didSelectVideo: video) 156 | strongSelf.delegate2?.galleryController(strongSelf, didSelectAssets: [video.asset]) 157 | } 158 | } 159 | 160 | EventHub.shared.stackViewTouched = { [weak self] in 161 | if let strongSelf = self { 162 | strongSelf.delegate?.galleryController(strongSelf, requestLightbox: Cart.shared.UIImages()) 163 | } 164 | } 165 | } 166 | 167 | // MARK: - PermissionControllerDelegate 168 | 169 | func permissionControllerDidFinish(_ controller: PermissionController) { 170 | showMain() 171 | permissionController.g_removeFromParentController() 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/Utils/VideoEditor/AdvancedVideoEditor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import Photos 4 | 5 | public class AdvancedVideoEditor: VideoEditing { 6 | var writer: AVAssetWriter! 7 | var videoInput: AVAssetWriterInput? 8 | var audioInput: AVAssetWriterInput? 9 | 10 | var reader: AVAssetReader! 11 | var videoOutput: AVAssetReaderVideoCompositionOutput? 12 | var audioOutput: AVAssetReaderAudioMixOutput? 13 | 14 | var audioCompleted: Bool = false 15 | var videoCompleted: Bool = false 16 | 17 | let requestQueue = DispatchQueue(label: "no.hyper.Gallery.AdvancedVideoEditor.RequestQueue", qos: .background) 18 | let finishQueue = DispatchQueue(label: "no.hyper.Gallery.AdvancedVideoEditor.FinishQueue", qos: .background) 19 | 20 | // MARK: - Initialization 21 | 22 | public init() { 23 | 24 | } 25 | 26 | // MARK: - Edit 27 | 28 | public func edit(video: Video, completion: @escaping (_ video: Video?, _ tempPath: URL?) -> Void) { 29 | process(video: video, completion: completion) 30 | } 31 | 32 | public func crop(avAsset: AVAsset, completion: @escaping (URL?) -> Void) { 33 | guard let outputURL = EditInfo.outputURL else { 34 | completion(nil) 35 | return 36 | } 37 | 38 | guard let writer = try? AVAssetWriter(outputURL: outputURL as URL, fileType: EditInfo.file.type), 39 | let reader = try? AVAssetReader(asset: avAsset) 40 | else { 41 | completion(nil) 42 | return 43 | } 44 | 45 | // Config 46 | writer.shouldOptimizeForNetworkUse = true 47 | 48 | self.writer = writer 49 | self.reader = reader 50 | 51 | wire(avAsset) 52 | 53 | // Start 54 | writer.startWriting() 55 | reader.startReading() 56 | writer.startSession(atSourceTime: kCMTimeZero) 57 | 58 | // Video 59 | if let videoOutput = videoOutput, let videoInput = videoInput { 60 | videoInput.requestMediaDataWhenReady(on: requestQueue) { 61 | if !self.stream(from: videoOutput, to: videoInput) { 62 | self.finishQueue.async { 63 | self.videoCompleted = true 64 | if self.audioCompleted { 65 | self.finish(outputURL: outputURL, completion: completion) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | // Audio 73 | if let audioOutput = audioOutput, let audioInput = audioInput { 74 | audioInput.requestMediaDataWhenReady(on: requestQueue) { 75 | if !self.stream(from: audioOutput, to: audioInput) { 76 | self.finishQueue.async { 77 | self.audioCompleted = true 78 | if self.videoCompleted { 79 | self.finish(outputURL: outputURL, completion: completion) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | // MARK: - Finish 88 | 89 | fileprivate func finish(outputURL: URL, completion: @escaping (URL?) -> Void) { 90 | if reader.status == .failed { 91 | writer.cancelWriting() 92 | } 93 | 94 | guard reader.status != .cancelled 95 | && reader.status != .failed 96 | && writer.status != .cancelled 97 | && writer.status != .failed 98 | else { 99 | completion(nil) 100 | return 101 | } 102 | 103 | writer.finishWriting { 104 | switch self.writer.status { 105 | case .completed: 106 | completion(outputURL) 107 | default: 108 | completion(nil) 109 | } 110 | } 111 | } 112 | 113 | // MARK: - Helper 114 | 115 | fileprivate func wire(_ avAsset: AVAsset) { 116 | wireVideo(avAsset) 117 | wireAudio(avAsset) 118 | } 119 | 120 | fileprivate func wireVideo(_ avAsset: AVAsset) { 121 | let videoTracks = avAsset.tracks(withMediaType: AVMediaTypeVideo) 122 | if !videoTracks.isEmpty { 123 | // Output 124 | let videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: nil) 125 | videoOutput.videoComposition = EditInfo.composition(avAsset) 126 | if reader.canAdd(videoOutput) { 127 | reader.add(videoOutput) 128 | } 129 | 130 | // Input 131 | let videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, 132 | outputSettings: EditInfo.videoSettings, 133 | sourceFormatHint: avAsset.g_videoDescription) 134 | if writer.canAdd(videoInput) { 135 | writer.add(videoInput) 136 | } 137 | 138 | self.videoInput = videoInput 139 | self.videoOutput = videoOutput 140 | } 141 | } 142 | 143 | fileprivate func wireAudio(_ avAsset: AVAsset) { 144 | let audioTracks = avAsset.tracks(withMediaType: AVMediaTypeAudio) 145 | if !audioTracks.isEmpty { 146 | // Output 147 | let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) 148 | audioOutput.alwaysCopiesSampleData = true 149 | if reader.canAdd(audioOutput) { 150 | reader.add(audioOutput) 151 | } 152 | 153 | // Input 154 | let audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, 155 | outputSettings: EditInfo.audioSettings, 156 | sourceFormatHint: avAsset.g_audioDescription) 157 | if writer.canAdd(audioInput) { 158 | writer.add(audioInput) 159 | } 160 | 161 | self.audioOutput = audioOutput 162 | self.audioInput = audioInput 163 | } 164 | } 165 | 166 | fileprivate func stream(from output: AVAssetReaderOutput, to input: AVAssetWriterInput) -> Bool { 167 | while input.isReadyForMoreMediaData { 168 | guard reader.status == .reading && writer.status == .writing, 169 | let buffer = output.copyNextSampleBuffer() 170 | else { 171 | input.markAsFinished() 172 | return false 173 | } 174 | 175 | return input.append(buffer) 176 | } 177 | 178 | return true 179 | } 180 | } 181 | 182 | -------------------------------------------------------------------------------- /Sources/Videos/VideosController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | import AVKit 4 | 5 | class VideosController: UIViewController { 6 | 7 | lazy var gridView: GridView = self.makeGridView() 8 | lazy var videoBox: VideoBox = self.makeVideoBox() 9 | lazy var infoLabel: UILabel = self.makeInfoLabel() 10 | 11 | var items: [Video] = [] 12 | let library = VideosLibrary() 13 | let once = Once() 14 | 15 | // MARK: - Life cycle 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | setup() 21 | } 22 | 23 | // MARK: - Setup 24 | 25 | func setup() { 26 | view.backgroundColor = UIColor.white 27 | 28 | view.addSubview(gridView) 29 | 30 | [videoBox, infoLabel].forEach { 31 | gridView.bottomView.addSubview($0) 32 | } 33 | 34 | gridView.g_pinEdges() 35 | 36 | videoBox.g_pin(size: CGSize(width: 44, height: 44)) 37 | videoBox.g_pin(on: .centerY) 38 | videoBox.g_pin(on: .left, constant: 38) 39 | 40 | infoLabel.g_pin(on: .centerY) 41 | infoLabel.g_pin(on: .left, view: videoBox, on: .right, constant: 11) 42 | infoLabel.g_pin(on: .right, constant: -50) 43 | 44 | gridView.closeButton.addTarget(self, action: #selector(closeButtonTouched(_:)), for: .touchUpInside) 45 | gridView.doneButton.addTarget(self, action: #selector(doneButtonTouched(_:)), for: .touchUpInside) 46 | 47 | gridView.collectionView.dataSource = self 48 | gridView.collectionView.delegate = self 49 | gridView.collectionView.register(VideoCell.self, forCellWithReuseIdentifier: String(describing: VideoCell.self)) 50 | 51 | gridView.arrowButton.updateText("Gallery.AllVideos".g_localize(fallback: "ALL VIDEOS")) 52 | gridView.arrowButton.arrow.isHidden = true 53 | } 54 | 55 | // MARK: - Action 56 | 57 | func closeButtonTouched(_ button: UIButton) { 58 | EventHub.shared.close?() 59 | } 60 | 61 | func doneButtonTouched(_ button: UIButton) { 62 | EventHub.shared.doneWithVideos?() 63 | } 64 | 65 | // MARK: - View 66 | 67 | func refreshView() { 68 | if let selectedItem = Cart.shared.video { 69 | videoBox.imageView.g_loadImage(selectedItem.asset) 70 | } else { 71 | videoBox.imageView.image = nil 72 | } 73 | 74 | let hasVideo = (Cart.shared.video != nil) 75 | gridView.bottomView.g_fade(visible: hasVideo) 76 | gridView.collectionView.g_updateBottomInset(hasVideo ? gridView.bottomView.frame.size.height : 0) 77 | 78 | Cart.shared.video?.fetchDuration { [weak self] duration in 79 | self?.infoLabel.isHidden = duration <= Config.VideoEditor.maximumDuration 80 | } 81 | } 82 | 83 | // MARK: - Controls 84 | 85 | func makeGridView() -> GridView { 86 | let view = GridView() 87 | view.bottomView.alpha = 0 88 | 89 | return view 90 | } 91 | 92 | func makeVideoBox() -> VideoBox { 93 | let videoBox = VideoBox() 94 | videoBox.delegate = self 95 | 96 | return videoBox 97 | } 98 | 99 | func makeInfoLabel() -> UILabel { 100 | let label = UILabel() 101 | label.textColor = UIColor.white 102 | label.font = Config.Font.Text.regular.withSize(12) 103 | label.text = String(format: "Gallery.Videos.MaxiumDuration".g_localize(fallback: "FIRST %d SECONDS"), 104 | (Int(Config.VideoEditor.maximumDuration))) 105 | 106 | return label 107 | } 108 | } 109 | 110 | extension VideosController: PageAware { 111 | 112 | func pageDidHide() { 113 | 114 | } 115 | 116 | func pageDidShow() { 117 | once.run { 118 | library.reload { 119 | self.items = self.library.items 120 | self.gridView.collectionView.reloadData() 121 | self.gridView.emptyView.isHidden = !self.items.isEmpty 122 | } 123 | } 124 | } 125 | } 126 | 127 | extension VideosController: VideoBoxDelegate { 128 | 129 | func videoBoxDidTap(_ videoBox: VideoBox) { 130 | Cart.shared.video?.fetchPlayerItem { item in 131 | guard let item = item else { return } 132 | 133 | DispatchQueue.main.async { 134 | let controller = AVPlayerViewController() 135 | let player = AVPlayer(playerItem: item) 136 | controller.player = player 137 | 138 | self.present(controller, animated: true) { 139 | player.play() 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | extension VideosController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 147 | 148 | // MARK: - UICollectionViewDataSource 149 | 150 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 151 | return items.count 152 | } 153 | 154 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 155 | 156 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: VideoCell.self), for: indexPath) 157 | as! VideoCell 158 | let item = items[(indexPath as NSIndexPath).item] 159 | 160 | cell.configure(item) 161 | cell.frameView.label.isHidden = true 162 | configureFrameView(cell, indexPath: indexPath) 163 | 164 | return cell 165 | } 166 | 167 | // MARK: - UICollectionViewDelegateFlowLayout 168 | 169 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 170 | 171 | let size = (collectionView.bounds.size.width - (Config.Grid.Dimension.columnCount - 1) * Config.Grid.Dimension.cellSpacing) 172 | / Config.Grid.Dimension.columnCount 173 | return CGSize(width: size, height: size) 174 | } 175 | 176 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 177 | let item = items[(indexPath as NSIndexPath).item] 178 | 179 | if let selectedItem = Cart.shared.video , selectedItem == item { 180 | Cart.shared.video = nil 181 | } else { 182 | Cart.shared.video = item 183 | } 184 | 185 | refreshView() 186 | configureFrameViews() 187 | } 188 | 189 | func configureFrameViews() { 190 | for case let cell as VideoCell in gridView.collectionView.visibleCells { 191 | if let indexPath = gridView.collectionView.indexPath(for: cell) { 192 | configureFrameView(cell, indexPath: indexPath) 193 | } 194 | } 195 | } 196 | 197 | func configureFrameView(_ cell: VideoCell, indexPath: IndexPath) { 198 | let item = items[(indexPath as NSIndexPath).item] 199 | 200 | if let selectedItem = Cart.shared.video , selectedItem == item { 201 | cell.frameView.g_quickFade() 202 | } else { 203 | cell.frameView.alpha = 0 204 | } 205 | } 206 | } 207 | 208 | extension VideosController: CartDelegate { 209 | 210 | func cart(_ cart: Cart, didSet video: Video) { 211 | self.items.insert(video, at: 0) 212 | self.gridView.collectionView.reloadData() 213 | self.gridView.emptyView.isHidden = !self.items.isEmpty 214 | refreshView() 215 | } 216 | 217 | func cart(_ cart: Cart, didAdd image: Image, newlyTaken: Bool) { 218 | 219 | } 220 | 221 | func cart(_ cart: Cart, didRemove image: Image) { 222 | 223 | } 224 | 225 | func cartDidReload(_ cart: Cart) { 226 | 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/Images/ImagesController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | class ImagesController: UIViewController { 5 | 6 | lazy var dropdownController: DropdownController = self.makeDropdownController() 7 | lazy var gridView: GridView = self.makeGridView() 8 | lazy var stackView: StackView = self.makeStackView() 9 | 10 | var items: [Image] = [] 11 | let library = ImagesLibrary() 12 | var selectedAlbum: Album? 13 | let once = Once() 14 | 15 | // MARK: - Life cycle 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | setup() 21 | } 22 | 23 | // MARK: - Setup 24 | 25 | func setup() { 26 | view.backgroundColor = UIColor.white 27 | 28 | view.addSubview(gridView) 29 | 30 | addChildViewController(dropdownController) 31 | gridView.insertSubview(dropdownController.view, belowSubview: gridView.topView) 32 | dropdownController.didMove(toParentViewController: self) 33 | 34 | gridView.bottomView.addSubview(stackView) 35 | 36 | gridView.g_pinEdges() 37 | 38 | dropdownController.view.g_pin(on: .left) 39 | dropdownController.view.g_pin(on: .right) 40 | dropdownController.view.g_pin(on: .height, constant: -40) 41 | dropdownController.topConstraint = dropdownController.view.g_pin(on: .top, 42 | view: gridView.topView, on: .bottom, 43 | constant: view.frame.size.height, priority: 999) 44 | 45 | stackView.g_pin(on: .centerY, constant: -4) 46 | stackView.g_pin(on: .left, constant: 38) 47 | stackView.g_pin(size: CGSize(width: 56, height: 56)) 48 | 49 | gridView.closeButton.addTarget(self, action: #selector(closeButtonTouched(_:)), for: .touchUpInside) 50 | gridView.doneButton.addTarget(self, action: #selector(doneButtonTouched(_:)), for: .touchUpInside) 51 | gridView.arrowButton.addTarget(self, action: #selector(arrowButtonTouched(_:)), for: .touchUpInside) 52 | stackView.addTarget(self, action: #selector(stackViewTouched(_:)), for: .touchUpInside) 53 | 54 | gridView.collectionView.dataSource = self 55 | gridView.collectionView.delegate = self 56 | gridView.collectionView.register(ImageCell.self, forCellWithReuseIdentifier: String(describing: ImageCell.self)) 57 | } 58 | 59 | // MARK: - Action 60 | 61 | func closeButtonTouched(_ button: UIButton) { 62 | EventHub.shared.close?() 63 | } 64 | 65 | func doneButtonTouched(_ button: UIButton) { 66 | EventHub.shared.doneWithImages?() 67 | } 68 | 69 | func arrowButtonTouched(_ button: ArrowButton) { 70 | dropdownController.toggle() 71 | button.toggle(dropdownController.expanding) 72 | } 73 | 74 | func stackViewTouched(_ stackView: StackView) { 75 | EventHub.shared.stackViewTouched?() 76 | } 77 | 78 | // MARK: - Logic 79 | 80 | func show(album: Album) { 81 | gridView.arrowButton.updateText(album.collection.localizedTitle ?? "") 82 | items = album.items 83 | gridView.collectionView.reloadData() 84 | gridView.collectionView.g_scrollToTop() 85 | gridView.emptyView.isHidden = !items.isEmpty 86 | } 87 | 88 | func refreshSelectedAlbum() { 89 | if let selectedAlbum = selectedAlbum { 90 | selectedAlbum.reload() 91 | show(album: selectedAlbum) 92 | } 93 | } 94 | 95 | // MARK: - View 96 | 97 | func refreshView() { 98 | let hasImages = !Cart.shared.images.isEmpty 99 | gridView.bottomView.g_fade(visible: hasImages) 100 | gridView.collectionView.g_updateBottomInset(hasImages ? gridView.bottomView.frame.size.height : 0) 101 | } 102 | 103 | // MARK: - Controls 104 | 105 | func makeDropdownController() -> DropdownController { 106 | let controller = DropdownController() 107 | controller.delegate = self 108 | 109 | return controller 110 | } 111 | 112 | func makeGridView() -> GridView { 113 | let view = GridView() 114 | view.bottomView.alpha = 0 115 | 116 | return view 117 | } 118 | 119 | func makeStackView() -> StackView { 120 | let view = StackView() 121 | 122 | return view 123 | } 124 | } 125 | 126 | extension ImagesController: PageAware { 127 | 128 | func pageDidHide() { 129 | 130 | } 131 | 132 | func pageDidShow() { 133 | once.run { 134 | library.reload { 135 | self.dropdownController.albums = self.library.albums 136 | self.dropdownController.tableView.reloadData() 137 | 138 | if let album = self.library.albums.first { 139 | self.selectedAlbum = album 140 | self.show(album: album) 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | extension ImagesController: CartDelegate { 148 | 149 | func cart(_ cart: Cart, didSet video: Video) { 150 | 151 | } 152 | 153 | func cart(_ cart: Cart, didAdd image: Image, newlyTaken: Bool) { 154 | stackView.reload(cart.images, added: true) 155 | refreshView() 156 | 157 | if newlyTaken { 158 | refreshSelectedAlbum() 159 | } 160 | } 161 | 162 | func cart(_ cart: Cart, didRemove image: Image) { 163 | stackView.reload(cart.images) 164 | refreshView() 165 | } 166 | 167 | func cartDidReload(_ cart: Cart) { 168 | stackView.reload(cart.images) 169 | refreshView() 170 | refreshSelectedAlbum() 171 | } 172 | } 173 | 174 | extension ImagesController: DropdownControllerDelegate { 175 | 176 | func dropdownController(_ controller: DropdownController, didSelect album: Album) { 177 | selectedAlbum = album 178 | show(album: album) 179 | 180 | dropdownController.toggle() 181 | gridView.arrowButton.toggle(controller.expanding) 182 | } 183 | } 184 | 185 | extension ImagesController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 186 | 187 | // MARK: - UICollectionViewDataSource 188 | 189 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 190 | return items.count 191 | } 192 | 193 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 194 | 195 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ImageCell.self), for: indexPath) 196 | as! ImageCell 197 | let item = items[(indexPath as NSIndexPath).item] 198 | 199 | cell.configure(item) 200 | configureFrameView(cell, indexPath: indexPath) 201 | 202 | return cell 203 | } 204 | 205 | // MARK: - UICollectionViewDelegateFlowLayout 206 | 207 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 208 | 209 | let size = (collectionView.bounds.size.width - (Config.Grid.Dimension.columnCount - 1) * Config.Grid.Dimension.cellSpacing) 210 | / Config.Grid.Dimension.columnCount 211 | return CGSize(width: size, height: size) 212 | } 213 | 214 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 215 | let item = items[(indexPath as NSIndexPath).item] 216 | 217 | if Cart.shared.images.contains(item) { 218 | Cart.shared.remove(item) 219 | } else { 220 | Cart.shared.add(item) 221 | } 222 | 223 | configureFrameViews() 224 | } 225 | 226 | func configureFrameViews() { 227 | for case let cell as ImageCell in gridView.collectionView.visibleCells { 228 | if let indexPath = gridView.collectionView.indexPath(for: cell) { 229 | configureFrameView(cell, indexPath: indexPath) 230 | } 231 | } 232 | } 233 | 234 | func configureFrameView(_ cell: ImageCell, indexPath: IndexPath) { 235 | let item = items[(indexPath as NSIndexPath).item] 236 | 237 | if let index = Cart.shared.images.index(of: item) { 238 | cell.frameView.g_quickFade() 239 | cell.frameView.label.text = "\(index + 1)" 240 | } else { 241 | cell.frameView.alpha = 0 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Sources/Camera/CameraController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import AVFoundation 4 | import AVKit 5 | import QuartzCore 6 | 7 | class CameraController: UIViewController { 8 | 9 | var locationManager: LocationManager? 10 | lazy var cameraMan: CameraMan = self.makeCameraMan() 11 | lazy var cameraView: CameraView = self.makeCameraView() 12 | lazy var videoBox: VideoBox = self.makeVideoBox() 13 | 14 | let once = Once() 15 | 16 | // MARK: - Life cycle 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | setup() 22 | setupLocation() 23 | } 24 | 25 | override func viewWillAppear(_ animated: Bool) { 26 | super.viewWillAppear(animated) 27 | 28 | locationManager?.start() 29 | } 30 | 31 | override func viewWillDisappear(_ animated: Bool) { 32 | super.viewWillDisappear(animated) 33 | 34 | locationManager?.stop() 35 | } 36 | 37 | // MARK: - Setup 38 | 39 | func setup() { 40 | view.addSubview(cameraView) 41 | cameraView.g_pinEdges() 42 | 43 | if Config.Camera.recordMode == .video { 44 | 45 | cameraView.bottomView.addSubview(videoBox) 46 | 47 | videoBox.g_pin(size: CGSize(width: 44, height: 44)) 48 | videoBox.g_pin(on: .centerY) 49 | videoBox.g_pin(on: .left, constant: 38) 50 | } 51 | 52 | cameraView.closeButton.addTarget(self, action: #selector(closeButtonTouched(_:)), for: .touchUpInside) 53 | cameraView.flashButton.addTarget(self, action: #selector(flashButtonTouched(_:)), for: .touchUpInside) 54 | cameraView.rotateButton.addTarget(self, action: #selector(rotateButtonTouched(_:)), for: .touchUpInside) 55 | cameraView.stackView.addTarget(self, action: #selector(stackViewTouched(_:)), for: .touchUpInside) 56 | cameraView.shutterButton.addTarget(self, action: #selector(shutterButtonTouched(_:)), for: .touchUpInside) 57 | cameraView.doneButton.addTarget(self, action: #selector(doneButtonTouched(_:)), for: .touchUpInside) 58 | } 59 | 60 | func setupLocation() { 61 | if Config.Camera.recordLocation { 62 | locationManager = LocationManager() 63 | } 64 | } 65 | 66 | // MARK: - Action 67 | 68 | func closeButtonTouched(_ button: UIButton) { 69 | EventHub.shared.close?() 70 | } 71 | 72 | func flashButtonTouched(_ button: UIButton) { 73 | cameraView.flashButton.toggle() 74 | 75 | if let flashMode = AVCaptureFlashMode(rawValue: cameraView.flashButton.selectedIndex) { 76 | cameraMan.flash(flashMode) 77 | } 78 | } 79 | 80 | func rotateButtonTouched(_ button: UIButton) { 81 | UIView.animate(withDuration: 0.3, animations: { 82 | self.cameraView.rotateOverlayView.alpha = 1 83 | }, completion: { _ in 84 | self.cameraMan.switchCamera { 85 | UIView.animate(withDuration: 0.7, animations: { 86 | self.cameraView.rotateOverlayView.alpha = 0 87 | }) 88 | } 89 | }) 90 | } 91 | 92 | func stackViewTouched(_ stackView: StackView) { 93 | EventHub.shared.stackViewTouched?() 94 | } 95 | 96 | 97 | func shutterButtonTouched(_ button: ShutterButton) { 98 | guard let previewLayer = cameraView.previewLayer else { return } 99 | 100 | switch Config.Camera.recordMode { 101 | case .photo: 102 | 103 | button.isEnabled = false 104 | UIView.animate(withDuration: 0.1, animations: { 105 | self.cameraView.shutterOverlayView.alpha = 1 106 | }, completion: { _ in 107 | UIView.animate(withDuration: 0.1, animations: { 108 | self.cameraView.shutterOverlayView.alpha = 0 109 | }) 110 | }) 111 | 112 | self.cameraView.stackView.startLoading() 113 | cameraMan.takePhoto(previewLayer, location: locationManager?.latestLocation) { asset in 114 | button.isEnabled = true 115 | self.cameraView.stackView.stopLoading() 116 | 117 | if let asset = asset { 118 | Cart.shared.add(Image(asset: asset), newlyTaken: true) 119 | } 120 | } 121 | case .video: 122 | 123 | if self.cameraMan.isRecording() { 124 | button.isEnabled = false 125 | self.cameraView.morphToVideoRecordingSavingStarted() 126 | self.cameraMan.stopVideoRecording() 127 | } else { 128 | button.isEnabled = false 129 | self.cameraMan.startVideoRecord(location: locationManager?.latestLocation, startCompletion: { result in 130 | button.isEnabled = true 131 | self.cameraView.morphToVideoRecordingStarted() 132 | }, stopCompletion: { asset in 133 | button.isEnabled = true 134 | self.cameraView.morphToVideoRecordingSavingDone() 135 | if let asset = asset { 136 | Cart.shared.setVideo(Video(asset: asset)) 137 | } 138 | 139 | }) 140 | } 141 | } 142 | } 143 | 144 | func stopVideoRecordingIfStarted() { 145 | if self.cameraMan.isRecording() { 146 | self.cameraMan.stopVideoRecording() 147 | self.cameraView.morphToVideoRecordingReset() 148 | } 149 | } 150 | 151 | func doneButtonTouched(_ button: UIButton) { 152 | EventHub.shared.doneWithImages?() 153 | } 154 | 155 | // MARK: - View 156 | 157 | func refreshView() { 158 | let hasImages = !Cart.shared.images.isEmpty 159 | cameraView.bottomView.g_fade(visible: hasImages) 160 | } 161 | 162 | // MARK: - Controls 163 | 164 | func makeCameraMan() -> CameraMan { 165 | let man = CameraMan() 166 | man.delegate = self 167 | 168 | return man 169 | } 170 | 171 | func makeCameraView() -> CameraView { 172 | let view = CameraView() 173 | view.delegate = self 174 | 175 | return view 176 | } 177 | 178 | func makeVideoBox() -> VideoBox { 179 | let videoBox = VideoBox() 180 | videoBox.delegate = self 181 | 182 | return videoBox 183 | } 184 | } 185 | 186 | extension CameraController: CartDelegate { 187 | 188 | func cart(_ cart: Cart, didSet video: Video) { 189 | self.videoBox.imageView.g_loadImage(video.asset) 190 | } 191 | 192 | func cart(_ cart: Cart, didAdd image: Image, newlyTaken: Bool) { 193 | cameraView.stackView.reload(cart.images, added: true) 194 | refreshView() 195 | } 196 | 197 | func cart(_ cart: Cart, didRemove image: Image) { 198 | cameraView.stackView.reload(cart.images) 199 | refreshView() 200 | } 201 | 202 | func cartDidReload(_ cart: Cart) { 203 | cameraView.stackView.reload(cart.images) 204 | refreshView() 205 | } 206 | } 207 | 208 | extension CameraController: PageAware { 209 | 210 | func pageDidHide() { 211 | self.stopVideoRecordingIfStarted() 212 | } 213 | 214 | func pageDidShow() { 215 | once.run { 216 | cameraMan.setup() 217 | } 218 | } 219 | 220 | 221 | } 222 | 223 | extension CameraController: CameraViewDelegate { 224 | 225 | func cameraView(_ cameraView: CameraView, didTouch point: CGPoint) { 226 | cameraMan.focus(point) 227 | } 228 | } 229 | 230 | extension CameraController: CameraManDelegate { 231 | 232 | func cameraManDidStart(_ cameraMan: CameraMan) { 233 | cameraView.setupPreviewLayer(cameraMan.session) 234 | } 235 | 236 | func cameraManNotAvailable(_ cameraMan: CameraMan) { 237 | cameraView.focusImageView.isHidden = true 238 | } 239 | 240 | func cameraMan(_ cameraMan: CameraMan, didChangeInput input: AVCaptureDeviceInput) { 241 | cameraView.flashButton.isHidden = !input.device.hasFlash 242 | } 243 | 244 | } 245 | 246 | extension CameraController: VideoBoxDelegate { 247 | 248 | func videoBoxDidTap(_ videoBox: VideoBox) { 249 | Cart.shared.video?.fetchPlayerItem { item in 250 | guard let item = item else { return } 251 | 252 | DispatchQueue.main.async { 253 | let controller = AVPlayerViewController() 254 | let player = AVPlayer(playerItem: item) 255 | controller.player = player 256 | 257 | self.present(controller, animated: true) { 258 | player.play() 259 | } 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /Sources/Camera/CameraMan.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import PhotosUI 4 | import Photos 5 | 6 | protocol CameraManDelegate: class { 7 | func cameraManNotAvailable(_ cameraMan: CameraMan) 8 | func cameraManDidStart(_ cameraMan: CameraMan) 9 | func cameraMan(_ cameraMan: CameraMan, didChangeInput input: AVCaptureDeviceInput) 10 | } 11 | 12 | class CameraMan { 13 | weak var delegate: CameraManDelegate? 14 | 15 | let session = AVCaptureSession() 16 | let queue = DispatchQueue(label: "no.hyper.Gallery.Camera.SessionQueue", qos: .background) 17 | let savingQueue = DispatchQueue(label: "no.hyper.Gallery.Camera.SavingQueue", qos: .background) 18 | 19 | var backCamera: AVCaptureDeviceInput? 20 | var frontCamera: AVCaptureDeviceInput? 21 | var stillImageOutput: AVCaptureStillImageOutput? 22 | var movieOutput: ClosuredAVCaptureMovieFileOutput? 23 | 24 | deinit { 25 | stop() 26 | } 27 | 28 | // MARK: - Setup 29 | 30 | func setup() { 31 | if Permission.Camera.hasPermission { 32 | self.start() 33 | } else { 34 | self.delegate?.cameraManNotAvailable(self) 35 | } 36 | } 37 | 38 | func setupDevices() { 39 | // Input 40 | AVCaptureDevice 41 | .devices().flatMap { 42 | return $0 as? AVCaptureDevice 43 | }.filter { 44 | return $0.hasMediaType(AVMediaTypeVideo) 45 | }.forEach { 46 | switch $0.position { 47 | case .front: 48 | self.frontCamera = try? AVCaptureDeviceInput(device: $0) 49 | case .back: 50 | self.backCamera = try? AVCaptureDeviceInput(device: $0) 51 | default: 52 | break 53 | } 54 | } 55 | 56 | // Output 57 | stillImageOutput = AVCaptureStillImageOutput() 58 | stillImageOutput?.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG] 59 | 60 | movieOutput = ClosuredAVCaptureMovieFileOutput(sessionQueue: queue) 61 | } 62 | 63 | func addInput(_ input: AVCaptureDeviceInput) { 64 | configurePreset(input) 65 | 66 | if session.canAddInput(input) { 67 | session.addInput(input) 68 | 69 | DispatchQueue.main.async { 70 | self.delegate?.cameraMan(self, didChangeInput: input) 71 | } 72 | } 73 | } 74 | 75 | // MARK: - Session 76 | 77 | var currentInput: AVCaptureDeviceInput? { 78 | return session.inputs.first as? AVCaptureDeviceInput 79 | } 80 | 81 | fileprivate func start() { 82 | // Devices 83 | setupDevices() 84 | 85 | guard let input = backCamera, let imageOutput = stillImageOutput, let movieOutput = movieOutput else { return } 86 | 87 | addInput(input) 88 | 89 | if session.canAddOutput(imageOutput) { 90 | session.addOutput(imageOutput) 91 | } 92 | 93 | movieOutput.addToSession(session) 94 | 95 | queue.async { 96 | self.session.startRunning() 97 | 98 | DispatchQueue.main.async { 99 | self.delegate?.cameraManDidStart(self) 100 | } 101 | } 102 | } 103 | 104 | func stop() { 105 | self.session.stopRunning() 106 | } 107 | 108 | func switchCamera(_ completion: (() -> Void)? = nil) { 109 | guard let currentInput = currentInput 110 | else { 111 | completion?() 112 | return 113 | } 114 | 115 | queue.async { 116 | guard let input = (currentInput == self.backCamera) ? self.frontCamera : self.backCamera 117 | else { 118 | DispatchQueue.main.async { 119 | completion?() 120 | } 121 | return 122 | } 123 | 124 | self.configure { 125 | self.session.removeInput(currentInput) 126 | self.addInput(input) 127 | } 128 | 129 | DispatchQueue.main.async { 130 | completion?() 131 | } 132 | } 133 | } 134 | 135 | func takePhoto(_ previewLayer: AVCaptureVideoPreviewLayer, location: CLLocation?, completion: @escaping ((PHAsset?) -> Void)) { 136 | guard let connection = stillImageOutput?.connection(withMediaType: AVMediaTypeVideo) else { return } 137 | 138 | connection.videoOrientation = Utils.videoOrientation() 139 | 140 | queue.async { 141 | self.stillImageOutput?.captureStillImageAsynchronously(from: connection) { 142 | buffer, error in 143 | 144 | guard error == nil, let buffer = buffer, CMSampleBufferIsValid(buffer), 145 | let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer), 146 | let image = UIImage(data: imageData) 147 | else { 148 | DispatchQueue.main.async { 149 | completion(nil) 150 | } 151 | return 152 | } 153 | 154 | self.savePhoto(image, location: location, completion: completion) 155 | } 156 | } 157 | } 158 | 159 | func savePhoto(_ image: UIImage, location: CLLocation?, completion: @escaping ((PHAsset?) -> Void)) { 160 | self.save({ 161 | PHAssetChangeRequest.creationRequestForAsset(from: image) 162 | }, location: location, completion: completion) 163 | } 164 | 165 | func save(_ req: @escaping ((Void) -> PHAssetChangeRequest?), location: CLLocation?, completion: ((PHAsset?) -> Void)?) { 166 | savingQueue.async { 167 | var localIdentifier: String? 168 | do { 169 | try PHPhotoLibrary.shared().performChangesAndWait { 170 | if let request = req() { 171 | localIdentifier = request.placeholderForCreatedAsset?.localIdentifier 172 | request.creationDate = Date() 173 | request.location = location 174 | } 175 | } 176 | DispatchQueue.main.async { 177 | if let localIdentifier = localIdentifier { 178 | completion?(Fetcher.fetchAsset(localIdentifier)) 179 | } else { 180 | completion?(nil) 181 | } 182 | } 183 | } catch { 184 | DispatchQueue.main.async { 185 | completion?(nil) 186 | } 187 | } 188 | } 189 | } 190 | 191 | func isRecording() -> Bool { 192 | return self.movieOutput?.isRecording() ?? false 193 | } 194 | 195 | func startVideoRecord(location: CLLocation?, startCompletion: ((Bool) -> Void)?, stopCompletion: ((PHAsset?) -> Void)? = nil) { 196 | self.movieOutput?.startRecording(startCompletion: startCompletion, stopCompletion: { url in 197 | if let url = url { 198 | self.saveVideo(at: url, location: location, completion: stopCompletion) 199 | } else { 200 | stopCompletion?(nil) 201 | } 202 | }) 203 | } 204 | 205 | func stopVideoRecording() { 206 | self.movieOutput?.stopVideoRecording() 207 | } 208 | 209 | func saveVideo(at path: URL, location: CLLocation?, completion: ((PHAsset?) -> Void)?) { 210 | self.save({ 211 | PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path) 212 | }, location: location, completion: completion) 213 | } 214 | 215 | 216 | func flash(_ mode: AVCaptureFlashMode) { 217 | guard let device = currentInput?.device , device.isFlashModeSupported(mode) else { return } 218 | 219 | queue.async { 220 | self.lock { 221 | device.flashMode = mode 222 | } 223 | } 224 | } 225 | 226 | func focus(_ point: CGPoint) { 227 | guard let device = currentInput?.device , device.isFocusModeSupported(AVCaptureFocusMode.locked) else { return } 228 | 229 | queue.async { 230 | self.lock { 231 | device.focusPointOfInterest = point 232 | } 233 | } 234 | } 235 | 236 | // MARK: - Lock 237 | 238 | func lock(_ block: () -> Void) { 239 | if let device = currentInput?.device , (try? device.lockForConfiguration()) != nil { 240 | block() 241 | device.unlockForConfiguration() 242 | } 243 | } 244 | 245 | // MARK: - Configure 246 | func configure(_ block: () -> Void) { 247 | session.beginConfiguration() 248 | block() 249 | session.commitConfiguration() 250 | } 251 | 252 | // MARK: - Preset 253 | 254 | func configurePreset(_ input: AVCaptureDeviceInput) { 255 | for asset in preferredPresets() { 256 | if input.device.supportsAVCaptureSessionPreset(asset) && self.session.canSetSessionPreset(asset) { 257 | self.session.sessionPreset = asset 258 | return 259 | } 260 | } 261 | } 262 | 263 | func preferredPresets() -> [String] { 264 | return [ 265 | AVCaptureSessionPresetHigh, 266 | AVCaptureSessionPresetMedium, 267 | AVCaptureSessionPresetLow 268 | ] 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /Sources/Camera/CameraView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import AVFoundation 3 | 4 | protocol CameraViewDelegate: class { 5 | func cameraView(_ cameraView: CameraView, didTouch point: CGPoint) 6 | } 7 | 8 | class CameraView: UIView, UIGestureRecognizerDelegate { 9 | 10 | lazy var closeButton: UIButton = self.makeCloseButton() 11 | lazy var flashButton: TripleButton = self.makeFlashButton() 12 | lazy var rotateButton: UIButton = self.makeRotateButton() 13 | fileprivate lazy var bottomContainer: UIView = self.makeBottomContainer() 14 | lazy var bottomView: UIView = self.makeBottomView() 15 | lazy var stackView: StackView = self.makeStackView() 16 | lazy var shutterButton: ShutterButton = self.makeShutterButton() 17 | lazy var doneButton: UIButton = self.makeDoneButton() 18 | lazy var focusImageView: UIImageView = self.makeFocusImageView() 19 | lazy var tapGR: UITapGestureRecognizer = self.makeTapGR() 20 | lazy var rotateOverlayView: UIView = self.makeRotateOverlayView() 21 | lazy var shutterOverlayView: UIView = self.makeShutterOverlayView() 22 | lazy var blurView: UIVisualEffectView = self.makeBlurView() 23 | lazy var recLabel: UILabel = self.makeRecLabel() 24 | lazy var saveLabel: UILabel = self.makeSaveLabel() 25 | lazy var elapsedVideoRecordingTimeLabel: UILabel = self.makeVideoRecordingElapsedTimeLabel() 26 | 27 | var timer: Timer? 28 | var videoRecordingTimer: Timer? 29 | var previewLayer: AVCaptureVideoPreviewLayer? 30 | weak var delegate: CameraViewDelegate? 31 | 32 | // MARK: - Initialization 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | 37 | backgroundColor = UIColor.black 38 | setup() 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | // MARK: - Setup 46 | 47 | func setup() { 48 | addGestureRecognizer(tapGR) 49 | 50 | [closeButton, flashButton, rotateButton, bottomContainer, recLabel, saveLabel, elapsedVideoRecordingTimeLabel].forEach { 51 | addSubview($0) 52 | } 53 | 54 | [bottomView, shutterButton].forEach { 55 | bottomContainer.addSubview($0) 56 | } 57 | 58 | [stackView, doneButton].forEach { 59 | bottomView.addSubview($0 as! UIView) 60 | } 61 | 62 | [closeButton, flashButton, rotateButton, recLabel].forEach { 63 | $0.g_addShadow() 64 | } 65 | 66 | rotateOverlayView.addSubview(blurView) 67 | insertSubview(rotateOverlayView, belowSubview: rotateButton) 68 | insertSubview(focusImageView, belowSubview: bottomContainer) 69 | insertSubview(shutterOverlayView, belowSubview: bottomContainer) 70 | 71 | closeButton.g_pin(on: .top) 72 | closeButton.g_pin(on: .left) 73 | closeButton.g_pin(size: CGSize(width: 44, height: 44)) 74 | 75 | flashButton.g_pin(on: .centerY, view: closeButton) 76 | flashButton.g_pin(on: .centerX) 77 | flashButton.g_pin(size: CGSize(width: 60, height: 44)) 78 | 79 | recLabel.g_pin(on: .centerY, view: closeButton) 80 | recLabel.g_pin(on: .centerX) 81 | 82 | recLabel.sizeToFit() 83 | recLabel.g_pin(size: recLabel.bounds.size) 84 | 85 | rotateButton.g_pin(on: .top) 86 | rotateButton.g_pin(on: .right) 87 | rotateButton.g_pin(size: CGSize(width: 44, height: 44)) 88 | 89 | bottomContainer.g_pinDownward() 90 | bottomContainer.g_pin(height: 80) 91 | bottomView.g_pinEdges() 92 | 93 | stackView.g_pin(on: .centerY, constant: -4) 94 | stackView.g_pin(on: .left, constant: 38) 95 | stackView.g_pin(size: CGSize(width: 56, height: 56)) 96 | 97 | shutterButton.g_pinCenter() 98 | shutterButton.g_pin(size: CGSize(width: 60, height: 60)) 99 | 100 | saveLabel.g_pin(on: .centerY, view: shutterButton, constant: -45) 101 | saveLabel.g_pin(on: .centerX, view: shutterButton) 102 | 103 | saveLabel.sizeToFit() 104 | saveLabel.g_pin(size: saveLabel.bounds.size) 105 | 106 | elapsedVideoRecordingTimeLabel.g_pin(on: .centerY, view: shutterButton, constant: -45) 107 | elapsedVideoRecordingTimeLabel.g_pin(on: .centerX, view: shutterButton) 108 | 109 | doneButton.g_pin(on: .centerY) 110 | doneButton.g_pin(on: .right, constant: -38) 111 | 112 | rotateOverlayView.g_pinEdges() 113 | blurView.g_pinEdges() 114 | shutterOverlayView.g_pinEdges() 115 | } 116 | 117 | func setupPreviewLayer(_ session: AVCaptureSession) { 118 | guard previewLayer == nil else { return } 119 | 120 | let layer = AVCaptureVideoPreviewLayer(session: session) 121 | layer?.autoreverses = true 122 | layer?.videoGravity = AVLayerVideoGravityResizeAspectFill 123 | 124 | self.layer.insertSublayer(layer!, at: 0) 125 | layer?.frame = self.layer.bounds 126 | 127 | previewLayer = layer 128 | } 129 | 130 | // MARK: - Action 131 | 132 | func viewTapped(_ gr: UITapGestureRecognizer) { 133 | let point = gr.location(in: self) 134 | 135 | focusImageView.transform = CGAffineTransform.identity 136 | timer?.invalidate() 137 | delegate?.cameraView(self, didTouch: point) 138 | 139 | focusImageView.center = point 140 | 141 | UIView.animate(withDuration: 0.5, animations: { 142 | self.focusImageView.alpha = 1 143 | self.focusImageView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) 144 | }, completion: { _ in 145 | self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, 146 | selector: #selector(CameraView.timerFired(_:)), userInfo: nil, repeats: false) 147 | }) 148 | } 149 | 150 | // MARK: - Timer 151 | 152 | func timerFired(_ timer: Timer) { 153 | UIView.animate(withDuration: 0.3, animations: { 154 | self.focusImageView.alpha = 0 155 | }, completion: { _ in 156 | self.focusImageView.transform = CGAffineTransform.identity 157 | }) 158 | } 159 | 160 | func videoRecodringTimerFired(_ timer: Timer) { 161 | guard let dictionary = timer.userInfo as? [String: Any], let start = dictionary["start"] as? TimeInterval else { 162 | return 163 | } 164 | let now = Date().timeIntervalSince1970 165 | let minutes = Int(now - start) / 60 166 | let seconds = Int(now - start) % 60 167 | self.elapsedVideoRecordingTimeLabel.text = String(format: "%0.2d:%0.2d", minutes, seconds) 168 | } 169 | 170 | // MARK: - UIGestureRecognizerDelegate 171 | override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 172 | let point = gestureRecognizer.location(in: self) 173 | 174 | return point.y > closeButton.frame.maxY 175 | && point.y < bottomContainer.frame.origin.y 176 | } 177 | 178 | // MARK: - Video recording. 179 | 180 | func morphToVideoRecordingStarted() { 181 | 182 | let userInfo = ["start": Date().timeIntervalSince1970] 183 | self.videoRecordingTimer = Timer.scheduledTimer( 184 | timeInterval: 0.5, target: self, selector: #selector(CameraView.videoRecodringTimerFired(_:)), userInfo: userInfo, repeats: true) 185 | self.elapsedVideoRecordingTimeLabel.text = self.videoRecordingLabelPlaceholder() 186 | 187 | UIView.animate(withDuration: 0.2) { 188 | self.bottomView.alpha = 0.0 189 | self.recLabel.alpha = 1.0 190 | self.flashButton.alpha = 0.0 191 | self.elapsedVideoRecordingTimeLabel.alpha = 1.0 192 | self.shutterButton.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) 193 | } 194 | } 195 | 196 | func morphToVideoRecordingSavingStarted() { 197 | self.videoRecordingTimer?.invalidate() 198 | UIView.animate(withDuration: 0.2) { 199 | self.saveLabel.alpha = 1.0 200 | self.elapsedVideoRecordingTimeLabel.alpha = 0.0 201 | } 202 | } 203 | 204 | func morphToVideoRecordingSavingDone() { 205 | self.videoRecordingTimer?.invalidate() 206 | UIView.animate(withDuration: 0.2) { 207 | self.bottomView.alpha = 1.0 208 | self.recLabel.alpha = 0.0 209 | self.flashButton.alpha = 1.0 210 | self.saveLabel.alpha = 0.0 211 | self.shutterButton.transform = CGAffineTransform(scaleX: 1, y: 1) 212 | } 213 | } 214 | 215 | func morphToVideoRecordingReset() { 216 | self.videoRecordingTimer?.invalidate() 217 | UIView.animate(withDuration: 0.2) { 218 | self.bottomView.alpha = 0.0 219 | self.recLabel.alpha = 0.0 220 | self.flashButton.alpha = 1.0 221 | self.saveLabel.alpha = 0.0 222 | self.elapsedVideoRecordingTimeLabel.alpha = 0.0 223 | self.shutterButton.transform = CGAffineTransform(scaleX: 1, y: 1) 224 | } 225 | } 226 | 227 | // MARK: - Controls 228 | 229 | func makeCloseButton() -> UIButton { 230 | let button = UIButton(type: .custom) 231 | button.setImage(Bundle.image("gallery_close"), for: UIControlState()) 232 | 233 | return button 234 | } 235 | 236 | func makeFlashButton() -> TripleButton { 237 | let states: [TripleButton.State] = [ 238 | TripleButton.State(title: "Gallery.Camera.Flash.Off".g_localize(fallback: "OFF"), image: Bundle.image("gallery_camera_flash_off")!), 239 | TripleButton.State(title: "Gallery.Camera.Flash.On".g_localize(fallback: "ON"), image: Bundle.image("gallery_camera_flash_on")!), 240 | TripleButton.State(title: "Gallery.Camera.Flash.Auto".g_localize(fallback: "AUTO"), image: Bundle.image("gallery_camera_flash_auto")!) 241 | ] 242 | 243 | let button = TripleButton(states: states) 244 | 245 | return button 246 | } 247 | 248 | func makeRotateButton() -> UIButton { 249 | let button = UIButton(type: .custom) 250 | button.setImage(Bundle.image("gallery_camera_rotate"), for: UIControlState()) 251 | 252 | return button 253 | } 254 | 255 | func makeBottomContainer() -> UIView { 256 | let view = UIView() 257 | 258 | return view 259 | } 260 | 261 | func makeBottomView() -> UIView { 262 | let view = UIView() 263 | view.backgroundColor = Config.Camera.BottomContainer.backgroundColor 264 | view.alpha = 0 265 | 266 | return view 267 | } 268 | 269 | func makeStackView() -> StackView { 270 | let view = StackView() 271 | 272 | return view 273 | } 274 | 275 | func makeShutterButton() -> ShutterButton { 276 | let button = ShutterButton() 277 | 278 | switch Config.Camera.recordMode { 279 | case .photo: 280 | button.overlayView.backgroundColor = .white 281 | case .video: 282 | button.overlayView.backgroundColor = .red 283 | } 284 | button.g_addShadow() 285 | 286 | return button 287 | } 288 | 289 | func makeDoneButton() -> UIButton { 290 | let button = UIButton(type: .system) 291 | button.setTitleColor(UIColor.white, for: UIControlState()) 292 | button.setTitleColor(UIColor.lightGray, for: .disabled) 293 | button.titleLabel?.font = Config.Font.Text.regular.withSize(16) 294 | button.setTitle("Gallery.Done".g_localize(fallback: "Done"), for: UIControlState()) 295 | 296 | return button 297 | } 298 | 299 | func makeRecLabel() -> UILabel { 300 | let label = UILabel() 301 | label.text = "REC" 302 | label.textColor = .red 303 | label.alpha = 0.0 304 | return label 305 | } 306 | 307 | func makeSaveLabel() -> UILabel { 308 | let label = UILabel() 309 | label.text = "Saving video..." 310 | label.textColor = .white 311 | label.alpha = 0.0 312 | label.font = UIFont.systemFont(ofSize: 12) 313 | return label 314 | } 315 | 316 | func makeVideoRecordingElapsedTimeLabel() -> UILabel { 317 | let label = UILabel() 318 | label.text = self.videoRecordingLabelPlaceholder() 319 | label.textAlignment = .center 320 | label.textColor = .white 321 | label.alpha = 0.0 322 | label.font = UIFont.systemFont(ofSize: 12) 323 | return label 324 | } 325 | 326 | func videoRecordingLabelPlaceholder() -> String { 327 | return "--:--" 328 | } 329 | 330 | func makeFocusImageView() -> UIImageView { 331 | let view = UIImageView() 332 | view.frame.size = CGSize(width: 110, height: 110) 333 | view.image = Bundle.image("gallery_camera_focus") 334 | view.backgroundColor = .clear 335 | view.alpha = 0 336 | 337 | return view 338 | } 339 | 340 | func makeTapGR() -> UITapGestureRecognizer { 341 | let gr = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:))) 342 | gr.delegate = self 343 | 344 | return gr 345 | } 346 | 347 | func makeRotateOverlayView() -> UIView { 348 | let view = UIView() 349 | view.alpha = 0 350 | 351 | return view 352 | } 353 | 354 | func makeShutterOverlayView() -> UIView { 355 | let view = UIView() 356 | view.alpha = 0 357 | view.backgroundColor = UIColor.black 358 | 359 | return view 360 | } 361 | 362 | func makeBlurView() -> UIVisualEffectView { 363 | let effect = UIBlurEffect(style: .dark) 364 | let blurView = UIVisualEffectView(effect: effect) 365 | 366 | return blurView 367 | } 368 | 369 | } 370 | -------------------------------------------------------------------------------- /Example/GalleryDemo/GalleryDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0E686D73545480162EA73BA3 /* Pods_GalleryDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 138256B692D9ED2745231ECC /* Pods_GalleryDemo.framework */; }; 11 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */; }; 12 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */; }; 13 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */; }; 14 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 138256B692D9ED2745231ECC /* Pods_GalleryDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GalleryDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 5F7B6C52F12A573A5EB555CD /* Pods-GalleryDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GalleryDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-GalleryDemo/Pods-GalleryDemo.release.xcconfig"; sourceTree = ""; }; 20 | D5C7F7401C3BC9CE008CDDBA /* GalleryDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GalleryDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | D5C7F74D1C3BC9CE008CDDBA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 22 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | F456066575BDFE4E9F169B27 /* Pods-GalleryDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GalleryDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GalleryDemo/Pods-GalleryDemo.debug.xcconfig"; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 0E686D73545480162EA73BA3 /* Pods_GalleryDemo.framework in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 1C1FA6DF13B95F286F3DECD3 /* Pods */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | F456066575BDFE4E9F169B27 /* Pods-GalleryDemo.debug.xcconfig */, 45 | 5F7B6C52F12A573A5EB555CD /* Pods-GalleryDemo.release.xcconfig */, 46 | ); 47 | name = Pods; 48 | sourceTree = ""; 49 | }; 50 | 5161BA177581E9E79A75089C /* Frameworks */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 138256B692D9ED2745231ECC /* Pods_GalleryDemo.framework */, 54 | ); 55 | name = Frameworks; 56 | sourceTree = ""; 57 | }; 58 | D5C7F7371C3BC9CE008CDDBA = { 59 | isa = PBXGroup; 60 | children = ( 61 | D5C7F7421C3BC9CE008CDDBA /* GalleryDemo */, 62 | D5C7F7411C3BC9CE008CDDBA /* Products */, 63 | 1C1FA6DF13B95F286F3DECD3 /* Pods */, 64 | 5161BA177581E9E79A75089C /* Frameworks */, 65 | ); 66 | indentWidth = 2; 67 | sourceTree = ""; 68 | tabWidth = 2; 69 | }; 70 | D5C7F7411C3BC9CE008CDDBA /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | D5C7F7401C3BC9CE008CDDBA /* GalleryDemo.app */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | D5C7F7421C3BC9CE008CDDBA /* GalleryDemo */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | D5C7F7561C3BCA1E008CDDBA /* Resources */, 82 | D5C7F7581C3BCA1E008CDDBA /* Sources */, 83 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */, 84 | ); 85 | path = GalleryDemo; 86 | sourceTree = ""; 87 | }; 88 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */, 92 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */, 93 | ); 94 | name = "Supporting Files"; 95 | sourceTree = ""; 96 | }; 97 | D5C7F7561C3BCA1E008CDDBA /* Resources */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */, 101 | ); 102 | path = Resources; 103 | sourceTree = ""; 104 | }; 105 | D5C7F7581C3BCA1E008CDDBA /* Sources */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */, 109 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */, 110 | ); 111 | path = Sources; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | D5C7F73F1C3BC9CE008CDDBA /* GalleryDemo */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "GalleryDemo" */; 120 | buildPhases = ( 121 | 05EE77D70DD3996ABD0E57F1 /* [CP] Check Pods Manifest.lock */, 122 | D5C7F73C1C3BC9CE008CDDBA /* Sources */, 123 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */, 124 | D5C7F73E1C3BC9CE008CDDBA /* Resources */, 125 | ACA61BF7868946EE744BF9C0 /* [CP] Embed Pods Frameworks */, 126 | 3D880632C9718154C7679D24 /* [CP] Copy Pods Resources */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | name = GalleryDemo; 133 | productName = GalleryDemo; 134 | productReference = D5C7F7401C3BC9CE008CDDBA /* GalleryDemo.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | D5C7F7381C3BC9CE008CDDBA /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastSwiftUpdateCheck = 0720; 144 | LastUpgradeCheck = 0810; 145 | ORGANIZATIONNAME = "Hyper Interaktiv AS"; 146 | TargetAttributes = { 147 | D5C7F73F1C3BC9CE008CDDBA = { 148 | CreatedOnToolsVersion = 7.2; 149 | DevelopmentTeam = B528TA44LR; 150 | LastSwiftMigration = 0800; 151 | ProvisioningStyle = Automatic; 152 | }; 153 | }; 154 | }; 155 | buildConfigurationList = D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "GalleryDemo" */; 156 | compatibilityVersion = "Xcode 3.2"; 157 | developmentRegion = English; 158 | hasScannedForEncodings = 0; 159 | knownRegions = ( 160 | en, 161 | Base, 162 | ); 163 | mainGroup = D5C7F7371C3BC9CE008CDDBA; 164 | productRefGroup = D5C7F7411C3BC9CE008CDDBA /* Products */; 165 | projectDirPath = ""; 166 | projectRoot = ""; 167 | targets = ( 168 | D5C7F73F1C3BC9CE008CDDBA /* GalleryDemo */, 169 | ); 170 | }; 171 | /* End PBXProject section */ 172 | 173 | /* Begin PBXResourcesBuildPhase section */ 174 | D5C7F73E1C3BC9CE008CDDBA /* Resources */ = { 175 | isa = PBXResourcesBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */, 179 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */, 180 | ); 181 | runOnlyForDeploymentPostprocessing = 0; 182 | }; 183 | /* End PBXResourcesBuildPhase section */ 184 | 185 | /* Begin PBXShellScriptBuildPhase section */ 186 | 05EE77D70DD3996ABD0E57F1 /* [CP] Check Pods Manifest.lock */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "[CP] Check Pods Manifest.lock"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 199 | showEnvVarsInLog = 0; 200 | }; 201 | 3D880632C9718154C7679D24 /* [CP] Copy Pods Resources */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | ); 208 | name = "[CP] Copy Pods Resources"; 209 | outputPaths = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GalleryDemo/Pods-GalleryDemo-resources.sh\"\n"; 214 | showEnvVarsInLog = 0; 215 | }; 216 | ACA61BF7868946EE744BF9C0 /* [CP] Embed Pods Frameworks */ = { 217 | isa = PBXShellScriptBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | ); 221 | inputPaths = ( 222 | ); 223 | name = "[CP] Embed Pods Frameworks"; 224 | outputPaths = ( 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | shellPath = /bin/sh; 228 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GalleryDemo/Pods-GalleryDemo-frameworks.sh\"\n"; 229 | showEnvVarsInLog = 0; 230 | }; 231 | /* End PBXShellScriptBuildPhase section */ 232 | 233 | /* Begin PBXSourcesBuildPhase section */ 234 | D5C7F73C1C3BC9CE008CDDBA /* Sources */ = { 235 | isa = PBXSourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */, 239 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */, 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | }; 243 | /* End PBXSourcesBuildPhase section */ 244 | 245 | /* Begin PBXVariantGroup section */ 246 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */ = { 247 | isa = PBXVariantGroup; 248 | children = ( 249 | D5C7F74D1C3BC9CE008CDDBA /* Base */, 250 | ); 251 | name = LaunchScreen.storyboard; 252 | sourceTree = ""; 253 | }; 254 | /* End PBXVariantGroup section */ 255 | 256 | /* Begin XCBuildConfiguration section */ 257 | D5C7F7501C3BC9CE008CDDBA /* Debug */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 262 | CLANG_CXX_LIBRARY = "libc++"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_CONSTANT_CONVERSION = YES; 267 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 277 | COPY_PHASE_STRIP = NO; 278 | DEBUG_INFORMATION_FORMAT = dwarf; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | ENABLE_TESTABILITY = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu99; 282 | GCC_DYNAMIC_NO_PIC = NO; 283 | GCC_NO_COMMON_BLOCKS = YES; 284 | GCC_OPTIMIZATION_LEVEL = 0; 285 | GCC_PREPROCESSOR_DEFINITIONS = ( 286 | "DEBUG=1", 287 | "$(inherited)", 288 | ); 289 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 290 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 291 | GCC_WARN_UNDECLARED_SELECTOR = YES; 292 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 293 | GCC_WARN_UNUSED_FUNCTION = YES; 294 | GCC_WARN_UNUSED_VARIABLE = YES; 295 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 296 | MTL_ENABLE_DEBUG_INFO = YES; 297 | ONLY_ACTIVE_ARCH = YES; 298 | SDKROOT = iphoneos; 299 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 300 | SWIFT_VERSION = 3.0; 301 | }; 302 | name = Debug; 303 | }; 304 | D5C7F7511C3BC9CE008CDDBA /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 309 | CLANG_CXX_LIBRARY = "libc++"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 320 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 321 | CLANG_WARN_UNREACHABLE_CODE = YES; 322 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 323 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 324 | COPY_PHASE_STRIP = NO; 325 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 326 | ENABLE_NS_ASSERTIONS = NO; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu99; 329 | GCC_NO_COMMON_BLOCKS = YES; 330 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 331 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 332 | GCC_WARN_UNDECLARED_SELECTOR = YES; 333 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 334 | GCC_WARN_UNUSED_FUNCTION = YES; 335 | GCC_WARN_UNUSED_VARIABLE = YES; 336 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 337 | MTL_ENABLE_DEBUG_INFO = NO; 338 | SDKROOT = iphoneos; 339 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 340 | SWIFT_VERSION = 3.0; 341 | VALIDATE_PRODUCT = YES; 342 | }; 343 | name = Release; 344 | }; 345 | D5C7F7531C3BC9CE008CDDBA /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | baseConfigurationReference = F456066575BDFE4E9F169B27 /* Pods-GalleryDemo.debug.xcconfig */; 348 | buildSettings = { 349 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 351 | CODE_SIGN_IDENTITY = "iPhone Developer"; 352 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 353 | DEVELOPMENT_TEAM = B528TA44LR; 354 | INFOPLIST_FILE = GalleryDemo/Info.plist; 355 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 356 | PRODUCT_BUNDLE_IDENTIFIER = no.hyper.Gallery.GalleryDemo; 357 | PRODUCT_NAME = "$(TARGET_NAME)"; 358 | PROVISIONING_PROFILE = ""; 359 | SWIFT_VERSION = 3.0; 360 | }; 361 | name = Debug; 362 | }; 363 | D5C7F7541C3BC9CE008CDDBA /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | baseConfigurationReference = 5F7B6C52F12A573A5EB555CD /* Pods-GalleryDemo.release.xcconfig */; 366 | buildSettings = { 367 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | CODE_SIGN_IDENTITY = "iPhone Developer"; 370 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 371 | DEVELOPMENT_TEAM = B528TA44LR; 372 | INFOPLIST_FILE = GalleryDemo/Info.plist; 373 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 374 | PRODUCT_BUNDLE_IDENTIFIER = no.hyper.Gallery.GalleryDemo; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | PROVISIONING_PROFILE = ""; 377 | SWIFT_VERSION = 3.0; 378 | }; 379 | name = Release; 380 | }; 381 | /* End XCBuildConfiguration section */ 382 | 383 | /* Begin XCConfigurationList section */ 384 | D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "GalleryDemo" */ = { 385 | isa = XCConfigurationList; 386 | buildConfigurations = ( 387 | D5C7F7501C3BC9CE008CDDBA /* Debug */, 388 | D5C7F7511C3BC9CE008CDDBA /* Release */, 389 | ); 390 | defaultConfigurationIsVisible = 0; 391 | defaultConfigurationName = Release; 392 | }; 393 | D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "GalleryDemo" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | D5C7F7531C3BC9CE008CDDBA /* Debug */, 397 | D5C7F7541C3BC9CE008CDDBA /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | /* End XCConfigurationList section */ 403 | }; 404 | rootObject = D5C7F7381C3BC9CE008CDDBA /* Project object */; 405 | } 406 | --------------------------------------------------------------------------------