glance_widget_ios 2.0.1 copy "glance_widget_ios: ^2.0.1" to clipboard
glance_widget_ios: ^2.0.1 copied to clipboard

PlatformiOS

iOS implementation of the glance_widget plugin using WidgetKit.

2.0.1 #

Added: an example/README.md next to the Swift templates that were already shipped in example/ios/GlanceWidgets/, explaining why the SwiftUI has to live in your own Xcode target and naming the two settings that fail silently when they are wrong: the GlanceWidgetAppGroup key and the iOS 17.0 minimum. No code change.

2.0.0 #

Added: getWidgetData. Every update now writes a record of the payload under widgetRecord_<widgetId> -- deliberately outside the <template>WidgetData_ family the configuration picker scans -- and forgetWidget drops it. The image path records the payload as it arrived, not the copy it rewrites on the way to storage with a resolved file path and without the base64 bytes. See #37.

Added: Live Activities, on iOS 16.2+. The plugin owns GlanceActivityAttributes and requests activities itself; the extension declares its own copy, and GlanceLiveActivityWidget.swift is a new template to copy alongside the other seven.

That the two copies match at all was measured rather than assumed -- ActivityKit matches by the attributes type's name and the shape of its ContentState, not by module. See findings-live-activity-module-boundary.md. Since a drift between the copies fails nothing at compile time and simply stops the activity appearing, GlanceLiveActivityContractTests compares them: both names, and both encoded shapes byte for byte.

Running activities are looked up through Activity.activities rather than a registry. An activity outlives the process that started it, so anything held in memory would be wrong exactly when it mattered -- which is also why isLiveActivityRunning exists.

Fixed: the widget templates are now compiled by this repository. They were copied into a consumer's extension and nothing here built them, so a type error could sit in one indefinitely -- which is how the image fit key drifted apart from what Dart sends. The example app now ships a real widget extension, and the same twelve files are compiled into its test target, where they can be asserted about rather than only type-checked. Two bugs surfaced the moment anything built them: ImagePreview had not compiled since imagePath was added (#if DEBUG was never a defined condition anywhere), and the example app had no version:, which an app extension cannot install without.

Added: forgetWidget(_:) drops every template's payload for an id, its downsampled image, and the id itself. WidgetKit gives an app no signal when a widget is removed -- an extension is not told, and WidgetCenter.getCurrentConfigurations answers with configuration intents whose type only the app that defined them can decode -- so unlike Android, where onDelete handles this, iOS needs the app to say.

Changed: getActiveWidgetIds() is sorted. It returned Array(activeWidgetIds), and a Set has no order to promise.

Fixed: every widget update silently did nothing in any app whose App Group was not the example's. GlanceWidgetManager.appGroupId was a constant holding group.com.example.glancewidget, and nothing in the plugin ever assigned it -- no Dart API, no method channel, no Info.plist read. An app following the README got its entitlements right and still had the plugin writing to a suite it held no entitlement for, so UserDefaults(suiteName:) returned nil and the widget sat on placeholder data with nothing in the console. The doc comment said "users must configure this in their app's entitlements", which does not change a constant.

The App Group is now read from the app's Info.plist key GlanceWidgetAppGroup, which is available before any Dart runs -- a setter called from Dart would arrive after the first update. Assigning GlanceWidgetManager.appGroupId still works and still wins, so apps that found that escape hatch are not broken. A value that cannot work -- blank, an unsubstituted build setting, a bundle id pasted in by mistake -- is refused rather than passed to UserDefaults, where it would fail further from the mistake.

Changed: every plugin diagnostic moved from print to os.Logger under subsystem dev.glance.widget. print reaches the Xcode console during a debug run and nowhere else, which is the wrong half of the problem: the failures reported here -- an App Group with no entitlement, a payload that will not encode -- all end in a widget that simply does not change, so whoever is investigating is holding a phone, not a debugger. The messages now survive to Console.app and sysdiagnose, and the App Group one names the two Info.plist entries to compare.

Added: the ListWidget checkbox now runs an App Intent instead of opening a URL, so ticking it no longer launches the app. The new state is written by the widget extension and the interaction is queued in the App Group; the plugin drains that queue into dev.glance.widget/events when Dart is next listening, carrying the time the tap actually happened. The queue holds 100 actions and drops the oldest past that.

Fixed: a ListWidget with a deepLinkUri set never reported a checkbox toggle at all. The checkbox built its URL from the deep link when one was present, which discarded the widget id, the action type and the item index, so the app opened and heard nothing. The checkbox no longer builds a URL.

GlanceInteractive.swift is new and shared by the templates -- copy it into your widget extension along with the rest; WIDGET_SETUP.md lists the full set.

Added: the widget templates now support the iOS accessory families -- .accessoryCircular, .accessoryRectangular and .accessoryInline -- so six of the seven render on the lock screen and in the Smart Stack. Each template draws a layout built for the space rather than a scaled-down home screen one. ImageWidget deliberately does not offer them: the system tints an accessory widget one colour at roughly 58pt across, and a photo at that size is a smear.

Fixed: .systemExtraLarge -- the iPad family -- was laid out with .systemMedium's fonts and padding. Every template switched on WidgetFamily with an @unknown default arm, and the iPad family, which is not unknown, fell into it. The families are now classified once in GlanceFamily.swift, which makes those switches exhaustive: a family added by a future iOS is a compile error in one file rather than a wrong number in seven.

Fixed: an accessory family no longer paints the theme's background colour behind itself. Accessory widgets sit on the user's wallpaper and are expected to be transparent.

Two files are new and shared by every template -- GlanceFamily.swift and AccessoryViews.swift. Copy both into your widget extension alongside the templates; WIDGET_SETUP.md lists the full set.

Breaking: matches glance_widget_platform_interface 2.0.0. Update methods return Future<void> and throw GlanceWidgetException on failure instead of returning Future<bool>. See the main package's migration guide for details.

Breaking: the minimum deployment target is now iOS 17, up from iOS 16. AppIntentConfiguration is the only widget configuration that carries a per-instance parameter, and so the only way a placed widget can know which widgetId it renders -- see the fix below. It requires iOS 17.

Breaking: the widget templates use AppIntentConfiguration instead of StaticConfiguration, and their timeline providers are AppIntentTimelineProvider. If you copied the templates and edited them, take the new versions: the change is mechanical, and a StaticConfiguration widget cannot be pointed at a widgetId.

Added #

  • updateBatch applies many widget updates, of any mix of templates, from one method call. Entries are applied in order -- they all write to the same App Group container, and the win a batch is after is the single round trip, not parallelism -- and every one is attempted; the reply carries a failures list naming the widgets that could not be updated. The image template is the only one that needs the network, so it is the only one that cannot answer synchronously; the batch waits for it before moving on.

Fixed #

  • Every placed instance of a template rendered the same data. widgetId was honoured when writing and ignored when reading: each widget called load...Widget() with no id and fell through to "whichever payload was written last", so two SimpleWidgets could not show 'btc' and 'eth'. This was the mirror image of the Android defect fixed in 2.0.0, where the write path clobbered instead. Widgets now take a widgetId from their configuration, and the person placing the widget picks it from the ids the app has actually sent data for. An unconfigured instance keeps the old most-recent behaviour so a freshly placed widget is never blank.
  • imageUrl did nothing. It is documented, validated in Dart and sent over the channel, and no iOS code read it -- the widget drew a placeholder saying "Remote images limited in widgets". Image sources are now resolved when the update is applied: fetched, downsampled through CGImageSourceCreateThumbnail and written into the App Group container, so the extension only ever does a small file read. A widget extension runs under a far tighter memory budget than the app, and WidgetKit will not wait for a network round trip during a timeline reload.
  • Images were carried through App Group UserDefaults as base64. The bytes now live in a file, so every widget reload no longer has to read them back.
  • Documented the iOS floor as something to set in Xcode or the Podfile. Neither on its own is the whole story. The Xcode target (IPHONEOS_DEPLOYMENT_TARGET) is the value that counts, and it reaches FlutterGeneratedPluginSwiftPackage only when flutter build ios runs; flutter pub get rewrites that manifest at Flutter's own 15.0 default whatever both the Xcode target and the Podfile say. The example app hit exactly that.

Changed #

  • updateImageWidgetWithResult is asynchronous and takes a completion handler. Resolving imageUrl needs the network, so the outcome cannot be known synchronously. The plugin hops the reply back to the main thread.

Security #

  • Only http and https image URLs are fetched, and redirects are followed by hand, at most five deep, with every hop put back through the same check. URLSession would have applied it to the first hop only. Downloads are capped at 16 MB and time out.

Changed #

  • Dropping the cached image file when a widget stops having a picture moved from the caller into GlanceImageStore, which owns those files. The behaviour is unchanged here; Android had the same code with the call missing, so the rule now lives where it cannot be forgotten on either platform.
  • The seven App Group key prefixes were private string literals in GlanceWidgetManager, typed out again by hand in the widget templates. They now live in GlanceStorageKeys, which the plugin uses and the tests pin to their exact values -- a rename used to fail nothing at all, leaving the configuration picker empty and every configured widget blank.

Added #

  • Swift unit tests, run in CI on a simulator through the example app's XCTest target, and a swiftc -typecheck gate for the widget extension templates -- nothing in the repository compiled those before, which is how the image fit key silently drifted apart from what Dart sends.
  • Method channel handlers replied success(true) before knowing whether the update had been applied, so a rejected update was reported to Dart as a success. Handlers now await the real outcome and answer with an error when the platform refused.

Changelog #

1.0.0 #

  • Complete all platform method overrides (was 9/18, now 18/18)
  • Method channel namespace changed from com.example.glance_widget to dev.glance.widget
  • Added dispose() override for resource cleanup
  • Updated dependency: glance_widget_platform_interface: ^1.0.0

0.5.0 #

  • New Widget Templates - Calendar, Image, Chart, Gauge
    • CalendarWidget - Date header with event list and colored dot indicators
    • ImageWidget - Base64 image decoding with UIImage, content mode support
    • ChartWidget - SwiftUI Path + GeometryReader for line/bar/sparkline charts
    • GaugeWidget - Path.addArc() for radial, LazyVGrid for dashboard metrics
  • Deep Link Support - All widgets support deepLinkUri via .widgetURL()
  • Timeline Refresh - Configurable .after(date) policy for periodic widget refresh
    • configureTimelineRefresh() / cancelTimelineRefresh() methods
  • Interactive Actions - Item tap and checkbox toggle URLs in List Widget
  • Widget Configuration - completeWidgetConfiguration handler
  • Updated SDK constraints to Dart >=3.6.0, Flutter >=3.27.0
  • Updated dependency on glance_widget_platform_interface to ^0.6.0

0.3.0 #

  • Updated dependency on glance_widget_platform_interface to ^0.4.0
  • Background update methods return stub responses (iOS uses Push Updates instead)

0.2.1 #

  • Updated dependency on glance_widget_platform_interface to ^0.3.1

0.2.0 #

  • Error Handling - Added GlanceResult enum for structured error reporting
  • App Group Validation - Added isAvailable check and improved error messages
  • Save Feedback - save() methods now return Bool indicating success/failure
  • Updated dependency on glance_widget_platform_interface to ^0.3.0

0.1.0 #

  • Initial release
  • iOS implementation using WidgetKit
  • Three widget templates: Simple, Progress, List
  • Instant widget updates when app is in foreground (no budget limit)
  • Theme support with dark/light modes
  • Widget tap actions sent back to Flutter via URL schemes
  • App Group storage for Flutter-Widget data sharing
  • Widget Push Updates support (iOS 26+)
  • Privacy manifest included (UserDefaults usage)
  • Minimum iOS version: 16.0