Improves responsiveness of camera when switching to/from full-screen camera view.

This commit is contained in:
Diego Sanchez 2016-07-05 22:42:48 +01:00
parent e4315bf4e8
commit 8f9ce5df4e
9 changed files with 495 additions and 519 deletions

View File

@ -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 */,

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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