Skip to content

Commit bdc0450

Browse files
authored
Merge pull request #91 from hyperoslo/feature/camera-position
Feature: camera position button
2 parents 515d0cd + cad9061 commit bdc0450

File tree

5 files changed

+131
-35
lines changed

5 files changed

+131
-35
lines changed

BarcodeScanner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
D50BE3E91C9FE7A80000A34C /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = D50BE3E51C9FE7A80000A34C /* [email protected] */; };
1313
D50BE3EA1C9FE7A80000A34C /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = D50BE3E61C9FE7A80000A34C /* [email protected] */; };
1414
D50BE3EB1C9FE7A80000A34C /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = D50BE3E71C9FE7A80000A34C /* [email protected] */; };
15+
D5349DF8201E42D900CD53EA /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = D5349DF7201E42D900CD53EA /* [email protected] */; };
1516
D55281B62016758F00FF3CDD /* HeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B52016758F00FF3CDD /* HeaderViewController.swift */; };
1617
D55281B8201675D500FF3CDD /* MessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B7201675D500FF3CDD /* MessageViewController.swift */; };
1718
D55281BA2016770800FF3CDD /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55281B92016770800FF3CDD /* CameraViewController.swift */; };
@@ -31,6 +32,7 @@
3132
D50BE3E51C9FE7A80000A34C /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
3233
D50BE3E61C9FE7A80000A34C /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
3334
D50BE3E71C9FE7A80000A34C /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
35+
D5349DF7201E42D900CD53EA /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
3436
D55281B52016758F00FF3CDD /* HeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderViewController.swift; sourceTree = "<group>"; };
3537
D55281B7201675D500FF3CDD /* MessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewController.swift; sourceTree = "<group>"; };
3638
D55281B92016770800FF3CDD /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = "<group>"; };
@@ -60,6 +62,7 @@
6062
D50BE3E31C9FE7A80000A34C /* Images */ = {
6163
isa = PBXGroup;
6264
children = (
65+
D5349DF7201E42D900CD53EA /* [email protected] */,
6366
D50BE3E51C9FE7A80000A34C /* [email protected] */,
6467
D50BE3E61C9FE7A80000A34C /* [email protected] */,
6568
D50BE3E71C9FE7A80000A34C /* [email protected] */,
@@ -215,6 +218,7 @@
215218
buildActionMask = 2147483647;
216219
files = (
217220
D50BE3E91C9FE7A80000A34C /* [email protected] in Resources */,
221+
D5349DF8201E42D900CD53EA /* [email protected] in Resources */,
218222
D50BE3EB1C9FE7A80000A34C /* [email protected] in Resources */,
219223
D50BE3EA1C9FE7A80000A34C /* [email protected] in Resources */,
220224
);

Images/[email protected]

9.13 KB
Loading

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,16 @@ viewController.messageViewController.textLabel.textColor = .black
181181
**Camera**
182182
```swift
183183
let viewController = BarcodeScannerViewController()
184-
viewController.barCodeFocusViewType = .animated
184+
// Change focus view style
185+
viewController.cameraViewController.barCodeFocusViewType = .animated
186+
// Show camera position button
187+
viewController.cameraViewController.showsCameraButton = true
188+
// Set settings button text
185189
let title = NSAttributedString(
186190
string: "Settings",
187191
attributes: [.font: UIFont.boldSystemFont(ofSize: 17), .foregroundColor : UIColor.white]
188192
)
189-
viewController.settingButton.setAttributedTitle(title, for: UIControlState())
193+
viewController.cameraViewController.settingButton.setAttributedTitle(title, for: UIControlState())
190194
```
191195

192196
**Metadata**

Sources/Controllers/BarcodeScannerViewController.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import AVFoundation
55

66
/// Delegate to handle the captured code.
77
public protocol BarcodeScannerCodeDelegate: class {
8-
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String)
8+
func scanner(
9+
_ controller: BarcodeScannerViewController,
10+
didCaptureCode code: String,
11+
type: String
12+
)
913
}
1014

1115
/// Delegate to report errors.
@@ -28,6 +32,7 @@ public protocol BarcodeScannerDismissalDelegate: class {
2832
- Not found error message
2933
*/
3034
open class BarcodeScannerViewController: UIViewController {
35+
private static let footerHeight: CGFloat = 75
3136

3237
// MARK: - Public properties
3338

@@ -66,9 +71,9 @@ open class BarcodeScannerViewController: UIViewController {
6671
public private(set) lazy var cameraViewController: CameraViewController = .init()
6772

6873
// Constraints that are activated when the view is used as a footer.
69-
private lazy var collapsedConstraints: [NSLayoutConstraint] = self.makeCollapsedMessageConstraints()
74+
private lazy var collapsedConstraints: [NSLayoutConstraint] = self.makeCollapsedConstraints()
7075
// Constraints that are activated when the view is used for loading animation and error messages.
71-
private lazy var expandedConstraints: [NSLayoutConstraint] = self.makeExpandedMessageConstraints()
76+
private lazy var expandedConstraints: [NSLayoutConstraint] = self.makeExpandedConstraints()
7277

7378
private var messageView: UIView {
7479
return messageViewController.view
@@ -220,7 +225,10 @@ private extension BarcodeScannerViewController {
220225
NSLayoutConstraint.activate(
221226
cameraView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
222227
cameraView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
223-
cameraView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
228+
cameraView.bottomAnchor.constraint(
229+
equalTo: view.bottomAnchor,
230+
constant: -BarcodeScannerViewController.footerHeight
231+
)
224232
)
225233

226234
if navigationController != nil {
@@ -241,7 +249,7 @@ private extension BarcodeScannerViewController {
241249
}
242250
}
243251

244-
private func makeExpandedMessageConstraints() -> [NSLayoutConstraint] {
252+
private func makeExpandedConstraints() -> [NSLayoutConstraint] {
245253
return [
246254
messageView.topAnchor.constraint(equalTo: view.topAnchor),
247255
messageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
@@ -250,12 +258,14 @@ private extension BarcodeScannerViewController {
250258
]
251259
}
252260

253-
private func makeCollapsedMessageConstraints() -> [NSLayoutConstraint] {
261+
private func makeCollapsedConstraints() -> [NSLayoutConstraint] {
254262
return [
255263
messageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
256264
messageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
257265
messageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
258-
messageView.heightAnchor.constraint(equalToConstant: 75)
266+
messageView.heightAnchor.constraint(
267+
equalToConstant: BarcodeScannerViewController.footerHeight
268+
)
259269
]
260270
}
261271
}

Sources/Controllers/CameraViewController.swift

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public final class CameraViewController: UIViewController {
1919

2020
/// Focus view type.
2121
public var barCodeFocusViewType: FocusViewType = .animated
22+
public var showsCameraButton: Bool = false {
23+
didSet {
24+
cameraButton.isHidden = showsCameraButton
25+
}
26+
}
2227
/// `AVCaptureMetadataOutput` metadata object types.
2328
var metadata = [AVMetadataObject.ObjectType]()
2429

@@ -30,6 +35,8 @@ public final class CameraViewController: UIViewController {
3035
public private(set) lazy var flashButton: UIButton = .init(type: .custom)
3136
/// Button that opens settings to allow camera usage.
3237
public private(set) lazy var settingsButton: UIButton = self.makeSettingsButton()
38+
// Button to switch between front and back camera.
39+
public private(set) lazy var cameraButton: UIButton = self.makeCameraButton()
3340

3441
// Constraints for the focus view when it gets smaller in size.
3542
private var regularFocusViewConstraints = [NSLayoutConstraint]()
@@ -41,7 +48,7 @@ public final class CameraViewController: UIViewController {
4148
/// Video preview layer.
4249
private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
4350
/// Video capture device. This may be nil when running in Simulator.
44-
private lazy var captureDevice: AVCaptureDevice! = AVCaptureDevice.default(for: .video)
51+
private var captureDevice: AVCaptureDevice?
4552
/// Capture session.
4653
private lazy var captureSession: AVCaptureSession = AVCaptureSession()
4754
// Service used to check authorization status of the capture device
@@ -62,6 +69,14 @@ public final class CameraViewController: UIViewController {
6269
}
6370
}
6471

72+
private var frontCameraDevice: AVCaptureDevice? {
73+
return AVCaptureDevice.devices(for: .video).first(where: { $0.position == .front })
74+
}
75+
76+
private var backCameraDevice: AVCaptureDevice? {
77+
return AVCaptureDevice.default(for: .video)
78+
}
79+
6580
// MARK: - Initialization
6681

6782
deinit {
@@ -73,31 +88,21 @@ public final class CameraViewController: UIViewController {
7388
public override func viewDidLoad() {
7489
super.viewDidLoad()
7590
view.backgroundColor = .black
76-
7791
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
78-
videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
92+
videoPreviewLayer?.videoGravity = .resizeAspectFill
7993

8094
guard let videoPreviewLayer = videoPreviewLayer else {
8195
return
8296
}
8397

8498
view.layer.addSublayer(videoPreviewLayer)
85-
view.addSubviews(settingsButton, flashButton, focusView)
99+
view.addSubviews(settingsButton, flashButton, focusView, cameraButton)
86100

87101
torchMode = .off
88102
focusView.isHidden = true
89103
setupCamera()
90104
setupConstraints()
91-
92-
flashButton.addTarget(self, action: #selector(flashButtonDidPress), for: .touchUpInside)
93-
settingsButton.addTarget(self, action: #selector(settingsButtonDidPress), for: .touchUpInside)
94-
95-
NotificationCenter.default.addObserver(
96-
self,
97-
selector: #selector(appWillEnterForeground),
98-
name: NSNotification.Name.UIApplicationWillEnterForeground,
99-
object: nil
100-
)
105+
setupActions()
101106
}
102107

103108
public override func viewDidAppear(_ animated: Bool) {
@@ -149,19 +154,49 @@ public final class CameraViewController: UIViewController {
149154

150155
// MARK: - Actions
151156

157+
private func setupActions() {
158+
flashButton.addTarget(
159+
self,
160+
action: #selector(handleFlashButtonTap),
161+
for: .touchUpInside
162+
)
163+
settingsButton.addTarget(
164+
self,
165+
action: #selector(handleSettingsButtonTap),
166+
for: .touchUpInside
167+
)
168+
cameraButton.addTarget(
169+
self,
170+
action: #selector(handleCameraButtonTap),
171+
for: .touchUpInside
172+
)
173+
174+
NotificationCenter.default.addObserver(
175+
self,
176+
selector: #selector(appWillEnterForeground),
177+
name: NSNotification.Name.UIApplicationWillEnterForeground,
178+
object: nil
179+
)
180+
}
181+
152182
/// `UIApplicationWillEnterForegroundNotification` action.
153183
@objc private func appWillEnterForeground() {
154184
torchMode = .off
155185
animateFocusView()
156186
}
157187

158188
/// Opens setting to allow camera usage.
159-
@objc private func settingsButtonDidPress() {
189+
@objc private func handleSettingsButtonTap() {
160190
delegate?.cameraViewControllerDidTapSettingsButton(self)
161191
}
162192

193+
/// Swaps camera position.
194+
@objc private func handleCameraButtonTap() {
195+
swapCamera()
196+
}
197+
163198
/// Sets the next torch mode.
164-
@objc private func flashButtonDidPress() {
199+
@objc private func handleFlashButtonTap() {
165200
torchMode = torchMode.next
166201
}
167202

@@ -179,7 +214,8 @@ public final class CameraViewController: UIViewController {
179214
}
180215

181216
if error == nil {
182-
strongSelf.setupSession()
217+
strongSelf.setupSessionInput(for: .back)
218+
strongSelf.setupSessionOutput()
183219
strongSelf.delegate?.cameraViewControllerDidSetupCaptureSession(strongSelf)
184220
} else {
185221
strongSelf.delegate?.cameraViewControllerDidFailToSetupCaptureSession(strongSelf)
@@ -188,16 +224,34 @@ public final class CameraViewController: UIViewController {
188224
}
189225

190226
/// Sets up capture input, output and session.
191-
private func setupSession() {
192-
guard let captureDevice = captureDevice, !isSimulatorRunning else {
227+
private func setupSessionInput(for position: AVCaptureDevice.Position) {
228+
guard !isSimulatorRunning else {
229+
return
230+
}
231+
232+
guard let device = position == .front ? frontCameraDevice : backCameraDevice else {
193233
return
194234
}
195235

196236
do {
197-
let input = try AVCaptureDeviceInput(device: captureDevice)
198-
captureSession.addInput(input)
237+
let newInput = try AVCaptureDeviceInput(device: device)
238+
captureDevice = device
239+
// Swap capture device inputs
240+
captureSession.beginConfiguration()
241+
if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput {
242+
captureSession.removeInput(currentInput)
243+
}
244+
captureSession.addInput(newInput)
245+
captureSession.commitConfiguration()
199246
} catch {
200247
delegate?.cameraViewController(self, didReceiveError: error)
248+
return
249+
}
250+
}
251+
252+
private func setupSessionOutput() {
253+
guard !isSimulatorRunning else {
254+
return
201255
}
202256

203257
let output = AVCaptureMetadataOutput()
@@ -209,6 +263,14 @@ public final class CameraViewController: UIViewController {
209263
view.setNeedsLayout()
210264
}
211265

266+
/// Switch front/back camera.
267+
private func swapCamera() {
268+
guard let input = captureSession.inputs.first as? AVCaptureDeviceInput else {
269+
return
270+
}
271+
setupSessionInput(for: input.device.position == .back ? .front : .back)
272+
}
273+
212274
// MARK: - Animations
213275

214276
/// Performs focus view animation.
@@ -248,25 +310,34 @@ private extension CameraViewController {
248310
flashButton.trailingAnchor.constraint(
249311
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
250312
constant: -13
313+
),
314+
cameraButton.bottomAnchor.constraint(
315+
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
316+
constant: -30
251317
)
252318
)
253319
} else {
254320
NSLayoutConstraint.activate(
255321
flashButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30),
256-
flashButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -13)
322+
flashButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -13),
323+
cameraButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30)
257324
)
258325
}
259326

260-
let flashButtonSize: CGFloat = 37
327+
let imageButtonSize: CGFloat = 37
261328

262329
NSLayoutConstraint.activate(
263-
flashButton.widthAnchor.constraint(equalToConstant: flashButtonSize),
264-
flashButton.heightAnchor.constraint(equalToConstant: flashButtonSize),
330+
flashButton.widthAnchor.constraint(equalToConstant: imageButtonSize),
331+
flashButton.heightAnchor.constraint(equalToConstant: imageButtonSize),
265332

266333
settingsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
267334
settingsButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
268335
settingsButton.widthAnchor.constraint(equalToConstant: 150),
269-
settingsButton.heightAnchor.constraint(equalToConstant: 50)
336+
settingsButton.heightAnchor.constraint(equalToConstant: 50),
337+
338+
cameraButton.widthAnchor.constraint(equalToConstant: 48),
339+
cameraButton.heightAnchor.constraint(equalToConstant: 48),
340+
cameraButton.trailingAnchor.constraint(equalTo: flashButton.trailingAnchor)
270341
)
271342

272343
setupFocusViewConstraints()
@@ -345,6 +416,13 @@ private extension CameraViewController {
345416
button.sizeToFit()
346417
return button
347418
}
419+
420+
func makeCameraButton() -> UIButton {
421+
let button = UIButton(type: .custom)
422+
button.setImage(imageNamed("cameraRotate"), for: UIControlState())
423+
button.isHidden = showsCameraButton
424+
return button
425+
}
348426
}
349427

350428
// MARK: - AVCaptureMetadataOutputObjectsDelegate

0 commit comments

Comments
 (0)