This is another issue found after changing to use a @StateObject for my data model when populating a List.
Previous issue is here: https://developer.apple.com/forums/thread/805202 - the entire List was being redrawn when one value changed, and it jumped to the top.
Here's some code:
struct ItemListView: View {
@State private var showAlert: Bool = false
...
fileprivate func drawItemRow(_ item: ItemDetails) -> some View {
return ItemRow(item: item)
.id(item.id)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
RightSwipeButtons(showAlert: $showAlert, item: item)
}
}
...
List {
ForEach(modelData.filteredItems.filter { !$0.archived }) { item in
drawItemRow(item)
}
}
...
.alert("Delete Item"), isPresented: $showAlert) {
Button("Yes, delete", role: .destructive) {
deleteItem(item.id) // Not important how this item.id is gained
}
Button("Cancel", role: .cancel) { }
} message: {
Text("Are you sure you want to delete this item? You cannot undo this.")
}
}
struct RightSwipeButtons: View {
@Binding var showAlert: Bool
var body: some View {
Button { showAlert = true } label: { Label("", systemImage: "trash") }
}
}
The issue I have now is that when you swipe from the right to show the Delete button, and tap it, the alert is displayed but the list has jumped back to the top again. At this point you haven't pressed the delete button on the alert.
Using let _ = Self._printChanges() on both the ItemsListView and the individual ItemRows shows this:
ItemsListView: _showAlert changed.
ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor changed.
So yeah, that's correct, showAlert did change in ItemsListView, but why does the entire view get redrawn again, and fire me back to the top of the list?
You'll notice that it also says _accessibilityDifferentiateWithoutColor changed on the ItemRows, so I commented out their use to see if they were causing the issue, and... no.
Any ideas?
(Or can someone provide a working example of how to ditch SwiftUI's List and go back to a UITableView...?)
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
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.
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?
Topic:
App Store Distribution & Marketing
SubTopic:
App Store Connect
Tags:
App Review
App Store
App Store Connect
Xcode
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 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.
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'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?
Current project structure:
Main iOS app (targets iOS 17).
Widget Extension (targets iOS 17).
Watch app (targets watchOS 10).
Complications Extension (a Widget Extension, targets watchOS 10).
I did have the complications embedded within the Watch app, but you cannot have two @mains in the same target so the complications are in their own WidgetKit extension.
Sadly, when you add a WidgetKit extension to your project, Xcode ALWAYS sets it as an iOS widget extension; it doesn't give you the choice to make it a watchOS extension so you have to figure it out yourself.
Now, I'm hitting an issue where the main iOS app works fine, the iOS widgets work fine, the Watch app runs fine, but the complications dot show up anywhere. I can't preview them in Xcode because it says various things like:
This app was not built to support this device family; app is compatible with (
1,2
) but this device supports (
4
}
I can't find any information on what goes where in the build settings.
Can someone tell me how each bit of the project should look in the build settings, please?
(There's no information anywhere in Apple's developer documentation - which is a bit weird. They've released Xcode and they have no information on the various bits. And why doesn't Xcode set up a WidgetKit extension properly when you add it? Must we all do this manually, Apple?)
Here's my WidgetConfigurationIntent:
WidgetEventDetails is a struct conforming to AppEntity, Identifiable, Hashable. It contains all the data needed to show in a widget.
Here's my placeholder, snapshot, timeline etc.:
When I go to add a widget to my iPhone Home Screen I choose my app, and the small and medium widgets are displayed, and they use the data from the placeholder function correctly, i.e. a random set of data (an 'event') is returned and seen in the widget picker.
I select a widget and it appears on the Home Screen, but it's now just a placeholder view, and it stays like that forever. If I hold down and edit the widget, there's no selected event, as in, I might've picked "Christmas" from the widget picker but when it gets added to the Home Screen that link is lost.
So, I edit the widget and go to choose any event, and this appears for ages:
When it finally displays the list of events I pick one and the widget looks this forever:
I can't see what I'm doing wrong. Any ideas? If you need any more of the code, please just ask. I'm getting really frustrated with this stuff and just want it to work.
I get so far with it, it all goes well, then something happens and it just breaks. And it's not my coding as I'm using git and can can go back to previous commits where stuff was working, only to find it doesn't. I'm glad iOS 17 now has this "State of Mind" logging 'cos it shows exactly how I feel developing for iOS! 🥸
I want to add a large widget to my app, but it doesn't make sense to simply expand the information that's displayed in a small or medium widget to fill up the space of a large widget.
Let's say I have a bunch of events in my app, and the user can add a small/medium widget to their Home Screen and choose which one of their events the widget shows. This is fine and works well, but for a large widget I want to have a totally different experience.
I want to display the next four upcoming events in a list. These events are set by the order they're going to occur (date-wise) so you can't really pick which four you want to display, so I don't want to be able to edit the widget. However, if I add such a widget you can still hold down and edit the widget to select an event.
Also - and I'm not sure why it does this - when you hold down and edit the widget, it displays the title and description that are assigned to my small/medium AppIntentConfiguration widget, and not the title and description that are provided to the large StaticConfiguration widget, i.e.:
struct WidgetExtension: Widget // Small/medium dynamic widget
{
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kWidgetKind,
intent: WidgetEventIntent.self,
provider: WidgetEventTimelineProvider()
) { entry in
WidgetEntry(entry: entry)
}
.configurationDisplayName(smallMediumTitle). // This and the line below...
.description(NSLocalizedString(smallMediumDescription) // are seen when editing the large widget
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct WidgetExtension_Large: Widget // Large static widget
{
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kWidgetKind,
provider: WidgetEventTimelineProvider_Large()
) { entry in
WidgetEntry_Large(entry: entry)
}
.configurationDisplayName(largeTitle)
.description(largeDescription)
.supportedFamilies([.systemLarge])
}
}
If it's not possible to have a non-editable widget, it might be more fun to let the user select four events themselves, so I'd need to change the large widget to an AppIntentConfiguration widget, but what would the four parameters look like?
Currently, I have this for the small/medium widget that lets you select one event:
@Parameter(title: "Event")
var event: WidgetEventDetails?
init(event: WidgetEventDetails? = nil) {
self.event = event
}
init() {
}
static var parameterSummary: some ParameterSummary {
Summary {
\.$event
}
}
How would I change it to select four events? It might be helpful to only be able to select a second event if a first one has been chosen, and a third if a first & second have been chosen etc. Any ideas? Thanks.
Hey all. I've been rewriting my Objective-C app in SwiftUI (it's going well, thanks!) and I'm hitting this issue that I thought I'd solved ages ago.
I have version 5.1 of my original ObjC app using version 11 of the Core Data model. I've created a new version 12, added a couple of new attributes, and created a mapping model from 11 to 12.
The fields I've added are booleans, and I've set them to have a default value of NO so that new entries will automatically get NO, but there are existing records that don't have that field at all, and I'd like them to also get NO (or YES, depending on something in the other fields, but I haven't figured out how to do that yet).
This is how Core Data is set up in the new Swift target:
public extension URL {
// Returns a URL for the given app group and database pointing to the sqlite database
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)?.appending(path: kCoreDataFolder) else {
logger.error("Shared file container could not be created.")
fatalError("CoreData: Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(kCoreDataModel)")
}
}
// MARK: - Persistence Controller
struct CoreData {
static let shared = CoreData()
let container: NSPersistentContainer
init() {
let storeURL = URL.storeURL(for: kAppGroup, databaseName: kCoreDataModel)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container = NSPersistentContainer(name: kCoreDataModel)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
logger.error("Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)")
fatalError("CoreData: Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
When I run the old app on the Simulator it adds some demo data (using model v11), and the app functions properly. If I then install the new version - using v12 - it fails with a Core Data error because it couldn't migrate the data. (Apologies, I cannot get the exact error at the moment as I'm part-way through redoing some other stuff and the app won't build.)
I've read somewhere that "When we use the NSPersistentContainer class to create and manage the Core Data stack, we don't need to do any additional setup work, the lightweight migration is automatically activated for us." But if that's the case, why does my app crash? I'll try and get the exact error for you...
How do I implement lightweight migration in Swift? This page suggests creating a persistent coordinator and adding a couple of options, but I can't quite figure out how to do that with the code I already have, and each time I try it seems to beat the original store so I keep going back to the above code because it works.