I've had to rewrite my app to get widgets working properly, and I've got this project structure:
Main iOS app:
Bundle identifier = com.me.myapp
Contains:
Widget Extension:
Bundle identifier = com.me.myapp.widgets
Targets iOS 17
Provides widgets to iOS
Watch app:
Bundle identifier = com.me.myapp.watchapp
Contains:
Complications Extension (a Widget Extension):
Bundle identifier = com.me.myapp.watchapp.complications
Targets watchOS 10
Provides widgets to watchOS
On the Signing & Capabilities tab in Xcode 15, all four targets have:
Provisioning Profile: Xcode Managed Profile
Signing Certificate: Apple Development: My team
App Groups: all use the same one: group.com.me.myapp
I can build and deploy to a physical iPhone and Apple Watch, but the Watch app doesn't seem to be able to access the shared Core Data, which should be in the shared app group container.
When running the main iOS app, the store location is:
file:///private/var/mobile/Containers/Shared/AppGroup/189E5907-E6E4-4790-833F-06944E4FF5FF/data-model/TheDataModel
When running the widget extension, the store location is:
file:///private/var/mobile/Containers/Shared/AppGroup/189E5907-E6E4-4790-833F-06944E4FF5FF/data-model/TheDataModel
When I run the Watch app, the store location is different:
file:///private/var/mobile/Containers/Shared/AppGroup/55381E6D-410E-4322-93BA-64BD1933909E/data-model/TheDataModel
How do I get the Watch app to see the Core Data store from the main app? Do I have to replicate the store to that location every time something changes in the main app? Or do I have to go back to sending massive data sets as dictionaries/data via WatchConnectivity?
Post
Replies
Boosts
Views
Activity
This should be simple, but it's proving immensely difficult and annoying.
I have three targets in the project:
an iOS app written in Objective-C
a Watch App written in Swift/SwiftUI
a Widget Extension with my widgets and complications in, written in Swift/SwiftUI
I want to access the Core Data that's got all my app's data in it from the Widget Extension and Watch App. How do I do this? Every internet search result assumes all your targets are in Swift, and it's really not feasible for me to rewrite the entire app.
All three targets have the same App Group set up, and I can access UserDefaults in each target.
I have a CoreData.swift class in a shared folder, and it's a member of both the Watch App and Widget Extension targets. It has this in it:
lazy var persistentContainer: NSPersistentContainer = {
let storeURL = URL.storeURL(for: kAppGroup, databaseName: kCoreDataModel)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
let container = NSPersistentContainer(name: kCoreDataModel)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("CoreData: Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)")
}
})
return container
}()
and the storeURL extension is:
public extension URL {
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("CoreData: Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(kCoreDataModel).sqlite")
}
}
To be clear, the main iOS app (in Objective-C) has created the Core Data model etc., and it all works fine. I just want to be able to access the data in that model from the other targets. (I only need read access.)
When I deploy to a device the stack looks like this:
NSURL *dataModelFolderURL = [[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kAppGroup] URLByAppendingPathComponent:kCoreDataFolder];
Is: /private/var/mobile/Containers/Shared/AppGroup/9F329A90-C897-4AA2-87DF-D98A9E85356A/data-model
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:kCoreDataModel withExtension:@"momd"];
Is: file:///private/var/containers/Bundle/Application/CA9E3697-C4C6-48CB-89EA-CC441A6F81AF/MyApp.app/TheDataModel.momd/
// Wait until we have the store, or fetches will sometimes return no data
[self performSelectorOnMainThread:@selector(getStore) withObject:nil waitUntilDone:YES];
storeURL = file:///private/var/mobile/Containers/Shared/AppGroup/9F329A90-C897-4AA2-87DF-D98A9E85356A/data-model/TheDataModel
I've tried getting the Swift targets to look in a bunch of different paths, but nothing works. When I try to get a count of objects in the Core Data I always get zero results.
The second of those outputs is from the bundle. I guess that's because the data model is held within the iOS app's structure? Does that make any difference? Should it be held somewhere else? If so, what should it be?
(Minor rant: There really should be an option in Xcode that says, "This core data is shared here, here and here, and all you need to do is type CoreData.getSomeData() and you're done", but no, we have to write boilerplate code for everything.)
Anyway... any help is appreciated.
I have a NavigationSplitView in my Watch App for watchOS 10, but it seems to have padding on the items in the list.
A simplified version is:
struct ListView: View
{
let allItems: [ItemDetail] = [testData0]
@State var selectedItem: ItemDetail? = nil
var body: some View {
VStack(alignment: .center) {
NavigationSplitView {
List(allItems, selection: $selectedItem) { item in
NavigationLink {
ItemView(name: item.name)
} label: {
TableRowView()
}
}
} detail: {
if let selectedItem {
ItemView(name: selectedItem.name)
} else {
ItemUnavailableView()
}
}
}
}
}
struct TableRowView: View
{
var body: some View {
ZStack {
Rectangle().fill(.blue)
}
}
}
When I run this on a watchOS Simulator (any Series/size) the list has leading and trailing padding. On an Ultra 2 it's about 18 pixels each side. On a Series 8 it's about 15 pixels. Why is this here, and how do I get rid of It? I want the list to fill the width of the screen.
Look on this screenshot. The blue rectangle is in the horizontal centre of the row, and to the left and right is a grey area with curved corners, like the blue rectangle is on top of a wider grey rectangle with curved corners:
It's very difficult to see, so you may have to zoom in a bunch.
Anyway, how do I get rid of it? I can move my content by applying negative leading padding, like .padding(.leading, -18) but then it'll be different for each device, and I hate using magic numbers in my code.
In my Watch app on watchOS 9 I was using .foregroundColor(myColour) to colour the text in a widgetLabel on a corner complication like this:
let myColour: Color = functionThatReturnsAColorObjectConstructedLike Color.init(...) // green
.widgetLabel {
Text(myText)
.foregroundColor(myColour)
}
It worked fine; the widget label was green.
Now, in watchOS 10, I see that foregroundColor() is being deprecated in favour of foregroundStyle(), and I can use .foregroundStyle(.green), and - importantly - foregroundStyle() is only available on watchOS 10 and newer.
myColour is calculated depending on some other info, so I can't just write .green, and when I use .foregroundStyle(myColour) the widget label comes out as white every time, even if I set myColour = .green.
I think I have to use some sort of extension to pick the right combination, something like:
extension View {
func foregroundType(colour: Colour, style: any ShapeStyle) -> some THING? {
if #available(watchOS 10.0, *) {
return foregroundStyle(style)
} else {
return foregroundColor(colour)
}
}
}
// Usage
let myStyle: any ShapeStyle = SOMETHING?
...
.widgetLabel {
Text(myText)
.foregroundType(colour: myColour, style: myStyle)
It doesn't work. I just can't figure out what should be returned, nor how to return it. Any ideas?
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.
I've been happily building and deploying my app to my iPhone and Watch S8, and the app was ready to submit to App Store Connect last night. However, when archiving it I got an error saying that my DynamicEventSelectionIntent was in multiple extensions. It was, kind of. When I started working on the complications I copied the Widgets intents into the complications, and left the name the same, but they were not in multiple targets.
It looks like the info plist only had one item in the IntentsSupported (because they're the same name), so I decided to rename them so I had a widget one and a complications one.
The problem I have now is that I can't deploy to my iPhone and Watch anymore because I'm getting this error:
This app contains a WatchKit app with one or more Siri Intents app extensions that declare IntentsSupported that are not declared in any of the companion app's Siri Intents app extensions. WatchKit Siri Intents extensions' IntentsSupported values must be a subset of the companion app's Siri Intents extensions' IntentsSupported values.
All I've done is rename one intent, and locate every instance of it in the info plist files, and add the appropriate new one into the right places.
Here's what I've got. Main App contains Widget and WidgetIntentHandler, plus Watch App, which contains Complications and ComplicationsIntentHandler.
Target: Main app: (I've removed everything that has no bearing on extensions.)
Target: Widget:
Target: WidgetIntentHandler:
Target: Watch App:
Target: Complications:
Target: ComplicationsIntentHandler:
Please, can someone tell me what should and should not be in the various parts, as I've tried for 12 hours now and I cannot get this to deploy to my iPhone anymore :( Thanks.
I have a settings screen in my ObjC iOS app and in the SwiftUI Watch app.
When you change something in the Watch app's screen and click the Save button it sends a dictionary of the new values to the iOS app. The iOS app receives it and updates its settings in the defaults, and also updates the Settings screen in the iOS app if it's on-screen when you made the changes on the Watch. I post a notification to update the Settings screen, and this works fine.
I now need to handle it the other way round, i.e. you make changes in the iOS app, it sends a dictionary to the Watch, the Watch receives it and applies the settings, then updates the Watch app's Settings screen if it was open when the settings changed.
I added the setting values in the Watch app to my model data, and the settings do correctly update when they're changed by the iOS app. However, the controls within the Settings screen on the Watch don't update because they're tied to @State vars, not the modelData vars.
Here's some pseudocode to explain that:
var newMode: Int = modelData.mode // Get the initial value from the modelData
struct SettingsView: View {
@State private var mode = newMode
var body: some View {
Text("\(modelData.mode)") // This updates when settings are changed in the iOS app
Text("\(mode)") // This doesn't update
Text("\(newMode)") // This doesn't update
Picker("Mode", selection: $mode) {
Text("0")).tag(0)
Text("1")).tag(1)
Text("2")).tag(2)
}
.pickerStyle(.navigationLink)
.onChange(of: mode) { value in
updateMode(value)
}
...
}
func updateMode(_ value: Int) {
newMode = value
}
In the two Text() lines that don't update it's understandable because they aren't tied to anything external to the screen, but even if I add an extra .onChange like this it doesn't update the value in the UI:
.onChange(of: modelData.mode) { value in
mode = value
newMode = mode
}
The idea of the Settings screen is to allow the user to make changes, and either save their changes and apply them, or cancel and revert to the original settings.
I don't want to update the settings unless the user specifically presses the "Save" button, and I really don't want to remove the "Save" button and simply apply the changes each time a change is made as I'd be sending a dictionary of data to the iOS app each time - I have a stepper in there, which would send it every time the value changed, which could be tens of times.
So, the problem is that although the modelData.mode value is correctly changed by the receipt of new data from the iOS app, the Settings controls on the Watch do not update because they aren't tied to it. How do I get that to work? Is there a way of updating the newMode value and where it's displayed in the UI when the modelData changes?
I updated one of my apps with the released version of Xcode 14.
The minimum deployment version in the project and target are both iOS 13 (iPadOS 13).
I built and tested it with the Simulator from the released version of Xcode 14.
I built and tested it on some real devices with the released version of Xcode 14.
I archived it, validated it, and uploaded it to the App Store for review.
My "What's New in the Version" text says, "Supports all iPads compatible with iPadOS 13-15."
App Store Review are rejecting it because:
"Your app or its metadata contains references to a pre-release version of Apple software, products, or hardware. Apps with compatibility references to a pre-release candidate version of an Apple operating system or pre-released Apple products or hardware are not in compliance with the Apple Developer Program License Agreement."
or:
"We are unable to approve your app because it was built for iPadOS only using a pre-release version of iPadOS 16. While your app may not support new features in iPadOS 16, we are only accepting iOS 16, iPadOS 15.7, and universal apps at this time. To resolve this issue, resubmit your app built for a released version of iOS or iPadOS."
I checked, and there was a random provisioning profile that seems to have been put in by having Xcode 14 update a copy of the original project to the recommended project settings. That seems to be a bug. So, I removed that profile and built again. App Store Review are still rejecting it.
How do I resolve this? I've searched the app, and there is nothing in there targeting iPadOS 16. Nothing at all.
I'm minded to think that because I have the latest Xcode-beta installed on my Mac, that it's using/writing data to a shared location, and when building with the released version of Xcode 14, something is being contaminated in that way?
Any ideas?
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.
I recently raised this post explaining how I couldn't seem to get watchOS 9 complications to work, and I've figured out a partial fix.
The original post details the issues with complications - and some are still valid - but this fix applies to both my complications and Home Screen / Lock Screen widgets.
I was following the various WWDC 2020/2022 videos and the Emoji Rangers sample code, adding bits here and there, and assuming they were completely valid. Sadly, this bit of code in the widget's dynamic intents IntentTimelineProvider getTimeline really just banjaxed everything:
// Create entries for one day, 15 minutes apart
let currentDate = Date()
for minuteOffset in stride(from: 0, to: 60 * 60 * 24, by: 15) {
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: currentDate)!
entries.append(EventEntry(date: entryDate, event: event))
}
If I remove that, and generate a different timeline with specific dates and times (for example: now, in 10 mins, in 2 hours, in a day, etc.) the complications appear correctly, as do Home Screen and Lock Screen widgets.
The outstanding issues with complications are:
The previews all use the same data, but getSnapshot() is supposed to return the data specific to that event from the configuration, i.e. if let theId = configuration.event?.identifier. "Christmas" is correct, but "Gallery Opening" is using Christmas's data.
Once I've selected the event I want to use in a complication the edit screen shows it as totally blank, not even a placeholder:
I hope this little fix works for you guys. And, if you know how to fix the above issues, let me know.
(iOS 16.1 beta 1, Xcode 14.1 beta 1)
... all talking about customer care numbers.
Maybe Apple should make these Dev Forums just for signed-in Developers? If a Developer spams, they can be blocked. But if just anyone can register and post, they can spam, get banned, then start a new account.
Or, Apple should implement some way of blocking the spam before it gets posted.
I really don't understand what these spammers think they're getting out of doing this? Who is going to look through the forums and think, "Oh, a customer care number for something I've never heard of? I should phone that immediately!" Really does prove that spammers are dumb.
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.
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 understand that complications in WidgetKit and watchOS 9 are different to the old ClockKit way, and I'm trying to populate the getSnapshot, getPlaceholder and getTimeline functions appropriately for the new method.
I'm using dynamic intents, and I've got that working and providing a set of items I want the user to choose from, but at the various stages of selecting and adding a complication to the watch face, nothing I do seems to work properly on a device, and sometimes in the Simulator.
On the Simulator:
1: The previews all use the same data, but getSnapshot() is supposed to return the data specific to that event from the configuration, i.e. if let theId = configuration.event?.identifier. "Christmas" is correct, but "Gallery Opening" is using Christmas's data.
Are you only supposed to show ONE particular bit of data, like some dummy preview data or something? I can do that, but the complication picker then just looks really boring, with just the text below being different. I note that Apple's weather complications show different data. How did they do that?
2: You can see that once I pick an event it's displayed correctly in the Watch face edit screen:
The "72d" circular complication is the "New York" event, and the rectangular one is correctly using the data for the "London Party!".
3: Once I've selected the events I want to use in those complications the edit screen shows them as totally blank, not even a placeholder. Which method gives us that preview?
4: Once I confirm the edits and return to the Watch face, the complications appear correctly:
Note: This is all from the Simulator (apart from the image of Apple's Weather complications). This stuff barely works on a device. Half the time the complications are all placeholders, and half the time they're using the wrong data! If it works in the Simulator, it should work on the phone, or there is no point in giving us the Simulator if the results are different.
So basically:
Q1. How do I use different data in the complications picker?
Q2. How do you get the previews to show when you're in edit mode?
Q3. Does anyone know how to get print() statements to work when I'm running the Complications scheme? I could answer all these questions myself if I could output some debug info to the console, but all I see is the output from the Watch App target...
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?