Skip to main content

priv/native/ios/mob_scanner_nif.m

/* mob_scanner_nif — iOS QR/barcode scanner tier-1 plugin NIF (Objective-C).
 *
 * Extracted from mob-core ios/mob_nif.m (the "QR / barcode scanner" section,
 * mob_nif.m:2939-3046): MobScannerVC, a full-screen AVCaptureMetadataOutput
 * view controller. Self-contained — core's mob_send2 / mob_root_vc are
 * private statics, so this ships its own (scan_send2 / scan_root_vc).
 * Compiled as ObjC (-fobjc-arc) via the plugin objc-NIF path (manifest
 * lang: :objc).
 *
 * Delivered message shapes (exact core parity):
 *   not_available -> {scan, not_available}   (mob_nif.m:2957)
 *   cancelled     -> {scan, cancelled}       (mob_nif.m:2991)
 *   result        -> {scan, result, #{type => atom, value => binary}}
 *                    (mob_nif.m:3016-3031 — type as an ATOM, value as a
 *                    binary, delivered as direct Erlang terms; the Android
 *                    side goes through the {:mob_file_result, ...} JSON
 *                    path instead, decoded by core Mob.Screen into the
 *                    same user-facing tuple, lib/mob/screen.ex:382-384)
 *
 * PARITY: core's nif_scanner_scan is arity 1 (mob_nif.m:6014) but ignores
 * the formats JSON argument — the metadataObjectTypes list is hardcoded
 * (mob_nif.m:2966-2971). Preserved exactly. The :camera permission flow is
 * NOT here — it is owned by the mob_camera plugin.
 */
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#include <erl_nif.h>

// Self-contained {atom, atom} send (core's mob_send2 is a private static).
static void scan_send2(const ErlNifPid *pid, const char *a1, const char *a2) {
    ErlNifEnv *e = enif_alloc_env();
    ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e, a1), enif_make_atom(e, a2));
    enif_send(NULL, (ErlNifPid *)pid, e, msg);
    enif_free_env(e);
}

// Root view controller for presenting the scanner (core's mob_root_vc is a
// private static).
static UIViewController *scan_root_vc(void) {
    for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
        if ([scene isKindOfClass:[UIWindowScene class]]) {
            UIWindowScene *ws = (UIWindowScene *)scene;
            UIWindow *w = ws.keyWindow ?: ws.windows.firstObject;
            if (w.rootViewController)
                return w.rootViewController;
        }
    }
    return nil;
}

// ── QR / barcode scanner ──────────────────────────────────────────────────

@interface MobScannerVC : UIViewController <AVCaptureMetadataOutputObjectsDelegate>
@property(nonatomic) ErlNifPid pid;
@property(nonatomic, strong) AVCaptureSession *session;
@property(nonatomic, strong) AVCaptureVideoPreviewLayer *preview;
@end

static MobScannerVC *g_scanner_vc = nil;

@implementation MobScannerVC
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
    NSError *err = nil;
    AVCaptureDevice *dev = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    AVCaptureDeviceInput *inp = [AVCaptureDeviceInput deviceInputWithDevice:dev error:&err];
    if (!inp) {
        scan_send2(&_pid, "scan", "not_available");
        [self dismissViewControllerAnimated:YES completion:nil];
        return;
    }
    self.session = [[AVCaptureSession alloc] init];
    [self.session addInput:inp];
    AVCaptureMetadataOutput *out = [[AVCaptureMetadataOutput alloc] init];
    [self.session addOutput:out];
    [out setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    out.metadataObjectTypes = @[
        AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code,
        AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeCode39Code,
        AVMetadataObjectTypeAztecCode, AVMetadataObjectTypePDF417Code,
        AVMetadataObjectTypeDataMatrixCode
    ];
    self.preview = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
    self.preview.videoGravity = AVLayerVideoGravityResizeAspectFill;
    self.preview.frame = self.view.bounds;
    [self.view.layer addSublayer:self.preview];
    // Cancel button
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn setTitle:@"Cancel" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    btn.frame = CGRectMake(16, 60, 80, 44);
    [btn addTarget:self action:@selector(cancel) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
    [self.session startRunning];
}
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.preview.frame = self.view.bounds;
}
- (void)cancel {
    [self.session stopRunning];
    scan_send2(&_pid, "scan", "cancelled");
    [self dismissViewControllerAnimated:YES completion:nil];
    g_scanner_vc = nil;
}
- (void)captureOutput:(AVCaptureOutput *)out
    didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metas
              fromConnection:(AVCaptureConnection *)conn {
    AVMetadataMachineReadableCodeObject *code = metas.firstObject;
    if (!code || !code.stringValue)
        return;
    [self.session stopRunning];
    NSString *val = code.stringValue;
    NSString *typ = @"qr";
    if ([code.type isEqualToString:AVMetadataObjectTypeEAN13Code])
        typ = @"ean13";
    else if ([code.type isEqualToString:AVMetadataObjectTypeEAN8Code])
        typ = @"ean8";
    else if ([code.type isEqualToString:AVMetadataObjectTypeCode128Code])
        typ = @"code128";
    else if ([code.type isEqualToString:AVMetadataObjectTypeCode39Code])
        typ = @"code39";
    ErlNifPid p = self.pid;
    g_scanner_vc = nil;
    [self dismissViewControllerAnimated:YES
                             completion:^{
                               ErlNifEnv *e = enif_alloc_env();
                               const char *cval = val.UTF8String;
                               const char *ctyp = typ.UTF8String;
                               ErlNifBinary vb;
                               enif_alloc_binary(strlen(cval), &vb);
                               memcpy(vb.data, cval, strlen(cval));
                               ERL_NIF_TERM keys[2] = {enif_make_atom(e, "type"),
                                                       enif_make_atom(e, "value")};
                               ERL_NIF_TERM vals[2] = {enif_make_atom(e, ctyp),
                                                       enif_make_binary(e, &vb)};
                               ERL_NIF_TERM map;
                               enif_make_map_from_arrays(e, keys, vals, 2, &map);
                               ERL_NIF_TERM msg = enif_make_tuple3(
                                   e, enif_make_atom(e, "scan"), enif_make_atom(e, "result"), map);
                               enif_send(NULL, &p, e, msg);
                               enif_free_env(e);
                             }];
}
@end

static ERL_NIF_TERM nif_scanner_scan(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifPid pid;
    enif_self(env, &pid);
    dispatch_async(dispatch_get_main_queue(), ^{
      g_scanner_vc = [[MobScannerVC alloc] init];
      g_scanner_vc.pid = pid;
      g_scanner_vc.modalPresentationStyle = UIModalPresentationFullScreen;
      [scan_root_vc() presentViewController:g_scanner_vc animated:YES completion:nil];
    });
    return enif_make_atom(env, "ok");
}

// ── Registration ──────────────────────────────────────────────────────────
// No load callback needed (unlike mob_camera, which registers a permission
// handler at load) — the :camera permission flow is owned by mob_camera.

static ErlNifFunc nif_funcs[] = {
    {"scanner_scan", 1, nif_scanner_scan, 0},
};

ERL_NIF_INIT(mob_scanner_nif, nif_funcs, NULL, NULL, NULL, NULL)