找回密码
 立即注册
首页 业界区 业界 【译】 如何使用 .NET MAUI 构建 iOS 小部件 ...

【译】 如何使用 .NET MAUI 构建 iOS 小部件

剩鹄逅 昨天 00:40
原文 | Toine de Boer
翻译 | 郑子铭
这是Toine de Boer的客座博文。
我是一名 .NET 开发人员,主要专注于 .NET MAUI 到 ASP.NET 后端服务的开发。由于最近我大量使用 Widgets,并在初期阶段遇到了许多障碍和非常有限的文档,因此我决定撰写本文,以证明使用 .NET MAUI 构建完整的 Widgets 是完全可行的。而且,这种方法可以像使用原生开发环境一样专业,无需担心每次构建或更新都会导致所有功能失效。
本文并非实战教程;相反,它按顺序介绍了构建 iOS 小组件时遇到的最大障碍及其最关键的部分。建议您具备 .NET MAUI 或 Xamarin 的相关经验,并且需要 macOS 系统,因为在没有 macOS 的情况下无法创建 iOS 小组件。您可以根据需要选择阅读部分内容,但我建议您从头到尾阅读,否则可能会错过一些导致小组件无法正常工作的细节。本文从创建一个简单的静态小组件开始,最终介绍一个用于构建完全交互式小组件的基本系统。
为了帮助您快速入门,我创建了一个功能齐全的交互式小部件,您可以在 GitHub 上找到它  Maui.WidgetExample
1.webp

笔记
iOS 小部件是与宿主应用程序链接的独立应用程序。为简便起见,我通常将 .NET MAUI 应用程序称为“应用程序”,将小部件应用程序称为“小部件”。
先决条件

开始之前,我们需要从苹果开发者控制台获取一些信息。除了现有应用的 Bundle ID 之外,您还需要 Widget 的 Bundle ID。如果您的应用使用 Widget,则com.enbyin.WidgetExample通常会在 Bundle ID 后附加一些 Widget 的前缀,例如  com.enbyin.WidgetExample.WidgetExtension。此外,这两个 Bundle ID 都需要启用 App Groups 功能,并关联到一个专用的 Group。您可以通过在应用 Bundle ID 前加上  来创建 Group ID group,例如   group.enbyin.WidgetExample。
需要从 Apple Developer Console 收集哪些信息:

  • 应用包 ID(例如com.enbyin.WidgetExample)
  • 小部件(应用)包 ID(例如com.enbyin.WidgetExample.WidgetExtension)
  • 组 ID(例如group.enbyin.WidgetExample)
为了演示,我创建了一个默认的 .NET MAUI 应用,目标平台仅为 iOS 和 Android。我将 iOS 目标平台设置为新创建的 Bundle ID com.enbyin.WidgetExample。我还添加了一个非常醒目的应用图标,以便我们能够轻松观察 Widget 屏幕上是否使用了正确的图标。
创建小部件项目

让我们从我作为 .NET 开发者的最大挑战——使用 Xcode 和 Swift 开始说起。在 Xcode 中创建项目之后,我建议切换到 VS Code 并搭配 Copilot,以便快速迭代。这样,你就能快速搭建一个符合苹果规范的小型应用程序。
我首先在 Xcode 中使用 App 模板,用 Swift 代码创建一个应用项目。这个项目作为基础项目,我将实际的 Widget 扩展附加到该项目上,并且我还可以选择性地用它来进行一些简单的测试。我给它分配了与 .NET MAUI 应用相同的 Bundle ID;重用 Bundle ID 不会有问题,因为这个几乎是空的 Xcode 应用永远不会发布。
2.webp

应用项目创建完成后,接下来创建 Widget 扩展。在 Xcode 中,依次点击“文件”>“新建”>“目标”,然后选择Widget Extension模板。选择一个已包含正确 Bundle ID 的名称,以避免后续编辑。为了简化示例数据的生成,请选择相应选项Include Configuration App Intent;这样可以立即生成一个可用的 Widget。
3.webp

创建所有项目后,我总是会统一设置所需的 iOS 版本,确保所有 Xcode 目标使用的版本相同。要检查这一点,只需点击解决方案名称,即可在主窗口中打开解决方案设置,然后在“通用”选项卡下的“最低部署 iOS 版本”中进行设置。现在,使用 Xcode 中的“产品”>“构建”在设备上进行试运行。
小部件内部的对象和流程

在 Xcode 中创建 Widget 项目会生成大量对象。一开始难免会感到不知所措,尤其因为几乎所有内容都放在同一个文件中。因此,我总是先进行重构:将每个对象移到各自的文件中,并添加一些文件夹结构。这样做不会造成任何性能损失,因为 Swift 实际上并不使用命名空间;无论文件夹结构如何,项目中的所有内容实际上都属于同一个命名空间。
重构之后,流程其实很简单。以下是主要对象、函数及其作用:

  • WidgetBundle:Widget 扩展的入口点,您可以在此处向最终用户公开一个或多个小部件。
  • 小部件:特定小部件的配置,此处列出了所有信息,例如视图、提供程序、配置意图和支持的尺寸。
  • AppIntentTimelineProvider:提供构建视图所需的数据模型,可以提供多个模型,这些模型会根据时间线发布。

    • func placeholder:在小部件加载时提供一个最小的数据模型(几乎从不可见)。
    • func snapshot:提供小部件在图库中作为预览显示以及首次添加到屏幕时的数据模型。
    • func timeline:提供一个用于常规用途的单一数据模型(或集合),这是该组件所有数据模型的主要来源。

  • TimelineEntry:数据模型实例
  • 视图:小部件的视觉元素
  • WidgetConfigurationIntent:允许最终用户配置小部件。在timeline()AppIntentTimelineProvider 中,您将收到这些设置,以便在需要时将其处理到数据模型中。
在内存中管理模型或其他任何数据(例如缓存系统或简单的静态字段)意义不大。iOS Widget 是一个静态对象,生命周期很短,执行的操作也很小。在 AppIntentTimelineProvider 中,函数几乎同时被调用,但实际上是作为独立的进程运行的。对于数据的交换和存储,最好使用某种形式的本地存储(稍后会介绍)。
应用图标

之前我遇到过小部件在不同视图下显示图标错误的问题。自从我明确地将 AppIcon 图片添加到小部件扩展的 Assets 文件夹中,并在其 info.plist 文件中引用它们之后,问题就几乎没有出现了。如果在更新 Assets 和 info.plist 文件后图标仍然显示错误,请重启测试设备,因为 iOS 似乎会对小部件的图标进行某种缓存。
在 Xcode 中,AppIcon 资源已在 Widget 项目中预定义。打开 AppIcon 资源页面,然后打开属性检查器(右上角),您可以选择 iOS 的“所有尺寸”。这样您就可以设置所有图像尺寸。我个人觉得手动操作太麻烦,所以我使用在线 iOS 图标生成器,它可以生成所有格式的图标,然后我直接将它们复制到文件Assets.xcassets/AppIcon.appiconset夹中。
要调整 plist 设置,请Info.plist在 Xcode 之外(例如在 VS Code 中)打开小部件扩展,并将以下条目插入到相应NSExtension部分中:
  1. <key>NSExtensionPrincipalClass</key>
  2. <string>MyWidgetExtension.MyWidgetBundle</string>
  3. <key>CFBundleIcons</key>
  4. <dict>
  5. <key>CFBundlePrimaryIcon</key>
  6. <dict>
  7.   <key>CFBundleIconFiles</key>
  8.   
  9.    <string>AppIcon</string>
  10.   </array>
  11.   <key>UIPrerenderedIcon</key>
  12.   <false/>
  13. </dict>
  14. </dict>
  15. <key>CFBundleIconName</key>
  16. <string>AppIcon</string>
复制代码
请使用以下格式调整 NSExtensionPrincipalClass:
  1. <key>NSExtensionPrincipalClass</key>
  2. <string>{YourWidgetModuleName}.{YourWidgetName}</string>
复制代码
创建小部件的发布版本

在 Xcode 中创建发布版本很容易,但找到合适的设置对我来说却很麻烦。因此,我使用一个标准脚本,它可以更轻松地将发布版本收集到一个专用文件夹中,该文件夹也可以用于构建管道。我从 Xcode 项目的根目录运行此脚本,发布版本就会被保存到一个XReleases文件夹中,之所以使用 X 是为了防止它们被 Visual Studio 默认的排除项排除.gitignore。
  1. rm -Rf XReleases
  2. xcodebuild -project XCodeWidgetExample.xcodeproj \
  3. -scheme "MyWidgetExtension" \
  4. -configuration Release \
  5. -sdk iphoneos \
  6. BUILD_DIR=$(PWD)/XReleases clean build
  7. xcodebuild -project XCodeWidgetExample.xcodeproj \
  8. -scheme "MyWidgetExtension" \
  9. -configuration Release \
  10. -sdk iphonesimulator \
  11. BUILD_DIR=$(PWD)/XReleases clean build
复制代码
将 Widget 版本添加到 MAUI 应用

小部件构建输出是一个.appexmacOS 的神奇捆绑文件夹,类似于.app.js 文件。之前在 Windows 系统上使用 Visual Studio 时,我经常遇到找不到 appex 文件的构建错误。为了避免这种情况,我现在将发布输出放在 .js 目录下Platforms/iOS/,并使用 .js 文件包含它们CopyToOutput。
请在代码中使用以下代码片段.csproj来获取构建所需的文件:
  1. <ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  2.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  3.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  4.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  5. </ItemGroup>
复制代码
现在将 Widget 扩展添加到 .NET MAUI 应用程序项目中。下面的 ItemGroup 确保在构建过程中完成此操作,请注意路径和文件名,因为要求非常严格。
  1. <ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  2.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  3.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  4.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  5. </ItemGroup>        MyWidgetExtension<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  6.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  7.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  8.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  9. </ItemGroup>  Release-iphoneos      Release-iphonesimulator   
复制代码
此时,该组件应该可以在您的 .NET MAUI 应用构建中看到。目前,它是一个完全独立运行的组件,无需与您的 .NET MAUI 应用进行任何数据或通信。
笔记
当您从 Visual Studio 为“iOS 本地设备”构建时,小部件扩展很可能不可见。
应用与小部件之间的数据共享

iOS 小部件最好被视为独立应用。.NET MAUI 应用和小部件无法自由交换数据或进行通信。我们可以使用 .NET MAUI Preferences 来实现数据交换,它对应于 iOS 上的 UserDefaults。为了确保它们使用相同的数据源,两个项目都需要指定Entitlements.plist相同的 Group ID,该 ID 是我们之前在使用 App Groups 功能设置 Bundle ID 时创建的。
Entitlements.plist以组 ID为例group.com.enbyin.WidgetExample:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  3. <plist version="1.0">
  4.   <dict>
  5.   <key>com.apple.security.application-groups</key>
  6.   
  7.   <string>group.com.enbyin.WidgetExample</string>
  8.   </array>
  9.   </dict>
  10. </plist>
复制代码
为明确起见:Widget Xcode 项目和 .NET MAUI 项目都必须使用这些授权,并且在添加授权后不要忘记为 Xcode 项目创建一个新的版本。此外,Widget Xcode 项目的授权也必须在 .NET MAUI 项目的  元素中引用,AdditionalAppExtensions以.csproj用于 .NET MAUI 构建。
  1. <ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  2.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  3.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  4.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  5. </ItemGroup>        MyWidgetExtension<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  6.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  7.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  8.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  9. </ItemGroup>  Release-iphoneos      Release-iphonesimulator<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  10.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  11.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  12.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  13. </ItemGroup><ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
  14.    <Content Remove="Platforms\iOS\WidgetExtensions\**" />
  15.    <Content Condition="'$(ComputedPlatform)' == 'iPhone'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphoneos\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  16.    <Content Condition="'$(ComputedPlatform)' == 'iPhoneSimulator'" Include=".\Platforms\iOS\WidgetExtensions\Release-iphonesimulator\MyWidgetExtension.appex\**" CopyToOutputDirectory="PreserveNewest" />
  17. </ItemGroup>    Platforms/iOS/Entitlements.MyWidgetExtension.plist   
复制代码
此时,应用程序和控件应该能够使用相同的数据源。在两个项目中,都必须在代码中明确指定使用特定的组 ID。在 .NET MAUI 中,请勿使用分号Preferences.Default(
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册