Dealing with OS differences

This doc explains how to deal with differences between iOS and macOS APIs, and differences between API versions.

iOS/macOS API differences

Objective-C can guard iOS/macOS specific code with #if macros, so that it will only be compiled on specific platforms:

#if TARGET_OS_IPHONE
@interface WKWebView : UIView
#else
@interface WKWebView : NSView
#endif

Dart has no equivalent of this, because Dart‘s kernel files are designed to work on any platform. Platform.isMacOS etc are runtime values, not compile time constants (technical detail: they’re compile time constant in AOT mode, but not in JIT mode). So it's not possible to conditionally import dart files based on Platform.isMacOS/isIOS.

There are two approaches for dealing with platform differences, depending on how significant the differences are. If the API you‘re working with only has small differences between iOS and macOS, you can try to generate a single FFIgen wrapper for both, and use runtime Platform.isMacOS/isIOS checks to call different methods. At runtime, FFIgen lazy loads all classes and methods, so if a class/method doesn’t exist in your plugin/app‘s native code, that’s fine as long as the class/method isn't used at runtime.

final foo = Foo();
if (Platform.isMacOS) {
  final bar = MacSpecificBar();
  foo.macSpecificMethod(bar);
} else {
  assert(Platform.isIOS);
  final bar = IosSpecificBar();
  foo.iosSpecificMethod(bar);
}

If the API differences are severe enough that they would cause compile time errors if they‘re in a single library, that approach won’t work. For example, WKWebView inherits from UIView on iOS and NSView on macOS, so there‘s no way FFIgen can generate a single WKWebView class for both platforms. In cases like this, it’s necessary to run FFIgen separately for each platform, and generate different bindings for iOS and macOS. Since you can‘t conditionally import based on the OS, you need a way of pulling both sets of bindings into your plugin. The simplest approach is to use the rename config option to rename the APIs so they don’t conflict (eg WKWebViewIOS and WKWebViewMacOS), then import both sets of bindings and use Platform checks to call different APIs.

if (Platform.isMacOS) {
  final webView = WKWebViewMacOS();
  // ...
} else {
  assert(Platform.isIOS);
  final webView = WKWebViewIOS();
  // ...
}

If renaming isn't practical (e.g. there would be too many renames or Platform checks), then you can write separate Dart classes for each platform that implement a shared interface:

// Both imports were generated by FFIgen.
import 'web_view_bindings_mac.dart' as mac;
import 'web_view_bindings_ios.dart' as ios;

abstract interface class WebView {
  void load(Uri uri);

  factory WebView() {
    if (Platform.isMacOS) {
      return WebViewMac();
    } else {
      assert(Platform.isIOS);
      return WebViewIOS();
    }
  }
}

class WebViewMac implements WebView {
  mac.WKWebView _view;

  @override
  void load(Uri uri) {
    // ...
  }
}

class WebViewIOS implements WebView {
  ios.WKWebView _view;

  @override
  void load(Uri uri) {
    // ...
  }
}

API version differences

Objective-C uses the @available annotation to allow developers to write if statements that do different things on different OS versions. It also generates a compiler warning if the developer uses an API that is only available in particular OS versions, without guarding it with an @available if statement.

if (@available(iOS 18, *)) {
  // Use newer iOS 18 API.
} else {
  // Fallback to old API.
}

We can't replicate the compiler warning in Dart/FFIgen at the moment, but we can write the runtime version check. The recommended way of doing this is using package:objective_c's checkOsVersion:

// If you only need to support iOS:
if (checkOsVersion(iOS: Version(18, 0, 0))) {
  // Use newer iOS 18 API.
} else {
  // Fallback to old API.
}

// If you need to support iOS and macOS:
if (checkOsVersion(iOS: Version(18, 0, 0), macOS: Version(15, 3, 0))) {
  // Use newer API available in iOS 18 and macOS 15.3.
} else {
  // Fallback to old API.
}

checkOsVersion returns true if the current OS version is equal to or greater than the given version.

FFIgen‘s generated code includes version checks that will throw an OsVersionError if the API is not available in the current OS version. But it’s better to write if statements like above, rather than trying to catch this error.