Merge branch 'nt/live-camera'

This commit is contained in:
Nora Trapp 2019-08-09 12:57:37 -07:00
commit 7aadee0062
10 changed files with 190 additions and 39 deletions

View File

@ -5,7 +5,7 @@
import Foundation
protocol AttachmentFormatPickerDelegate: class {
func didTapCamera()
func didTapCamera(withPhotoCapture: PhotoCapture?)
func didTapGif()
func didTapFile()
func didTapContact()
@ -20,6 +20,7 @@ class AttachmentFormatPickerView: UICollectionView {
}
private let collectionViewFlowLayout = UICollectionViewFlowLayout()
private var photoCapture: PhotoCapture?
override var bounds: CGRect {
didSet {
@ -47,6 +48,30 @@ class AttachmentFormatPickerView: UICollectionView {
updateLayout()
}
deinit {
stopCameraPreview()
}
func startCameraPreview() {
guard photoCapture == nil else { return }
let photoCapture = PhotoCapture()
self.photoCapture = photoCapture
photoCapture.startVideoCapture().done { [weak self] in
self?.reloadData()
}.retainUntilComplete()
}
func stopCameraPreview() {
photoCapture?.stopCapture().retainUntilComplete()
photoCapture = nil
}
func orientationDidChange() {
updateLayout()
}
private func updateLayout() {
AssertIsOnMainThread()
@ -77,7 +102,11 @@ extension AttachmentFormatPickerView: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
switch AttachmentType.allCases[indexPath.row] {
case .camera:
attachmentFormatPickerDelegate?.didTapCamera()
// Since we're trying to pass on our prepared capture session to
// the camera view, nil it out so we don't try and stop it here.
let photoCapture = self.photoCapture
self.photoCapture = nil
attachmentFormatPickerDelegate?.didTapCamera(withPhotoCapture: photoCapture)
case .contact:
attachmentFormatPickerDelegate?.didTapContact()
case .file:
@ -108,7 +137,7 @@ extension AttachmentFormatPickerView: UICollectionViewDataSource {
}
let type = AttachmentType.allCases[indexPath.item]
cell.configure(type: type)
cell.configure(type: type, cameraPreview: photoCapture?.previewView)
return cell
}
}
@ -121,6 +150,11 @@ class AttachmentFormatCell: UICollectionViewCell {
let label = UILabel()
var attachmentType: AttachmentType?
weak var cameraPreview: CapturePreviewView?
private var hasCameraAccess: Bool {
return AVCaptureDevice.authorizationStatus(for: .video) == .authorized
}
override init(frame: CGRect) {
@ -164,8 +198,9 @@ class AttachmentFormatCell: UICollectionViewCell {
notImplemented()
}
public func configure(type: AttachmentType) {
public func configure(type: AttachmentType, cameraPreview: CapturePreviewView?) {
self.attachmentType = type
self.cameraPreview = cameraPreview
let imageName: String
let text: String
@ -196,6 +231,25 @@ class AttachmentFormatCell: UICollectionViewCell {
}
label.text = text
showLiveCameraIfAvailable()
}
func showLiveCameraIfAvailable() {
guard case .camera? = attachmentType, hasCameraAccess else { return }
// If we have access to the camera, we'll want to show it eventually.
// Style this in prepration for that.
imageView.setTemplateImageName("camera-outline-32", tintColor: .white)
label.textColor = .white
backgroundColor = UIColor.black.withAlphaComponent(0.4)
guard let cameraPreview = cameraPreview else { return }
contentView.insertSubview(cameraPreview, belowSubview: imageView)
cameraPreview.autoPinEdgesToSuperviewEdges()
cameraPreview.contentMode = .scaleAspectFill
}
override public func prepareForReuse() {
@ -203,5 +257,12 @@ class AttachmentFormatCell: UICollectionViewCell {
attachmentType = nil
imageView.image = nil
label.textColor = Theme.attachmentKeyboardItemImageColor
backgroundColor = Theme.attachmentKeyboardItemBackgroundColor
if let cameraPreview = cameraPreview, cameraPreview.superview == contentView {
cameraPreview.removeFromSuperview()
}
}
}

View File

@ -10,7 +10,7 @@ import PromiseKit
protocol AttachmentKeyboardDelegate {
func didSelectRecentPhoto(_ attachment: SignalAttachment)
func didTapGalleryButton()
func didTapCamera()
func didTapCamera(withPhotoCapture: PhotoCapture?)
func didTapGif()
func didTapFile()
func didTapContact()
@ -147,6 +147,19 @@ class AttachmentKeyboard: CustomKeyboard {
checkPermissionsAndLayout()
}
override func wasDismissed() {
super.wasDismissed()
attachmentFormatPickerView.stopCameraPreview()
}
override func orientationDidChange() {
super.orientationDidChange()
recentPhotosCollectionView.orientationDidChange()
attachmentFormatPickerView.orientationDidChange()
}
func checkPermissionsAndLayout() {
switch mediaLibraryAuthorizationStatus {
case .authorized:
@ -154,9 +167,9 @@ class AttachmentKeyboard: CustomKeyboard {
case .denied, .restricted:
showRecentPhotosError()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { _ in
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
self.recentPhotosCollectionView.permissionChanged()
if status == .authorized { self.recentPhotosCollectionView.permissionChanged() }
self.checkPermissionsAndLayout()
}
}
@ -164,6 +177,21 @@ class AttachmentKeyboard: CustomKeyboard {
showRecentPhotosError()
break
}
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
attachmentFormatPickerView.startCameraPreview()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async { self.attachmentFormatPickerView.startCameraPreview() }
}
}
case .denied, .restricted:
break
@unknown default:
break
}
}
}
@ -178,8 +206,8 @@ extension AttachmentKeyboard: RecentPhotosDelegate {
}
extension AttachmentKeyboard: AttachmentFormatPickerDelegate {
func didTapCamera() {
delegate?.didTapCamera()
func didTapCamera(withPhotoCapture photoCapture: PhotoCapture?) {
delegate?.didTapCamera(withPhotoCapture: photoCapture)
}
func didTapGif() {

View File

@ -88,6 +88,10 @@ class RecentPhotosCollectionView: UICollectionView {
updateLayout()
}
func orientationDidChange() {
updateLayout()
}
private func updateLayout() {
AssertIsOnMainThread()

View File

@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class PhotoCapture;
@class SignalAttachment;
@class StickerInfo;
@ -34,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cameraButtonPressed;
- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture;
- (void)galleryButtonPressed;
- (void)gifButtonPressed;

View File

@ -504,11 +504,6 @@ const CGFloat kMaxTextViewHeight = 98;
{
if (!self.desiredFirstResponder.isFirstResponder) {
[self.desiredFirstResponder becomeFirstResponder];
if ([self.desiredFirstResponder isKindOfClass:[OWSCustomKeyboard class]]) {
OWSCustomKeyboard *customKeyboard = (OWSCustomKeyboard *)self.desiredFirstResponder;
[customKeyboard wasPresented];
}
}
}
@ -1532,9 +1527,9 @@ const CGFloat kMaxTextViewHeight = 98;
[self.inputToolbarDelegate galleryButtonPressed];
}
- (void)didTapCamera
- (void)didTapCameraWithPhotoCapture:(nullable PhotoCapture *)photoCapture
{
[self.inputToolbarDelegate cameraButtonPressed];
[self.inputToolbarDelegate cameraButtonPressedWithPhotoCapture:photoCapture];
}
- (void)didTapGif

View File

@ -2867,7 +2867,7 @@ typedef enum : NSUInteger {
#pragma mark - Media Libary
- (void)takePictureOrVideo
- (void)takePictureOrVideoWithPhotoCapture:(nullable PhotoCapture *)photoCapture
{
[BenchManager startEventWithTitle:@"Show-Camera" eventId:@"Show-Camera"];
[self ows_askForCameraPermissions:^(BOOL cameraGranted) {
@ -2882,7 +2882,8 @@ typedef enum : NSUInteger {
// be silent.
}
SendMediaNavigationController *pickerModal = [SendMediaNavigationController showingCameraFirst];
SendMediaNavigationController *pickerModal =
[SendMediaNavigationController showingCameraFirstWithPhotoCapture:photoCapture];
pickerModal.sendMediaNavDelegate = self;
[self dismissKeyBoard];
@ -3215,7 +3216,14 @@ typedef enum : NSUInteger {
{
OWSAssertIsOnMainThread();
[self takePictureOrVideo];
[self takePictureOrVideoWithPhotoCapture:nil];
}
- (void)cameraButtonPressedWithPhotoCapture:(nullable PhotoCapture *)photoCapture
{
OWSAssertIsOnMainThread();
[self takePictureOrVideoWithPhotoCapture:photoCapture];
}
- (void)galleryButtonPressed

View File

@ -30,6 +30,7 @@ protocol PhotoCaptureDelegate: AnyObject {
func endCaptureButtonAnimation(_ duration: TimeInterval)
}
@objc
class PhotoCapture: NSObject {
weak var delegate: PhotoCaptureDelegate?
@ -38,6 +39,9 @@ class PhotoCapture: NSObject {
}
let session: AVCaptureSession
// There can only ever be one `CapturePreviewView` per AVCaptureSession
lazy private(set) var previewView = CapturePreviewView(session: session)
let sessionQueue = DispatchQueue(label: "PhotoCapture.sessionQueue")
private var currentCaptureInput: AVCaptureDeviceInput?
@ -101,6 +105,9 @@ class PhotoCapture: NSObject {
}
func startVideoCapture() -> Promise<Void> {
// If the session is already running, no need to do anything.
guard !self.session.isRunning else { return Promise.value(()) }
return sessionQueue.async(.promise) { [weak self] in
guard let self = self else { return }
@ -410,11 +417,11 @@ extension PhotoCapture: VolumeButtonObserver {
func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
handleLongPressBegin()
}
func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
handleLongPressComplete()
}
func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
handleLongPressCancel()
}

View File

@ -36,15 +36,13 @@ class PhotoCaptureViewController: OWSViewController {
weak var delegate: PhotoCaptureViewControllerDelegate?
private var photoCapture: PhotoCapture!
@objc public lazy var photoCapture = PhotoCapture()
deinit {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
if let photoCapture = photoCapture {
photoCapture.stopCapture().done {
Logger.debug("stopCapture completed")
}.retainUntilComplete()
}
photoCapture.stopCapture().done {
Logger.debug("stopCapture completed")
}.retainUntilComplete()
}
// MARK: - Overrides
@ -136,7 +134,9 @@ class PhotoCaptureViewController: OWSViewController {
// MARK: - Views
let captureButton = CaptureButton()
var previewView: CapturePreviewView!
var previewView: CapturePreviewView {
return photoCapture.previewView
}
class PhotoControl {
let button: OWSButton
@ -315,21 +315,26 @@ class PhotoCaptureViewController: OWSViewController {
var hasCaptureStarted = false
private func setupPhotoCapture() {
photoCapture = PhotoCapture()
photoCapture.delegate = self
captureButton.delegate = photoCapture
previewView = CapturePreviewView(session: photoCapture.session)
previewView.contentMode = .scaleAspectFit
photoCapture.startVideoCapture().done { [weak self] in
let captureReady = { [weak self] in
guard let self = self else { return }
self.hasCaptureStarted = true
self.showCaptureUI()
BenchEventComplete(eventId: "Show-Camera")
}.catch { [weak self] error in
guard let self = self else { return }
}
self.showFailureUI(error: error)
}.retainUntilComplete()
// If the session is already running, we're good to go.
guard !photoCapture.session.isRunning else { return captureReady() }
photoCapture.startVideoCapture()
.done(captureReady)
.catch { [weak self] error in
guard let self = self else { return }
self.showFailureUI(error: error)
}.retainUntilComplete()
}
private func showCaptureUI() {
@ -646,10 +651,38 @@ class CapturePreviewView: UIView {
}
}
override var contentMode: UIView.ContentMode {
set {
switch newValue {
case .scaleAspectFill:
previewLayer.videoGravity = .resizeAspectFill
case .scaleAspectFit:
previewLayer.videoGravity = .resizeAspect
case .scaleToFill:
previewLayer.videoGravity = .resize
default:
owsFailDebug("Unexpected contentMode")
}
}
get {
switch previewLayer.videoGravity {
case .resizeAspectFill:
return .scaleAspectFill
case .resizeAspect:
return .scaleAspectFit
case .resize:
return .scaleToFill
default:
owsFailDebug("Unexpected contentMode")
return .scaleToFill
}
}
}
init(session: AVCaptureSession) {
previewLayer = AVCaptureVideoPreviewLayer(session: session)
super.init(frame: .zero)
self.contentMode = .scaleAspectFill
self.contentMode = .scaleAspectFit
previewLayer.frame = bounds
layer.addSublayer(previewLayer)
}

View File

@ -106,8 +106,11 @@ class SendMediaNavigationController: OWSNavigationController {
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc
public class func showingCameraFirst() -> SendMediaNavigationController {
public class func showingCameraFirst(photoCapture: PhotoCapture?) -> SendMediaNavigationController {
let navController = SendMediaNavigationController()
if let photoCapture = photoCapture {
navController.captureViewController.photoCapture = photoCapture
}
navController.setViewControllers([navController.captureViewController], animated: false)
return navController
}

View File

@ -31,7 +31,8 @@ open class CustomKeyboard: UIInputView {
fatalError("init(coder:) has not been implemented")
}
@objc open func wasPresented() {}
open func wasPresented() {}
open func wasDismissed() {}
@objc public func registerWithView(_ view: UIView) {
view.addSubview(responder)
@ -48,6 +49,14 @@ open class CustomKeyboard: UIInputView {
return responder.resignFirstResponder()
}
open override func didMoveToSuperview() {
if superview == nil {
wasDismissed()
} else {
wasPresented()
}
}
// MARK: - Height Management
private lazy var heightConstraint = autoSetDimension(.height, toSize: 0)
@ -107,7 +116,7 @@ open class CustomKeyboard: UIInputView {
}
private class CustomKeyboardResponder: UIView {
@objc public let customKeyboard: CustomKeyboard
@objc public weak var customKeyboard: CustomKeyboard?
init(customKeyboard: CustomKeyboard) {
self.customKeyboard = customKeyboard