Skip to content

Commit

Permalink
Add MockCaptureDevice and tests for focus and flash states
Browse files Browse the repository at this point in the history
  • Loading branch information
julianschiavo committed Nov 28, 2018
1 parent 1723f90 commit 9831cbe
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 100 deletions.
25 changes: 18 additions & 7 deletions WeScan.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@
A1F22EA5202DB3AA001723AD /* CGPoint+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F22EA4202DB3AA001723AD /* CGPoint+Utils.swift */; };
A1F22ECE2031937E001723AD /* EditScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F22ECD2031937E001723AD /* EditScanViewController.swift */; };
B9274D3A219B951000F9FCD1 /* CIImage+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9274D39219B951000F9FCD1 /* CIImage+Utils.swift */; };
B92A2C8321578D28002874E5 /* CaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92A2C8221578D28002874E5 /* CaptureSession.swift */; };
B940E38A21A77192003B3C0B /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = B940E38721A77192003B3C0B /* [email protected] */; };
B940E38B21A77192003B3C0B /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = B940E38821A77192003B3C0B /* [email protected] */; };
B940E38C21A77192003B3C0B /* enhance.png in Resources */ = {isa = PBXBuildFile; fileRef = B940E38921A77192003B3C0B /* enhance.png */; };
B940E3B821A95C44003B3C0B /* FocusRectangleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3B721A95C44003B3C0B /* FocusRectangleViewTests.swift */; };
B940E3AD21A829EE003B3C0B /* CaptureSession+Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3AC21A829ED003B3C0B /* CaptureSession+Orientation.swift */; };
B940E3B821A95C44003B3C0B /* FocusRectangleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3B721A95C44003B3C0B /* FocusRectangleViewTests.swift */; };
B940E3D821AE0B42003B3C0B /* CaptureDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3D721AE0B42003B3C0B /* CaptureDevice.swift */; };
B940E3DA21AE2919003B3C0B /* CaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3D921AE2919003B3C0B /* CaptureSession.swift */; };
B940E3DC21AE2A64003B3C0B /* CaptureSession+Flash.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3DB21AE2A64003B3C0B /* CaptureSession+Flash.swift */; };
B940E3DE21AE2A79003B3C0B /* CaptureSession+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B940E3DD21AE2A79003B3C0B /* CaptureSession+Focus.swift */; };
B94FBBAD2178652B001ED1B4 /* flash.png in Resources */ = {isa = PBXBuildFile; fileRef = B94FBBAA2178652B001ED1B4 /* flash.png */; };
B94FBBAE2178652B001ED1B4 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = B94FBBAB2178652B001ED1B4 /* [email protected] */; };
B94FBBAF2178652B001ED1B4 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = B94FBBAC2178652B001ED1B4 /* [email protected] */; };
Expand Down Expand Up @@ -223,16 +226,19 @@
A1F22EA4202DB3AA001723AD /* CGPoint+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Utils.swift"; sourceTree = "<group>"; };
A1F22ECD2031937E001723AD /* EditScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditScanViewController.swift; sourceTree = "<group>"; };
B9274D39219B951000F9FCD1 /* CIImage+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CIImage+Utils.swift"; sourceTree = "<group>"; };
B92A2C8221578D28002874E5 /* CaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureSession.swift; sourceTree = "<group>"; };
B940E38721A77192003B3C0B /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
B940E38821A77192003B3C0B /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
B940E38921A77192003B3C0B /* enhance.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = enhance.png; sourceTree = "<group>"; };
B940E3AC21A829ED003B3C0B /* CaptureSession+Orientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CaptureSession+Orientation.swift"; sourceTree = "<group>"; };
B940E3B721A95C44003B3C0B /* FocusRectangleViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusRectangleViewTests.swift; sourceTree = "<group>"; };
B940E3BD21AC2553003B3C0B /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
B940E3C521AC2560003B3C0B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
B940E3C621AC2568003B3C0B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
B940E3C721AC256E003B3C0B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = "<group>"; };
B940E3AC21A829ED003B3C0B /* CaptureSession+Orientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CaptureSession+Orientation.swift"; sourceTree = "<group>"; };
B940E3D721AE0B42003B3C0B /* CaptureDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureDevice.swift; sourceTree = "<group>"; };
B940E3D921AE2919003B3C0B /* CaptureSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureSession.swift; sourceTree = "<group>"; };
B940E3DB21AE2A64003B3C0B /* CaptureSession+Flash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaptureSession+Flash.swift"; sourceTree = "<group>"; };
B940E3DD21AE2A79003B3C0B /* CaptureSession+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaptureSession+Focus.swift"; sourceTree = "<group>"; };
B94FBBAA2178652B001ED1B4 /* flash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = flash.png; sourceTree = "<group>"; };
B94FBBAB2178652B001ED1B4 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
B94FBBAC2178652B001ED1B4 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -422,14 +428,14 @@
isa = PBXGroup;
children = (
A1DF90E320358CB000841A11 /* Transformable.swift */,
B940E3D721AE0B42003B3C0B /* CaptureDevice.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
A1DF90F720371A0800841A11 /* Common */ = {
isa = PBXGroup;
children = (
B92A2C8221578D28002874E5 /* CaptureSession.swift */,
A1DF90A320331A0B00841A11 /* CIRectangleDetector.swift */,
B992E830210C36A400C33A21 /* VisionRectangleDetector.swift */,
A1F22E9E202C8D70001723AD /* Quadrilateral.swift */,
Expand Down Expand Up @@ -484,7 +490,9 @@
B940E3A021A82931003B3C0B /* Session */ = {
isa = PBXGroup;
children = (
B92A2C8221578D28002874E5 /* CaptureSession.swift */,
B940E3D921AE2919003B3C0B /* CaptureSession.swift */,
B940E3DB21AE2A64003B3C0B /* CaptureSession+Flash.swift */,
B940E3DD21AE2A79003B3C0B /* CaptureSession+Focus.swift */,
B940E3AC21A829ED003B3C0B /* CaptureSession+Orientation.swift */,
);
path = Session;
Expand Down Expand Up @@ -767,12 +775,14 @@
A1DF90E420358CB100841A11 /* Transformable.swift in Sources */,
A1D4BD0C202C504F00FCDDEC /* ScannerViewController.swift in Sources */,
A11C5B9C2046A20C005075FE /* Error.swift in Sources */,
B940E3DE21AE2A79003B3C0B /* CaptureSession+Focus.swift in Sources */,
B940E3DC21AE2A64003B3C0B /* CaptureSession+Flash.swift in Sources */,
A1D4BD15202C6CC000FCDDEC /* Array+Utils.swift in Sources */,
B92A2C8321578D28002874E5 /* CaptureSession.swift in Sources */,
C3E2EB8E20B8970800A42E58 /* UIImage+Utils.swift in Sources */,
A165F67E2044741B002D5ED6 /* ShutterButton.swift in Sources */,
B940E3AD21A829EE003B3C0B /* CaptureSession+Orientation.swift in Sources */,
B992E831210C36A400C33A21 /* VisionRectangleDetector.swift in Sources */,
B940E3D821AE0B42003B3C0B /* CaptureDevice.swift in Sources */,
A1F22EA5202DB3AA001723AD /* CGPoint+Utils.swift in Sources */,
A14089BA204D92EA0009530F /* EditScanCornerView.swift in Sources */,
A1DF90A02031D89D00841A11 /* ImageScannerController.swift in Sources */,
Expand All @@ -783,6 +793,7 @@
A1DF90F62037187500841A11 /* UIImage+Orientation.swift in Sources */,
A1F22E99202C7B66001723AD /* QuadrilateralView.swift in Sources */,
A194E97220431DD7003493E2 /* ReviewViewController.swift in Sources */,
B940E3DA21AE2919003B3C0B /* CaptureSession.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
63 changes: 63 additions & 0 deletions WeScan/Protocols/CaptureDevice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// CaptureDevice.swift
// WeScan
//
// Created by Julian Schiavo on 28/11/2018.
// Copyright © 2018 WeTransfer. All rights reserved.
//

import Foundation
import AVFoundation

protocol CaptureDevice: class {
func unlockForConfiguration()
func lockForConfiguration() throws

var torchMode: AVCaptureDevice.TorchMode { get set }
var isTorchAvailable: Bool { get }

var focusMode: AVCaptureDevice.FocusMode { get set }
var focusPointOfInterest: CGPoint { get set }
var isFocusPointOfInterestSupported: Bool { get }
func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool

var exposureMode: AVCaptureDevice.ExposureMode { get set }
var exposurePointOfInterest: CGPoint { get set }
var isExposurePointOfInterestSupported: Bool { get }
func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool

var isSubjectAreaChangeMonitoringEnabled: Bool { get set }
}

extension AVCaptureDevice: CaptureDevice { }

class MockCaptureDevice: CaptureDevice {
func unlockForConfiguration() {
return
}

func lockForConfiguration() throws {
return
}

var torchMode: AVCaptureDevice.TorchMode = .off
var isTorchAvailable: Bool = true

var focusMode: AVCaptureDevice.FocusMode = .continuousAutoFocus
var focusPointOfInterest: CGPoint = .zero
var isFocusPointOfInterestSupported: Bool = true

var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure
var exposurePointOfInterest: CGPoint = .zero
var isExposurePointOfInterestSupported: Bool = true

func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool {
return true
}

func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool {
return true
}

var isSubjectAreaChangeMonitoringEnabled: Bool = false
}
45 changes: 45 additions & 0 deletions WeScan/Session/CaptureSession+Flash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// CaptureSession+Flash.swift
// WeScan
//
// Created by Julian Schiavo on 28/11/2018.
// Copyright © 2018 WeTransfer. All rights reserved.
//

import Foundation

/// Extension to CaptureSession to manage the device flashlight
extension CaptureSession {
/// The possible states that the current device's flashlight can be in
enum FlashState {
case on
case off
case unavailable
case unknown
}

/// Toggles the current device's flashlight on or off.
func toggleFlash() -> FlashState {
guard let device = device, device.isTorchAvailable else { return .unavailable }

do {
try device.lockForConfiguration()
} catch {
return .unknown
}

defer {
device.unlockForConfiguration()
}

if device.torchMode == .on {
device.torchMode = .off
return .off
} else if device.torchMode == .off {
device.torchMode = .on
return .on
}

return .unknown
}
}
72 changes: 72 additions & 0 deletions WeScan/Session/CaptureSession+Focus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// CaptureSession+Focus.swift
// WeScan
//
// Created by Julian Schiavo on 28/11/2018.
// Copyright © 2018 WeTransfer. All rights reserved.
//

import Foundation

/// Extension to CaptureSession that controls auto focus
extension CaptureSession {
/// Sets the camera's exposure and focus point to the given point
func setFocusPointToTapPoint(_ tapPoint: CGPoint) throws {
guard let device = device else {
let error = ImageScannerControllerError.inputDevice
throw error
}

try device.lockForConfiguration()

defer {
device.unlockForConfiguration()
}

if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) {
device.focusPointOfInterest = tapPoint
device.focusMode = .autoFocus
}

if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
device.exposurePointOfInterest = tapPoint
device.exposureMode = .continuousAutoExposure
}
}

/// Resets the camera's exposure and focus point to automatic
func resetFocusToAuto() throws {
guard let device = device else {
let error = ImageScannerControllerError.inputDevice
throw error
}

try device.lockForConfiguration()

defer {
device.unlockForConfiguration()
}

if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) {
device.focusMode = .continuousAutoFocus
}

if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure
}
}

/// Removes an existing focus rectangle if one exists, optionally animating the exit
func removeFocusRectangleIfNeeded(_ focusRectangle: FocusRectangleView?, animated: Bool) {
guard let focusRectangle = focusRectangle else { return }
if animated {
UIView.animate(withDuration: 0.3, delay: 1.0, animations: {
focusRectangle.alpha = 0.0
}, completion: { (_) in
focusRectangle.removeFromSuperview()
})
} else {
focusRectangle.removeFromSuperview()
}
}
}
96 changes: 5 additions & 91 deletions WeScan/Session/CaptureSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ final class CaptureSession {

static let current = CaptureSession()

/// The AVCaptureDevice used for the flash and focus setting
var device: CaptureDevice?

/// Whether the user is past the scanning screen or not (needed to disable auto scan on other screens)
var isEditing: Bool

Expand All @@ -24,100 +27,11 @@ final class CaptureSession {
var editImageOrientation: CGImagePropertyOrientation

private init(isAutoScanEnabled: Bool = true, editImageOrientation: CGImagePropertyOrientation = .up) {
self.device = AVCaptureDevice.default(for: .video)

self.isEditing = false
self.isAutoScanEnabled = isAutoScanEnabled
self.editImageOrientation = editImageOrientation
}

}

/// Extension to CaptureSession to manage the device flashlight
extension CaptureSession {
/// The possible states that the current device's flashlight can be in
enum FlashState {
case on
case off
case unavailable
case unknown
}

/// Toggles the current device's flashlight on or off.
func toggleFlash() -> FlashState {
guard let device = AVCaptureDevice.default(for: AVMediaType.video), device.isTorchAvailable else { return .unavailable }

do {
try device.lockForConfiguration()
} catch {
return .unknown
}

defer {
device.unlockForConfiguration()
}

if device.torchMode == .on {
device.torchMode = .off
return .off
} else if device.torchMode == .off {
device.torchMode = .on
return .on
}

return .unknown
}
}

/// Extension to CaptureSession that controls auto focus
extension CaptureSession {
/// Sets the camera's exposure and focus point to the given point
func setFocusPointToTapPoint(_ tapPoint: CGPoint) throws {
guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return }
try device.lockForConfiguration()

defer {
device.unlockForConfiguration()
}

if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) {
device.focusPointOfInterest = tapPoint
device.focusMode = .autoFocus
}

if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
device.exposurePointOfInterest = tapPoint
device.exposureMode = .continuousAutoExposure
}
}

/// Resets the camera's exposure and focus point to automatic
func resetFocusToAuto() throws {
guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return }
try device.lockForConfiguration()

defer {
device.unlockForConfiguration()
}

if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) {
device.focusMode = .continuousAutoFocus
}

if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure
}
}

/// Removes an existing focus rectangle if one exists, optionally animating the exit
func removeFocusRectangleIfNeeded(_ focusRectangle: FocusRectangleView?, animated: Bool) {
guard let focusRectangle = focusRectangle else { return }
if animated {
UIView.animate(withDuration: 0.3, delay: 1.0, animations: {
focusRectangle.alpha = 0.0
}, completion: { (_) in
focusRectangle.removeFromSuperview()
})
} else {
focusRectangle.removeFromSuperview()
}
}
}
Loading

0 comments on commit 9831cbe

Please sign in to comment.