| # 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: |
| |
| ```obj-c |
| #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. |
| |
| ```dart |
| 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. |
| |
| ```dart |
| 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: |
| |
| ```dart |
| // 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. |
| |
| ```obj-c |
| 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`: |
| |
| ```dart |
| // 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. |