From 30617ca465757ec8cd79457b145137c9b5a35c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E7=9F=B3=E7=A3=8A?= Date: Tue, 22 Jul 2025 17:02:41 +0800 Subject: [PATCH 1/4] fix: Dependency :qr_code_scanner requires core library desugaring to be enabled --- android/build.gradle | 6 +++--- android/settings.gradle | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 9773f759..670e6309 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'net.touchcapture.qr.flutterqr' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.5.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -32,7 +32,7 @@ android { } defaultConfig { // minSdkVersion is determined by Native View. - minSdkVersion 20 + minSdkVersion 21 targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true diff --git a/android/settings.gradle b/android/settings.gradle index b5e1b3f5..e96689b0 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.2" apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id "com.android.application" version "8.5.2" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" From 2ea26ddad669fc671767f725d557954aa9fec63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E7=9F=B3=E7=A3=8A?= Date: Tue, 17 Mar 2026 14:51:37 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20ios26+=20=E5=85=B3=E9=97=AD=E5=BB=B6?= =?UTF-8?q?=E8=BF=9F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 + .../ios/Flutter/ephemeral/flutter_lldbinit | 5 + ios/Classes/FlutterQrPlugin.h | 1 + ios/Classes/MTBBarcodeScanner.h | 330 ++++++ ios/Classes/MTBBarcodeScanner.m | 1028 +++++++++++++++++ ios/Classes/QRView.swift | 1 - ios/qr_code_scanner.podspec | 3 +- 7 files changed, 1398 insertions(+), 2 deletions(-) create mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit create mode 100644 ios/Classes/MTBBarcodeScanner.h create mode 100644 ios/Classes/MTBBarcodeScanner.m diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Classes/FlutterQrPlugin.h b/ios/Classes/FlutterQrPlugin.h index c3d077dd..0deed901 100644 --- a/ios/Classes/FlutterQrPlugin.h +++ b/ios/Classes/FlutterQrPlugin.h @@ -1,4 +1,5 @@ #import +#import "MTBBarcodeScanner.h" @interface FlutterQrPlugin : NSObject @end diff --git a/ios/Classes/MTBBarcodeScanner.h b/ios/Classes/MTBBarcodeScanner.h new file mode 100644 index 00000000..32829729 --- /dev/null +++ b/ios/Classes/MTBBarcodeScanner.h @@ -0,0 +1,330 @@ +// +// MTBBarcodeScanner.h +// MTBBarcodeScannerExample +// +// Created by Mike Buss on 2/8/14. +// +// + +#import +#import +#import + +typedef NS_ENUM(NSUInteger, MTBCamera) { + MTBCameraBack, + MTBCameraFront +}; + +/** + * Available torch modes when scanning barcodes. + * + * While AVFoundation provides an additional automatic + * mode, it is not supported here because it only works + * with video recordings, not barcode scanning. + */ +typedef NS_ENUM(NSUInteger, MTBTorchMode) { + MTBTorchModeOff, + MTBTorchModeOn, +}; + +@interface MTBBarcodeScanner : NSObject + +/** + * The currently set camera. See MTBCamera for options. + * + * @sa setCamera:error: + */ +@property (nonatomic, assign, readonly) MTBCamera camera; + +/** + * Control the torch on the device, if present. + * + * Attempting to set the torch mode to an unsupported state + * will fail silently, and the value passed into the setter + * will be discarded. + * + * @sa setTorchMode:error: + */ +@property (nonatomic, assign) MTBTorchMode torchMode; + +/** + * Allow the user to tap the previewView to focus a specific area. + * Defaults to YES. + */ +@property (nonatomic, assign) BOOL allowTapToFocus; + +/** + * If set, only barcodes inside this area will be scanned. + * + * Setting this property is only supported while the scanner is active. + * Use the didStartScanningBlock if you want to set it as early as + * possible. + */ +@property (nonatomic, assign) CGRect scanRect; + +/** + * Layer used to present the camera input. If the previewView + * does not use auto layout, it may be necessary to adjust the layers frame. + */ +@property (nonatomic, strong) CALayer *previewLayer; + +/*! + @property didStartScanningBlock + @abstract + Optional callback block that's called when the scanner finished initializing. + + @discussion + Optional callback that will be called when the scanner is initialized and the view + is presented on the screen. This is useful for presenting an activity indicator + while the scanner is initializing. + + The block is always called on the main queue. + */ +@property (nonatomic, copy) void (^didStartScanningBlock)(void); + +/*! + @property didTapToFocusBlock + @abstract + Block that's called when the user taps the screen to focus the camera. If allowsTapToFocus + is set to NO, this will never be called. + */ +@property (nonatomic, copy) void (^didTapToFocusBlock)(CGPoint point); + +/*! + @property resultBlock + @abstract + Block that's called every time one or more barcodes are recognized. + + @discussion + The resultBlock is called on the main queue once for every frame that at least one valid barcode is found. + + This block is automatically set when you call startScanningWithResultBlock: + */ +@property (nonatomic, copy) void (^resultBlock)(NSArray *codes); + +/*! + @property preferredAutoFocusRangeRestriction + @abstract + Auto focus range restriction, if supported. + + @discussion + Defaults to AVCaptureAutoFocusRangeRestrictionNear. Will be ignored on unsupported devices. + */ +@property (nonatomic, assign) AVCaptureAutoFocusRangeRestriction preferredAutoFocusRangeRestriction; + +/** + * Initialize a scanner that will feed the camera input + * into the given UIView. + * + * @param previewView View that will be overlayed with the live feed from the camera input. + * + * @return An instance of MTBBarcodeScanner + */ +- (instancetype)initWithPreviewView:(UIView *)previewView; + +/** + * Initialize a scanner that will feed the camera input + * into the given UIView. Only codes with a type given in + * the metaDataObjectTypes array will be reported to the result + * block when scanning is started using startScanningWithResultBlock: + * + * @see startScanningWithResultBlock: + * + * @param metaDataObjectTypes Array of AVMetadataObjectTypes to scan for. Only codes with types given in this array will be reported to the resultBlock. + * @param previewView View that will be overlayed with the live feed from the camera input. + * + * @return An instance of MTBBarcodeScanner + */ +- (instancetype)initWithMetadataObjectTypes:(NSArray *)metaDataObjectTypes + previewView:(UIView *)previewView; + +/** + * Returns whether any camera exists in this device. + * Be aware that this returns NO if camera access is restricted. + * + * @return YES if the device has a camera and authorization state is not AVAuthorizationStatusRestricted + */ ++ (BOOL)cameraIsPresent; + +/** + * You can use this flag to check whether flipCamera can potentially + * be successful. You may want to hide your button to flip the camera + * if the device only has one camera. + * + * @return YES if a second camera is present and authorization state is not AVAuthorizationStatusRestricted. + * @sa flipCamera + */ +- (BOOL)hasOppositeCamera; + +/** + * Returns whether scanning is prohibited by the user of the device. + * + * @return YES if the user has prohibited access to (or is prohibited from accessing) the camera. + */ ++ (BOOL)scanningIsProhibited; + +/** + * Request permission to access the camera on the device. + * + * The success block will return YES if the user granted permission, has granted permission in the past, or if the device is running iOS 7. + * The success block will return NO if the user denied permission, is restricted from the camera, or if there is no camera present. + */ ++ (void)requestCameraPermissionWithSuccess:(void (^)(BOOL success))successBlock; + +/** + * Start scanning for barcodes. The camera input will be added as a sublayer + * to the UIView given for previewView during initialization. + * + * This method assumes you have already set the `resultBlock` property directly. + * + * This method returns quickly and does not wait for the internal session to + * start. Set the didStartScanningBlock to get a callback when the session + * is ready, i.e., a camera picture is visible. + * + * @param error Error supplied if the scanning could not start. + * + * @return YES if scanning started successfully, NO if there was an error. + */ +- (BOOL)startScanningWithError:(NSError **)error; + +/** + * Start scanning for barcodes. The camera input will be added as a sublayer + * to the UIView given for previewView during initialization. + * + * This method returns quickly and does not wait for the internal session to + * start. Set the didStartScanningBlock to get a callback when the session + * is ready, i.e., a camera picture is visible. + * + * @param resultBlock Callback block for captured codes. If the scanner was instantiated with initWithMetadataObjectTypes:previewView, only codes with a type given in metaDataObjectTypes will be reported. + * @param error Error supplied if the scanning could not start. + * + * @return YES if scanning started successfully, NO if there was an error. + */ +- (BOOL)startScanningWithResultBlock:(void (^)(NSArray *codes))resultBlock error:(NSError **)error; + +/** + * Start scanning for barcodes using a specific camera. The camera input will be added as a sublayer + * to the UIView given for previewView during initialization. + * + * This method returns quickly and does not wait for the internal session to + * start. Set the didStartScanningBlock to get a callback when the session + * is ready, i.e., a camera picture is visible. + * + * @param camera The camera to use when scanning starts. + * @param resultBlock Callback block for captured codes. If the scanner was instantiated with initWithMetadataObjectTypes:previewView, only codes with a type given in metaDataObjectTypes will be reported. + * @param error Error supplied if the scanning could not start. + * + * + * @return YES if scanning started successfully, NO if there was an error. + */ +- (BOOL)startScanningWithCamera:(MTBCamera)camera resultBlock:(void (^)(NSArray *codes))resultBlock error:(NSError **)error; + +/** + * Stop scanning for barcodes. The live feed from the camera will be removed as a sublayer from the previewView given during initialization. + */ +- (void)stopScanning; + +/** + * Whether the scanner is currently scanning for barcodes + * + * @return YES if the scanner is currently scanning for barcodes + */ +- (BOOL)isScanning; + +/** + * If using the front camera, switch to the back, or visa-versa. + * If this method is called when isScanning=NO, it has no effect + * + * If the opposite camera is not available, this method will do nothing. + * + * @sa hasOppositeCamera + */ +- (void)flipCamera; + +/** + * Sets the camera and ignores any errors. + * + * Deprecated. + * + * @sa setCamera:error: + */ +- (void)setCamera:(MTBCamera)camera NS_DEPRECATED_IOS(2.0, 2.0, "Use setCamera:error: instead"); + +/** + * Sets the camera. This operation may fail, e.g., when the device + * does not have the specified camera. + * + * @param camera The camera to use, see MTBCamera for options. + * @error Any error that occurred while trying to set the camera. + * @return YES, if the specified camera was set successfully, NO if any error occurred. + */ +- (BOOL)setCamera:(MTBCamera)camera error:(NSError **)error; + +/** + * If using the front camera, switch to the back, or visa-versa. + * + * If this method is called when (isScanning == NO), it will return + * NO and provide an error. + * + * If the opposite camera is not available, the error parameter + * will explain the error. + * + * @return YES if the camera was flipped, NO if any error occurred. + */ +- (BOOL)flipCameraWithError:(NSError **)error; + +/** + * Return a BOOL value that specifies whether the current capture device has a torch. + * + * @return YES if the the current capture device has a torch. + */ +- (BOOL)hasTorch; + +/** + * Toggle the torch from on to off, or off to on. + * If the device does not support a torch or the opposite mode, calling + * this method will have no effect. + * To set the torch to on/off directly, set the `torchMode` property, or + * use setTorchMode:error: if you care about errors. + */ +- (void)toggleTorch; + +/** + * Attempts to set a new torch mode. + * + * @return YES, if setting the new mode was successful, and the torchMode + * property reflects the new state. NO if there was an error - use the + * error parameter to learn about the reason. + * + * @sa torchMode + */ +- (BOOL)setTorchMode:(MTBTorchMode)torchMode error:(NSError **)error; + +/** + * Freeze capture keeping the last frame on previewView. + * If this method is called before startScanning, it has no effect. + * + * Returns immediately – actually freezing the capture is + * done asynchronously. + */ +- (void)freezeCapture; + +/** + * Unfreeze a frozen capture. + * + * Returns immediately – actually unfreezing the capture is + * done asynchronously. + */ +- (void)unfreezeCapture; + +/** + * Captures a still image of the current camera feed + */ +- (void)captureStillImage:(void (^)(UIImage *image, NSError *error))captureBlock; + +/** + * Determine if currently capturing a still image + */ +- (BOOL)isCapturingStillImage; + +@end \ No newline at end of file diff --git a/ios/Classes/MTBBarcodeScanner.m b/ios/Classes/MTBBarcodeScanner.m new file mode 100644 index 00000000..daece00f --- /dev/null +++ b/ios/Classes/MTBBarcodeScanner.m @@ -0,0 +1,1028 @@ +// +// MTBBarcodeScanner.m +// MTBBarcodeScannerExample +// +// Created by Mike Buss on 2/8/14. +// +// + +#import +#import "MTBBarcodeScanner.h" + +CGFloat const kFocalPointOfInterestX = 0.5; +CGFloat const kFocalPointOfInterestY = 0.5; + +static NSString *kErrorDomain = @"MTBBarcodeScannerError"; + +// Error Codes +static const NSInteger kErrorCodeStillImageCaptureInProgress = 1000; +static const NSInteger kErrorCodeSessionIsClosed = 1001; +static const NSInteger kErrorCodeNotScanning = 1002; +static const NSInteger kErrorCodeSessionAlreadyActive = 1003; +static const NSInteger kErrorCodeTorchModeUnavailable = 1004; +static const NSInteger kErrorMethodNotAvailableOnIOSVersion = 1005; + +@interface MTBBarcodeScanner () + +/*! + @property privateSessionQueue + @abstract + Starting or stopping the capture session should only be done on this queue. + */ +@property (strong) dispatch_queue_t privateSessionQueue; + +/*! + @property session + @abstract + The capture session used for scanning barcodes. + */ +@property (nonatomic, strong) AVCaptureSession *session; + +/*! + @property captureDevice + @abstract + Represents the physical device that is used for scanning barcodes. + */ +@property (nonatomic, strong) AVCaptureDevice *captureDevice; + +/*! + @property capturePreviewLayer + @abstract + The layer used to view the camera input. This layer is added to the + previewView when scanning starts. + */ +@property (nonatomic, strong) AVCaptureVideoPreviewLayer *capturePreviewLayer; + +/*! + @property currentCaptureDeviceInput + @abstract + The current capture device input for capturing video. This is used + to reset the camera to its initial properties when scanning stops. + */ +@property (nonatomic, strong) AVCaptureDeviceInput *currentCaptureDeviceInput; + +/* + @property captureDeviceOnput + @abstract + The capture device output for capturing video. + */ +@property (nonatomic, strong) AVCaptureMetadataOutput *captureOutput; + +/*! + @property metaDataObjectTypes + @abstract + The MetaDataObjectTypes to look for in the scanning session. + + @discussion + Only objects with a MetaDataObjectType found in this array will be + reported to the result block. + */ +@property (nonatomic, copy) NSArray *metaDataObjectTypes; + +/*! + @property previewView + @abstract + The view used to preview the camera input. + + @discussion + The AVCaptureVideoPreviewLayer is added to this view to preview the + camera input when scanning starts. When scanning stops, the layer is + removed. + */ +@property (nonatomic, weak) UIView *previewView; + +/*! + @property initialAutoFocusRangeRestriction + @abstract + The auto focus range restriction the AVCaptureDevice was initially configured for when scanning started. + + @discussion + When startScanning is called, the auto focus range restriction of the default AVCaptureDevice + is stored. When stopScanning is called, the AVCaptureDevice is reset to the initial range restriction + to prevent a bug in the AVFoundation framework. + */ +@property (nonatomic, assign) AVCaptureAutoFocusRangeRestriction initialAutoFocusRangeRestriction; + +/*! + @property initialFocusPoint + @abstract + The focus point the AVCaptureDevice was initially configured for when scanning started. + + @discussion + When startScanning is called, the focus point of the default AVCaptureDevice + is stored. When stopScanning is called, the AVCaptureDevice is reset to the initial focal point + to prevent a bug in the AVFoundation framework. + */ +@property (nonatomic, assign) CGPoint initialFocusPoint; + +/*! + @property stillImageOutput + @abstract + Used for still image capture prior to iOS 10 + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +@property (nonatomic, strong) AVCaptureStillImageOutput *stillImageOutput; +#pragma GCC diagnostic pop + +/*! + @property gestureRecognizer + @abstract + If allowTapToFocus is set to YES, this gesture recognizer is added to the `previewView` + when scanning starts. When the user taps the view, the `focusPointOfInterest` will change + to the location the user tapped. + */ +@property (nonatomic, strong) UITapGestureRecognizer *gestureRecognizer; + +/*! + @property stillImageCaptureBlock + @abstract + Reference to the block passed in when capturing a still image. + */ +@property (nonatomic, copy) void (^stillImageCaptureBlock)(UIImage *image, NSError *error); + +/*! + @property output + @abstract + Property used for capturing still photos during barcode capture. + */ +@property (nonatomic, strong) AVCapturePhotoOutput *output NS_AVAILABLE_IOS(10.0); + +@end + +@implementation MTBBarcodeScanner + +#pragma mark - Lifecycle + +- (instancetype)init { + NSAssert(NO, @"MTBBarcodeScanner init is not supported. Please use initWithPreviewView: \ + or initWithMetadataObjectTypes:previewView: to instantiate a MTBBarcodeScanner"); + return nil; +} + +- (instancetype)initWithPreviewView:(UIView *)previewView { + return [self initWithMetadataObjectTypes:[self defaultMetaDataObjectTypes] previewView:previewView]; +} + +- (instancetype)initWithMetadataObjectTypes:(NSArray *)metaDataObjectTypes previewView:(UIView *)previewView { + NSParameterAssert(metaDataObjectTypes); + NSAssert(metaDataObjectTypes.count > 0, + @"Must initialize MTBBarcodeScanner with at least one metaDataObjectTypes value."); + + self = [super init]; + if (self) { + // Library does not support scanning for faces + NSAssert(!([metaDataObjectTypes indexOfObject:AVMetadataObjectTypeFace] != NSNotFound), + @"The type %@ is not supported by MTBBarcodeScanner.", AVMetadataObjectTypeFace); + + _metaDataObjectTypes = metaDataObjectTypes; + _previewView = previewView; + _allowTapToFocus = YES; + _preferredAutoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear; + [self setupSessionQueue]; + [self addObservers]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Scanning + ++ (BOOL)cameraIsPresent { + // capture device is nil if status is AVAuthorizationStatusRestricted + return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] != nil; +} + ++ (BOOL)hasCamera:(MTBCamera)camera { + AVCaptureDevicePosition position = [self devicePositionForCamera:camera]; + + if (@available(iOS 10.0, *)) { + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera + mediaType:AVMediaTypeVideo + position:position]; + return (device != nil); + } else { + // We can ignore the deprecation warning here because + // we are using the new AVCaptureDeviceDiscoverySession when it is available +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // Array is empty if status is AVAuthorizationStatusRestricted + for (AVCaptureDevice *device in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { + if (device.position == position) { + return YES; + } + } +#pragma GCC diagnostic pop + } + return NO; +} + ++ (MTBCamera)oppositeCameraOf:(MTBCamera)camera { + switch (camera) { + case MTBCameraBack: + return MTBCameraFront; + + case MTBCameraFront: + return MTBCameraBack; + } + + NSAssert(NO, @"Invalid camera type: %lu", (unsigned long)camera); + return MTBCameraBack; +} + ++ (BOOL)scanningIsProhibited { + switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { + case AVAuthorizationStatusDenied: + case AVAuthorizationStatusRestricted: + return YES; + break; + + default: + return NO; + break; + } +} + ++ (void)requestCameraPermissionWithSuccess:(void (^)(BOOL success))successBlock { + if (![self cameraIsPresent]) { + successBlock(NO); + return; + } + + switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { + case AVAuthorizationStatusAuthorized: + successBlock(YES); + break; + + case AVAuthorizationStatusDenied: + case AVAuthorizationStatusRestricted: + successBlock(NO); + break; + + case AVAuthorizationStatusNotDetermined: + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo + completionHandler:^(BOOL granted) { + + dispatch_async(dispatch_get_main_queue(), ^{ + successBlock(granted); + }); + + }]; + break; + } +} + +- (BOOL)startScanningWithError:(NSError **)error { + return [self startScanningWithResultBlock:self.resultBlock error:error]; +} + +- (BOOL)startScanningWithResultBlock:(void (^)(NSArray *codes))resultBlock error:(NSError **)error { + return [self startScanningWithCamera:MTBCameraBack resultBlock:resultBlock error:error]; +} + +- (BOOL)startScanningWithCamera:(MTBCamera)camera resultBlock:(void (^)(NSArray *codes))resultBlock error:(NSError **)error { + NSAssert([MTBBarcodeScanner cameraIsPresent], @"Attempted to start scanning on a device with no camera. Check requestCameraPermissionWithSuccess: method before calling startScanningWithResultBlock:"); + NSAssert(![MTBBarcodeScanner scanningIsProhibited], @"Scanning is prohibited on this device. \ + Check requestCameraPermissionWithSuccess: method before calling startScanningWithResultBlock:"); + NSAssert(resultBlock, @"startScanningWithResultBlock: requires a non-nil resultBlock."); + + if (self.session) { + if (error) { + *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeSessionAlreadyActive + userInfo:@{NSLocalizedDescriptionKey : @"Do not start scanning while another session is in use."}]; + } + + return NO; + } + + // Configure the session + _camera = camera; + self.captureDevice = [self newCaptureDeviceWithCamera:self.camera]; + AVCaptureSession *session = [self newSessionWithCaptureDevice:self.captureDevice error:error]; + + if (!session) { + // we rely on newSessionWithCaptureDevice:error: to populate the error + return NO; + } + + self.session = session; + + // Configure the preview layer + self.capturePreviewLayer.cornerRadius = self.previewView.layer.cornerRadius; + [self.previewView.layer insertSublayer:self.capturePreviewLayer atIndex:0]; // Insert below all other views + [self refreshVideoOrientation]; + + // Configure 'tap to focus' functionality + [self configureTapToFocus]; + + self.resultBlock = resultBlock; + + dispatch_async(self.privateSessionQueue, ^{ + // Configure the rect of interest + self.captureOutput.rectOfInterest = [self rectOfInterestFromScanRect:self.scanRect]; + + // Start the session after all configurations: + // Must be dispatched as it is blocking + [self.session startRunning]; + + if (self.didStartScanningBlock) { + // Call that block now that we've started scanning: + // Dispatch back to main + dispatch_async(dispatch_get_main_queue(), ^{ + self.didStartScanningBlock(); + }); + } + }); + + return YES; +} + +- (void)stopScanning { + if (!self.session) { + return; + } + + // Turn the torch off + self.torchMode = MTBTorchModeOff; + + // Remove the preview layer + AVCaptureVideoPreviewLayer *layer = self.capturePreviewLayer; + [layer removeFromSuperlayer]; + + // Stop recognizing taps for the 'Tap to Focus' feature + [self stopRecognizingTaps]; + + self.resultBlock = nil; + + AVCaptureSession *session = self.session; + AVCaptureDeviceInput *deviceInput = self.currentCaptureDeviceInput; + self.session = nil; + + dispatch_async(self.privateSessionQueue, ^{ + // When we're finished scanning, reset the settings for the camera + // to their original states + // Must be dispatched as it is blocking + [self removeDeviceInput:deviceInput session:session]; + for (AVCaptureOutput *output in session.outputs) { + [session removeOutput:output]; + } + + // Must be dispatched as it is blocking + [session stopRunning]; + + // Disconnect preview layer AFTER session has fully stopped to avoid + // blocking on iOS 26+ where AVFoundation waits for camera pipeline sync + dispatch_async(dispatch_get_main_queue(), ^{ + layer.session = nil; + }); + + self.capturePreviewLayer = nil; + }); +} + +- (BOOL)isScanning { + return [self.session isRunning]; +} + +- (BOOL)hasOppositeCamera { + MTBCamera otherCamera = [[self class] oppositeCameraOf:self.camera]; + return [[self class] hasCamera:otherCamera]; +} + +- (void)flipCamera { + [self flipCameraWithError:nil]; +} + +- (BOOL)flipCameraWithError:(NSError **)error { + if (!self.isScanning) { + if (error) { + *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeNotScanning + userInfo:@{NSLocalizedDescriptionKey : @"Camera cannot be flipped when isScanning is NO"}]; + } + + return NO; + } + + MTBCamera otherCamera = [[self class] oppositeCameraOf:self.camera]; + return [self setCamera:otherCamera error:error]; +} + +#pragma mark - Tap to Focus + +- (void)configureTapToFocus { + if (self.allowTapToFocus) { + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(focusTapped:)]; + [self.previewView addGestureRecognizer:tapGesture]; + self.gestureRecognizer = tapGesture; + } +} + +- (void)focusTapped:(UITapGestureRecognizer *)tapGesture { + CGPoint tapPoint = [self.gestureRecognizer locationInView:self.gestureRecognizer.view]; + CGPoint devicePoint = [self.capturePreviewLayer captureDevicePointOfInterestForPoint:tapPoint]; + + AVCaptureDevice *device = self.captureDevice; + NSError *error = nil; + + if ([device lockForConfiguration:&error]) { + if (device.isFocusPointOfInterestSupported && + [device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + + device.focusPointOfInterest = devicePoint; + device.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } + [device unlockForConfiguration]; + } else { + NSLog(@"Failed to acquire lock for focus change: %@", error); + } + + if (self.didTapToFocusBlock) { + self.didTapToFocusBlock(tapPoint); + } +} + +- (void)stopRecognizingTaps { + if (self.gestureRecognizer) { + [self.previewView removeGestureRecognizer:self.gestureRecognizer]; + } +} + +#pragma mark - AVCaptureMetadataOutputObjects Delegate + +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { + if (!self.resultBlock) return; + + NSMutableArray *codes = [[NSMutableArray alloc] init]; + + for (AVMetadataObject *metaData in metadataObjects) { + AVMetadataMachineReadableCodeObject *barCodeObject = (AVMetadataMachineReadableCodeObject *)[self.capturePreviewLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metaData]; + if (barCodeObject) { + [codes addObject:barCodeObject]; + } + } + + self.resultBlock(codes); +} + +#pragma mark - Rotation + +- (void)handleApplicationDidChangeStatusBarNotification:(NSNotification *)notification { + [self refreshVideoOrientation]; +} + +- (void)refreshVideoOrientation { + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + self.capturePreviewLayer.frame = self.previewView.bounds; + if ([self.capturePreviewLayer.connection isVideoOrientationSupported]) { + self.capturePreviewLayer.connection.videoOrientation = [self captureOrientationForInterfaceOrientation:orientation]; + } +} + +- (AVCaptureVideoOrientation)captureOrientationForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + switch (interfaceOrientation) { + case UIInterfaceOrientationPortrait: + return AVCaptureVideoOrientationPortrait; + case UIInterfaceOrientationPortraitUpsideDown: + return AVCaptureVideoOrientationPortraitUpsideDown; + case UIInterfaceOrientationLandscapeLeft: + return AVCaptureVideoOrientationLandscapeLeft; + case UIInterfaceOrientationLandscapeRight: + return AVCaptureVideoOrientationLandscapeRight; + default: + return AVCaptureVideoOrientationPortrait; + } +} + +#pragma mark - Background Handling + +- (void)applicationWillEnterForegroundNotification:(NSNotification *)notification { + // the torch is switched off when the app is backgrounded so we restore the + // previous state once the app is foregrounded again + [self updateForTorchMode:self.torchMode error:nil]; +} + +#pragma mark - Session Configuration + +- (AVCaptureSession *)newSessionWithCaptureDevice:(AVCaptureDevice *)captureDevice error:(NSError **)error { + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:error]; + + if (!input) { + // we rely on deviceInputWithDevice:error: to populate the error + return nil; + } + + AVCaptureSession *newSession = [[AVCaptureSession alloc] init]; + [self setDeviceInput:input session:newSession]; + + // Set an optimized preset for barcode scanning + [newSession setSessionPreset:AVCaptureSessionPresetHigh]; + + self.captureOutput = [[AVCaptureMetadataOutput alloc] init]; + [self.captureOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; + + [newSession addOutput:self.captureOutput]; + self.captureOutput.metadataObjectTypes = self.metaDataObjectTypes; + + [newSession beginConfiguration]; + + if (@available(iOS 10.0, *)) { + self.output = [[AVCapturePhotoOutput alloc] init]; + self.output.highResolutionCaptureEnabled = YES; + + if ([newSession canAddOutput:self.output]) { + [newSession addOutput:self.output]; + } + } else { + // Still image capture configuration +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init]; + self.stillImageOutput.outputSettings = @{AVVideoCodecKey: AVVideoCodecJPEG}; + + if ([self.stillImageOutput isStillImageStabilizationSupported]) { + self.stillImageOutput.automaticallyEnablesStillImageStabilizationWhenAvailable = YES; + } + + if ([self.stillImageOutput respondsToSelector:@selector(isHighResolutionStillImageOutputEnabled)]) { + self.stillImageOutput.highResolutionStillImageOutputEnabled = YES; + } + [newSession addOutput:self.stillImageOutput]; +#pragma GCC diagnostic pop + } + + dispatch_async(self.privateSessionQueue, ^{ + self.captureOutput.rectOfInterest = [self rectOfInterestFromScanRect:self.scanRect]; + }); + + self.capturePreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:newSession]; + self.capturePreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + self.capturePreviewLayer.frame = self.previewView.bounds; + + [newSession commitConfiguration]; + + return newSession; +} + +- (AVCaptureDevice *)newCaptureDeviceWithCamera:(MTBCamera)camera { + AVCaptureDevice *newCaptureDevice = nil; + AVCaptureDevicePosition position = [[self class] devicePositionForCamera:camera]; + + if (@available(iOS 10.0, *)) { + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera + mediaType:AVMediaTypeVideo + position:position]; + newCaptureDevice = device; + } else { + // We can ignore the deprecation here because we are using + // AVCaptureDeviceDiscoverySession if it is available +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; + for (AVCaptureDevice *device in videoDevices) { + if (device.position == position) { + newCaptureDevice = device; + break; + } + } +#pragma GCC diagnostic pop + } + + // If the front camera is not available, use the back camera + if (!newCaptureDevice) { + newCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + } + + // Using AVCaptureFocusModeContinuousAutoFocus helps improve scan times + NSError *error = nil; + if ([newCaptureDevice lockForConfiguration:&error]) { + if ([newCaptureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + newCaptureDevice.focusMode = AVCaptureFocusModeContinuousAutoFocus; + } + [newCaptureDevice unlockForConfiguration]; + } else { + NSLog(@"Failed to acquire lock for initial focus mode: %@", error); + } + + return newCaptureDevice; +} + ++ (AVCaptureDevicePosition)devicePositionForCamera:(MTBCamera)camera { + switch (camera) { + case MTBCameraFront: + return AVCaptureDevicePositionFront; + case MTBCameraBack: + return AVCaptureDevicePositionBack; + } + + NSAssert(NO, @"Invalid camera type: %lu", (unsigned long)camera); + return AVCaptureDevicePositionUnspecified; +} + +#pragma mark - Default Values + +- (NSArray *)defaultMetaDataObjectTypes { + NSMutableArray *types = [@[AVMetadataObjectTypeQRCode, + AVMetadataObjectTypeUPCECode, + AVMetadataObjectTypeCode39Code, + AVMetadataObjectTypeCode39Mod43Code, + AVMetadataObjectTypeEAN13Code, + AVMetadataObjectTypeEAN8Code, + AVMetadataObjectTypeCode93Code, + AVMetadataObjectTypeCode128Code, + AVMetadataObjectTypePDF417Code, + AVMetadataObjectTypeAztecCode] mutableCopy]; + + if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_7_1) { + [types addObjectsFromArray:@[AVMetadataObjectTypeInterleaved2of5Code, + AVMetadataObjectTypeITF14Code, + AVMetadataObjectTypeDataMatrixCode + ]]; + } + + return [types copy]; +} + +#pragma mark - Helper Methods + +- (void)addObservers { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleApplicationDidChangeStatusBarNotification:) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForegroundNotification:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + +- (void)setupSessionQueue { + NSAssert(self.privateSessionQueue == NULL, @"Queue should only be set up once"); + + if (self.privateSessionQueue) { + return; + } + + self.privateSessionQueue = dispatch_queue_create("com.mikebuss.MTBBarcodeScanner.captureSession", DISPATCH_QUEUE_SERIAL); +} + +- (void)setDeviceInput:(AVCaptureDeviceInput *)deviceInput session:(AVCaptureSession *)session { + if (deviceInput == nil) { + // Nil device inputs cannot be added to instances of AVCaptureSession + return; + } + + [self removeDeviceInput:self.currentCaptureDeviceInput session:session]; + + self.currentCaptureDeviceInput = deviceInput; + [self updateFocusPreferencesOfDevice:deviceInput.device reset:NO]; + + [session addInput:deviceInput]; +} + +- (void)removeDeviceInput:(AVCaptureDeviceInput *)deviceInput session:(AVCaptureSession *)session { + if (deviceInput == nil) { + // No need to remove the device input if it was never set + return; + } + + // Restore focus settings to the previously saved state + [self updateFocusPreferencesOfDevice:deviceInput.device reset:YES]; + + [session removeInput:deviceInput]; + self.currentCaptureDeviceInput = nil; +} + +- (void)updateFocusPreferencesOfDevice:(AVCaptureDevice *)inputDevice reset:(BOOL)reset { + NSParameterAssert(inputDevice); + + if (!inputDevice) { + return; + } + + NSError *lockError; + + if (![inputDevice lockForConfiguration:&lockError]) { + NSLog(@"Failed to acquire lock to (re)set focus options: %@", lockError); + return; + } + + // Prioritize the focus on objects near to the device + if (inputDevice.isAutoFocusRangeRestrictionSupported) { + if (!reset) { + self.initialAutoFocusRangeRestriction = inputDevice.autoFocusRangeRestriction; + inputDevice.autoFocusRangeRestriction = self.preferredAutoFocusRangeRestriction; + } else { + inputDevice.autoFocusRangeRestriction = self.initialAutoFocusRangeRestriction; + } + } + + // Focus on the center of the image + if (inputDevice.isFocusPointOfInterestSupported) { + if (!reset) { + self.initialFocusPoint = inputDevice.focusPointOfInterest; + inputDevice.focusPointOfInterest = CGPointMake(kFocalPointOfInterestX, kFocalPointOfInterestY); + } else { + inputDevice.focusPointOfInterest = self.initialFocusPoint; + } + } + + [inputDevice unlockForConfiguration]; + + // this method will acquire its own lock + [self updateForTorchMode:self.torchMode error:nil]; +} + +#pragma mark - Torch Control + +- (void)setTorchMode:(MTBTorchMode)torchMode { + [self setTorchMode:torchMode error:nil]; +} + +- (BOOL)setTorchMode:(MTBTorchMode)torchMode error:(NSError **)error { + if ([self updateForTorchMode:torchMode error:error]) { + // we only update our internal state if setting the torch mode was successful + _torchMode = torchMode; + return YES; + } + + return NO; +} + +- (void)toggleTorch { + switch (self.torchMode) { + case MTBTorchModeOn: + self.torchMode = MTBTorchModeOff; + break; + + case MTBTorchModeOff: + self.torchMode = MTBTorchModeOn; + break; + } +} + +- (BOOL)updateForTorchMode:(MTBTorchMode)preferredTorchMode error:(NSError **)error { + AVCaptureDevice *backCamera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + AVCaptureTorchMode avTorchMode = [self avTorchModeForMTBTorchMode:preferredTorchMode]; + + if (!([backCamera isTorchAvailable] && [backCamera isTorchModeSupported:avTorchMode])) { + if (error) { + *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeTorchModeUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Torch unavailable or mode not supported."}]; + } + + return NO; + } + + if (![backCamera lockForConfiguration:error]) { + NSLog(@"Failed to acquire lock to update torch mode."); + return NO; + } + + [backCamera setTorchMode:avTorchMode]; + [backCamera unlockForConfiguration]; + + return YES; +} + +- (BOOL)hasTorch { + AVCaptureDevice *captureDevice = [self newCaptureDeviceWithCamera:self.camera]; + NSError *error = nil; + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error]; + return input.device.hasTorch; +} + +- (AVCaptureTorchMode)avTorchModeForMTBTorchMode:(MTBTorchMode)torchMode { + switch (torchMode) { + case MTBTorchModeOn: + return AVCaptureTorchModeOn; + + case MTBTorchModeOff: + return AVCaptureTorchModeOff; + } + + NSAssert(NO, @"Invalid torch mode: %lu", (unsigned long)torchMode); + return AVCaptureTorchModeOff; +} + +#pragma mark - Capture + +- (void)freezeCapture { + // we must access the layer on the main thread, but manipulating + // the capture connection is blocking and should be dispatched + AVCaptureConnection *connection = self.capturePreviewLayer.connection; + + dispatch_async(self.privateSessionQueue, ^{ + connection.enabled = NO; + [self.session stopRunning]; + }); +} + +- (void)unfreezeCapture { + if (!self.session) { + return; + } + + AVCaptureConnection *connection = self.capturePreviewLayer.connection; + + if (!self.session.isRunning) { + [self setDeviceInput:self.currentCaptureDeviceInput session:self.session]; + + dispatch_async(self.privateSessionQueue, ^{ + [self.session startRunning]; + connection.enabled = YES; + }); + } +} + + +- (void)captureStillImage:(void (^)(UIImage *image, NSError *error))captureBlock { + if ([self isCapturingStillImage]) { + if (captureBlock) { + NSError *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeStillImageCaptureInProgress + userInfo:@{NSLocalizedDescriptionKey : @"Still image capture is already in progress. Check with isCapturingStillImage"}]; + captureBlock(nil, error); + } + return; + } + + if (@available(iOS 10.0, *)) { + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + settings.autoStillImageStabilizationEnabled = NO; + settings.flashMode = AVCaptureFlashModeOff; + settings.highResolutionPhotoEnabled = YES; + + dispatch_async(self.privateSessionQueue, ^{ + [self.output capturePhotoWithSettings:settings delegate:self]; + self.stillImageCaptureBlock = captureBlock; + + }); + } else { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + AVCaptureConnection *stillConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo]; + if (stillConnection == nil) { + if (captureBlock) { + NSError *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeSessionIsClosed + userInfo:@{NSLocalizedDescriptionKey : @"AVCaptureConnection is closed"}]; + captureBlock(nil, error); + } + return; + } + + [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:stillConnection + completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) { + if (error) { + captureBlock(nil, error); + return; + } + + NSData *jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]; + UIImage *image = [UIImage imageWithData:jpegData]; + if (captureBlock) { + captureBlock(image, nil); + } + }]; +#pragma GCC diagnostic pop + } +} + +#pragma mark - AVCapturePhotoCaptureDelegate + +- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error NS_AVAILABLE_IOS(11.0) { + NSAssert(_stillImageCaptureBlock, @"Unable to capture photos with a nil stillImageCaptureBlock"); + if (@available(iOS 11.0, *)) { + NSData *data = photo.fileDataRepresentation; + UIImage *image = nil; + if (data) { + image = [UIImage imageWithData:data]; + } + + self.stillImageCaptureBlock(image, error); + } else { + NSError *error = [NSError errorWithDomain:kErrorDomain + code:kErrorMethodNotAvailableOnIOSVersion + userInfo:@{NSLocalizedDescriptionKey : @"Unable to capture still image: the method is not available on this device."}]; + self.stillImageCaptureBlock(nil, error); + } +} + +// This method uses methods that are deprecated in iOS 10. We also implement the updated method (captureOutput:didFinishProcessingPhoto:error:), so we can ignore the warning here. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma GCC diagnostic ignored "-Wdeprecated-implementations" +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error NS_AVAILABLE_IOS(10.0) { + if (photoSampleBuffer == nil) { + return; + } + + NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; + UIImage *image = nil; + if (data) { + image = [UIImage imageWithData:data]; + } + + if (self.stillImageCaptureBlock) { + self.stillImageCaptureBlock(image, error); + } +} +#pragma GCC diagnostic pop + +- (BOOL)isCapturingStillImage { + return self.stillImageOutput.isCapturingStillImage; +} + +#pragma mark - Setters + +- (void)setCamera:(MTBCamera)camera { + [self setCamera:camera error:nil]; +} + +- (BOOL)setCamera:(MTBCamera)camera error:(NSError **)error { + if (camera == _camera) { + return YES; + } + + if (!self.isScanning) { + if (error) { + *error = [NSError errorWithDomain:kErrorDomain + code:kErrorCodeNotScanning + userInfo:@{NSLocalizedDescriptionKey : @"Camera cannot be set when isScanning is NO"}]; + } + + return NO; + } + + AVCaptureDevice *captureDevice = [self newCaptureDeviceWithCamera:camera]; + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:error]; + + if (!input) { + // we rely on deviceInputWithDevice:error to populate the error in this case + return NO; + } + + [self setDeviceInput:input session:self.session]; + _camera = camera; + + return YES; +} + +- (void)setScanRect:(CGRect)scanRect { + NSAssert(!CGRectIsEmpty(scanRect), @"Unable to set an empty rectangle as the scanRect of MTBBarcodeScanner"); + NSAssert(self.isScanning, @"Scan rect cannot be set when not (yet) scanning. You may want to set it within didStartScanningBlock."); + + if (!self.isScanning) { + return; + } + + [self refreshVideoOrientation]; + + _scanRect = scanRect; + + dispatch_async(self.privateSessionQueue, ^{ + self.captureOutput.rectOfInterest = [self.capturePreviewLayer metadataOutputRectOfInterestForRect:self->_scanRect]; + }); +} + +- (void)setPreferredAutoFocusRangeRestriction:(AVCaptureAutoFocusRangeRestriction)preferredAutoFocusRangeRestriction { + if (preferredAutoFocusRangeRestriction == _preferredAutoFocusRangeRestriction) { + return; + } + + _preferredAutoFocusRangeRestriction = preferredAutoFocusRangeRestriction; + + if (!self.currentCaptureDeviceInput) { + // the setting will be picked up once a new session incl. device input is created + return; + } + + [self updateFocusPreferencesOfDevice:self.currentCaptureDeviceInput.device reset:NO]; +} + +#pragma mark - Getters + +- (CALayer *)previewLayer { + return self.capturePreviewLayer; +} + +#pragma mark - Helper Methods + +- (CGRect)rectOfInterestFromScanRect:(CGRect)scanRect { + CGRect rect = CGRectZero; + if (!CGRectIsEmpty(self.scanRect)) { + rect = [self.capturePreviewLayer metadataOutputRectOfInterestForRect:self.scanRect]; + } else { + rect = CGRectMake(0, 0, 1, 1); // Default rectOfInterest for AVCaptureMetadataOutput + } + return rect; +} + +@end \ No newline at end of file diff --git a/ios/Classes/QRView.swift b/ios/Classes/QRView.swift index 1af75816..5f45d23d 100644 --- a/ios/Classes/QRView.swift +++ b/ios/Classes/QRView.swift @@ -6,7 +6,6 @@ // import Foundation -import MTBBarcodeScanner public class QRView:NSObject,FlutterPlatformView { @IBOutlet var previewView: UIView! diff --git a/ios/qr_code_scanner.podspec b/ios/qr_code_scanner.podspec index f94e7cc1..3caad1ed 100644 --- a/ios/qr_code_scanner.podspec +++ b/ios/qr_code_scanner.podspec @@ -16,8 +16,9 @@ A new Flutter project. s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'MTBBarcodeScanner' + s.frameworks = 'AVFoundation', 'QuartzCore' s.ios.deployment_target = '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64', 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' } s.swift_version = '4.0' end From fc619f8472d3fa6884ba10c63f965eb80fdf8e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E7=9F=B3=E7=A3=8A?= Date: Wed, 18 Mar 2026 10:34:12 +0800 Subject: [PATCH 3/4] doc: version & changelog --- CHANGELOG.md | 5 +++++ ios/qr_code_scanner.podspec | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f91f3c4..3b01d633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.1.0 + +* Android kotlin_version and minSdkVersion upgrade. +* [Fix] iOS26+, App hanging after qr_code_scnaner unmounted. + ## 1.0.0 Breaking changes: Minimum Flutter version is now Flutter 3.0.0 (Dart 2.17.0). diff --git a/ios/qr_code_scanner.podspec b/ios/qr_code_scanner.podspec index 3caad1ed..a2a8c701 100644 --- a/ios/qr_code_scanner.podspec +++ b/ios/qr_code_scanner.podspec @@ -4,7 +4,7 @@ Pod::Spec.new do |s| s.name = 'qr_code_scanner' - s.version = '0.2.0' + s.version = '1.1.0' s.summary = 'QR Code Scanner for flutter.' s.description = <<-DESC A new Flutter project. diff --git a/pubspec.yaml b/pubspec.yaml index 72f21cb8..6912d32d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: qr_code_scanner description: QR code scanner that can be embedded inside flutter. It uses zxing in Android and MTBBarcode scanner in iOS. -version: 1.0.0 +version: 1.1.0 homepage: https://juliuscanute.com repository: https://github.com/juliuscanute/qr_code_scanner environment: - sdk: '>=2.17.0 <3.0.0' + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.12.0" dependencies: From 4c94e54490f4c4e0e35331c4fb74d9b3f97d3a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E7=9F=B3=E7=A3=8A?= Date: Wed, 18 Mar 2026 10:43:28 +0800 Subject: [PATCH 4/4] chore: comment del --- ios/Classes/MTBBarcodeScanner.h | 8 -------- ios/Classes/MTBBarcodeScanner.m | 8 -------- 2 files changed, 16 deletions(-) diff --git a/ios/Classes/MTBBarcodeScanner.h b/ios/Classes/MTBBarcodeScanner.h index 32829729..4ba01949 100644 --- a/ios/Classes/MTBBarcodeScanner.h +++ b/ios/Classes/MTBBarcodeScanner.h @@ -1,11 +1,3 @@ -// -// MTBBarcodeScanner.h -// MTBBarcodeScannerExample -// -// Created by Mike Buss on 2/8/14. -// -// - #import #import #import diff --git a/ios/Classes/MTBBarcodeScanner.m b/ios/Classes/MTBBarcodeScanner.m index daece00f..d5f82eda 100644 --- a/ios/Classes/MTBBarcodeScanner.m +++ b/ios/Classes/MTBBarcodeScanner.m @@ -1,11 +1,3 @@ -// -// MTBBarcodeScanner.m -// MTBBarcodeScannerExample -// -// Created by Mike Buss on 2/8/14. -// -// - #import #import "MTBBarcodeScanner.h"