glance_widget_ios 2.0.1
glance_widget_ios: ^2.0.1 copied to clipboard
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 #
updateBatchapplies 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 afailureslist 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.
widgetIdwas honoured when writing and ignored when reading: each widget calledload...Widget()with no id and fell through to "whichever payload was written last", so twoSimpleWidgets 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 awidgetIdfrom 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. imageUrldid 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 throughCGImageSourceCreateThumbnailand 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
UserDefaultsas 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 reachesFlutterGeneratedPluginSwiftPackageonly whenflutter build iosruns;flutter pub getrewrites that manifest at Flutter's own 15.0 default whatever both the Xcode target and thePodfilesay. The example app hit exactly that.
Changed #
updateImageWidgetWithResultis asynchronous and takes a completion handler. ResolvingimageUrlneeds the network, so the outcome cannot be known synchronously. The plugin hops the reply back to the main thread.
Security #
- Only
httpandhttpsimage URLs are fetched, and redirects are followed by hand, at most five deep, with every hop put back through the same check.URLSessionwould 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 inGlanceStorageKeys, 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 -typecheckgate for the widget extension templates -- nothing in the repository compiled those before, which is how the imagefitkey 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_widgettodev.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 indicatorsImageWidget- Base64 image decoding with UIImage, content mode supportChartWidget- SwiftUI Path + GeometryReader for line/bar/sparkline chartsGaugeWidget- Path.addArc() for radial, LazyVGrid for dashboard metrics
- Deep Link Support - All widgets support
deepLinkUrivia.widgetURL() - Timeline Refresh - Configurable
.after(date)policy for periodic widget refreshconfigureTimelineRefresh()/cancelTimelineRefresh()methods
- Interactive Actions - Item tap and checkbox toggle URLs in List Widget
- Widget Configuration -
completeWidgetConfigurationhandler - 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
GlanceResultenum for structured error reporting - App Group Validation - Added
isAvailablecheck 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