Post

Replies

Boosts

Views

Activity

Reply to Backward compatibility with iOS13
I'm having this problem but when trying to add an iOS 14 widget to my iOS 13 Objective-C app in Xcode 12 beta 6. I've added the new widget as a .swift file, but when I try to build it, it fails because: 'main()' is only available in iOS 14.0 or newer The target for the app is iOS 13, and the target for the widget is iOS 14. The iOS 13 widget still works perfectly fine. If I remove @main from the widget swift file I can see the preview correctly in the SwiftUI in Xcode, but when I add it back, it fails to build. How do I create a new widget for an Objective-C app?! (This app has too much code in it to convert to Swift.)
Sep ’20
Reply to Removing MagicalRecord from app, can't see data via Core Data methods
Updated code. In AppDelegate.m I do this: self.dataController = [[DataController alloc] init]; self.viewContext = self.dataController.managedObjectContext;	// This is an NSManagedObjectContext property This is the init method of my DataController class: (id)init { self = [super init]; if(!self) return nil; NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MyModel" withExtension:@"momd"]; NSAssert(modelURL, @"Failed to locate momd bundle in application"); NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; NSAssert(managedObjectModel, @"Failed to initialise Managed Object Model from url: %@", modelURL); NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel]; NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [managedObjectContext setPersistentStoreCoordinator:coordinator]; [self setManagedObjectContext:managedObjectContext]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ NSPersistentStoreCoordinator *psc = [[self managedObjectContext] persistentStoreCoordinator]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *documentsURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; NSURL *storeURL = [documentsURL URLByAppendingPathComponent:@"MyModel.sqlite"]; NSDictionary *options = @{NSInferMappingModelAutomaticallyOption : @(true), NSMigratePersistentStoresAutomaticallyOption : @(true)}; NSError *error = nil; NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]; if(!store) { NSLog(@"Error initialising Persistent Store: %@\n%@", [error localizedDescription], [error userInfo]); abort(); } }); return self; } All this code runs without error. A context is created and I can read and write to it, BUT I still cannot see the original data stored by the MagicalRecord version. Why can't I see that data? I cannot find ANY info on the internet about accessing your data store if you move away from MagicalRecord and go with Core Data, which seems like something hundreds of people will have done. Please help. I can't spend more days on this - seemingly - simple problem.
Sep ’20
Reply to Removing MagicalRecord from app, can't see data via Core Data methods
Well, that wasn't easy. The fix - for anyone out there who needs to do this - is to find out where MagicalRecord has saved your data store, and either tell Core Data that location, or to move the MR store file(s) to the location Core Data wants to use by default. In my case, MR had stored them inside <my app's folder>/Library/Application Support/<my app's name>/ Core Data was looking in <my app's folder>/Documents/ Simple fix, but gods this took ages to figure out. Would've been lovely for the MagicalRecord developers to have provided some easy instructions on how to migrate away from MR after they shut it down ¯&#92;&#95;(ツ)_/¯
Sep ’20
Reply to is there a demo app for the "Add configuration and intelligence to your widgets" WWDC session
I'm going to reply in a few stages, as there's a limit to what I can post... The downloadable code is here: h t t p s : / / developer.apple.com/documentation/widgetkit/building <underscore> widgets <underscore> using <underscore> widgetkit <underscore> and <underscore> swiftui (Sorry about that, can't put a url from their own website in here...?!) I've used it to successfully figure out how to get a widget to work, and with Intents, too. I won't show you how to create the Intents. I just opened the sample code and created the same intent definition thing within my own app. Note that you need to create a new target. Mine is called WidgetIntentHandler, and it has just an info.plist, WidgetIntentHandler.entitlements file and the IntentHandler.swift file. If I recall correctly it was generated, but I changed one line: import Intents class IntentHandler: INExtension, DynamicEventSelectionIntentHandling { func provideEventOptionsCollection(for intent: DynamicEventSelectionIntent, with completion: @escaping (INObjectCollection<Event>?, Error?) -> Void) { /* let events: [Event] = EventDetail.availableEvents.map { event in */ /* I removed that line because I want the updated events from my defaults */ let events: [Event] = getAllEventsFromDefaults().map { event in let event = Event(identifier: event.name, display: event.name) return event } let collection = INObjectCollection(items: events) completion(collection, nil) } override func handler(for intent: INIntent) -> Any { return self } }
Sep ’20
Reply to is there a demo app for the "Add configuration and intelligence to your widgets" WWDC session
If it helps, here's some code and a little explanation in the comments. Note, my widget uses things called Events, like the EmojiRanger widget uses Characters. I also have one main event called the Cover Event. A note about EventDetail.getUpdatedEventsFromDefaults(), which is called in a few places: In the EmojiRanger widget, the CharacterDetail struct has a line: static let availableCharacters = [panda, egghead, spouty] I don't have any default events (panda, egghead, spouty) to put in my array, so I replaced this with: static var availableEvents: [EventDetail] = getAllEventsFromDefaults() getAllEventsFromDefaults() is in a separate MyWidgetUtils.swift file. It gets an NSDictionary from my defaults, converts each record into an EventDetail object, and returns an array of them, so my availableEvents array has the right data from the start. You might have it grab data from an API. I then have a func in EventDetail.swift to re-populate the array. This is called in the main widget code, as you'll see if you read on, i.e. EventDetail.getUpdatedEventsFromDefaults(): static func getUpdatedEventsFromDefaults() { availableEvents = getAllEventsFromDefaults() }
Sep ’20
Reply to is there a demo app for the "Add configuration and intelligence to your widgets" WWDC session
MyWidget.swift: @main struct WhensThatWidget: Widget { public var body: some WidgetConfiguration { IntentConfiguration(kind: "com.company.product.widget", intent: DynamicEventSelectionIntent.self, provider: Provider()) { entry in MyWidgetEntryView(entry: entry) } .supportedFamilies([.systemSmall, .systemMedium]) .configurationDisplayName("Title in the Preview screen") .description("Description under the preview title") } } struct EventEntry: TimelineEntry { public let date: Date let relevance: TimelineEntryRelevance? let event: EventDetail&#9;/* Like CharacterDetail */ } struct Provider: IntentTimelineProvider { typealias Intent = DynamicEventSelectionIntent&#9;/* The Event intent type */ public typealias Entry = EventEntry&#9;/* The struct above */ &#9;&#9;/* The placeholder is what's seen when you go to add the widget. It needs info up-front, so either preview data or an actual Event. */ func placeholder(in context: Context) -> EventEntry { EventDetail.getUpdatedEventsFromDefaults() let coverEventDetail: EventDetail? = getCoverEventFromAllEvents()&#9;/* This gets a specific event I want to put in the preview, rather than fake preview data. */ if(coverEventDetail == nil) { &#9;&#9;/* There is no cover event, so return preview data */ return EventEntry(date: Date(), relevance: nil, event: .previewData) } else { &#9;&#9;/* We have a cover event, so we can use that. It means that when the user goes to add the widget, the preview screen will have their actual cover event. */ return EventEntry(date: Date(), relevance: nil, event: coverEventDetail!) } } &#9;&#9;/* The snapshot is where the widget gets its running data from. Think of it as a timeline of data. You start with data A, then at 10am it displays data B, etc. &#9;&#9;&#9; My app doesn't need to change the data, so it doesn't here. The code in the EmojiRanger widget is easy enough to understand. */ func getSnapshot(for configuration: DynamicEventSelectionIntent, in context: Context, completion: @escaping (EventEntry) -> Void) { EventDetail.getUpdatedEventsFromDefaults() let coverEventDetail: EventDetail? = getCoverEventFromAllEvents() var entry: EventEntry if(coverEventDetail == nil) { /* No cover event means no events at all */ if(context.isPreview) { /* This lets you check if you're in the preview screen. If so, we can use the cover event, or the preview data. */ entry = EventEntry(date: Date(), relevance: nil, event: .previewData) } else { /* Not in a preview, and no cover event, so return an error. &#9; This is an EventDetail object stored in the EventDetail.swift struct. It shows a specific error if there are no events. */ entry = EventEntry(date: Date(), relevance: nil, event: .errorData) } } else { /* Otherwise, return the cover event details */ entry = EventEntry(date: Date(), relevance: nil, event: coverEventDetail!) EventDetail.setLastSelectedEvent(eventName: coverEventDetail!.name) } completion(entry) } &#9;&#9;/* This is where the widget gets its running data from. You start with data A, then at 10am it displays data B, etc. &#9;&#9;&#9; Once the timeline runs out of data, you can ask the main app for new data. My app doesn't need to change the data, so it doesn't here. The code in the EmojiRanger widget is easy enough to understand. */ func getTimeline(for configuration: DynamicEventSelectionIntent, in context: Context, completion: @escaping (Timeline<EventEntry>) -> Void) { EventDetail.getUpdatedEventsFromDefaults() /* Get the event from its name. This simply runs through all available events and gets the object that has this name. */ let selectedEvent = event(for: configuration) let relevance = TimelineEntryRelevance(score: 10) /* Create and return the EventEntry for the selected event */ EventDetail.setLastSelectedEvent(eventName: selectedEvent.name) let entry = EventEntry(date: selectedEvent.date, relevance: relevance, event: selectedEvent)&#9;/* Using the data we just got from above. */ let timeline = Timeline(entries: [entry], policy: .never)&#9;/* Don't need to change the data, so just pass an array of this one EventEntry. */ completion(timeline) } /* This is how we get the specific EventDetail for the name passed in from the intent. I've left simple NSLog debugging lines in in case you hit a problem. */ func event(for configuration: DynamicEventSelectionIntent) -> EventDetail { /* Gets an event object from its name, then sets this event as the last selected one. &#9; Returns the errorData if the event name couldn't be found. */ if let actualName = configuration.event?.identifier { NSLog("Event name = %@", actualName) if let event = EventDetail.eventFromName(eventName: actualName) { EventDetail.setLastSelectedEvent(eventName: actualName) NSLog("Found event with name = %@", actualName) return event } else { NSLog("Couldn't get event from event name: %@", actualName) } } else { NSLog("actualName not valid") } EventDetail.getUpdatedEventsFromDefaults() let coverEventDetail: EventDetail? = getCoverEventFromAllEvents() if(coverEventDetail == nil) { NSLog("Returning .errorData") return .errorData } else { NSLog("Returning coverEventDetail") EventDetail.setLastSelectedEvent(eventName: coverEventDetail!.name) return coverEventDetail! } } }
Sep ’20
Reply to is there a demo app for the "Add configuration and intelligence to your widgets" WWDC session
Still in MyWidget.swift: struct MyWidgetEntryView: View { var entry: Provider.Entry @Environment(\.widgetFamily) var family @ViewBuilder var body: some View { if(entry.event.category == "ERROR") {&#9;/* Here, I'm checking if the data the widget has been passed is the errorData. If so, I know that it has a category value of "ERROR" so I'm able to show an error view instead of the actual widget. This is simply a different View object entirely with specific Text objects. */ switch family { case .systemSmall: EventNotSelectedView() case .systemMedium: EventNotSelectedView() default: EventNotSelectedView() } } else { switch family { /* Just return the actual widget. */ case .systemSmall: SmallWidgetView(event: entry.event) case .systemMedium: MediumWidgetView(event: entry.event) default: EventNotSelectedView() } } } } struct SmallWidgetView: View { var event: EventDetail var body: some View { /* Your actual widget view for small */ } } struct MediumWidgetView: View { var event: EventDetail var body: some View { /* Your actual widget view for medium */ } } struct EventNotSelectedView: View { var body: some View { /* My error view */ } } struct PlaceholderView : View { var body: some View { MyWidgetEntryView(entry: EventEntry(date: Date(), relevance: nil, event: .previewData))/* .redacted(reason: .placeholder) */ /* You can show the widget without any actual data, and a bunch of redacted rounded rectangles if you uncomment that bit on the line above. */ } } /* Shows previews in the SwiftUI of the types above */ struct MyWidget_Previews: PreviewProvider { static var previews: some View { Group { PlaceholderView().previewContext(WidgetPreviewContext(family: .systemSmall)) MyWidgetEntryView(entry: EventEntry(date: Date(), relevance: nil, event: .previewData)).previewContext(WidgetPreviewContext(family: .systemSmall)) MyWidgetEntryView(entry: EventEntry(date: Date(), relevance: nil, event: .previewData)).previewContext(WidgetPreviewContext(family: .systemMedium)) EventNotSelectedView().previewContext(WidgetPreviewContext(family: .systemSmall)) EventNotSelectedView().previewContext(WidgetPreviewContext(family: .systemMedium)) } } } Hope this helps!
Sep ’20
Reply to How to use .widgetURL?
Right, I have no idea whatsoever if this is the right way of doing it, but it seems to work, so here it is for others who have this issue. In my MyWidget.swift file, I have this constant and this function: let activity: NSUserActivity = NSUserActivity.init(activityType: "ViewEventIntent") func updateEventToOpenInMainApp(event: EventDetail) -> URL! { activity.title = "widget_PreviewEvent:" activity.userInfo = ["widget_PreviewEvent:" : event.name] activity.becomeCurrent() return URL(string: "widget_PreviewEvent:" + event.name) } In my widget's View: struct MyWidgetView: View { var event: EventDetail /* This object contains the name of the event */ var body: some View { &#9;&#9;&#9;ZStack { &#9;&#9;&#9;&#9;&#9;GeometryReader { g in &#9;&#9;&#9;&#9;&#9;&#9;&#9;Link(destination: updateEventToOpenInMainApp(event: event)) { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;HStack { &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;... &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;} /* HStack */ &#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;&#9;.widgetURL(updateEventToOpenInMainApp(event: event)) &#9;&#9;&#9;&#9;&#9;&#9;&#9;} /* Link */ &#9;&#9;&#9;&#9;&#9;} /* GeometryReader */ &#9;&#9;&#9;} /* ZStack */ &#9; } } It doesn't seem to work properly unless I put both the .widgetURL and the Link() in there ¯\&#92;&#95;(ツ)_/¯
Sep ’20
Reply to WidgetKit refresh policy
This seems particularly rubbish, to be honest. They've given us a countdown timer: Text.init(aDate, style: .timer) You can't style it, so it counts down in hours, minutes and seconds regardless of how many days there are, i.e. "36:34:56" instead of something more friendly like"1 day 12:34:56" or "1d:12h:34m:56s" etc. Once that countdown timer hits 0:00:00 and you want the timeline to reload and do something, you have to wait for about 5 minutes because TimelineReloadPolicy.atEnd and .after(date:) don't actually trigger at the time you set them to. Is there much point having these reload policies if they don't trigger when you need them to? Are these things beyond the abilities of the Apple developers, or is there some special setting I've missed?
Oct ’20
Reply to Can't Share Data Between Main App And Extension
Have you tried synchronising the defaults after changing them in the main app, to ensure they're saved? If that doesn't fix it, can you NSLog() the UserDefaults(suiteName: AppGroup) to see if the data is in there? Can you write and read different data types other than "data"? Maybe save an Int in there? Also, as an aside, the AppGroup doesn't change so you can use a let: let kAppGroup: String = "group.someGroup" (I use "kBlahBlahBlah" to denote a constant).
Oct ’20
Reply to Widget is cleared if the intent identifier changes: Easy fix
My widget was based on the EmojiRangerWidget sample code, which contained this: func character(for configuration: DynamicCharacterSelectionIntent) -> CharacterDetail { &#9;&#9;if let name = configuration.hero?.identifier, let character = CharacterDetail.characterFromName(name: name) { &#9;&#9;&#9;&#9;/* Save the last selected character to our App Group. */ &#9;&#9;&#9;&#9;CharacterDetail.setLastSelectedCharacter(heroName: name) &#9;&#9;&#9;&#9;return character &#9;&#9;} &#9;&#9;return .panda } My event() function is just based on that. You don't need it; it's just the way I get the event for my case. Since I get the event in two places it saves duplicating the code. In getSnapshot() and getTimeline(): let selectedEvent = event(for: configuration) That passes the intent configuration to the event() function, and returns the first event where its uniqueId matches the one provided by the intent. So, the user still selects the event's name when they edit the widget (as that's way more user-friendly), but what we're actually basing the widget on is the uniqueId instead. Does that make sense?
Oct ’20
Reply to What is the purpose of getSnapshot method from WidgetKit?
Think of the lifecycle of the Widget as follows: The user chooses to add a widget to their home screen so they press the + button, and they select your app. They haven't yet added the widget, they're just looking at it. At this point, you should be showing a placeholder representation of your widget, so return something from the placeholder() func. For my widget, I'm just returning a fake event (an event is my object type) that gets rendered by iOS and shows as a fake event in the preview. 2. The user taps the "Add Widget" button. Now, the widget is gonna flip around and settle on your home screen. Here, is where the getSnapshot() func comes in - it is shown whenever there's a transition. In my widget, I check if the context.isPreview, and if so, I display the fake data again. I also check if the configuration.event == nil. If so, at this point the user hasn't chosen an event to display (from the dynamic selection intent) so I'm showing a widget that tells the user to edit the widget and select an event. You could, however, set a default event and have that returned here instead. Finally, if it's not context.isPreview and configuration.event != nil I then get the actual event the user has chosen so the transition shows that event. 3. The widget is now on-screen. In my case it's telling the user to edit the widget and choose an event. (If you've set a default in the IntentHandler then they wouldn't see this bit asking to edit the widget.) They edit the widget and choose an event. Now, getSnapshot() is called again to show the transition, but this time it has the actual event (configuration.event != nil) so it displays the actual event in the transition. getTimeline() is also called so it can provide the timeline of when the widget should refresh. So, in your case just think about when you want to show actual data. Do you want actual data showing in the placeholder? If so, remember that the placeholder should return quickly, so show *something* then update it with the actual data. If you have that data, you can cache it and use it in getSnapshot() and getTimeline().
Oct ’20