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