演练:绑定 iOS Swift 库

重要

我们当前正在调查 Xamarin 平台上的自定义绑定使用情况。 请参与此调查,告诉我们将来应该进行哪些开发工作。

借助 Xamarin,移动开发人员能够通过 Visual Studio 和 C# 创建跨平台的本机移动体验。 可以使用现成的 iOS 平台 SDK 组件。 但在许多情况下,你还希望使用为该平台开发的第三方 SDK,Xamarin 允许通过绑定来实现这一点。 若要将第三方 Objective-C 框架合并到 Xamarin.iOS 应用程序,需要先为其创建一个 Xamarin.iOS 绑定,然后才能在应用程序中使用它。

iOS 平台及其本机语言和工具正在不断演变,Swift 是目前 iOS 开发领域中最具活力的领域之一。 有许多第三方 SDK,它们已从 Objective-C 迁移到 Swift,并且为我们带来了新的挑战。 尽管 Swift 绑定过程与 Objective-C 相似,但它需要额外的步骤和配置设置才能成功生成并运行 AppStore 可接受的 Xamarin.iOS 应用程序。

本文档的目的是概述对此方案进行寻址的高级方法,并通过一个简单的示例提供了详细分步指南。

背景

Swift 最初由 Apple 于 2014 年推出,目前版本为 5.1,第三方框架的采用率正在迅速增长。 可以使用几个选项来绑定 Swift 框架,本文档概述了使用 Objective-C 生成的接口标头的方法。 该标头由 Xcode 工具在创建框架时自动创建,并用作从托管领域与 Swift 领域通信的一种方式。

先决条件

若要完成本演练,你需要:

生成本机库

第一步是生成启用了 Objective-C 标头的本机 Swift 框架。 该框架通常由第三方开发人员提供,并且标头已嵌入到位于以下目录的包中:<FrameworkName>.framework/Headers/<FrameworkName>-Swift.h

此标头可公开公共接口,后者将用于创建 Xamarin.iOS 绑定元数据并生成用于公开 Swift 框架成员的 C# 类。 如果该标头不存在或具有不完整的公共接口(例如,看不到类/成员),则你有两个选项:

  • 更新 Swift 源代码以生成标头,并使用 @objc 属性标记所需的成员
  • 生成代理框架,可在其中控制公共接口和代理对基础框架的所有调用

在本教程中,介绍了第二种方法,因为它对第三方源代码的依赖较少,而第三方代码并非总是可用。 避免第一种方法的另一个原因是需要额外的工作来支持未来的框架更改。 开始向第三方源代码添加更改后,你将负责支持这些更改,并且可能需要将它们与未来的更新合并。

例如,在本教程中,为 Gigya Swift SDK 创建了绑定:

  1. 打开 Xcode 并创建新的 Swift 框架,该框架将用作 Xamarin.iOS 代码与第三方 Swift 框架之间的代理。 单击“文件”>“新建”>“项目”并按照向导步骤操作:

    xcode create framework project

    xcode name framework project

  2. 从开发人员网站下载 Gigya xcframework 并将其解压缩。 撰写本文时,最新版本为 Gigya Swift SDK 1.5.3

  3. 从项目文件资源管理器中选择 SwiftFrameworkProxy,然后选择“常规”选项卡

  4. Gigya.xcframework 包拖放到“常规”选项卡下的“Xcode 框架和库”列表中,并在添加框架时选中“需要时复制项”选项:

    xcode copy framework

    验证 Swift 框架是否已添加到项目中,否则以下选项将不可用。

  5. 确保已选择“不嵌入”选项,稍后将手动控制该选项:

    xcode donotembed option

  6. 确保“生成设置”选项“始终嵌入 Swift 标准库”(包括 Swift 库和框架)设置为“否”。 稍后将手动进行控制,Swift dylibs 将包含在最终包中:

    xcode always embed false option

  7. 确保“启用 Bitcode”选项设置为“”。 截至目前,Xamarin.iOS 不包括 Bitcode,而 Apple 需要所有库来支持相同的体系结构:

    xcode enable bitcode false option

    可以通过对框架运行以下终端命令来验证生成的框架是否禁用了 Bitcode 选项:

    otool -l SwiftFrameworkProxy.framework/SwiftFrameworkProxy | grep __LLVM
    

    输出应为空,否则需要查看特定配置的项目设置。

  8. 确保已启用“Objective-C 生成的接口标头名称”选项并指定标头名称。 默认名称为 <FrameworkName>-Swift.h

    xcode objectice-c header enabled option

    提示

    如果此选项不可用,请先确保将 .swift 文件添加到项目(如下所述),然后返回到 Build Settings,该设置应该是可发现的。

  9. 公开所需的方法,使用 @objc 属性标记它们,并应用下面定义的其他规则。 如果在未执行此步骤的情况下生成框架,则生成的 Objective-C 标头将为空,并且 Xamarin.iOS 将无法访问 Swift 框架成员。 通过创建新的 Swift 文件 SwiftFrameworkProxy.swift 并定义以下代码,公开基础 Gigya Swift SDK 的初始化逻辑:

    import Foundation
    import UIKit
    import Gigya
    
    @objc(SwiftFrameworkProxy)
    public class SwiftFrameworkProxy : NSObject {
    
        @objc
        public func initFor(apiKey: String) -> String {
            Gigya.sharedInstance().initFor(apiKey: apiKey)
            let gigyaDomain = Gigya.sharedInstance().config.apiDomain
            let result = "Gigya initialized with domain: \(gigyaDomain)"
            return result
        }
    }
    

    关于上述代码的一些重要说明:

    • 从原始第三方 Gigya SDK 导入 Gigya 模块后,即可访问框架的任何成员。
    • 使用指定名称的 @objc 属性标记 SwiftFrameworkProxy 类,否则将生成唯一不可读的名称,例如 _TtC19SwiftFrameworkProxy19SwiftFrameworkProxy。 应明确定义类型名称,因为它稍后将按名称使用。
    • NSObject 中继承代理类,否则不会在 Objective-C 头文件中生成它。
    • 将所有要公开的成员标记为 public
  10. 将方案生成配置从“调试”更改为“发布”。 为此,请打开“Xcode”>“目标”>“编辑方案”对话框,然后将“生成配置”选项设置为“发布”:

    xcode edit scheme

    xcode edit scheme release

  11. 此时,已准备好创建框架。 为模拟器和设备体系结构生成框架,然后将输出合并为单个二进制框架捆绑包 (.xcframework)。 使用以下命令执行生成:

    xcodebuild -project "Swift/SwiftFrameworkProxy/SwiftFrameworkProxy.xcodeproj" archive \
      -scheme "SwiftFrameworkProxy" \
      -configuration Release \
      -archivePath "build/SwiftFrameworkProxy-simulator.xcarchive" \
      -destination "generic/platform=iOS Simulator" \
      -derivedDataPath "build" \
      -IDECustomBuildProductsPath="" -IDECustomBuildIntermediatesPath="" \
      ENABLE_BITCODE=NO \
      SKIP_INSTALL=NO \
      BUILD_LIBRARY_FOR_DISTRIBUTION=YES
    
     xcodebuild -project "Swift/SwiftFrameworkProxy/SwiftFrameworkProxy.xcodeproj" archive \
       -scheme "SwiftFrameworkProxy" \
       -configuration Release \
       -archivePath "build/SwiftFrameworkProxy-ios.xcarchive" \
       -destination "generic/platform=iOS" \
       -derivedDataPath "build" \
       -IDECustomBuildProductsPath="" -IDECustomBuildIntermediatesPath="" \
       ENABLE_BITCODE=NO \
       SKIP_INSTALL=NO \
       BUILD_LIBRARY_FOR_DISTRIBUTION=YES
    

    提示

    如果你拥有工作区而不是项目,请生成工作区,并将目标指定为必需参数。 你还希望指定输出目录,因为对于工作区,此目录将不同于项目生成。

    提示

    你还可以使用帮助程序脚本为所有适用的体系结构生成框架,或者只需从目标选择器中的“Xcode 交换模拟器和设备”生成该框架。

  12. 生成的框架有两个存档,每个平台一个,将它们合并为一个二进制框架捆绑包,以便稍后将其嵌入到 Xamarin.iOS 绑定项目中。 若要创建合并了这两种体系结构的二进制框架捆绑包,需要执行以下步骤。 .xcarchive 包只是一个文件夹,因此你可以执行所有类型的操作,例如添加、删除和替换文件:

    • 使用之前在存档中生成的框架创建一个 xcframework

      xcodebuild -create-xcframework \
        	-framework "build/SwiftFrameworkProxy-simulator.xcarchive/Products/Library/Frameworks/SwiftFrameworkProxy.framework" \
        	-framework "build/SwiftFrameworkProxy-ios.xcarchive/Products/Library/Frameworks/SwiftFrameworkProxy.framework" \
        	-output "build/SwiftFrameworkProxy.xcframework"
      

    提示

    如果只想支持单个平台(例如,生成只能在一台设备上运行的应用),则可以跳过创建 .xcframework 库的步骤,并使用设备早期生成的输出框架。

    提示

    你还可以使用帮助程序脚本创建 .xcframework,该脚本可自动执行上述所有步骤。

准备元数据

此时,应该已准备好 .xcframework 和 Objective-C 生成的接口标头,它们可供 Xamarin.iOS 绑定使用。 下一步是准备 API 定义接口,绑定项目使用这些接口来生成 C# 类。 可以通过 Objective Sharpie 工具和生成的头文件来手动创建或自动创建这些定义。 使用 Sharpie 生成元数据:

  1. 从官方下载网站下载最新的 Objective Sharpie 工具,并按照向导安装该工具。 安装完成后,可以通过运行 sharpie 命令来验证它:

    sharpie -v
    
  2. 使用 sharpie 和自动生成的 Objective-C 头文件来生成元数据:

    sharpie bind --sdk=iphoneos16.4 --output="XamarinApiDef" --namespace="Binding" --scope="build/SwiftFrameworkProxy.xcframework/ios-arm64/SwiftFrameworkProxy.framework/Headers/" "build/SwiftFrameworkProxy.xcframework/ios-arm64/SwiftFrameworkProxy.framework/Headers/SwiftFrameworkProxy-Swift.h"
    

    输出反映生成的元数据文件:ApiDefinitions.cs。 保存此文件,以便在下一步中将其与本机引用一起包含到 Xamarin.iOS 绑定项目中:

    Parsing 1 header files...
    Binding...
        [write] ApiDefinitions.cs
    

    该工具将为每个公开的 Objective-C 成员生成 C# 元数据,其外观类似于以下代码。 正如你所看到的,它可以手动定义,因为它具有可读的格式和简单的成员映射:

    [Export ("initForApiKey:")]
    string InitForApiKey (string apiKey);
    

    提示

    如果更改了标头名称的默认 Xcode 设置,则头文件名可能有所不同。 默认情况下,它具有后缀为 -Swift 的的项目名称。 始终可以通过导航到框架包的标头文件夹来检查文件及其名称。

    提示

    在自动化过程中,可以使用帮助程序脚本在创建 .xcframework 后自动生成元数据。

生成绑定库

下一步是使用 Visual Studio 绑定模板创建 Xamarin.iOS 绑定项目,添加所需的元数据和本机引用,然后生成项目以生成可使用的库:

  1. 打开 Visual Studio for Mac 并创建新的 Xamarin.iOS 绑定库项目,为其指定一个名称,在本例中为“SwiftFrameworkProxy.Binding”并完成向导。 Xamarin.iOS 绑定模板位于以下路径:“iOS”>“库”>“绑定库”:

    visual studio create binding library

  2. 删除现有元数据文件 ApiDefinition.cs,因为它将被完全替换为 Objective Sharpie 工具生成的元数据。

  3. 复制 Sharpie 在前面的某个步骤中生成的元数据,在属性窗口中选择以下生成操作:ObjBindingApiDefinition(对于 ApiDefinitions.cs 文件)和 ObjBindingCoreSource(对于 StructsAndEnums.cs 文件):

    visual studio project structure metadata

    元数据本身使用 C# 语言描述每个公开的 Objective-C 类和成员。 可以看到原始 Objective-C 标头定义以及 C# 声明:

    // @interface SwiftFrameworkProxy : NSObject
    [BaseType (typeof(NSObject))]
    interface SwiftFrameworkProxy
    {
        // -(NSString * _Nonnull)initForApiKey:(NSString * _Nonnull)apiKey __attribute__((objc_method_family("none"))) __attribute__((warn_unused_result));
        [Export ("initForApiKey:")]
        string InitForApiKey (string apiKey);
    }
    

    即使它是有效的 C# 代码,也不会按原样使用它,而是由 Xamarin.iOS 工具用来基于此元数据定义生成 C# 类。 因此,与接口 SwiftFrameworkProxy 相反,你会获得具有相同名称的 C# 类,该类可由 Xamarin.iOS 代码实例化。 此类可获取由元数据定义的方法、属性和其他成员,它们将以 C# 方式调用。

  4. 添加对之前生成的二进制框架捆绑包以及该框架的每个依赖项的本机引用。 在本例中,请将 SwiftFrameworkProxy 和 Gigya 框架本机引用添加到绑定项目:

    • 若要添加本机框架引用,请打开查找器,然后导航到包含框架的文件夹。 将框架拖放到解决方案资源管理器的“本机引用”位置下。 或者,可以在“本机引用”文件夹中使用上下文菜单选项,然后单击“添加本机引用”以查找框架并添加它们:

    visual studio project structure native references

    • 更新每个本机引用的属性,并检查三个重要选项:

      • 设置智能链接 = true
      • 设置强制加载 = false
      • 设置用于创建原始框架的框架列表。 在本例中,每个框架只有两个依赖项:Foundation 和 UIKit。 将其设置为“框架”字段:

      visual studio nativeref proxy options

      如果要指定任何其他链接器标志,请在“链接器标志”字段中设置它们。 在本例中,请将其保留为空。

    • 根据需要指定其他链接器标志。 如果绑定的库仅公开 Objective-C API,但内部使用的是 Swift,则你可能会看到以下问题:

      error MT5209 : Native linking error : warning: Auto-Linking library not found for -lswiftCore
      error MT5209 : Native linking error : warning: Auto-Linking library not found for -lswiftQuartzCore
      error MT5209 : Native linking error : warning: Auto-Linking library not found for -lswiftCoreImage
      

      在本机库的绑定项目属性中,必须将以下值添加到链接器标志:

      L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator/ -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos -Wl,-rpath -Wl,@executable_path/Frameworks
      

      前两个选项(-L ... 选项)告知本机编译器在何处查找 swift 库。 本机编译器将忽略没有正确体系结构的库,这意味着可以同时传递模拟器库和设备库的位置,以便它适用于模拟器和设备生成(这些路径仅适用于 iOS;对于 tvOS 和 watchOS,它们必须更新)。 一个缺点是,这种方法要求正确的 Xcode 位于 /Application/Xcode.app 中,如果绑定库的使用者将 Xcode 放在不同的位置,则它不起作用。 另一种方法是在可执行项目的 iOS 生成选项 (--gcc_flags -L... -L...) 的附加 mtouch 参数中添加这些选项。 第三个选项使本机链接器将 swift 库的位置存储在可执行文件中,以便 OS 能够找到它们。

  5. 最后一个操作是生成库,并确保不会产生任何编译错误。 你经常会发现 Objective Sharpie 生成的绑定元数据会使用 [Verify] 属性进行批注。 这些属性指示应通过将绑定与原始 Objective-C 声明进行比较(将在绑定声明上方的注释中提供)来验证 Objective Sharpie 是否执行了正确的操作。 可以通过以下链接了解有关使用属性标记的成员的详细信息。 生成项目后,Xamarin.iOS 应用程序可以使用该项目。

使用绑定库

最后一步是在 Xamarin.iOS 应用程序中使用 Xamarin.iOS 绑定库。 创建新的 Xamarin.iOS 项目,添加对绑定库的引用,然后激活 Gigya Swift SDK:

  1. 创建 Xamarin.iOS 项目。 可以将“iOS”>“应用”>“单视图应用”用作起点:

    visual studio app new

  2. 将绑定项目引用添加到之前创建的目标项目或 .dll。 将绑定库视为常规 Xamarin.iOS 库:

    visual studio app refs

  3. 更新应用的源代码,并将初始化逻辑添加到主要 ViewController,这会激活 Gigya SDK

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
        var proxy = new SwiftFrameworkProxy();
        var result = proxy.InitForApiKey("APIKey");
        System.Diagnostics.Debug.WriteLine(result);
    }
    
  4. 创建名为 btnLogin 的按钮,并添加以下按钮单击处理程序以激活身份验证流:

    private void btnLogin_Tap(object sender, EventArgs e)
    {
        _proxy.LoginWithProvider(GigyaSocialProvidersProxy.Instagram, this, (result, error) =>
        {
            // process your login result here
        });
    }
    
  5. 运行应用,在调试输出中,你应该会看到以下行:Gigya initialized with domain: us1.gigya.com。 单击该按钮激活身份验证流:

    swift proxy result

祝贺你! 你已成功创建了 Xamarin.iOS 应用和使用 Swift 框架的绑定库。 上述应用程序将在 iOS 12.2+ 上成功运行,因为从此 iOS 版本开始,Apple 引入了 ABI 稳定性,并且从 12.2+ 开始的每个 iOS 都包含 Swift 运行时库,这些库可用于运行使用 Swift 5.1+ 编译的应用程序。 如果需要添加对早期 iOS 版本的支持,则还需要执行几个步骤:

  1. 若要添加对 iOS 12.1 及更早版本的支持,需要提供用于编译框架的特定 Swift dylibs。 使用 Xamarin.iOS.SwiftRuntimeSupport NuGet 包处理和复制 IPA 所需的库。 将 NuGet 引用添加到目标项目并重新生成应用程序。 无需执行任何进一步的步骤,NuGet 包将安装使用生成过程执行的特定任务,识别所需的 Swift 动态库,并使用最终 IPA 来打包它们。

  2. 若要将应用提交到应用商店,你需要使用 Xcode 和分发选项,该选项将更新 IPA 文件和 SwiftSupport 文件夹 dylibs,以便 AppStore 接受它:

    ○ 存档应用。 从 Visual Studio for Mac 菜单中,选择“生成”>“存档以供发布”:

    visual studio archive for publishing

    此操作将生成项目,并为组织者存档,Xcode 可以访问该项目以进行分发。

    ○ 通过 Xcode 进行分发。 打开 Xcode 并导航到“窗口”>“组织者”菜单选项:

    visual studio archives

    选择在上一步创建的存档,然后单击“分发应用”按钮。 按照向导将应用程序上传到 AppStore。

  3. 此步骤是可选的,但请务必验证你的应用是否可以在 iOS 12.1 及更早版本以及 12.2 上运行。 可以在 Test Cloud 和 UITest 框架的帮助下执行此操作。 创建一个 UITest 项目和一个基本 UI 测试,后者将运行应用:

    • 创建 UITest 项目并针对 Xamarin.iOS 应用程序配置它:

      visual studio uitest new

      提示

      可以通过以下链接找到有关如何创建 UITest 项目并为应用配置它的详细信息。

    • 创建基本测试以运行应用并使用一些 Swift SDK 功能。 此测试会激活应用,尝试登录,然后按“取消”按钮:

      [Test]
      public void HappyPath()
      {
          app.WaitForElement(StatusLabel);
          app.WaitForElement(LoginButton);
          app.Screenshot("App loaded.");
          Assert.AreEqual(app.Query(StatusLabel).FirstOrDefault().Text, "Gigya initialized with domain: us1.gigya.com");
      
          app.Tap(LoginButton);
          app.WaitForElement(GigyaWebView);
          app.Screenshot("Login activated.");
      
          app.Tap(CancelButton);
          app.WaitForElement(LoginButton);
          app.Screenshot("Login cancelled.");
      }
      

      提示

      通过以下链接详细了解 UITests 框架和 UI 自动化。

    • 在应用中心创建 iOS 应用,使用设置为运行测试的新设备创建新的测试运行:

      visual studio app center new

      提示

      通过以下链接了解有关 AppCenter Test Cloud 的详细信息。

    • 安装 appcenter CLI

      npm install -g appcenter-cli
      

      重要

      确保已安装节点 v6.3 或更高版本

    • 使用以下命令运行测试。 此外,请确保 appcenter 命令行当前已登录。

      appcenter test run uitest --app "Mobile-Customer-Advisory-Team/SwiftBinding.iOS" --devices a7e7cb50 --app-path "Xamarin.SingleView.ipa" --test-series "master" --locale "en_US" --build-dir "Xamarin/Xamarin.SingleView.UITests/bin/Debug/"
      
    • 验证结果。 在 AppCenter 门户中,导航到“应用”>“测试”>“测试运行”:

      visual studio appcenter uitest result

      然后选择所需的测试运行并验证结果:

      visual studio appcenter uitest runs

你已经开发了一个基本 Xamarin.iOS 应用程序,该应用程序通过 Xamarin.iOS 绑定库使用本机 Swift 框架。 此示例提供了一种简单的方式来使用所选框架,在实际应用程序中,你需要公开更多的 API 并为这些 API 准备元数据。 用于生成元数据的脚本将简化对框架 API 的未来更改。