Skip to content

Commit bf49d5d

Browse files
authored
fix(ios): replace QRCodeReader with custom implementation (#677)
1 parent 50ba9b6 commit bf49d5d

File tree

9 files changed

+195
-59
lines changed

9 files changed

+195
-59
lines changed

.clang-format

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ IncludeBlocks: Regroup
1212
IncludeCategories:
1313
- Regex: '"pch\.h"'
1414
Priority: -1
15-
- Regex: '^<QRCodeReader'
16-
Priority: 4
1715
- Regex: '^<React'
1816
Priority: 4
1917
- Regex: '^<[._a-z]+>'

example/ios/Podfile.lock

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ PODS:
6262
- glog (0.3.5)
6363
- libevent (2.1.12)
6464
- OpenSSL-Universal (1.1.180)
65-
- QRCodeReader.swift (10.1.0)
6665
- RCT-Folly (2020.01.13.00):
6766
- boost-for-react-native
6867
- DoubleConversion
@@ -354,7 +353,6 @@ DEPENDENCIES:
354353
- FlipperKit/FlipperKitUserDefaultsPlugin (~> 0.75.1)
355354
- FlipperKit/SKIOSNetworkPlugin (~> 0.75.1)
356355
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
357-
- QRCodeReader.swift
358356
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
359357
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
360358
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
@@ -398,7 +396,6 @@ SPEC REPOS:
398396
- FlipperKit
399397
- libevent
400398
- OpenSSL-Universal
401-
- QRCodeReader.swift
402399
- SwiftLint
403400
- YogaKit
404401

@@ -483,7 +480,6 @@ SPEC CHECKSUMS:
483480
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
484481
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
485482
OpenSSL-Universal: 1aa4f6a6ee7256b83db99ec1ccdaa80d10f9af9b
486-
QRCodeReader.swift: 373a389fe9a22d513c879a32a6f647c58f4ef572
487483
RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c
488484
RCTRequired: d34bf57e17cb6e3b2681f4809b13843c021feb6c
489485
RCTTypeSafety: 8dab4933124ed39bb0c1d88d74d61b1eb950f28f

ios/ReactTestApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
192DD201240FCAF5004E9CEB /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192DD200240FCAF5004E9CEB /* Manifest.swift */; };
1212
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22602490CB7600449D3C /* React+Compatibility.m */; };
1313
196C7215232F1788006556ED /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C7214232F1788006556ED /* ReactInstance.swift */; };
14-
196C724123319A85006556ED /* QRCodeReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C724023319A85006556ED /* QRCodeReaderDelegate.swift */; };
14+
196C724123319A85006556ED /* QRCodeScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C724023319A85006556ED /* QRCodeScannerViewController.swift */; };
1515
1988284524105BEC005057FF /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */; };
1616
19A624A4258C95F000032776 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A624A3258C95F000032776 /* Session.swift */; };
1717
19ECD0D6232ED425003D8557 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ECD0D5232ED425003D8557 /* AppDelegate.swift */; };
@@ -51,7 +51,7 @@
5151
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
5252
196C7214232F1788006556ED /* ReactInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactInstance.swift; sourceTree = "<group>"; };
5353
196C7216232F6CD9006556ED /* ReactTestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactTestApp.entitlements; sourceTree = "<group>"; };
54-
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderDelegate.swift; sourceTree = "<group>"; };
54+
196C724023319A85006556ED /* QRCodeScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScannerViewController.swift; sourceTree = "<group>"; };
5555
1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+ReactTestApp.h"; sourceTree = "<group>"; };
5656
1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+ReactTestApp.m"; sourceTree = "<group>"; };
5757
19A624A3258C95F000032776 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = "<group>"; };
@@ -122,7 +122,7 @@
122122
19ECD0D7232ED425003D8557 /* SceneDelegate.swift */,
123123
19ECD0D9232ED425003D8557 /* ContentView.swift */,
124124
192DD200240FCAF5004E9CEB /* Manifest.swift */,
125-
196C724023319A85006556ED /* QRCodeReaderDelegate.swift */,
125+
196C724023319A85006556ED /* QRCodeScannerViewController.swift */,
126126
196C7214232F1788006556ED /* ReactInstance.swift */,
127127
19A624A3258C95F000032776 /* Session.swift */,
128128
196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */,
@@ -336,7 +336,7 @@
336336
19ECD0D6232ED425003D8557 /* AppDelegate.swift in Sources */,
337337
19ECD0DA232ED425003D8557 /* ContentView.swift in Sources */,
338338
192DD201240FCAF5004E9CEB /* Manifest.swift in Sources */,
339-
196C724123319A85006556ED /* QRCodeReaderDelegate.swift in Sources */,
339+
196C724123319A85006556ED /* QRCodeScannerViewController.swift in Sources */,
340340
1914199A234B2DD800D856AE /* RCTDevSupport+UIScene.m in Sources */,
341341
196C22622490CB7600449D3C /* React+Compatibility.m in Sources */,
342342
196C7215232F1788006556ED /* ReactInstance.swift in Sources */,

ios/ReactTestApp/ContentView.swift

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// LICENSE file in the root directory of this source tree.
66
//
77

8-
import QRCodeReader
8+
import AVFoundation
99
import UIKit
1010

1111
private struct NavigationLink {
@@ -44,9 +44,6 @@ final class ContentViewController: UITableViewController {
4444
private let reactInstance: ReactInstance
4545
private var sections: [SectionData]
4646

47-
// swiftlint:disable:next weak_delegate
48-
private lazy var qrCodeReaderDelegate = QRCodeReaderDelegate()
49-
5047
public init() {
5148
reactInstance = ReactInstance()
5249
sections = []
@@ -256,20 +253,51 @@ final class ContentViewController: UITableViewController {
256253
}()
257254
return "React Native version: \(version)"
258255
}
256+
}
259257

258+
extension ContentViewController {
260259
@objc
261260
private func scanForQRCode(_: Notification) {
262-
let builder = QRCodeReaderViewControllerBuilder {
263-
$0.reader = QRCodeReader(
264-
metadataObjectTypes: [.qr],
265-
captureDevicePosition: .back
261+
switch AVCaptureDevice.authorizationStatus(for: .video) {
262+
case .notDetermined:
263+
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
264+
if granted {
265+
DispatchQueue.main.async {
266+
self?.navigationController?.present(QRCodeScannerViewController(), animated: true)
267+
}
268+
}
269+
}
270+
271+
case .restricted:
272+
let alert = UIAlertController(
273+
title: "Restricted Camera Access",
274+
message: """
275+
You've been restricted from using the camera on this device. \
276+
Without camera access, this feature won't work. Please contact \
277+
the device owner so they can give you access.
278+
""",
279+
preferredStyle: .alert
280+
)
281+
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
282+
navigationController?.present(alert, animated: true)
283+
284+
case .denied:
285+
let alert = UIAlertController(
286+
title: "Camera Access Needed",
287+
message: "To scan QR codes, please enable camera access in Settings.",
288+
preferredStyle: .alert
266289
)
267-
$0.showSwitchCameraButton = false
268-
$0.showOverlayView = true
269-
$0.rectOfInterest = CGRect(x: 0.2, y: 0.3, width: 0.6, height: 0.4)
290+
alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
291+
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
292+
})
293+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
294+
navigationController?.present(alert, animated: true)
295+
296+
case .authorized:
297+
navigationController?.present(QRCodeScannerViewController(), animated: true)
298+
299+
@unknown default:
300+
fatalError()
270301
}
271-
let viewController = QRCodeReaderViewController(builder: builder)
272-
viewController.delegate = qrCodeReaderDelegate
273-
present(viewController, animated: true)
274302
}
275303
}

ios/ReactTestApp/QRCodeReaderDelegate.swift

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import AVFoundation
2+
import Foundation
3+
import UIKit
4+
5+
final class QRCodeScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
6+
private lazy var captureSession = AVCaptureSession()
7+
private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
8+
private lazy var feedback = UINotificationFeedbackGenerator()
9+
10+
// MARK: - UIViewController overrides
11+
12+
override func viewWillTransition(to size: CGSize, with _: UIViewControllerTransitionCoordinator) {
13+
previewLayer.frame.size = size
14+
15+
if let captureConnection = previewLayer.connection,
16+
captureConnection.isVideoOrientationSupported
17+
{
18+
captureConnection.videoOrientation = UIDevice.current.videoOrientation
19+
}
20+
}
21+
22+
override func viewDidLoad() {
23+
super.viewDidLoad()
24+
25+
view.backgroundColor = .black
26+
27+
guard let videoCaptureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
28+
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
29+
captureSession.canAddInput(videoInput)
30+
else {
31+
return
32+
}
33+
34+
captureSession.addInput(videoInput)
35+
36+
let metadataOutput = AVCaptureMetadataOutput()
37+
guard captureSession.canAddOutput(metadataOutput) else {
38+
fatalError()
39+
}
40+
41+
captureSession.addOutput(metadataOutput)
42+
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
43+
metadataOutput.metadataObjectTypes = [.qr]
44+
45+
previewLayer.frame = view.layer.bounds
46+
previewLayer.videoGravity = .resizeAspectFill
47+
view.layer.addSublayer(previewLayer)
48+
}
49+
50+
override func viewWillAppear(_ animated: Bool) {
51+
super.viewWillAppear(animated)
52+
53+
if captureSession.isRunnable {
54+
captureSession.startRunning()
55+
feedback.prepare()
56+
} else {
57+
let alert = UIAlertController(
58+
title: "Cannot Start Scanner",
59+
message: """
60+
This device does not support scanning QR codes. Please use \
61+
another device with a camera.
62+
""",
63+
preferredStyle: .alert
64+
)
65+
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
66+
self?.dismiss(animated: true)
67+
})
68+
present(alert, animated: true)
69+
}
70+
}
71+
72+
override func viewWillDisappear(_ animated: Bool) {
73+
super.viewWillDisappear(animated)
74+
75+
if captureSession.isRunning {
76+
captureSession.stopRunning()
77+
}
78+
}
79+
80+
override var prefersStatusBarHidden: Bool {
81+
true
82+
}
83+
84+
// MARK: - AVCaptureMetadataOutputObjectsDelegate details
85+
86+
func metadataOutput(_: AVCaptureMetadataOutput,
87+
didOutput metadataObjects: [AVMetadataObject],
88+
from _: AVCaptureConnection)
89+
{
90+
captureSession.stopRunning()
91+
defer {
92+
dismiss(animated: true)
93+
}
94+
95+
if let metadataObject = metadataObjects.first {
96+
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
97+
let stringValue = readableObject.stringValue,
98+
let urlComponents = URLComponents(string: stringValue)
99+
else {
100+
feedback.notificationOccurred(.error)
101+
return
102+
}
103+
104+
feedback.notificationOccurred(.success)
105+
106+
NotificationCenter.default.post(
107+
name: .didReceiveRemoteBundleURL,
108+
object: self,
109+
userInfo: ["url": urlComponents]
110+
)
111+
}
112+
}
113+
}
114+
115+
// MARK: - Extensions
116+
117+
extension AVCaptureSession {
118+
var isRunnable: Bool {
119+
!inputs.isEmpty && !outputs.isEmpty && !isRunning
120+
}
121+
}
122+
123+
extension Notification.Name {
124+
static let didReceiveRemoteBundleURL = Notification.Name("didReceiveRemoteBundleURL")
125+
}
126+
127+
extension UIDevice {
128+
var videoOrientation: AVCaptureVideoOrientation {
129+
switch orientation {
130+
case .unknown:
131+
return .portrait
132+
case .portrait:
133+
return .portrait
134+
case .portraitUpsideDown:
135+
return .portraitUpsideDown
136+
case .landscapeLeft:
137+
// Flipped otherwise the picture is upside down
138+
return .landscapeRight
139+
case .landscapeRight:
140+
// Flipped otherwise the picture is upside down
141+
return .landscapeLeft
142+
case .faceUp, .faceDown:
143+
return .portrait
144+
@unknown default:
145+
return .portrait
146+
}
147+
}
148+
}

ios/ReactTestApp/ReactInstance.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
231231

232232
@objc
233233
private func onRemoteBundleURLReceived(_ notification: Notification) {
234-
guard let value = notification.userInfo?["value"] as? String,
235-
var urlComponents = URLComponents(string: value)
236-
else {
234+
guard var urlComponents = notification.userInfo?["url"] as? URLComponents else {
237235
return
238236
}
239237

ios/test_app.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ def use_test_app_internal!(target_platform, options)
325325
react_native_post_install = nil
326326

327327
target 'ReactTestApp' do
328-
pod 'QRCodeReader.swift' if target_platform == :ios
329328
pod 'SwiftLint'
330329

331330
react_native_post_install = use_react_native!(project_root, target_platform, options)

windows/.clang-format

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ IncludeBlocks: Regroup
1212
IncludeCategories:
1313
- Regex: '"pch\.h"'
1414
Priority: -1
15-
- Regex: '^<QRCodeReader'
16-
Priority: 4
1715
- Regex: '^<React'
1816
Priority: 4
1917
- Regex: '^<[._a-z]+>'

0 commit comments

Comments
 (0)