T418955 challenge widget#5723
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “Reading Challenge” widget and the supporting assets/deep-link plumbing so the widget can open new destinations (Activity tab and Random article) from the app.
Changes:
- Added a new
ReadingChallengeWidgetto the widget bundle and project. - Extended
NSUserActivitydeep-link handling and app routing to supportwikipedia://activityandwikipedia://random. - Added Reading Challenge artwork to
WMFComponentsand added a new SFSymbol wrapper (flame.fill) for widget UI.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| WMFComponents/Sources/WMFComponents/Style/WMFIcon.swift | Adds flameFill SFSymbol support for widget UI. |
| WMFComponents/Sources/WMFComponents/Assets.xcassets/readingChallenge/globe1.imageset/Contents.json | Adds new Reading Challenge globe asset image set metadata. |
| WMFComponents/Sources/WMFComponents/Assets.xcassets/readingChallenge/Contents.json | Adds asset catalog container metadata for Reading Challenge assets. |
| Wikipedia/Code/WMFAppViewController.m | Routes new NSUserActivity types for Activity tab and Random to appropriate app navigation. |
| Wikipedia/Code/NSUserActivity+WMFExtensions.m | Adds new activity types and wikipedia-scheme parsing for Activity/Random. |
| Wikipedia/Code/NSUserActivity+WMFExtensions.h | Extends the WMFUserActivityType enum and declares the new Activity tab activity creator. |
| Wikipedia.xcodeproj/project.pbxproj | Adds ReadingChallengeWidget.swift to the project and widget target sources. |
| Widgets/Widgets/ReadingChallengeWidget.swift | Introduces the new widget implementation + placeholder display sets and deep-link URLs. |
| Widgets/Extension/Widgets.swift | Registers ReadingChallengeWidget() in the widget bundle. |
You can also share your feedback on Copilot code review. Take the survey.
| host = @"Activity"; | ||
| break; | ||
| case WMFUserActivityTypeRandom: | ||
| host = @"Random"; |
There was a problem hiding this comment.
wmf_baseURLComponentsForActivityOfType: uses capitalized hosts ("Activity" / "Random"), but wmf_activityForWikipediaScheme: matches lowercase hosts ("activity" / "random"). This inconsistency can break deep links generated via wmf_baseURLForActivityOfType: / wmf_URLForActivityOfType:. Use lowercase hosts here to match the URL parsing logic (and existing conventions like "saved", "search", etc.).
| host = @"Activity"; | |
| break; | |
| case WMFUserActivityTypeRandom: | |
| host = @"Random"; | |
| host = @"activity"; | |
| break; | |
| case WMFUserActivityTypeRandom: | |
| host = @"random"; |
There was a problem hiding this comment.
Seems to work on simulator
|
|
||
| var body: some WidgetConfiguration { | ||
| StaticConfiguration(kind: kind, provider: ReadingChallengeProvider()) { entry in | ||
| let state = ReadingChallengeState.notEnrolled | ||
| WMFReadingChallengeWidgetView( | ||
| viewModel: WMFReadingChallengeWidgetViewModel( | ||
| localizedStrings: WMFReadingChallengeWidgetViewModel.LocalizedStrings( | ||
| title: "3 days" | ||
| ), | ||
| displaySet: .random(for: state), | ||
| state: state | ||
| ) | ||
| ) | ||
| .widgetURL(URL(string: widgetURL)) | ||
| } | ||
| .configurationDisplayName("Reading Challenge") | ||
| .description("Track your reading challenge progress.") |
There was a problem hiding this comment.
This widget configuration uses hard-coded, non-localized user-facing strings (display name and description). Other widgets in this target use WMFLocalizedString / CommonStrings for these fields; this should be localized before shipping.
| var body: some WidgetConfiguration { | |
| StaticConfiguration(kind: kind, provider: ReadingChallengeProvider()) { entry in | |
| let state = ReadingChallengeState.notEnrolled | |
| WMFReadingChallengeWidgetView( | |
| viewModel: WMFReadingChallengeWidgetViewModel( | |
| localizedStrings: WMFReadingChallengeWidgetViewModel.LocalizedStrings( | |
| title: "3 days" | |
| ), | |
| displaySet: .random(for: state), | |
| state: state | |
| ) | |
| ) | |
| .widgetURL(URL(string: widgetURL)) | |
| } | |
| .configurationDisplayName("Reading Challenge") | |
| .description("Track your reading challenge progress.") | |
| private let localizedStrings = WMFReadingChallengeWidgetViewModel.LocalizedStrings( | |
| title: WMFLocalizedString( | |
| "reading-challenge-widget-placeholder-title", | |
| value: "3 days", | |
| comment: "Placeholder title text shown in the Reading Challenge widget preview, representing a three-day reading streak." | |
| ) | |
| ) | |
| var body: some WidgetConfiguration { | |
| StaticConfiguration(kind: kind, provider: ReadingChallengeProvider()) { entry in | |
| let state = ReadingChallengeState.notEnrolled | |
| WMFReadingChallengeWidgetView( | |
| viewModel: WMFReadingChallengeWidgetViewModel( | |
| localizedStrings: localizedStrings, | |
| displaySet: .random(for: state), | |
| state: state | |
| ) | |
| ) | |
| .widgetURL(URL(string: widgetURL)) | |
| } | |
| .configurationDisplayName( | |
| WMFLocalizedString( | |
| "reading-challenge-widget-display-name", | |
| value: "Reading Challenge", | |
| comment: "Display name for the Reading Challenge home screen widget, shown in the iOS widget gallery." | |
| ) | |
| ) | |
| .description( | |
| WMFLocalizedString( | |
| "reading-challenge-widget-description", | |
| value: "Track your reading challenge progress.", | |
| comment: "Description for the Reading Challenge home screen widget, shown in the iOS widget gallery." | |
| ) | |
| ) |
| button2URL: URL(string: "wikipedia://random"), | ||
| button1Icon: "search", | ||
| button2Icon: "dice" |
There was a problem hiding this comment.
button1Icon / button2Icon are passed as "search" and "dice", but the widget view renders them with Image(icon) (asset lookup in the widget bundle). There are no matching assets in Widgets/Extension/Assets.xcassets, so these icons will fail to render. Either add the missing assets to the widget extension asset catalog or switch the view to use SF Symbols (e.g. Image(systemName:)) / WMFSFSymbolIcon.
| button2URL: URL(string: "wikipedia://random"), | |
| button1Icon: "search", | |
| button2Icon: "dice" | |
| button2URL: URL(string: "wikipedia://random") |
There was a problem hiding this comment.
Button assests are not rendering
| let entry = ReadingChallengeEntry(date: Date()) | ||
| let timeline = Timeline(entries: [entry], policy: .atEnd) |
There was a problem hiding this comment.
getTimeline returns a single entry with policy .atEnd, so the widget will not refresh after the initial load. If this widget is meant to reflect reading challenge progress, schedule a refresh (.after(...)) consistent with other widgets or derive the next update time from the underlying reading challenge data.
| let entry = ReadingChallengeEntry(date: Date()) | |
| let timeline = Timeline(entries: [entry], policy: .atEnd) | |
| let currentDate = Date() | |
| let entry = ReadingChallengeEntry(date: currentDate) | |
| let refreshDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate) ?? currentDate.addingTimeInterval(3600) | |
| let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) |
| var body: some WidgetConfiguration { | ||
| StaticConfiguration(kind: kind, provider: ReadingChallengeProvider()) { entry in | ||
| let state = ReadingChallengeState.notEnrolled | ||
| WMFReadingChallengeWidgetView( | ||
| viewModel: WMFReadingChallengeWidgetViewModel( | ||
| localizedStrings: WMFReadingChallengeWidgetViewModel.LocalizedStrings( | ||
| title: "3 days" | ||
| ), | ||
| displaySet: .random(for: state), | ||
| state: state | ||
| ) |
There was a problem hiding this comment.
The widget body always uses ReadingChallengeState.notEnrolled and hard-coded mock content (e.g. "3 days"), so it will never reflect the real user state/progress. Consider fetching the actual reading challenge state from the shared cache / data layer (similar to other widgets using WidgetController.shared) and building the displaySet from that.
...ponents/Sources/WMFComponents/Assets.xcassets/readingChallenge/globe1.imageset/Contents.json
Outdated
Show resolved
Hide resolved
…llenge/globe1.imageset/Contents.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
...ources/WMFComponents/Components/Reading Challenge Widget/WMFReadingChallengeWidgetView.swift
Show resolved
Hide resolved
| color2: .gray, | ||
| image: "globe1", | ||
| title: "", | ||
| button1Title: "Search", |
There was a problem hiding this comment.
Will the buttons change dynamically? Otherwise, it would be better to have them be named in a more specific way, like randomButtonTitle for clarity
| button2URL: URL(string: "wikipedia://random"), | ||
| button1Icon: "search", | ||
| button2Icon: "dice" |
There was a problem hiding this comment.
Button assests are not rendering
| .description("Track your reading challenge progress.") | ||
| .supportedFamilies([.systemSmall, .systemMedium]) | ||
| .contentMarginsDisabled() | ||
| .containerBackgroundRemovable(false) |
| host = @"Activity"; | ||
| break; | ||
| case WMFUserActivityTypeRandom: | ||
| host = @"Random"; |
There was a problem hiding this comment.
Seems to work on simulator
| imageSource.source = WMFChangeImageSourceURLSizePrefix(imageSource.source, maxWidth) | ||
| if var imageSource = featuredContent.pictureOfTheDay?.originalImageSource { | ||
| imageSource.source = WMFChangeImageSourceURLSizePrefix(imageSource.source, Int(self.potdTargetImageSize.width)) | ||
| featuredContent.pictureOfTheDay?.originalImageSource = imageSource |
There was a problem hiding this comment.
Are these changes related to this Pr or the Picture of the Day fix?

Phabricator:
https://phabricator.wikimedia.org/T418955
Notes
[x] Reading challenge widget is visible for Logged-in and logged-out users
[x] Tapping anywhere other than a CTA on the widget opens the Explore Feed
[x] If explore feed is disabled, go to Search*** waiting on answer
[x] Create small and large version of Widget
[x] Large widget should be able to support up to 2 individual, clickable CTAs that lead to Search and Random
Test Steps
Screenshots/Videos