kill-ios-app/Libraries/Plugins/iOS/NativeQRScanner.mm

191 lines
7.0 KiB
Plaintext
Raw Normal View History

2026-06-23 09:54:30 +08:00
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
// ──── 前向声明 ────
static void presentQRScannerWithName(NSString *goName);
@interface QRScannerViewController : UIViewController <AVCaptureMetadataOutputObjectsDelegate>
@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");
}
});
}
}