Skip to content

💻 Example macOS React Native Module interacting with Apple MusicKit using Swift.

Notifications You must be signed in to change notification settings

anatelli10/macOSNativeModuleExample

Repository files navigation

macOSNativeModuleExample

Note

This example is focused on writing logic/functions and may not be applicable to creating user interfaces.

Proof of concept/exemplification of how to write React Native Modules for macOS using Swift. Includes asynchronous and synchronous examples interacting with Apple MusicKit.

This almost entirely also applies to iOS, though for iOS you should probably instead use the Expo Modules API.

Implementation Files

Demo

Screen.Recording.2023-12-22.at.6.53.20.PM.mov

How to create a React Native Module for macOS

Instructions
  1. Install React Native for macOS

    Do you want to install CocoaPods now? y

    You'll want to make sure your project can build/run using Xcode.

    ⚠️ Build error: "Command PhaseScriptExecution failed with a nonzero exit code"

    There may be other better solutions for this such as changing Node related configuration or updating CocoaPods, but this worked for me:

    Modify node_modules/react-native/scripts/find-node.sh @ L7

    - set -e
    + set +e

    see facebook/react-native#36762 (comment)

  2. From project root dir run xed -b macos to open Xcode.

  3. Navigate to the folder containing AppDelegate.

    image
  4. Create a new macOS Swift file.

    image

    The name you use for this file will be reused throughout the project including in your React code. Leave the options to default and create. I'm naming mine MusicKitModule as I'll be exporting some methods that utilize Apple MusicKit. Suffixed with Module to prevent confusion, but use whatever naming you like.

  5. Create the bridging header automatically.

    image

    The name of this file is automatically prefixed by your Xcode project name.

  6. Add #import <React/RCTBridgeModule.h> to the ...-Bridging-Header.h file.

  7. Add the following boilerplate to your Swift file

    @objc(YourFileName) class YourFileName: NSObject {
      @objc static func requiresMainQueueSetup() -> Bool { return true }
    }
  8. Create a new Objective-C file with the same name

    image
  9. Add #import <React/RCTBridgeModule.h> to the YourFileName.m file.

  10. In macos/YourProjectName-macOS/Info.plist, add the following key/string pair

       <key>NSSupportsSuddenTermination</key>
       <true/>
    +  <key>NSAppleMusicUsageDescription</key>
    +  <string>A message that tells the user why the app is requesting access to the user's media library.</string>
       </dict>
    </plist>
  11. Test by running the Xcode project.

  12. Congratulations! You've completed all boilerplate. See below for examples on creating methods.

Example asynchronous method

Example

ℹ️ There also exists the ability to create callback based methods using RCTResponseSenderBlock, RCTResponseErrorBlock, but I will not be using those here.

Swift

  • Expose the function using @objc
  • Last two function parameters must be RCTPromiseResolveBlock, RCTPromiseRejectBlock
  • Use @escaping to use resolve or reject in a Task
@objc(MusicKitModule) class MusicKitModule: NSObject {
  @objc static func requiresMainQueueSetup() -> Bool { return true }

  /// Asynchronous
  @objc func requestAuthorization(_ resolve: @escaping(RCTPromiseResolveBlock), rejecter reject: RCTPromiseRejectBlock) {
    if #available(macOS 12.0, *) {
      Task {
        let status = (await MusicAuthorization.request()).rawValue
        resolve(status)
      }
    } else {
      resolve("Unsupported iOS")
    }
  }
}

Objective-C (.m)

  • Register the module once using RCT_EXTERN_MODULE
  • Register a method using RCT_EXTERN_METHOD, providing the method signature.
@interface RCT_EXTERN_MODULE(MusicKitModule, NSObject)

RCT_EXTERN_METHOD(requestAuthorization: (RCTPromiseResolveBlock)resolve
                  rejecter: (RCTPromiseRejectBlock)reject)

@end

React

This is a minimal example, you could expand this by following this guide.

  • Import NativeModules
  • Your module is a property on the NativeModules import, corresponds to the same file name used in ObjC/Swift.
  • Use await (or chain .then())
import {NativeModules} from 'react-native';
// Optionally destructure
const {MusicKitModule} = NativeModules;

const status = await MusicKitModule.requestAuthorization();

Example synchronous method

Example

⚠️ Runs a blocking function on the main thread. Highly discouraged by React Native. Use at own risk and please know what you're doing.

Swift

  • Expose the function using @objc
@objc(MusicKitModule) class MusicKitModule: NSObject {
  @objc static func requiresMainQueueSetup() -> Bool { return true }

  /// Synchronous (main thread)
  @objc func currentAuthorizationStatus() -> String {
    if #available(macOS 12.0, *) {
      let status = MusicAuthorization.currentStatus.rawValue
      return status
    } else {
      return "Unsupported iOS"
    }
  }
}

Objective-C (.m)

  • Register the module once using RCT_EXTERN_MODULE
  • Register a method using RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD, providing the method signature.
@interface RCT_EXTERN_MODULE(MusicKitModule, NSObject)

RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(currentAuthorizationStatus)

@end

React

This is a minimal example, you could expand this by following this guide.

  • Import NativeModules
  • Your module is a property on the NativeModules import, corresponds to the same file name used in ObjC/Swift.
import {NativeModules} from 'react-native';
// Optionally destructure
const {MusicKitModule} = NativeModules;

const status = MusicKitModule.currentAuthorizationStatus();

Resources