CodeScanner: a SwiftUI view for QR and barcode scanning on iOS, with a Simulator escape hatch
A SwiftUI view that is able to scan barcodes, QR codes, and more, and send back what was found.
At a glance
- What is it?
- CodeScanner wraps AVFoundation capture behind a single SwiftUI view, CodeScannerView, that returns a Result of ScanResult or ScanError, and lets you feed simulated data so the flow can be exercised in Simulator. The project is MIT licensed and its last push was on 2026-04-02, so treat it as settled rather than as an actively changing dependency.
- Who is it for?
- Adopt CodeScanner if you are building an iOS 13+ SwiftUI app that needs to scan QR codes or barcodes and you want the whole capture flow in one view with a Simulator-friendly simulatedData path. Do not adopt it if you need background scanning, camera control beyond the documented options, or a project with an active upstream cadence, since the last push was on 2026-04-02 and the last tagged release was 2.5.2 on 2024-12-01.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One view and one Result closure
The whole API surface of CodeScanner is a single SwiftUI view struct, CodeScannerView, and the README states the point of it plainly: the view can be shown inside a sheet so that all scanning occurs in one place. That is a small, deliberate footprint, and it is worth judging on that basis rather than expecting a scanner framework with a service layer.
You construct it with at least two parameters, an array of the types to scan for and a closure that is called when a result is ready. The closure must accept a Result<ScanResult, ScanError>. On success you get the code string and the type that was found, so a QR code containing an email address comes back tagged as a QR code.
Failure is modelled with three named cases rather than a loose error type.
- `badInput`, if the camera cannot be accessed
- `badOutput`, if the camera is not capable of detecting codes
- `initError`, if initialization failed.The split matters more than it first appears. badInput is a permission or availability problem on your side, badOutput is a camera that opened but cannot detect codes, and initError is a failure during setup. A caller that treats all three identically will end up showing one unhelpful message for three quite different situations.
There is one hard platform requirement the README calls out as important. iOS requires you to add the Privacy - Camera Usage Description key to your Info.plist file, providing a reason for why you want to access the camera. Miss it and the scanner fails at the point of use, not at build time.
scanMode, scanInterval and the rest of the initializer options
The initializer carries a set of options that cover the decisions you would otherwise make in your own app code.
scanMode has three values. .once scans a single code and is the default. .oncePerCode scans many codes but only triggers finding each unique code once. .continuous keeps finding codes until you dismiss the scanner, and scanInterval controls how fast individual codes are scanned, in seconds, when running in .continuous mode.
That interval only applies in .continuous, which is a detail that catches people. Setting scanInterval while leaving the mode at .once does nothing, and the default of .once means a single successful scan stops the flow.
The rest of the options are more cosmetic or hardware related. showViewfinder determines whether a box-like viewfinder is drawn over the UI, and it defaults to false. simulatedData allows you to provide test data to use in Simulator, when real scanning is not available, and it defaults to an empty string. shouldVibrateOnSuccess decides whether the device vibrates when a code is found, defaulting to true. isTorchOn turns the torch or flashlight on or off and defaults to false.
videoCaptureDevice lets you choose a different capture device that is most suitable for the code to scan, which leads directly to the trickiest part of using this library on modern iPhones.
For UI beyond the viewfinder, the README suggests wrapping CodeScannerView in a NavigationView and using the toolbar() modifier to add buttons such as a dedicated Cancel button. The view deliberately does not try to own your navigation chrome, which keeps it usable in a sheet without fighting the host screen.
Running the whole flow in Simulator
The simulatedData parameter exists for a specific reason, and it is the feature that makes this library pleasant to develop against. The README's first example sets the simulated data to a name, and notes that in the Simulator the scanner will return that name.
CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson") { response in
switch response {
case .success(let result):
print("Found code: \(result.string)")
case .failure(let error):
print(error.localizedDescription)
}
}You can develop the whole presentation, dismissal and downstream navigation path without a device or a physical code on a table. For a view that is otherwise only reachable with a camera pointed at something, that removes most of the friction in testing.
The second example shows the more common shape, a scanner presented as a sheet with the scanned value handed to another view. The completion closure is where you dismiss the scanner, and the README says so directly: your completion closure is probably where you want to dismiss the CodeScannerView.
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}Note the asymmetry in that snippet. The dismissal is inside the success branch, so a failure leaves the sheet up, which is what you want when the error is something the user can retry.
Small QR codes and the zoomedCameraForQRCode helper
Here is the one piece of real engineering beyond the wrapper, and the README explains why it is needed.
Scanning small QR codes on devices with dual or triple cameras has to be adjusted because of the minimum focus distance built into those cameras. The fix is two-part: choose the most suitable camera, and apply the recommended zoom factor.
CodeScannerView(codeTypes: [.qr], videoCaptureDevice: AVCaptureDevice.zoomedCameraForQRCode(withMinimumCodeSize: 20)) { response in
switch response {
case .success(let result):
print("Found code: \(result.string)")
case .failure(let error):
print(error.localizedDescription)
}
}The example is for 20x20mm QR codes, and the helper takes that minimum code size in millimetres. This is a static method on AVCaptureDevice, so you can call it without importing anything beyond what CodeScanner already pulls in.
The trade-off is real and undocumented in the README. A zoomed capture device is chosen to hit a physical focus distance, which means the framing of the preview changes, and the README does not say how the viewfinder or the torch behave on that path. If your app scans a fixed physical label size, this helper is the difference between working and not working, but you should check the preview alignment on your own devices rather than assuming the default camera will do.
Project shape, platform floor and maintenance state
The repository is small and conventional for a Swift package. The top level holds.github,.gitignore, LICENSE, Package.swift, README.md and a Sources directory, so the dependency is added as a Swift package rather than a framework binary.
The badges in the README state the platform floor as iOS 13.0+ and Swift 5.1. iOS 13 matters more in practice: it is the release where SwiftUI became available, which is what lets the library present capture through a view and a completion closure rather than a delegate object.
On maintenance, the facts are modest. The repository is not archived, and the last push was on 2026-04-02. The recent releases are 2.5.2 from 2024-12-01, 2.5.1 from 2024-09-18 and 2.5.0 from 2024-05-31, so the tagged release line stopped before the last commit activity. Nothing in the README claims an active roadmap, and there is no changelog section describing a future direction.
The project was made by Paul Hudson, who writes free Swift tutorials at Hacking with Swift, and is now maintained by Nathan Fallet. For anyone planning to depend on this, pinning to a specific version and treating the library as finished rather than evolving is the sensible reading of the release history.
Where CodeScanner is the wrong choice
The scope is narrow, and there are things it plainly is not.
First, it is iOS only, and only the SwiftUI presentation layer. The README notes that a Compose Multiplatform library also exists at the guimauvedigital/codescanner repository, which is a fair indication that the two implementations are separate projects rather than one shared core.
Second, there is no documented support for background or headless scanning. The design is a view you present, a camera the user points at a code, and a completion closure. If your requirement is continuous background capture, or scanning from a static image in your photo library, the README says nothing about it and the view model does not fit.
Third, error reporting stops at three cases. There is no documented distinction between a code format the library could not parse and a genuinely unreadable image, and no retry guidance.
Finally, the maintenance timeline is the practical limitation for a production dependency. A library whose last push was on 2026-04-02 and whose last release tag is 2.5.2 from 2024-12-01 will not receive an iOS SDK fix if Apple changes camera behaviour in a future release. That is a real consideration for an app you intend to ship, and it argues for wrapping CodeScanner behind your own thin protocol so the scanning dependency can be replaced without touching the rest of your app.
Editorial conclusion
Adopt CodeScanner if you are building an iOS 13+ SwiftUI app that needs to scan QR codes or barcodes and you want the whole capture flow in one view with a Simulator-friendly simulatedData path. Do not adopt it if you need background scanning, camera control beyond the documented options, or a project with an active upstream cadence, since the last push was on 2026-04-02 and the last tagged release was 2.5.2 on 2024-12-01. Verify first that the Privacy - Camera Usage Description key is present in your Info.plist, and decide deliberately whether to use the default .once scan mode or .continuous, since the mode changes how many times your completion closure fires.
Frequently asked questions
How do I use CodeScanner to scan a QR code in SwiftUI?
Create a CodeScannerView with the code types you want and a completion closure that accepts a Result of ScanResult and ScanError, then present it in a sheet. The README says your completion closure is probably where you want to dismiss the view.
How do I test CodeScanner in the iOS Simulator?
Pass a value to the simulatedData initializer parameter, which defaults to an empty string and is described as test data to use in Simulator when real scanning is not available. The README's first example passes a name and the scanner returns it.
Why does CodeScanner fail to access the camera on iOS?
The README states that iOS requires you to add the Privacy - Camera Usage Description key to your Info.plist file, giving a reason for camera access. Without it the camera cannot be accessed, which surfaces as the badInput error case.
How do I scan small QR codes with CodeScanner?
The README says small codes on devices with dual or triple cameras need adjusting because of the minimum focus distance, and the fix is to choose a suitable camera with a recommended zoom factor. It gives an example using AVCaptureDevice.zoomedCameraForQRCode(withMinimumCodeSize: 20) for 20x20mm codes.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/twostraws-codescanner)Was this useful?