| # 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. | |
| <!-- no-source-file --> | |
| ```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. | |
| <!-- no-source-file --> | |
| ```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: | |
| <!-- no-source-file --> | |
| ```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`: | |
| <!-- file://./../example/api/os_version_snippet.dart --> | |
| ```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. |