Sorry for the length of this post, and all the questions.
I have an iOS app written in Objective-C (too big to convert to Swift right now), and I successfully added a WatchKit app and WatchKit Extension some years ago. I also added Home Screen widgets when iOS 14 was released.
With the iOS 16 betas I'd like to support Lock Screen widgets, and have also decided to move the WatchKit app/extension to SwiftUI.
User journey:
MyApp is launched and an item with an image is created. This item is stored in Core Data and its image is stored in a directory in the app's documents directory. A version of it is stored in NSUserDefaults in a shared app group. The user adds a Home Screen widget to show that item. Its data is pulled from the defaults, and the image is loaded from the document's directory.
The user installs the Watch app and launches it. The Watch app looks inside the user defaults to retrieve the item. The iOS app sends the image to the Watch and the Watch app stores it locally (it's a small image, taking up barely a few Kb).
This all works fine right now.
Currently I have these targets:
MyApp = main iOS app.
MyApp WatchKit = Watch app storyboards and asset catalogs.
MyApp WatchKit Extension = code to update the Watch interface.
MyApp Widget = Home Screen widgets.
MyApp IntentHandler = dynamic intents handler for the Home Screen widgets.
Q1. Where do I put the code for the Lock Screen widgets? I figure these go into the My App Widget target because they're widgets and appear on the iPhone?
In this video (https://developer.apple.com/videos/play/wwdc2022/10050) at 07:00 it tells you to duplicate the existing Widget target, change the bundle, change it to run on watchOS and embed it in your existing Watch App. As my original Watch App is written in Objective-C (MyApp WatchKit Extension, above) I can't/shouldn't do that, so...
Q2. I think I have to create a new MyApp Watch App target, and perform the video steps on that target? I can create the views for that app, no problem.
Most people update to the latest watchOS, and it's only now that watchOS 9 won't support Watch Series 3.
Q3. Do I need to keep MyApp WatchKit and MyApp WatchKit Extension around? I can support older versions of watchOS if it helps my users, but they'll probably want to use the new version of watchOS, right? Can you install both versions of the app on your Watch (with watchOS 9), or does the new Swift app override the old WatchKit extension?
Q4. Once I've designed the new Watch App's views in SwiftUI to replace the old WatchKit extension, where do I put the code for the complications that are being replaced? The WWDC 2022 videos (above, and a couple linked to on that page) have confused me a bit. Do I put complications views in the new MyApp Watch App target along with the other views that replace the old Watch app, or in MyApp Widget?
Q5. The MyApp Widget target contains a bunch of code (WidgetUtils.swift) that populates the Home Screen widgets (and the new Lock Screen widgets), and it would fit right into the new MyApp Watch App target. Can I share that code between the two app targets just by adding WidgetUtils.swift to both target's membership?
MyApp sends small images to the current MyApp WatchKit Extension. There's no code in the existing WidgetUtils.swift to handle file transfers because the images for the Home Screen widgets are pulled from the iOS app, so I need to write that in Swift for the new MyApp Watch App. The logic is already there in the old target, but I can't see any sort of equivalent to the WatchKit extension delegate where I currently handle the file transfers.
Q6. Where does that go in MyApp Watch App?
The existing MyApp WatchKit Extension occasionally asks MyApp for some new data. This is all done in the extension delegate which wakes up the iOS app and updates the NSUserDefaults which the extension then reads from.
Q7. How do you do that in MyApp Watch App?
Don't be afraid to be verbose in your responses. The more detail the better! Thank you in advance.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Let's say you have a Text.init(Date().advanced(by: 5), style: .timer). It's set to countdown to a date 5 seconds from now (just for simplicity's sake).
It goes like this:
Countdown starts, and shows 0:05.
After 1 second it shows 0:04.
After 2 seconds it shows 0:03.
After 3 seconds it shows 0:02.
After 4 seconds it shows 0:01.
After 5 seconds it shows 0:00. Countdown done.
After 6 seconds it should show 0:01, right? It doesn't; it shows 0:00.
After 7 seconds it shows 0:01, even though only 6 seconds have passed.
It's almost as though the timer is being restarted once it hits zero, but that isn't right. There aren't "two seconds at 0:00" (that's not how time works...).
Anyone seeing this, or is just me?
Been at this for ages now, getting nowhere, thanks to Swift and the betas...
The iOS app schedules a local notification that has a userInfo dictionary, and one small JPEG image attachment.
Objective-C in iOS app:
content.attachments = @[[UNNotificationAttachment attachmentWithIdentifier:myIdentifier URL:[imageURL filePathURL] options:@{UNNotificationAttachmentOptionsTypeHintKey : UTTypeJPEG} error:&error]];
This works fine. The notification is correctly scheduled.
If I ignore the Watch and let the notification appear on my phone's Lock Screen, the image is there.
Going back to the Watch. The Watch app receives the notification, and the didReceive method is called in the NotificationController.
No matter what I try, I can't get the image from the notification.
NotificationController.swift: (image is sent to the NotificationView to use as the background.)
guard let attachment = notification.request.content.attachments.first
else {
print("Couldn't get the first attachment, using default")
image = Image.init(kDefaultImage)
return
}
// We get here, so we know there's an attachment
if attachment.url.startAccessingSecurityScopedResource() {
let imageData = try? Data.init(contentsOf: attachment.url)
if let imageData = imageData {
image = Image(uiImage: UIImage(data: imageData) ?? UIImage.init(imageLiteralResourceName: kDefaultImageMasked))
}
attachment.url.stopAccessingSecurityScopedResource()
} else {
// << I ALWAYS HIT THIS BIT >>
print("Couldn't access the file, using default")
image = Image.init(kDefaultImageMasked)
}
I always get told I can't access the file in the security scoped bit.
If I take out that check I get Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value because the code doesn't put anything into image. Obviously I would put the default image in there, but once the Watch app crashes the notification that's shown on the Watch shows the correct image, but it's obviously not using my NotificationView because that crashed.
How the hell do I get the image from the attachment? This was simple to do in the old WatchKit extension stuff, look:
NSArray *attachments = notification.request.content.attachments;
if(attachments.count == 1) {
[_groupAll setBackgroundImage:attachments[0]];
}
No problems there; it always worked.
Thanks.
I have a content blocker working fine, but have noticed some websites are putting a class in the main body tag, and removing it when the user taps to accept/decline cookies.
For example:
<body class="homepage ContentPage language-en modal-open no-overflow">
...
<div id="cookieNotice">...</div>
My content blocker can remove the cookieNotice div completely using: div[id="cookieNotice"], but the page doesn't scroll because the body tag includes modal-open no-overflow.
Can a content blocker remove values/classes from a tag? If so, what would the XPath look like? Thanks.
Just finalising some work on my app update, and it seems that when you go to select an item to show in a complication, when you select your app in the list, the subsequent list only shows 15 of your items.
If a user of my app has transferred 20 items to their Watch, they can't select five of them to be shown in a complication. Is that right?
If that's a hard limit then I need to be able to separate them out into bunches of 15 items, or maybe have them display under A-E, F-J etc.
Does this have to be done as a separate Widget in the WidgetBundle? And how do I do that? Given that I currently have one widget in that bundle that should show everything (20 items), how would I split it out to show an "A-E" widget with those items beginning with A...E? Do I have to have an A-E widget with its own set of data?
I have a Home Screen widget that contains a timer counting down to a specific date. In this image you can see the date calculations:
eventDate: 25th Dec 2023 at 09:00:00
entryDate(Date.now): This is just showing you the date in the first SimpleEntry for the widget.
getTimeRemaining(entryDate): This shows the number of seconds from the entryDate to the eventDate, figures out how many days there are ("43 days"), and how many hours:mins:secs to the event time, so "10:48:52".
Then there's a second entry, entryDate2, that's one hour later, and the values are appropriately calculated.
When I create the timeline entries, I add them for:
Now (1 entry)
Now plus one hour to a week away (one entry per hour = 167 entries)
Event date (1 entry)
Event date plus one hour to a week later (one entry per hour = 167 entries)
Each SimpleEntry entry contains a dictionary with the relevant timer details, and that's what the widget uses to determine what to display in the timer.
SwiftUI lacks any useful formatting for a timer. Even the developer docs state: "Example output: 36:59:01". Who wants to see a timer with 36 hours on it? I want it to say "1 day 12:59:01", so I munge the numbers about and grab the separate parts, converting 36:59:01 into "1 day" and "12:59:01". You can see that in the image above.
When the entry date of the timeline is reached and the widget is redrawn, it uses the entry containing the dictionary saying it should display "43 days" and the countdown timer should be 10:48:52, then an hour later the dictionary says it's 43 days 9:48:52, etc.
The issue is that the widgets, even though they're supposed to have entries at each hour, will always end up displaying something like "29:17:09". The timeline has an entry at every hour, so it should be using the right values. I've checked, and the right values are in the dictionary, so why does the timer keep getting out of sync?
I could cut out a massive amount of my widget code if only Text.init(date, style: .timer) would allow some proper formatting.
I have a countdown/up timer in a widget, and I want to format the timer to something more readable than the default that Text.init(myDate, style: .timer) provides.
The default outputs a timer for just the hours in the date range. So, for example, a timer of 1 week, 5 hours, 12 minutes and 45 seconds will appear as 173:12:45 (which is 7 * 24 + 5 = 173) - not very user-friendly.
An ideal output would be 1 week 05:12:45. Is there any way of doing that?
I've tried a number of different ways using TimeInterval, DateInterval, and modding (%) values - like modding the hours count by 168 to get a number of weeks - but they're pretty much useless when there are never a set number of days or weeks in a year due to leap years.
It would be great if you would provide actual code examples rather than saying to use a certain API, as I could very easily go down a rabbit hole like I have with TimeInterval and DateInterval. Thanks!
I have an iOS app that's written in Objective-C, uses Core Data, and has a number of SwiftUI targets (Widgets, Watch app). I'd like to convert the main app to SwiftUI and keep access to the data in the Core Data stack, but move to SwiftData immediately. Since I'm doing a lot of rewriting, it makes sense to leap ahead rather than have to rewrite it in a year or two. Effort isn't an issue; I'm a tenacious SOB ;)
But I have no idea how to do this, and can't find any examples on the net of this particular scenario. All I can find is how to start using SwiftData instead of Core Data in an app that's already written in Swift.
So, how do I go about the migration without losing data?
I guess I'll need to add a new Swift/SwiftUI target for the main app, but then how do I migrate the Core Data store over? In ObjC there's a lot of messing with stacks and the actual location of the model in the filesystem, but I doubt this is necessary in the new way of doing things?
Any help would be appreciated. Thanks!
In iOS 18 the following code works to set a state variable when you hold your finger on the Text() field (well, the ScrollView()), but it doesn't work in iOS 26:
@State private var pressed: Bool = false
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.onTapGesture {} // This is required to allow the long press gesture to be recognised
.gesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
pressed = true
})
.onEnded({ _ in
pressed = false
})
)
.background(pressed ? .black.opacity(0.4) : .clear)
I've tried changing this to:
var dragGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged({ _ in self.pressed = true })
.onEnded({ _ in self.pressed = false })
}
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.gesture(dragGesture)
.background(pressed ? .black.opacity(0.4) : .clear)
And this:
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.25)
.onChanged({ _ in self.pressed = true })
.onEnded({ _ in self.pressed = false })
}
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.gesture(longPress)
.background(pressed ? .black.opacity(0.4) : .clear)
Neither works.
Any ideas? Thanks.
I honestly thought I was getting somewhere with this, but alas, no. Every time I do anything in my List of ItemRows it jumps back to the top.
Here's the setup:
DataService.swift:
final class DataService {
static let shared = DataService()
private init() {}
let coreData: CoreData = CoreData()
let modelData: ModelData = ModelData()
}
ModelData.swift:
@Observable
class ModelData: ObservableObject {
var allItems: [ItemDetails]
var standardItems: [ItemDetails]
var archivedItems: [ItemDetails]
init() {
allItems = []
standardItems = []
archivedItems = []
}
func getInitialData() {
// Get all items, then split them into archived and non-archived sets, because you can't use `.filter` in a view...
allItems = dataService.coreData.getAllItems()
standardItems.append(contentsOf: allItems.filter { !$0.archived })
archivedItems.append(contentsOf: allItems.filter { $0.archived })
}
}
MainApp.swift:
// Get access to the data; this singleton is a global as non-view-based functions, including the `Scene`, need to access the model data
let dataService: DataService = DataService.shared
@main
struct MainApp: App {
// Should this be @ObservedObject or @StateObject?
@ObservedObject private var modelData: ModelData = dataService.modelData
// I would use @StateObject if the line was...
//@StateObject private var modelData: ModelData = ModelData() // right?
// But then I couldn't use modelData outside of the view hierarchy
var body: some Scene {
WindowGroup {
ZStack {
MainView()
.environment(modelData)
}
}
.onAppear {
modelData.getInitialData()
}
}
}
MainView.swift:
struct MainView: View {
@Environment(ModelData.self) private var modelData: ModelData
var body: some View {
...
ForEach(modelData.standardItems) { item in
ItemRow(item)
}
ForEach(modelData.archivedItems) { item in
ItemRow(item)
}
}
}
ItemRow.swift:
struct ItemRow: View {
@Environment(\.accessibilityDifferentiateWithoutColor) private var accessibilityDifferentiateWithoutColor
var item: ItemDetails
@State private var showDeleteConfirmation: Bool = false
var body: some View {
// Construct the row view
// `accessibilityDifferentiateWithoutColor` is used within the row to change colours if DWC is enabled, e.g. use different symbols instead of different colours for button images.
// Add the .leftSwipeButtons, .rightSwipeButtons, and .contextMenu
// Add the .confirmationDialog for when I want to ask for confirmation before deleting an item
}
}
Now, the problems:
Swipe an item row, tap one of the buttons, e.g. edit, and the list refreshes and jumps back to the top. In the console I see: ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor changed. Why did accessibilityDifferentiateWithoutColor change? The setting in Settings > Accessibility > Display & Text Size has not been changed, so why does the row's view think it changed?
With a .confirmationDialog attached to the end of the ItemRow (as seen in the code above), if I swipe and tap the delete button the list refreshes and jumps back to the top again. In the console I see: ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor, _showDeleteConfirmation changed. Right, it changed for the one row that I tapped the button for. Why does every row get redrawn?
I already had to shift from using the colorScheme environment variable to add new asset colours with light and dark variants to cover this, but you can't do that with DWC.
Honestly, managing state in SwiftUI is a nightmare. I had zero problems until iOS 26 started removing one or two rows when I scrolled, and the fix for that - using @Statebject/@ObservedObject - has introduced multiple further annoying, mind-bending problems, and necessitated massive daily refactorings. And, of course, plenty of my time islost trying to figure out where a problem is in the code because "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"...
Hi all. I have a timer working in a view on the Watch app, but I just can't get them working in widgets. Can you use timers in Home Screen & Lock Screen widgets? I can't find anything that says you can't...
Take this code:
struct ScratchPadView: View {
@State var backgroundGradient: LinearGradient = gradientOne
let gradientTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
let date: Date = getDateFromString("20220828 10:00")
ZStack {
backgroundGradient
.onReceive(gradientTimer) { _ in
self.backgroundGradient = (date >= Date()) ? gradientOne : gradientTwo
}
}
}
}
All this is supposed to do is change the gradient in the background from gradientOne to gradientTwo when the current date is after the date supplied. Doesn't work. The date can come and go and the gradient never changes from what it was initially set up to use: gradientOne.
Like I say, this works fine in a Watch app View, but just doesn't work in a widget.
Is the only alternative to provide a timeline entry for that date so the widget refreshes and notices that the date has now passed?
My app lets you create a list of items and pick one as the main item. For Home Screen widgets there are two bits of text you can use in the panel that appears when you want to add the widget:
public var body: some WidgetConfiguration {
IntentConfiguration(kind: "myWidgetKind", intent: DynamicSelectionIntent.self, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("Title")
.description("Description")
So, for a Home Screen widget the panel displays the title and description, and the preview shows the main item. Once you've added the widget you can edit it and pick a different item, so the title is "Display an Item" and the description is quite general, telling you what the widget can display.
The title and description are also displayed in the panel when you want to add a Lock Screen widget:
For the inline Lock Screen widget, you see "Title".
For rectangular and circular widgets below the clock, you see the usual Home Screen panel, so both "Title" and "Description".
There's no way of editing the item that sits behind the Lock Screen widget once you've added it, so the general text needs to be more specific and refer to the main item.
How do you give different title and description text when you're adding a Lock Screen widget?
In the first image you can see my app at the top of the list, and Apple's Weather app widget. Both are showing the SF Symbol rendered correctly.
In the second image the inline widget chosen is Apple's Weather app. The SF Symbol is correctly shown.
In the third image my app's inline widget has been chosen, but the image is not rendered correctly; it's just a block.
Is there something I have to do to my image to have it rendered correctly?
I have an app with Home Screen widgets, and new Lock Screen widgets, and I'm trying to get my complications to work on the Watch.
The widgets are running off dynamic intents, taking a list of items from user defaults and providing that list for the user to choose when they add a Home Screen or Lock Screen widget. This works fine.
In the "Complications and widgets: Reloaded" video from WWDC2022 at about 7:00 the guy tells you to duplicate the widget extension, rename it, and set it to run on watchOS. I ended up with these targets:
Main App (embeds Widget, Intents Handler and Watch App)
Widget
Intents Handler
Watch App (embeds Complications)
Complications (new, copy of Widget)
At about 8:30 he adds the supported families for the Watch to his EmojiRangersWidget WidgetConfiguration, so it looks like the code for the accessory widgets is used for both Lock Screen widgets and complications?
Should I be putting all my complications views in the Widget target, something like this?
@main
struct WidgetEntry: Widget
{
public var body: some WidgetConfiguration {
IntentConfiguration(kind: kWidgetKind,
intent: DynamicItemSelectionIntent.self,
provider: Provider()
) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("abc")
.description("def")
#if os(watchOS)
.supportedFamilies([.accessoryCircular, .accessoryInline, .accessoryRectangular, .accessoryCorner])
#else
.supportedFamilies([.accessoryCircular, .accessoryInline, .accessoryRectangular, .systemSmall, .systemMedium])
#endif
}
}
struct WidgetEntryView: View
{
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
@ViewBuilder
var body: some View {
#if os(watchOS)
switch family {
case .accessoryCircular, .accessoryCorner, .accessoryInline, .accessoryRectangular:
ComplicationView(item: entry.item)
@unknown default:
UnknownComplicationView(item: entry.item)
}
#else
switch family {
case .accessoryCircular, .accessoryInline, .accessoryRectangular:
LockScreenWidgetView(item: entry.item)
case .systemSmall:
SmallWidgetView(item: entry.item)
case .systemMedium, .systemLarge, .systemExtraLarge:
MediumWidgetView(item: entry.item)
@unknown default:
UnknownWidgetView(item: entry.item)
}
#endif
}
}
I've previously been told by an Apple engineer on these forums: "The complication code should be in the watch app target. That's where watchOS 7 and 8 will look for complications, and where watchOS 9 will look for old ClockKit complications for migration to their WidgetKit counterparts." I'm no longer supporting watchOS 7/8, but does this mean that watchOS 7/8 will look for old complications in there to migrate to the new stuff into the Widget target? I asked a couple of follow-up questions but they never got answered ¯\_(ツ)_/¯
Apple make this stuff so unnecessarily complex, even though their videos make it look so easy. How many times have we all paused their videos to see exactly what code they're writing and where they're putting it? There's practically zero help out there - these forums are full of questions and few answers. Xcode should have much better documentation and help to guide you through this. It takes so long to get anything done because there just isn't the information we need.
I just got an Apple Watch Series 8, and I've updated it to the latest beta of watchOS 9.1.
About 6 times a day I have to hard reboot the Watch because the Watch faces stop working. The screen goes black. I can access the app list, run apps etc., but the Watch face just isn't there anymore. Notification Centre and swipe up settings work fine; it's just the Watch faces.
As I said, a reboot fixes it, but it just dies randomly afterwards.
Is anyone else experiencing this with this beta? Given that I've got a new device and updated to new software, there are two variables to consider.
I don't see how it can be a hardware issue because the screen works fine for everything else. If it's just this beta, then I can wait for the next one.
Thanks.