Using Swift API availability to solve App Extension Compiled Error
Contents
Starting from Xcode12.5, Apple requires that all Extension Targets must set APPLICATION_EXTENSION_API_ONLY to true, otherwise it will cause a compilation error “Application extensions and any libraries they link to must be built with the APPLICATION_EXTENSION_API_ONLY
build setting set to YES”; but we Framework or other methods are usually used to share code between the main project and the extension. These codes use non-extension-only APIs, which leads to problems. This article will discuss how to solve this problem.
Explore
Let’s take a specific structure as an example, as shown in the following figure:
In our main project Host App, we created an extension Target of Share Extension
to do share-related operations; in addition, for modularization, we have a Library
project that contains all the basic components and Fundation extension methods, NetworkService
The project contains functional packaging and processing related to network requests, and they are all compiled into Framework for common use by the main project and Share Extension
;
We first need to set the APPLICATION_EXTENSION_API_ONLY in the Build Setting of the three projects Share Extension
, Library
, and NetworkService
to true; because we use UIApplication.shared.open
in both Library
and NetworkService
, UIApplication.shared.keyWindow
is a non-extension-only API, so it is impossible to compile these two sub-projects.
The first solution that comes to mind is code splitting. We can split Library
according to whether extension-only API is used or not, and split it into two projects Libray
and LibraryExtension
, and LibrayExtension
contains extension-only APIs for Share Extesnion
; Library
contains other unrestricted APIs, which are provided to the main project or other non-Extesnion projects;
Then NetworkService
also uses the same method for transformation. This method can solve the problem, but in addition to the cost of splitting the code to create a new project, it will also bring a lot of extra workload; for example, the Host App in the main project, the original It is to reference import Library
, and now you need to modify and confirm one by one, whether to use Libray
or LibraryExtension
, or add two references at the same time, this is a lot of work for an existing large project;
Obviously, the above methods have their own limitations, so we need to find more widely used solutions, with smaller changes; Swift language provides API usability identification, this function can solve the problems we encounter, Let’s first understand the availability of Swift API.
Swift API Availability
In Swift, you use the @available
attribute to annotate APIs with availability information,such as whether the API has been deprecated in a version. the API requires a Swift version greater than 5.4 to be used, and so on. Let’s look at a few specific aspects:
Platform Availability
@available(iOS 13.0, OSX 10.15, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public struct SearchField: View{
...
}
In SwiftUI, We define a SearchField component, and we restrict the platform and system versions that it applies to. The above code indicates that SearchField is applicable to versions iOS13
or OSX 10.15
or greater. Both tvOS
and watchOS
are not available.
@available(platform version , platform version ..., *)
- platform:Specify specific platforms,for example
iOS
,macCatalyst
,macOS/OSX
,tvOS
或者watchOS
, You can also specify Extension Target , such asApplicationExtension
ormacOSApplicationExtension
, this is one of the important tool to deal with the problem raised by the opening, this section will in detail later. - version:A version number consisting of one, two, or three positive integers, separated by a period (.), to denote the major, minor, and patch version.
- Zero or more versioned platforms in a comma-delimited (,) list.
- An asterisk (*), denoting that the API is available for all other platforms. An asterisk is always required for platform availability annotations to handle potential future platforms.
API Availability
As we go through the software development process, we constantly improve by introducing new APIs and discarding old ones, and the corresponding @available API allows us to do this part of the work.
// With introduced, deprecated, and/or obsoleted
@available(platform | *
, introduced: version , deprecated: version , obsoleted: version
, renamed: "..."
, message: "...")
// With unavailable
@available(platform | *, unavailable , renamed: "..." , message: "...")
- A platform, same as before, or an asterisk (*) for all platforms.
- Either introduced, deprecated, and/or obsoleted…
- An introduced version, denoting the first version in which the API is available
- A deprecated version, denoting the first version when using the API generates a compiler warning
- An obsoleted version, denoting the first version when using the API generates a compiler error
- …or unavailable, which causes the API to generate a compiler error when used
- renamed with a keypath to another API; when provided, Xcode provides an automatic “fix-it”
- A message string to be included in the compiler warning or error
Unlike shorthand specifications, this form allows for only one platform to be specified. So if you want to annotate availability for multiple platforms, you’ll need stack @available attributes.
#available
In Swift, you can predicate if, guard, and while statements with an availability condition, #available, to determine the availability of APIs at runtime. Unlike the @available attribute, an #available condition can’t be used for Swift language version checks.
The syntax of an #available expression resembles that of an @available attribute:
if | guard | while #available(platform version , platform version ..., *) …
You can’t combine multiple #available expressions using logical operators like && and , but you can use commas, which are equivalent to &&. In practice, this is only useful for conditioning Swift language version and the availability of a single platform (since a check for more than one would be either redundant or impossible).
Quoted from https://nshipster.com/available/
Solve Problem
Let’s discuss the concrete solution to the problem of App Extension Compiling Error,use Swift API Availability.
Take the following GoToAppSystemSetting
function as an example:
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
@available(watchOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
@available(tvOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
@available(iOSMacApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
@available(OSXApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
static func gotoAppSystemSetting() {
if let url = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
In this function, use UIApplication.shared.open
API, Extension Target can not use, So need add Swift API Availability
With this modification, the compilation will pass if APPLICATION_EXTENSION_API_ONLY is turned on to true, because the availability range of the API is clearly marked.
Call Function Chain
After the problem of a single API function is solved, you also need to consider the chain of function calls. For example, if UIApplication.shared.keyWindow
is used in function A, then function A needs to be marked as an API that cannot be used by Extension Target;
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
func A() {
...
//调用UIApplication.shared.keyWindow
...
}
Next, if function B calls function A, and both function C call function B, then both functions B and C also need the same @available
tag;
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
func B() {
...
A() //Function B call Function A
...
}
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
func C() {
...
B() //Function C call Function B
...
}
Protocol Function
Next, we continue to look at a problem related to the protocol function. There is a protocol DialogueViewProtocol, which declares a show method as shown in the following code:
public protocol DialogueViewProtocol{
func show(cancelHander acancelHander:(() -> Void)? ,comfirmHander acomfirmHander:(() -> Void)?)
}
We have two styles of DialogueView components that need to be implemented, namely DialogueView_1 and DialogueView_2, in order to extract common code, they both inherit from DialogueView;
//DialogueView_1 inherit DialogueView
public class DialogueView_1:DialogueView{
}
//DialogueView_2 inherit DialogueView
public class DialogueView_2:DialogueView{
}
//DialogueView_1 conform to protocol DialogueViewProtocol , Implement show function
extension DialogueView_1:DialogueViewProtocol{
public func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
...
}
//DialogueView_2 conform to protocol DialogueViewProtocol , Implement show function
extension DialogueView_2:DialogueViewProtocol{
public func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
...
}
When implementing two different styles of DialogueView subclasses, they all obey DialogueViewProtocol and implement different show methods;
In the show function, both methods use UIApplication.shared.keyWindow
, which obviously cannot be used by Extension, so the same @available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE. ")
mark. After this modification, the problem that the API is not used by Extension is solved, but a new problem is introduced, which causes the show method to be invisible, and the two child views do not implement the DialogueViewProtocol protocol;
So how to solve it? We need to continue to transform the code and implement the protocol in the DialogueView base class to solve this problem;
//Base Class DialogueView conform to DialogueViewProtocol
extension DialogueView:DialogueViewProtocol{
public func show(cancelHander acancelHander:(() -> Void)? ,comfirmHander acomfirmHander:(() -> Void)?){
//empty
}
}
extension DialogueView_1{
//use API Availability,override show function
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
public override func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
...
}
}
extension DialogueView_2{
//use API Availability,override show function
@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
public override func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
...
}
}
DialogUeView follows DialogUeView to realize the empty implementation of show function, and then override show function in DialogUeView_1 and DialogUeView_2. It should be noted that the show function in DialogUView does not need the @available flag because it does not use the Extension Restriction API, while DialogUEView_1 and DialogUEView_2 are required.
Objective-C Function API
A function written in Objective-C may also use an API that is not available with Extension. The Objective-C language also provides’ NS_EXTENSION_UNAVAILABLE_IOS ‘to flag it. There are many examples of this in system functions:
./EventKitUI.framework/Headers/EKEventViewController.h:NS_EXTENSION_UNAVAILABLE_IOS("EventKitUI is not supported in extensions")
./Foundation.framework/Headers/NSObjCRuntime.h:#define NS_EXTENSION_UNAVAILABLE_IOS(_msg) __IOS_EXTENSION_UNAVAILABLE(_msg)
./UIKit.framework/Headers/UIAlertView.h:- (instancetype)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id /*<UIAlertViewDelegate>*/)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... NS_REQUIRES_NIL_TERMINATION NS_EXTENSION_UNAVAILABLE_IOS("Use UIAlertController instead.");
./UIKit.framework/Headers/UIApplication.h:- (void)beginIgnoringInteractionEvents NS_EXTENSION_UNAVAILABLE_IOS(""); // nested. set should be set during animations & transitions to ignore touch and other events
NS_EXTENSION_UNAVAILABLE_IOS("...")
Mark Extension cannot use these APIs, there is a parameter behind, which can be used as a reminder, what API to replace
Common API
Mark the function in the header file
+ (UIImage *)launchImage NS_EXTENSION_UNAVAILABLE_IOS("");
Override System API
If the system method is override, such as inheriting UIViewController and overriding statusBarStyle, then marking it needs to be implemented in the function itself.
- (UIStatusBarStyle)statusBarStyle NS_EXTENSION_UNAVAILABLE_IOS("") {
return [UIApplication sharedApplication].statusBarStyle;
}
Conclusions
Due to the changes of new version Xcode12.5, we need to use the Swift API Availability for not extension-only APIs to use @available(iOSApplicationExtension, unavailable)
mark, Objective-C language also has corresponding NS_EXTENSION_UNAVAILABLE_IOS("...")
mark can be used; you also need to add mark to the upstream function on the current mark function call chain, in addition, You may also need to implement the default implementation for protocol functions to solve the problem of invisible mark @available.
Reference
- https://stackoverflow.com/questions/33308196/checking-for-protocol-availability-in-swift
- https://forums.swift.org/t/availability-checking-for-protocol-conformances/42066/8
- https://www.cnblogs.com/lxlx1798/articles/13060713.html
- https://apollozhu.github.io/2017/06/20/swift-and-ns-extension-unavailable/
- https://swift-cast.com/2021/04/38/
- https://nshipster.com/available/
- https://davedelong.com/blog/2019/04/09/conditional-compilation-part-3/