{"id":59146,"date":"2025-12-15T10:05:00","date_gmt":"2025-12-15T18:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=59146"},"modified":"2026-01-08T16:09:24","modified_gmt":"2026-01-09T00:09:24","slug":"how-to-build-ios-widgets-with-dotnet-maui","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/how-to-build-ios-widgets-with-dotnet-maui\/","title":{"rendered":"How to Build iOS Widgets with .NET MAUI"},"content":{"rendered":"<blockquote>\n<p>This is a guest blog from <a href=\"https:\/\/www.linkedin.com\/in\/toinedeboer\/\">Toine de Boer<\/a>.<\/p>\n<\/blockquote>\n<p>I&#8217;m a .NET developer primarily focused on .NET MAUI to ASP.NET backend services. Because I recently have worked a lot with Widgets and encountered many obstacles and very limited documentation in the initial phase, I decided to write this article to show that it is absolutely possible to build complete Widgets with .NET MAUI. And to do so in a professional way comparable to using the native development environment, without having to fear that everything might break with every new build or update.<\/p>\n<p>This isn&#8217;t a hands-on tutorial; instead, these are the biggest and most important parts in order of how to tackle the biggest obstacles when building an iOS widget. It&#8217;s advisable to have some experience with .NET MAUI or Xamarin, and access to macOS is required, as creating an iOS Widget without macOS unfortunately isn&#8217;t possible. You can cherry\u2011pick what you think you need, but I recommend reading from start to finish or you may miss small details that keep the Widget from working. The text starts with creating a simple static widget, and ends with a basic system for a fully interactive widget.<\/p>\n<p>To help you get started quickly, I have created a fully functional interactive widget, which is available on\n<a href=\"https:\/\/github.com\/Toine-db\/Maui.WidgetExample\">github &gt; Maui.WidgetExample<\/a><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/iphone-widgetexample.webp\" alt=\"Screenshot of an example widget\" \/><\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>iOS Widgets are standalone apps that are linked to a host app. For simplicity I mostly refer to the .NET MAUI app as the &#8216;app&#8217; and the Widget app as the &#8216;Widget&#8217;.<\/div><\/p>\n<h2>Prerequisites<\/h2>\n<p>Before we begin we need a few things from Apple&#8217;s developer console. Besides the Bundle ID of your existing app you also need a Bundle ID for your Widget. If your app uses <code>com.enbyin.WidgetExample<\/code> then conventionally you append something for a Widget such as <code>com.enbyin.WidgetExample.WidgetExtension<\/code>. Additionally, both Bundle IDs need the App Groups capability with a dedicated group. Create a Group ID by prefixing the app Bundle ID with <code>group<\/code>, for example <code>group.enbyin.WidgetExample<\/code>.<\/p>\n<p>What to collect from Apple Developer Console:<\/p>\n<ul>\n<li>App Bundle ID (e.g. <code>com.enbyin.WidgetExample<\/code>)<\/li>\n<li>Widget (App) Bundle ID (e.g. <code>com.enbyin.WidgetExample.WidgetExtension<\/code>)<\/li>\n<li>Group ID (e.g. <code>group.enbyin.WidgetExample<\/code>)<\/li>\n<\/ul>\n<p>For demo purposes I created a default .NET MAUI app targeting only iOS and Android. I set the iOS target to the newly created Bundle ID <code>com.enbyin.WidgetExample<\/code>. I also added a very noticeable app icon so we can easily observe if the correct icon has been used on the Widget screens.<\/p>\n<h2>Creating the Widget project<\/h2>\n<p>Let&#8217;s begin with the biggest step for me as a .NET developer: working on Xcode and Swift. After creating the projects in Xcode, I can recommend to switch to VS Code and pair with Copilot to iterate quickly. You can have a solid small app set up quickly while following Apple&#8217;s conventions.<\/p>\n<p>I begin in Xcode by creating an app project using the App template using Swift coding. This serves as the base project to which I attach the actual Widget extension, and optionally, I can use it for some light testing. I give it the same Bundle ID as the .NET MAUI app has; reusing a Bundle ID is not a problem because this almost empty Xcode app will never ship.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/xcode-new-app-project.webp\" alt=\"Creating a new Xcode project for apps\" \/><\/p>\n<p>With the app project in place, create the Widget extension next. In Xcode go to File &gt; New &gt; Target and choose the <code>Widget Extension<\/code> template. Pick a name that already uses the correct Bundle ID for the widget so you avoid later edits. To simplify generating sample data select the <code>Include Configuration App Intent<\/code> option; this gives a working widget immediately.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/xcode-new-widget-project.webp\" alt=\"Creating a new Xcode widget project\" \/><\/p>\n<p>When all projects are created I always align the desired iOS version, make sure it is the same for all Xcode targets. To check this just tap the solution name to open the solution settings in the main window, then set for all targets on tab &#8216;General&#8217; under &#8216;Minimum Deployments iOS version&#8217;. Now give it a trial run on a device using Product &gt; Build in Xcode.<\/p>\n<h3>Objects and flows inside the Widget<\/h3>\n<p>Creating the Widget project in XCode generates many objects. Understandably it is overwhelming at first, especially because almost everything is placed into one file. Therefore I always start by refactoring: moving every object into its own file and add some folder structure. You can do this without penalty because Swift does not really use namespaces; everything inside this project effectively falls under the same namespace regardless of folder structure.<\/p>\n<p>After refactoring the flow is actually straightforward. Here are the main objects, functions and their roles:<\/p>\n<ul>\n<li><strong>WidgetBundle<\/strong>: the entry point of the Widget extension, here you can expose one or more widgets to the end user<\/li>\n<li><strong>Widget<\/strong>: the configuration of a specific widget, here everything is listed such as the View, Provider, ConfigurationIntent and the supported sizes<\/li>\n<li><strong>AppIntentTimelineProvider<\/strong>: provides the data models to build the view, multiple models can be provided which are published according to a timeline.\n<ul>\n<li><strong>func placeholder<\/strong>: provides a minimal data model while the widget is loading (almost never visible)<\/li>\n<li><strong>func snapshot<\/strong>: provides a data model for when the widget is shown in the gallery as a preview and when first added to the screen<\/li>\n<li><strong>func timeline<\/strong>: provides a single data model (or a collection) for normal use, this is the main source where all data models for the widget come from<\/li>\n<\/ul>\n<\/li>\n<li><strong>TimelineEntry<\/strong>: the data model instance<\/li>\n<li><strong>View<\/strong>: the visual elements of the widget<\/li>\n<li><strong>WidgetConfigurationIntent<\/strong>: enables an end-user to configure the widget, in <code>timeline()<\/code> of the AppIntentTimelineProvider you receive these settings so they can be processed into the data model when needed<\/li>\n<\/ul>\n<p>Managing models or any other data in memory, like cache systems or just simple static fields, makes little sense. An iOS Widget is a static object that lives very briefly to perform very small action. In the AppIntentTimelineProvider the functions are invoked almost at the same time but they really run as distinct processes. For exchanging and storing data it is best to use some form of local storage (covered later).<\/p>\n<h3>App Icons<\/h3>\n<p>Previously I had recurring problems with the widget showing wrong icons on different views. Since I explicitly added the AppIcon images to Assets in the Widget extension and referencing them in its info.plist I had almost no problems. If icons are still wrong after updating the assets and info.plist, reboot your test device because iOS seems to do some kind of icon caching with Widgets.<\/p>\n<p>In Xcode the AppIcon Assets are predefined in the Widget project. When you open the AppIcon Assets page and then open the Attributes Inspector (top right) you can select iOS &#8216;All Sizes&#8217;. This gives you the ability to set all image sizes. Personally I find that too much manual work, so I use an online iOS icon generator that produces all formats and copy them straight into the <code>Assets.xcassets\/AppIcon.appiconset<\/code> folder.<\/p>\n<p>To adjust the plist settings, open the widget extension&#8217;s <code>Info.plist<\/code> outside of Xcode (e.g. in VS Code) and insert the entries below inside the <code>NSExtension<\/code> section:<\/p>\n<pre><code class=\"language-xml\">&lt;key&gt;NSExtensionPrincipalClass&lt;\/key&gt; \n&lt;string&gt;MyWidgetExtension.MyWidgetBundle&lt;\/string&gt;\n&lt;key&gt;CFBundleIcons&lt;\/key&gt;\n&lt;dict&gt;\n &lt;key&gt;CFBundlePrimaryIcon&lt;\/key&gt;\n &lt;dict&gt;\n  &lt;key&gt;CFBundleIconFiles&lt;\/key&gt;\n  &lt;array&gt;\n   &lt;string&gt;AppIcon&lt;\/string&gt;\n  &lt;\/array&gt;\n  &lt;key&gt;UIPrerenderedIcon&lt;\/key&gt;\n  &lt;false\/&gt;\n &lt;\/dict&gt;\n&lt;\/dict&gt;\n&lt;key&gt;CFBundleIconName&lt;\/key&gt;\n&lt;string&gt;AppIcon&lt;\/string&gt;<\/code><\/pre>\n<p>Adjust NSExtensionPrincipalClass using the following format:<\/p>\n<pre><code class=\"language-xml\">&lt;key&gt;NSExtensionPrincipalClass&lt;\/key&gt;\n&lt;string&gt;{YourWidgetModuleName}.{YourWidgetName}&lt;\/string&gt;\n\n&lt;!-- YourWidgetModuleName can be found in: Extension &gt; Build Settings &gt; Product Module Name --&gt;\n\n&lt;!-- YourWidgetName is the name of the Widget bundle, like \u2018MyWidgetsBundle\u2019 in: --&gt;\n&lt;!-- @main --&gt;\n&lt;!-- struct MyWidgetsBundle: WidgetBundle { --&gt;<\/code><\/pre>\n<h3>Creating a release build of the Widget<\/h3>\n<p>Release builds are easy to make in Xcode, but finding the right settings can be a hassle for me. Therefore, I use a standard script that makes it much easier to collect the releases into a dedicated folder, which can also be used in build pipelines. I run this script from the root of the Xcode projects and the releases go to an <code>XReleases<\/code> folder, the X to prevent them from being excluded by the default Visual Studio <code>.gitignore<\/code>.<\/p>\n<pre><code class=\"language-console\">rm -Rf XReleases\n\nxcodebuild -project XCodeWidgetExample.xcodeproj \\\n-scheme \"MyWidgetExtension\" \\\n-configuration Release \\\n-sdk iphoneos \\\nBUILD_DIR=$(PWD)\/XReleases clean build\n\nxcodebuild -project XCodeWidgetExample.xcodeproj \\\n-scheme \"MyWidgetExtension\" \\\n-configuration Release \\\n-sdk iphonesimulator \\\nBUILD_DIR=$(PWD)\/XReleases clean build<\/code><\/pre>\n<h3>Adding the Widget release to the MAUI app<\/h3>\n<p>The widget build output is an <code>.appex<\/code> (a magic macOS bundle folder, similar to an <code>.app<\/code>). On Windows with Visual Studio I previously had much build errors where the appex couldn&#8217;t be found. To avoid this I now place the release outputs under <code>Platforms\/iOS\/<\/code> and include them with <code>CopyToOutput<\/code>.<\/p>\n<p>Use the snippet below in your <code>.csproj<\/code> to get the files available for the build:<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup Condition=\"$(TargetFramework.Contains('-ios'))\"&gt; \n   &lt;Content Remove=\"Platforms\\iOS\\WidgetExtensions\\**\" \/&gt;\n   &lt;Content Condition=\"'$(ComputedPlatform)' == 'iPhone'\" Include=\".\\Platforms\\iOS\\WidgetExtensions\\Release-iphoneos\\MyWidgetExtension.appex\\**\" CopyToOutputDirectory=\"PreserveNewest\" \/&gt;\n   &lt;Content Condition=\"'$(ComputedPlatform)' == 'iPhoneSimulator'\" Include=\".\\Platforms\\iOS\\WidgetExtensions\\Release-iphonesimulator\\MyWidgetExtension.appex\\**\" CopyToOutputDirectory=\"PreserveNewest\" \/&gt;\n&lt;\/ItemGroup&gt;<\/code><\/pre>\n<p>Now add the Widget extension to the .NET MAUI app project. The ItemGroup below ensures that this is done during the build, pay attention to the paths and filenames because this is very strict.<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup Condition=\"$(TargetFramework.Contains('-ios'))\"&gt;\n   &lt;!-- the appex folder path without the platform suffix --&gt;\n   &lt;AdditionalAppExtensions Include=\"$(MSBuildProjectDirectory)\/Platforms\/iOS\/WidgetExtensions\"&gt;\n      &lt;!-- the appex file without the .appex suffix --&gt;\n      &lt;Name&gt;MyWidgetExtension&lt;\/Name&gt;\n      &lt;!-- the appex folder platform suffixes --&gt;\n      &lt;BuildOutput Condition=\"'$(ComputedPlatform)' == 'iPhone'\"&gt;Release-iphoneos&lt;\/BuildOutput&gt;\n      &lt;BuildOutput Condition=\"'$(ComputedPlatform)' == 'iPhoneSimulator'\"&gt;Release-iphonesimulator&lt;\/BuildOutput&gt;\n   &lt;\/AdditionalAppExtensions&gt;\n&lt;\/ItemGroup&gt;<\/code><\/pre>\n<p>At this point the Widget should be visible in your .NET MAUI app build. Right now it is a Widget that works entirely on its own, without data or communications from your .NET MAUI app.<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>Widget extensions will most likely not be visible when you build from Visual Studio for &#8216;iOS Local Devices&#8217;.<\/div><\/p>\n<h2>Data sharing between App and Widget<\/h2>\n<p>iOS Widgets can best be treated as standalone apps. The .NET MAUI app and the Widget can not freely exchange data or communicate. For data exchange we can use .NET MAUI Preferences which maps to UserDefaults on iOS. To ensure they use the same source, both projects need an <code>Entitlements.plist<\/code> specifying the same Group ID which we created earlier when setting up the Bundle ID with App Groups capability.<\/p>\n<p>An example <code>Entitlements.plist<\/code> with group id <code>group.com.enbyin.WidgetExample<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n&lt;!DOCTYPE plist PUBLIC \"-\/\/Apple\/\/DTD PLIST1.0\/\/EN\" \"http:\/\/www.apple.com\/DTDs\/PropertyList-1.0.dtd\"&gt;\n&lt;plist version=\"1.0\"&gt;\n  &lt;dict&gt;\n  &lt;key&gt;com.apple.security.application-groups&lt;\/key&gt;\n  &lt;array&gt;\n  &lt;string&gt;group.com.enbyin.WidgetExample&lt;\/string&gt;\n  &lt;\/array&gt;\n  &lt;\/dict&gt;\n&lt;\/plist&gt;<\/code><\/pre>\n<p>For clarity: both the Widget Xcode project and the .NET MAUI project must use such entitlements, and do not forget to create a new release of the Xcode project after adding the entitlements. Additionally the entitlements of the Widget Xcode project must also be referenced in the .NET MAUI project under the <code>AdditionalAppExtensions<\/code> element in the <code>.csproj<\/code> for the .NET MAUI build.<\/p>\n<pre><code class=\"language-xml\">&lt;ItemGroup Condition=\"$(TargetFramework.Contains('-ios'))\"&gt;\n   &lt;!-- the appex folder path without the platform suffix --&gt;\n   &lt;AdditionalAppExtensions Include=\"$(MSBuildProjectDirectory)\/Platforms\/iOS\/WidgetExtensions\"&gt;\n      &lt;!-- the appex file without the .appex suffix --&gt;\n      &lt;Name&gt;MyWidgetExtension&lt;\/Name&gt;\n      &lt;!-- the appex folder platform suffixes --&gt;\n      &lt;BuildOutput Condition=\"'$(ComputedPlatform)' == 'iPhone'\"&gt;Release-iphoneos&lt;\/BuildOutput&gt;\n      &lt;BuildOutput Condition=\"'$(ComputedPlatform)' == 'iPhoneSimulator'\"&gt;Release-iphonesimulator&lt;\/BuildOutput&gt;\n\n      &lt;!-- entitlements for the appex, without this the shared storage won't work --&gt;\n      &lt;!-- errors that entitlements could not be found: include the entitlements with CopyToOutput --&gt;\n      &lt;!-- errors when reading entitlements during build: store entitlements file with line-ending type LF --&gt;\n      &lt;CodesignEntitlements&gt;Platforms\/iOS\/Entitlements.MyWidgetExtension.plist&lt;\/CodesignEntitlements&gt;\n   &lt;\/AdditionalAppExtensions&gt;\n&lt;\/ItemGroup&gt; <\/code><\/pre>\n<p>At this point the App and Widget should be able to use the same data source. In both projects the usage of a specific Group ID must be indicated explicitly in code. In .NET MAUI do NOT use <code>Preferences.Default<\/code>; instead provide the Group ID in the <code>sharedName<\/code> parameter.<\/p>\n<pre><code class=\"language-csharp\">\/\/ example how to store data in .NET MAUI.\nPreferences.Set(\"MyDataKey\", \"my data to share\", \"group.com.enbyin.WidgetExample\");<\/code><\/pre>\n<pre><code class=\"language-swift\">\/\/ example how to store data in Swift.\nUserDefaults(suiteName: \"group.com.enbyin.WidgetExample\")?.set(\"my data to share\", forKey: \"MyDataKey\")\n\/\/ example how to get data in Swift.\nlet data = UserDefaults(suiteName: \"group.com.enbyin.WidgetExample\")?.string(forKey: \"MyDataKey\")<\/code><\/pre>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>Storage keys are case-sensitive; I advise to keep keys simple and optionally consistently lowercase to avoid problems.<\/div><\/p>\n<h2>Communication from App to Widget<\/h2>\n<p>The Widget does not know when the App shares data and the App does not know when the Widget does so. Signaling new available data from the App to the Widget uses a different mechanism than from Widget to App. Signaling from App to Widget is easy with Apple&#8217;s WidgetKit API. This API is not available in .NET MAUI so you must create a binding yourself. It is a very small API and great to experiment with bindings yourself. For this demo I use a NuGet package <code>WidgetKit.WidgetCenterProxy<\/code> where this has already been done for us.<\/p>\n<p>The WidgetKit API mainly provides two options: reload all widgets on the device or reload only widgets of a specific <code>kind<\/code>. I always use the latter because the platform will ignore you if you use one of these options too frequently; I imagine they also prefer that you only update your own specific widgets. The <code>kind<\/code> of your Widget is easy to find in your Widget object in Swift; under the <code>kind<\/code> property.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Example on how to refresh all Widgets of kind \u2018MyWidget\u2019 in .NET MAUI\nvar widgetCenterProxy = new WidgetKit.WidgetCenterProxy();\nwidgetCenterProxy.ReloadTimeLinesOfKind(\"MyWidget\");<\/code><\/pre>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>WidgetKit reload functions are a polite request to the OS; it decides when it happens and whether you are using it too often. Usually the widget refresh happens immediately.<\/div><\/p>\n<h2>Communication from Widget to App<\/h2>\n<p>Communication from Widget to App can happen in two ways, optionally with a very small amount of data. By default a Widget opens the corresponding app when you tap it, if you overwrite this with <code>widgetUrl()<\/code> you can open the app with a Deep Link that includes data in the url. A drawback is that a Widget is build as a static object, when using <code>widgetUrl<\/code> the URL must be determined beforehand when setting up the Widget view (often happens in the provider) and passed as string through the data model.<\/p>\n<pre><code class=\"language-swift\">\/\/ example of using a DeepLink URL in Swift\nstruct MyView: View {\n   var body: some View {\n      \/\/ my views\n   }.widgetURL(URL(string: \"mywidget:\/\/something?var1=dummy-data\"))\n}<\/code><\/pre>\n<p>A different way to communicate can originate from AppIntents. An AppIntent is a way to execute actions\/logic that you can attach to interactive elements like buttons. It is also the place where the OS gives you a bit of time to perform long-running actions, like for making http calls. When you combine local storage with AppIntents you can create a full &#8216;Interactive Widgets&#8217;, as shown in the rendering cycle image below.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2026\/01\/ios-widget-render-loop.webp\" alt=\"Widget rendering cycle in iOS\" \/><\/p>\n<p>For example you can attach a custom AppIntent to a button in your Widget that changes a value in storage, after which the AppIntent itself triggers a refresh of the Widget. The system reloads the widget by calling AppIntentTimelineProvider, creating a new data model (timeline entry) and re-rendering the view with the updated data.<\/p>\n<pre><code class=\"language-swift\">\/\/ example of an AppIntent changing data and reloading widget\nstruct IncrementCounterIntent: AppIntent {\n   static var title: LocalizedStringResource { \"Increment Counter\" }\n   static var description: IntentDescription { \"Increments the counter by 1\" }\n\n   func perform() async throws -&gt; some IntentResult {\n\n   var currentCount = 0\n\n   let userDefaults = UserDefaults(suiteName: Settings.groupId)\n   let storedValue = userDefaults?.integer(forKey: Settings.appIncommingDataKey)\n   if let storedValueCount = storedValue {\n      currentCount = storedValueCount\n   }\n\n   \/\/ do action\n   let newCount = currentCount + 1\n\n   \/\/ Save new value\n   userDefaults?.set(newCount, forKey: Settings.appIncommingDataKey)\n\n   \/\/ Reload timelines &gt; refreshing widget\n   WidgetCenter.shared.reloadTimelines(ofKind: \"MyWidget\")\n\n   return .result()\n}<\/code><\/pre>\n<pre><code class=\"language-swift\">\/\/ example of Button using AppIntent in Swift\nstruct MyWidgetView : View {\n   var entry: Provider.Entry\n\n   var body: some View {\n      VStack(spacing:4) {\n         Button(intent: IncrementCounterIntent()) {\n            Text(\"+\")\n         }\n      }\n      .padding()\n   }\n}<\/code><\/pre>\n<h3>Silent communication from Widget to App<\/h3>\n<p>On iOS, a Widget can NOT communicate with the App in the background. Any direct call brings the App to the foreground. To keep the App closed and still perform work, you can use AppIntents in the Widget to call your backend. The backend can execute the action and, if needed, send a silent push notification to the App. The App can then handle the update in the background if required. This can be done with any web service and existing push notification provider; For that reason, I&#8217;ve included only an illustrative SilentNotificationService as entry point in the demo code rather than a full implementation.<\/p>\n<h2>Create a configurable Widget<\/h2>\n<p>Because I selected the &#8216;Configuration App Intent&#8217; option when creating the Xcode Widget extension, there is already a working system in place that allows the user to configure the widget. In the loop of creating the data model in <code>AppIntentTimelineProvider<\/code>, this configuration becomes available and can be processed into the data that is passed to the Widget view. It&#8217;s a simple basic system that&#8217;s all implemented in a <code>WidgetConfigurationIntent<\/code> that is specified in the <code>WidgetConfiguration<\/code> for the specific Widget.<\/p>\n<pre><code class=\"language-swift\">struct MyWidget: Widget {\n   let kind: String = \"MyWidget\"\n\n   var body: some WidgetConfiguration {\n      AppIntentConfiguration(kind: kind, \n      intent: ConfigurationAppIntent.self,\n      ...<\/code><\/pre>\n<p>Visually, there is nothing to customize in the WidgetConfigurationIntent. You only need to add &#8216;@Parameter&#8217; fields for the settings you want users to configure; the platform handles the UI automatically.<\/p>\n<pre><code class=\"language-swift\">struct ConfigurationAppIntent: WidgetConfigurationIntent {\n   \/\/ title: mainly for Siri\/shortcuts, keep it simple if you don't use Siri\n   static var title: LocalizedStringResource { \"Configuration\" }\n   \/\/ description: mainly for developers in the app intents system, users will never see this \n   static var description: IntentDescription { \"This is an example widget.\" } \n\n   \/\/ An example configurable parameter.\n   @Parameter(title: \"Favorite Emoji\", default: \"\ud83d\ude03\")\n   var favoriteEmoji: String\n}<\/code><\/pre>\n<p>Most built-in types can be used with @Parameter, such as String, Int, Bool, Date, etc. For more complex types like lists or custom objects, a separate type can be created that implements <code>AppEntity<\/code>. This is a somewhat more extensive topic that I won&#8217;t cover completely here. The principle is simple: the AppEntity object is a selectable datamodel that provides its own options through an <code>EntityQuery<\/code>. This EntityQuery delivers a collection of AppEntity objects from which the user can choose. It&#8217;s important to know that the platform can iterate through the different functions of the EntityQuery multiple times, and again these functions have difficulty exchanging data with eachother. My advice is to use local storage (UserDefaults) to cache and exchange data between the different functions.<\/p>\n<h2>Wrapping Up and Practical Tips<\/h2>\n<p>With the interactive widget fully implemented, the next step naturally becomes refining your logic, layout, and overall design. While most of your core logic can remain inside your .NET MAUI app for reuse across platforms, a bit of Swift will always be required for widget specific tasks like handling storage, building views, or performing lightweight backend actions. Here are some final tips to help you get up to speed during the transition from C# to Swift:<\/p>\n<ul>\n<li>Use VS Code to pair-program with Copilot when creating your Swift code.<\/li>\n<li>Keep Xcode open for rapid build and preview cycles to catch issues early.<\/li>\n<li>Open the Canvas view in Xcode and use #preview data so you can see visual changes quickly.<\/li>\n<\/ul>\n<p>If you&#8217;re interested in taking your widget skills to Android, good news: an article on building Android Widgets with .NET MAUI is coming soon. Stay tuned!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build professional iOS widgets with .NET MAUI, from static displays to interactive widgets.<\/p>\n","protected":false},"author":201971,"featured_media":59323,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7812,7233],"tags":[7238,7245,8111,8108,8110,8109],"class_list":["post-59146","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-dotnet-ios","category-maui","tag-net-maui","tag-ios","tag-mobile-development","tag-swift","tag-widgets","tag-xcode"],"acf":[],"blog_post_summary":"<p>Build professional iOS widgets with .NET MAUI, from static displays to interactive widgets.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/59146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/201971"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=59146"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/59146\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/59323"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=59146"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=59146"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=59146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}