Improves responsiveness of camera when switching to/from full-screen camera view.
This commit is contained in:
parent
e4315bf4e8
commit
8f9ce5df4e
|
|
@ -61,7 +61,6 @@
|
|||
C3C0CC601BFE496A0052747C /* ReusableXibView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC161BFE496A0052747C /* ReusableXibView.swift */; };
|
||||
C3C0CC611BFE496A0052747C /* Text.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C3C0CC181BFE496A0052747C /* Text.xcassets */; };
|
||||
C3C0CC621BFE496A0052747C /* TextChatInputItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC191BFE496A0052747C /* TextChatInputItem.swift */; };
|
||||
C3C0CC631BFE496A0052747C /* KeyedOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */; };
|
||||
C3C0CC641BFE496A0052747C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC1B1BFE496A0052747C /* Observable.swift */; };
|
||||
C3C0CC661BFE496A0052747C /* CircleIconView.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C0CC1F1BFE496A0052747C /* CircleIconView.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C0CC671BFE496A0052747C /* CircleIconView.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C0CC201BFE496A0052747C /* CircleIconView.m */; };
|
||||
|
|
@ -151,7 +150,6 @@
|
|||
C3C0CC161BFE496A0052747C /* ReusableXibView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableXibView.swift; sourceTree = "<group>"; };
|
||||
C3C0CC181BFE496A0052747C /* Text.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Text.xcassets; sourceTree = "<group>"; };
|
||||
C3C0CC191BFE496A0052747C /* TextChatInputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextChatInputItem.swift; sourceTree = "<group>"; };
|
||||
C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedOperationQueue.swift; sourceTree = "<group>"; };
|
||||
C3C0CC1B1BFE496A0052747C /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
|
||||
C3C0CC1F1BFE496A0052747C /* CircleIconView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleIconView.h; sourceTree = "<group>"; };
|
||||
C3C0CC201BFE496A0052747C /* CircleIconView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleIconView.m; sourceTree = "<group>"; };
|
||||
|
|
@ -241,7 +239,6 @@
|
|||
C3C0CC021BFE496A0052747C /* Info.plist */,
|
||||
C38658B11BFE55620012F181 /* AnimationUtils.swift */,
|
||||
C3A646FE1BFE8D40001BC98B /* Utils.swift */,
|
||||
C3C0CC1A1BFE496A0052747C /* KeyedOperationQueue.swift */,
|
||||
C3C0CC1B1BFE496A0052747C /* Observable.swift */,
|
||||
C3C0CBD51BFE496A0052747C /* Chat Items */,
|
||||
C3C0CC031BFE496A0052747C /* Input */,
|
||||
|
|
@ -601,7 +598,6 @@
|
|||
C3C0CC351BFE496A0052747C /* PhotoMessagePresenter.swift in Sources */,
|
||||
C3C0CC341BFE496A0052747C /* PhotoMessageModel.swift in Sources */,
|
||||
C3C0CC5E1BFE496A0052747C /* PhotosInputView.swift in Sources */,
|
||||
C3C0CC631BFE496A0052747C /* KeyedOperationQueue.swift in Sources */,
|
||||
C3C0CC431BFE496A0052747C /* TextMessageCollectionViewCellDefaultStyle.swift in Sources */,
|
||||
C3C0CC521BFE496A0052747C /* ChatInputBarPresenter.swift in Sources */,
|
||||
C3C0CC401BFE496A0052747C /* TextBubbleView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,15 @@
|
|||
import Foundation
|
||||
import Photos
|
||||
|
||||
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
||||
protocol LiveCameraCaptureSessionProtocol {
|
||||
var captureLayer: AVCaptureVideoPreviewLayer? { get }
|
||||
var isInitialized: Bool { get }
|
||||
var isCapturing: Bool { get }
|
||||
func startCapturing(completion: () -> Void)
|
||||
func stopCapturing(completion: () -> Void)
|
||||
}
|
||||
|
||||
private enum OperationType: String {
|
||||
case start
|
||||
case stop
|
||||
}
|
||||
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
||||
|
||||
var isInitialized: Bool = false
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
|
||||
deinit {
|
||||
var layer = self.captureLayer
|
||||
layer?.removeFromSuperlayer()
|
||||
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
|
||||
// Analogously to AVCaptureSession creation, dealloc can take very long, so let's do it out of the main thread
|
||||
|
|
@ -51,33 +55,31 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
func startCapturing(completion: () -> Void) {
|
||||
let operation = NSBlockOperation()
|
||||
operation.addExecutionBlock { [weak operation, weak self] in
|
||||
guard let strongSelf = self, strongOperation = operation else { return }
|
||||
if !strongOperation.cancelled && !strongSelf.captureSession.running {
|
||||
strongSelf.captureSession.startRunning()
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
|
||||
sSelf.addInputDevicesIfNeeded()
|
||||
sSelf.captureSession.startRunning()
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
self.queue.cancelOperation(forKey: OperationType.stop.rawValue)
|
||||
self.queue.addOperation(operation, forKey: OperationType.start.rawValue)
|
||||
self.queue.cancelAllOperations()
|
||||
self.queue.addOperation(operation)
|
||||
}
|
||||
|
||||
func stopCapturing(completion: () -> Void) {
|
||||
let operation = NSBlockOperation()
|
||||
operation.addExecutionBlock { [weak operation, weak self] in
|
||||
guard let strongSelf = self, strongOperation = operation else { return }
|
||||
if !strongOperation.cancelled && strongSelf.captureSession.running {
|
||||
strongSelf.captureSession.stopRunning()
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
|
||||
sSelf.captureSession.stopRunning()
|
||||
sSelf.removeInputDevices()
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
self.queue.cancelOperation(forKey: OperationType.start.rawValue)
|
||||
self.queue.addOperation(operation, forKey: OperationType.stop.rawValue)
|
||||
self.queue.cancelAllOperations()
|
||||
self.queue.addOperation(operation)
|
||||
}
|
||||
|
||||
private (set) var captureLayer: AVCaptureVideoPreviewLayer?
|
||||
|
||||
private lazy var queue: KeyedOperationQueue = {
|
||||
let queue = KeyedOperationQueue()
|
||||
private lazy var queue: NSOperationQueue = {
|
||||
let queue = NSOperationQueue()
|
||||
queue.qualityOfService = .UserInitiated
|
||||
queue.maxConcurrentOperationCount = 1
|
||||
return queue
|
||||
|
|
@ -87,16 +89,29 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
|
||||
let session = AVCaptureSession()
|
||||
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
|
||||
do {
|
||||
let input = try AVCaptureDeviceInput(device: device)
|
||||
session.addInput(input)
|
||||
} catch {
|
||||
|
||||
}
|
||||
self.captureLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
self.captureLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
|
||||
self.isInitialized = true
|
||||
return session
|
||||
}()
|
||||
|
||||
private func addInputDevicesIfNeeded() {
|
||||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
if self.captureSession.inputs?.count == 0 {
|
||||
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
|
||||
do {
|
||||
let input = try AVCaptureDeviceInput(device: device)
|
||||
self.captureSession.addInput(input)
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeInputDevices() {
|
||||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
self.captureSession.inputs?.forEach { (input) in
|
||||
self.captureSession.removeInput(input as! AVCaptureInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,14 +27,6 @@ import Foundation
|
|||
import UIKit
|
||||
import Chatto
|
||||
|
||||
protocol LiveCameraCaptureSessionProtocol {
|
||||
var captureLayer: AVCaptureVideoPreviewLayer? { get }
|
||||
var isInitialized: Bool { get }
|
||||
var isCapturing: Bool { get }
|
||||
func startCapturing(completion: () -> Void)
|
||||
func stopCapturing(completion: () -> Void)
|
||||
}
|
||||
|
||||
class LiveCameraCell: UICollectionViewCell {
|
||||
|
||||
private struct Constants {
|
||||
|
|
@ -71,22 +63,19 @@ class LiveCameraCell: UICollectionViewCell {
|
|||
captureLayer.removeAnimationForKey(animationKey)
|
||||
captureLayer.addAnimation(animation, forKey: animationKey)
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias CellCallback = (cell: LiveCameraCell) -> Void
|
||||
|
||||
var onWillBeAddedToWindow: CellCallback?
|
||||
override func willMoveToWindow(newWindow: UIWindow?) {
|
||||
if newWindow != nil {
|
||||
self.onWillBeAddedToWindow?(cell: self)
|
||||
}
|
||||
}
|
||||
|
||||
var onWasAddedToWindow: CellCallback?
|
||||
var onWasRemovedFromWindow: CellCallback?
|
||||
override func didMoveToWindow() {
|
||||
if self.window == nil {
|
||||
if let _ = self.window {
|
||||
self.onWasAddedToWindow?(cell: self)
|
||||
} else {
|
||||
self.onWasRemovedFromWindow?(cell: self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ final class LiveCameraCellPresenter {
|
|||
func cellWillBeShown(cell: LiveCameraCell) {
|
||||
self.cell = cell
|
||||
self.configureCell()
|
||||
self.startCapturing()
|
||||
}
|
||||
|
||||
func cellWasHidden(cell: LiveCameraCell) {
|
||||
|
|
@ -51,23 +52,23 @@ final class LiveCameraCellPresenter {
|
|||
|
||||
cameraCell.updateWithAuthorizationStatus(self.cameraAuthorizationStatus)
|
||||
|
||||
self.startCapturing()
|
||||
|
||||
if self.captureSession.isCapturing {
|
||||
cameraCell.captureLayer = self.captureSession.captureLayer
|
||||
} else {
|
||||
cameraCell.captureLayer = nil
|
||||
}
|
||||
|
||||
cameraCell.onWillBeAddedToWindow = { [weak self] (cell) in
|
||||
if self?.cell === cell {
|
||||
self?.configureCell()
|
||||
cameraCell.onWasAddedToWindow = { [weak self] (cell) in
|
||||
guard let sSelf = self where sSelf.cell === cell else { return }
|
||||
if !sSelf.cameraPickerIsVisible {
|
||||
sSelf.startCapturing()
|
||||
}
|
||||
}
|
||||
|
||||
cameraCell.onWasRemovedFromWindow = { [weak self] (cell) in
|
||||
if self?.cell === cell {
|
||||
self?.stopCapturing()
|
||||
guard let sSelf = self where sSelf.cell === cell else { return }
|
||||
if !sSelf.cameraPickerIsVisible {
|
||||
sSelf.stopCapturing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,11 +103,22 @@ final class LiveCameraCellPresenter {
|
|||
}
|
||||
}
|
||||
|
||||
var cameraPickerIsVisible = false
|
||||
func cameraPickerWillAppear() {
|
||||
self.cameraPickerIsVisible = true
|
||||
self.stopCapturing()
|
||||
}
|
||||
|
||||
func cameraPickerDidDisappear() {
|
||||
self.cameraPickerIsVisible = false
|
||||
self.startCapturing()
|
||||
}
|
||||
|
||||
func startCapturing() {
|
||||
guard self.isCaptureAvailable else { return }
|
||||
guard self.isCaptureAvailable, let _ = self.cell else { return }
|
||||
|
||||
self.captureSession.startCapturing() { [weak self] in
|
||||
self?.configureCell()
|
||||
self?.cell?.captureLayer = self?.captureSession.captureLayer
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,19 +30,23 @@ class PhotosInputCameraPicker: NSObject {
|
|||
self.presentingController = presentingController
|
||||
}
|
||||
|
||||
private var requestImageCompletion: ((UIImage?) -> Void)?
|
||||
func requestImage(completion: (UIImage?) -> Void) {
|
||||
private var completionBlocks: (onImageTaken: ((UIImage?) -> Void)?, onCameraPickerDismissed: (() -> Void)?)?
|
||||
|
||||
func presentCameraPicker(onImageTaken onImageTaken: (UIImage?) -> Void, onCameraPickerDismissed: () -> Void) {
|
||||
guard UIImagePickerController.isSourceTypeAvailable(.Camera) else {
|
||||
completion(nil)
|
||||
onImageTaken(nil)
|
||||
onCameraPickerDismissed()
|
||||
return
|
||||
}
|
||||
|
||||
guard let presentingController = self.presentingController else {
|
||||
completion(nil)
|
||||
onImageTaken(nil)
|
||||
onCameraPickerDismissed()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.requestImageCompletion = completion
|
||||
self.completionBlocks = (onImageTaken: onImageTaken, onCameraPickerDismissed: onCameraPickerDismissed)
|
||||
let controller = UIImagePickerController()
|
||||
controller.delegate = self
|
||||
controller.sourceType = .Camera
|
||||
|
|
@ -50,8 +54,9 @@ class PhotosInputCameraPicker: NSObject {
|
|||
}
|
||||
|
||||
private func finishPickingImage(image: UIImage?, fromPicker picker: UIImagePickerController) {
|
||||
picker.dismissViewControllerAnimated(true, completion: nil)
|
||||
self.requestImageCompletion?(image)
|
||||
let (onImageTaken, onCameraPickerDismissed) = self.completionBlocks ?? (nil, nil)
|
||||
picker.dismissViewControllerAnimated(true, completion: onCameraPickerDismissed)
|
||||
onImageTaken?(image)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -204,11 +204,16 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
|
|||
if self.cameraAuthorizationStatus != .Authorized {
|
||||
self.delegate?.inputViewDidRequestCameraPermission(self)
|
||||
} else {
|
||||
self.cameraPicker.requestImage { image in
|
||||
self.liveCameraPresenter.cameraPickerWillAppear()
|
||||
self.cameraPicker.presentCameraPicker(onImageTaken: { [weak self] (image) in
|
||||
guard let sSelf = self else { return }
|
||||
|
||||
if let image = image {
|
||||
self.delegate?.inputView(self, didSelectImage: image)
|
||||
sSelf.delegate?.inputView(sSelf, didSelectImage: image)
|
||||
}
|
||||
}
|
||||
}, onCameraPickerDismissed: { [weak self] in
|
||||
self?.liveCameraPresenter.cameraPickerDidDisappear()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if self.photoLibraryAuthorizationStatus != .Authorized {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Badoo Trading Limited.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
class KeyedOperationQueue: NSOperationQueue {
|
||||
private var keyedOperations = NSMapTable.strongToWeakObjectsMapTable()
|
||||
func addOperation(operation: NSOperation, forKey key: String) {
|
||||
objc_sync_enter(self)
|
||||
if let existingOperation = self.keyedOperations.objectForKey(key) as? NSOperation {
|
||||
existingOperation.cancel()
|
||||
}
|
||||
self.keyedOperations.setObject(operation, forKey: key)
|
||||
objc_sync_exit(self)
|
||||
super.addOperation(operation)
|
||||
}
|
||||
|
||||
func cancelOperation(forKey key: String) {
|
||||
objc_sync_enter(self)
|
||||
if let existingOperation = self.keyedOperations.objectForKey(key) as? NSOperation {
|
||||
existingOperation.cancel()
|
||||
}
|
||||
objc_sync_exit(self)
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,8 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
self.presenter.captureSession = mockCaptureSession
|
||||
|
||||
self.presenter.cameraAuthorizationStatus = .Authorized
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)
|
||||
|
||||
|
|
@ -169,7 +171,9 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
self.presenter.cellWillBeShown(self.cell)
|
||||
self.cell.didMoveToWindow()
|
||||
|
||||
self.cell.willMoveToWindow(UIWindow())
|
||||
let window = UIWindow()
|
||||
window.addSubview(self.cell)
|
||||
|
||||
XCTAssertTrue(mockCaptureSession.isCapturing)
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue