#import #import // ──── 前向声明 ──── static void presentQRScannerWithName(NSString *goName); @interface QRScannerViewController : UIViewController @property (nonatomic, strong) AVCaptureSession *session; @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer; @property (nonatomic, copy) NSString *gameObjectName; @property (nonatomic, assign) BOOL hasScanned; @end @implementation QRScannerViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; self.session = [[AVCaptureSession alloc] init]; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; if (!device) { [self sendResultToUnity:@"ERROR:NO_CAMERA"]; return; } NSError *error = nil; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; if (!input) { [self sendResultToUnity:[NSString stringWithFormat:@"ERROR:CAMERA_INPUT:%@", error.localizedDescription]]; return; } [self.session addInput:input]; AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init]; [self.session addOutput:output]; [output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode]; self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session]; self.previewLayer.frame = self.view.bounds; self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; [self.view.layer addSublayer:self.previewLayer]; // 半透明遮罩 + 扫描框 CGFloat boxSize = self.view.bounds.size.width * 0.65; CGRect boxRect = CGRectMake( (self.view.bounds.size.width - boxSize) / 2, (self.view.bounds.size.height - boxSize) / 2 - 60, boxSize, boxSize ); CAShapeLayer *mask = [CAShapeLayer layer]; UIBezierPath *fullPath = [UIBezierPath bezierPathWithRect:self.view.bounds]; [fullPath appendPath:[[UIBezierPath bezierPathWithRoundedRect:boxRect cornerRadius:4] bezierPathByReversingPath]]; mask.path = fullPath.CGPath; mask.fillColor = [UIColor colorWithWhite:0 alpha:0.5].CGColor; [self.view.layer addSublayer:mask]; CAShapeLayer *border = [CAShapeLayer layer]; border.path = [UIBezierPath bezierPathWithRoundedRect:boxRect cornerRadius:4].CGPath; border.strokeColor = [UIColor greenColor].CGColor; border.lineWidth = 2; border.fillColor = UIColor.clearColor.CGColor; [self.view.layer addSublayer:border]; UILabel *hint = [[UILabel alloc] init]; hint.text = @"将二维码放入框内,即可自动扫描"; hint.textColor = UIColor.whiteColor; hint.font = [UIFont systemFontOfSize:14]; hint.textAlignment = NSTextAlignmentCenter; hint.frame = CGRectMake(20, boxRect.origin.y + boxRect.size.height + 20, self.view.bounds.size.width - 40, 30); [self.view addSubview:hint]; UIButton *cancel = [UIButton buttonWithType:UIButtonTypeSystem]; [cancel setTitle:@"关闭" forState:UIControlStateNormal]; [cancel setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; cancel.titleLabel.font = [UIFont systemFontOfSize:17]; cancel.frame = CGRectMake(0, self.view.bounds.size.height - 100, self.view.bounds.size.width, 50); [cancel addTarget:self action:@selector(onCancel) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:cancel]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.session startRunning]; }); } - (void)onCancel { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.session stopRunning]; }); [self dismissViewControllerAnimated:YES completion:^{ [self sendResultToUnity:@"CANCEL"]; }]; } - (void)sendResultToUnity:(NSString *)result { const char *go = [self.gameObjectName UTF8String]; const char *msg = [result UTF8String]; UnitySendMessage((char *)go, "OnScanResult", (char *)msg); } #pragma mark - AVCaptureMetadataOutputObjectsDelegate - (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { if (self.hasScanned) return; if (metadataObjects.count == 0) return; AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject; if (![code isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) return; self.hasScanned = YES; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.session stopRunning]; }); [self dismissViewControllerAnimated:YES completion:^{ [self sendResultToUnity:code.stringValue ?: @"ERROR:EMPTY_RESULT"]; }]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.session stopRunning]; }); } @end // ──── C 函数实现 ──── static void presentQRScannerWithName(NSString *goName) { dispatch_async(dispatch_get_main_queue(), ^{ UIViewController *rootVC = UnityGetGLViewController(); if (!rootVC) { UnitySendMessage((char *)[goName UTF8String], "OnScanResult", "ERROR:NO_ROOT_VC"); return; } QRScannerViewController *scanner = [[QRScannerViewController alloc] init]; scanner.gameObjectName = goName; scanner.modalPresentationStyle = UIModalPresentationFullScreen; [rootVC presentViewController:scanner animated:YES completion:nil]; }); } extern "C" { void _StartScanIOS(const char *gameObjectName) { NSString *goName = [NSString stringWithUTF8String:gameObjectName]; dispatch_async(dispatch_get_main_queue(), ^{ AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (status == AVAuthorizationStatusNotDetermined) { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { if (granted) { presentQRScannerWithName(goName); } else { UnitySendMessage((char *)[goName UTF8String], "OnScanResult", "ERROR:PERMISSION_DENIED"); } }]; return; } if (status == AVAuthorizationStatusAuthorized) { presentQRScannerWithName(goName); } else { UnitySendMessage((char *)[goName UTF8String], "OnScanResult", "ERROR:PERMISSION_DENIED"); } }); } }