191 lines
7.0 KiB
Plaintext
191 lines
7.0 KiB
Plaintext
|
|
#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");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|