Right, this is getting on my nerves now. iOS app installed on iPhone via Xcode. Watch app installed on Watch via Xcode. Both apps are running and are in the foreground.
iOS app launches on iPhone and reports:
WCSession.isSupported = YES
theDelegate.session.isPaired = YES
theDelegate.session.watchAppInstalled = NO
theDelegate.session.activationState = Activated
I press a button in the Watch app. It reports:
session == activated and reachable
iOS app delegate receives a message from the Watch app:
didReceiveMessage (from Watch): message = {
action = giveMeUpdatedItems;
}
The apps must be installed on the devices in order for the Watch app to have used sendMessage (which is only available if the session is reachable, which it is).
iOS app delegate passes that through as a notification to another bit of code that collates the info and sends it back to the Watch app.
watchNotificationUpdateData; userInfo = {
action = giveMeUpdatedItems;
}
That bit of code in the iOS app checks whether we can send data to the Watch app, and doesn't send the data because:
WCSession.isSupported = YES
theDelegate.session.isPaired = YES
theDelegate.session.watchAppInstalled = NO
theDelegate.session.activationState = Activated
If I remove the check for watchAppInstalled, I get this:
Error sending Watch application context: Watch app is not installed.
{
NSLocalizedDescription = "Watch app is not installed.";
NSLocalizedRecoverySuggestion = "Install the Watch app.";
}
I've deleted and reinstalled the app on both devices countless times. I've rebooted the devices, plus the Mac. I've reinstalled Xcode. I've cleaned builds. I've deleted DerivedData. And still it says the companion app isn't installed.
Post
Replies
Boosts
Views
Activity
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?
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?
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?
In my Watch app, the WatchDelegate class is the WCSession delegate and handles transferring data with the iOS app.
When the delegate receives some data it stores it in the UserDefaults.
When the Watch app is launched it reads the existing data stored in the defaults and creates the view.
I have a WatchApp file which contains this:
@main
struct WatchApp: App {
var body: some Scene {
WindowGroup {
if(getItems().count == 0) {
NoEventsView()
} else {
ItemsListView(available: getItems())
}
}
}
}
As you can see, if there are no events in the defaults it shows the NoEventsView; and if there are some it shows the EventsListView.
When the Watch delegate receives a change in the events, I need to refresh this view. The delegate can receive zero or more events.
How on Earth do I do that?
In iOS I could call a method to reload a table of data, or post a notification to another view controller to do that, but in the Watch and with SwiftUI there doesn't seem to be any obvious way of refreshing a view.
Is there any way of telling the App struct to refresh, or a particular view? For example, if I extracted the if statement into its own "struct WhichView: View", could I tell that to refresh?
I've read a LOT on the net these past few days on @State vars, @ObservedObject, @Published etc, but nothing I've seen works, or is far too weird and complex for my situation.
I literally just want WatchApp or WhichView to redraw when I tell it to. How do I do that?
Thanks.
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.
I have an Objective-C app with a new iOS 14 Widget (a Swift target), and I want to use a few methods from an Objective-C class in my Swift target, but am having one issue with it.
The class is called SharedUtils, and the methods are marked as class methods in the .h file. I don't create instances of this class, I just use the methods.
One of the methods I want to use is:
(NSString *)symbolForCategoryId:(NSNumber *)categoryId;
This works fine in the Objective-C main app, i.e.:
[SharedUtils symbolForCategoryId:myId];
In the Swift target, I start typing:
SharedUtils.symbol ...
and Xcode autocompletes to:
SharedUtils.symbol(forCategoryId: NSNumber(value: myId))
If I don't set the bridging header in the widget target's build settings, or I remove the import from within the bridging-header.h file, Xcode cannot autocomplete the line, so I know that the Swift target can see the SharedUtils code. All good so far.
However, when trying to build the code, Xcode errors with:
Cannot find type 'SharedUtils' in scope Any idea how to get this to work? I've used Objective-C code in a Swift app before and not had an issue, but the code was all in the same target, so I'm thinking it's something to do with that?
I just experienced this problem, and thought it might help someone else out later.
User adds a widget to their home screen. The widget has dynamic options, so they edit the widget and select a value. In my case, these are events, so the user selects their "Paris Trip". The widget shows the Paris Trip data and all is well.
The user then edits the event in the main app and renames it to "Paris Vacation".
The widget on the home screen clears because the identifier that that widget was created for (i.e. the name of that event) no longer exists.
Here's my fix...
The default settings for a dynamic selection intent's Type is to have two properties: identifier and displayString. The settings for these two are greyed-out (because they shouldn't be changed).
But, you can add a new property into there, so I added a unique identifier for the event, "uniqueId".
Now, here's a gotcha. This doesn't show up anywhere until the supporting files for the intent are recreated in the background. The easiest way I found of forcing this was to quit and restart Xcode.
Here's my provideEventOptionsCollection() func in IntentHandler.swift:
func provideEventOptionsCollection(for intent: DynamicEventSelectionIntent, with completion: @escaping (INObjectCollection<Event>?, Error?) -> Void) {
let events: [Event] = EventDetail.availableEvents.map { event in
let returnEvent = Event(
identifier: event.name,
display: event.name
)
returnEvent.uniqueId = event.uniqueId. /* <-- THIS IS THE UPDATE */
return returnEvent
}
let collection = INObjectCollection(items: events)
completion(collection, nil)
}
As you can see, you can access the uniqueId property of the event object and add it to the event you're returning.
In my IntentTimelineProvider the event func is now this:
if let uniqueId = configuration.event?.uniqueId {
if let event = EventDetail.eventFromUniqueId(uniqueId: uniqueId) {
return event
}
}
and the eventFromUniqueId() func in EventDetail:
static func eventFromUniqueId(uniqueId: String) -> EventDetail? {
return (availableEvents).first(where: { (event) -> Bool in
return event.uniqueId == uniqueId
})
}
Hope this helps someone out there.
I've read the documentation Apple have provided on the "Creating a Widget Extension" page, but it doesn't seem to match what's happening.
My app lets users create events, and the widget should let users choose an event to display. You can have multiple widgets, all displaying different events.
When the user installs the app for the first time and launches it, there are no events, so trying to add a widget will display some preview data (a fake event) to give the user an idea of what the widget will look like.
In the placeholder func I return the preview data, and it does show this fake event when you're in the preview screen (the panel with the "Add Widget" button on it). placeholder() is called twice because I'm using both systemSmall and Medium widget sizes.
getSnapshot() is then called, and the context.isPreview is true or the 'configuration' parameter is nil so I return the preview data again. This works fine.
Once the user adds the widget to the Home Screen, they get a different view that tells them to edit the widget (because no event has actually been chosen yet). This is set up in both getSnapshot() and getTimeline().
So, the user sees this widget telling them to edit it and select an event, or tap it to launch the app and create an event. The user tries to edit the widget and there are no events listed. Fine. All works well so far.
The user taps the widget and adds an event in the app. They go back to the Home Screen and edit the widget. Their event is present, so they select it.
getTimeline() is called, which calls a function to get the event the user selected. This works properly and returns the correct event object for the selected event, e.g. "Paris Trip".
getTimeline() then confirms it has the event and sets up the timeline, five entries, 5 minutes apart.
But the widget on the Home Screen still displays the preview data. I can wait five minutes, an hour, a day and it will not change.
So, based on the above and the following code, how do I get the widget to actually work?
func placeholder(in context: Context) -> EventEntry {
return entryPreviewData
}
func getSnapshot(for configuration: DynamicEventSelectionIntent, in context: Context, completion: @escaping (EventEntry) -> Void) {
var entry: EventEntry
if(context.isPreview || configuration.event == nil) {
entry = entryPreviewData
} else {
EventDetail.getUpdatedEventsFromDefaults()
if(EventDetail.availableEvents .isEmpty) {
entry = entryNoEventsData
} else {
let selectedEvent = event(for: configuration)
if(selectedEvent.type == kEventTypeNoEvents) {
entry = entryNoEventsData
} else if(selectedEvent.type == kEventTypeError) {
entry = entryErrorData
} else {
entry = EventEntry(date: Date(), relevance: nil, event: selectedEvent)
}
}
}
completion(entry)
}
func getTimeline(for configuration: DynamicEventSelectionIntent, in context: Context, completion: @escaping (Timeline<EventEntry>) -> Void) {
var entries: [EventEntry] = []
var timeline: Timeline<EventEntry>
if(context.isPreview || configuration.event == nil) {
entries.append(entryNoEventsData)
timeline = Timeline(entries: entries, policy: .after(Date().advanced(by: 15)))
} else {
let selectedEvent = event(for: configuration)
if(selectedEvent.type == kEventTypeNoEvents) {
/* There are no events, so return the no events data */
entries.append(entryNoEventsData)
timeline = Timeline(entries: entries, policy: .after(Date().advanced(by: 15)))
} else if(selectedEvent.type == kEventTypeError) {
/* Received error data, so return the error data */
entries.append(entryErrorData)
timeline = Timeline(entries: entries, policy: .after(Date().advanced(by: 15)))
} else {
var entryDate: Date = Date()
for _ in 0 ..< 5 {
entryDate = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
entries.append(EventEntry(date: entryDate, relevance: relevance, event: selectedEvent))
}
timeline = Timeline(entries: entries, policy: .atEnd)
}
}
completion(timeline)
}
I have a widget that shows one "event" at a time, so you can have as many copies of the widget as you like, each one showing a different event. This all works fine.
I'm trying to get the main app to open onto a specific page when the user taps a specific widget. For example, the user taps the "Birthday" widget, and the app should show the Birthday page.
In my widget view, I have this on the View object:
.widgetURL(URL(string: "widget_PreviewEvent:" + event.name))
This sends the text "widget_PreviewEvent:Birthday" to the app. The App's delegate implements the method:
application:openURL:options:
I have it logging this out:
Delegate: openURL: url = 'widget_PreviewEvent:Birthday'
Main view: showScreen: 'widget_PreviewEvent:Birthday'
Found eventName from url = 'Birthday'
Right, so that works fine. I can tell the app to open the Birthday event, and it does.
However, sometimes, and especially more so on the medium widget, this method is called instead:
application:continueUserActivity:restorationHandler:
and the userActivity object is this:
userActivity = DynamicEventSelectionIntent, userActivity.userInfo = {
WGWidgetUserInfoKeyFamily = systemMedium;
WGWidgetUserInfoKeyKind = "com.company.product.widget"; /* example */
}
"Birthday" isn't mentioned in that object anywhere, so I can't open the Birthday event. How do I fix this?
Does anyone have complete instructions - including ALL steps and information on everything that needs to be done - and example code that we can use to reload SwiftUI widgets from an Objective-C main app?
It's all well and good telling us to look at a page that gives us generic info on importing Swift header files, but that doesn't really help; it just gives us yet another thing to go searching for actual info on.
There are no classes in my widget code; just structs, extensions and some funcs to figure out dates, so there's nowhere to make anything public or to add "@objc" to.
If it is possible to reload a SwiftUI widget from an Objective-C main app, then Apple should provide us with such code. That would be really simple for them to do. Yeah, widgets are new, which is why the source of such information should be Apple themselves, not from random pages you find on the internet.
These forums don't have anywhere near as many responses from Apple engineers as are required. (I asked five days ago how to format a Text.init(theDate, style: .timer) - no response.)
Yes, this may seem a little like a rant, but why doesn't Apple give us example code for their new features? Not every app has been written in Swift, and for many it would be too costly to convert.
In this thread: https://developer.apple.com/forums/thread/650535 an Apple Developer Tools Engineer wrote:
66
You can use SwiftUI's new Text initializer to display the relative time to a certain timestamp. For more information you can checkout: https://developer.apple.com/documentation/swiftui/text/init(_:style:)
This Text view updates automatically without you needing to provide new entries for every second.
99
This does work, but there's no way of formatting the result, so a date 24 days 8 hours into the future shows as "584:00:00" and counts down like "583:59:59", "583:59:58" etc.
I'd like it to say something like "24 days, 08:00:00". Is there any way of doing that?
I have an app that currently uses MagicalRecord to store its data. As of 2017, MR is toast, so I've decided to remove it and go with standard Core Data (since it's been updated and is easier to use now).
My problem is that I can't seem to get the new Core Data methods to see the data in the store. Here's how it's set up:
Old version (MagicalRecord):
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:@"MyModel"];
New version (Core Data):
self.persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MyModel"];
[self.persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *description, NSError *error) {
if(error != nil) {
NSLog(@"Failed to load Core Data stack: %@", error);
abort();
} else {
self.viewContext = self.persistentContainer.viewContext;
}
}];
If I load the old version into Xcode 12b6 and run it on the iOS 13.7 Simulator I can read, update and delete data in the store, and it all works properly as before.
When I load the new code into Xcode 12b6 and run it in the same iOS 13.7 Simulator, the app launches but there is no data in the store.
Any data in the old version is not seen in the new version, and vice versa. It's like they're two different stores of data.
The new version can see the store because I change the name to, say, "MyModel99" it errors saying it can't find MyModel99.
I can re-launch the old code into the Simulator and the data is there, so I think the data model is all fine, it's just that I can't seem to get the new code to read that data.
Any ideas? I'm guessing there's something I'm missing when setting up the persistent container etc.? I guess the question is: how do you migrate data from MR to Core Data? If MR is basically a wrapper for CD, why can't I see the data in it?
Thanks.