Post

Replies

Boosts

Views

Activity

App Store Review rejecting app because it references upcoming software
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?
5
0
3.4k
Sep ’22
Refreshing widgets - policy and background tasks?
I have widgets providing their timeline using the .atEnd reload policy, i.e.: // AppIntentTimelineProvider: return Timeline(entries: entries, policy: .atEnd) // TimelineProvider let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) I can't seem to find any information on what happens after the end of the timeline. So, let's say I've got two days worth of entries, the dev docs for the reload policy say, "A policy that specifies that WidgetKit requests a new timeline after the last date in a timeline passes." Great! But how does it request the new timeline? Does iOS launch my app in the background and simply re-run the timeline to generate another two days worth of entries? I doubt it. I figure I need to implement some sort of background task, and the dev docs say how to do it with an Operation, but then I read that this is an old way of doing it? I've found some info online saying to use something like this, so this is what I've implemented: let kBackgroundWidgetRefreshTask = "my.refresh.task.identifier" // This has been registered in the info.plist correctly class SchedulingService { static let shared = SchedulingService() func registerBackgroundTasks() { let isRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: kBackgroundWidgetRefreshTask, using: nil) { task in print("Background task is executing: \(task.identifier)") // This does print "true" self.handleWidgetRefresh(task: task as! BGAppRefreshTask) } print("Is the background task registered? \(isRegistered)") } func scheduleWidgetRefresh() { let request = BGAppRefreshTaskRequest(identifier: kBackgroundWidgetRefreshTask) // Fetch no earlier than 1 hour from now - test, will be two days request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) do { try BGTaskScheduler.shared.submit(request) print("Scheduled widget refresh for one hour from now") } catch { print("Could not schedule widget refresh: \(error)") } } private func handleWidgetRefresh(task: BGAppRefreshTask) { // Schedule a new refresh task scheduleWidgetRefresh() // Start refresh of the widget data let refreshTask = Task { do { print("Going to refresh widgets") try await self.backgroundRefreshWidgets() task.setTaskCompleted(success: true) } catch { print("Could not refresh widgets: \(error)") task.setTaskCompleted(success: false) } } // Provide the background task with an expiration handler that cancels the operation task.expirationHandler = { refreshTask.cancel() } } func backgroundRefreshWidgets() async throws { print("backgroundRefreshWidgets() called") definitelyRefreshWidgets() } } As I've commented above, the line print("Background task is executing: \(task.identifier)") does print true so the task has been registered correctly. I've put the app into the background and left it for hours and nothing is printed to the console. I've implemented a logger that writes to a file in the app container, but that doesn't get anything either. So, is there something I'm misunderstanding? Should I change the reload policy to .after(date)? But what makes the timeline reload? As a second but linked issue, my widgets have countdown timers on them and the entire timeline shows that every entry is correct, but the widgets on the Home Screen simply fail to refresh correctly. For example, with timeline entries for every hour for the next two days from 6pm today (so, 7pm, 8pm...) every entry in the preview in Xcode shows the right countdown timer. However, if you put the widget on the Home Screen, after about five hours the timer shows 25:12:34 (for example). No entry in the timeline preview ever shows more than 24 hours because the entires are every hour, and the one that shows a timer starting at 23:00:00 should never get to 24:00:00 as the next entry would kick in from 0:00:00, so it should never show more than 23:59:59 on the timer. It's like the 23:00:00 timer is just left to run for hours instead of being replaced by the next entry. It's as though the widget isn't refreshing correctly and entries aren't loaded? Given this is the Simulator - and my development device - and both are set to Developer Mode so widget refresh budgets aren't an issue, why is this happening? How do you get widgets to refresh properly? The dev docs are not very helpful (neither is the Backyard Birds example Apple keep pushing). Thanks!
5
0
1.1k
Dec ’24
watchOS 9.1 beta 2 crashing Watch faces?
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.
7
0
1.7k
Oct ’22
SwiftUI state is maddening
I honestly thought I was getting somewhere with this, but alas, no. Every time I do anything in my List of ItemRows it jumps back to the top. Here's the setup: DataService.swift: final class DataService { static let shared = DataService() private init() {} let coreData: CoreData = CoreData() let modelData: ModelData = ModelData() } ModelData.swift: @Observable class ModelData: ObservableObject { var allItems: [ItemDetails] var standardItems: [ItemDetails] var archivedItems: [ItemDetails] init() { allItems = [] standardItems = [] archivedItems = [] } func getInitialData() { // Get all items, then split them into archived and non-archived sets, because you can't use `.filter` in a view... allItems = dataService.coreData.getAllItems() standardItems.append(contentsOf: allItems.filter { !$0.archived }) archivedItems.append(contentsOf: allItems.filter { $0.archived }) } } MainApp.swift: // Get access to the data; this singleton is a global as non-view-based functions, including the `Scene`, need to access the model data let dataService: DataService = DataService.shared @main struct MainApp: App { // Should this be @ObservedObject or @StateObject? @ObservedObject private var modelData: ModelData = dataService.modelData // I would use @StateObject if the line was... //@StateObject private var modelData: ModelData = ModelData() // right? // But then I couldn't use modelData outside of the view hierarchy var body: some Scene { WindowGroup { ZStack { MainView() .environment(modelData) } } .onAppear { modelData.getInitialData() } } } MainView.swift: struct MainView: View { @Environment(ModelData.self) private var modelData: ModelData var body: some View { ... ForEach(modelData.standardItems) { item in ItemRow(item) } ForEach(modelData.archivedItems) { item in ItemRow(item) } } } ItemRow.swift: struct ItemRow: View { @Environment(\.accessibilityDifferentiateWithoutColor) private var accessibilityDifferentiateWithoutColor var item: ItemDetails @State private var showDeleteConfirmation: Bool = false var body: some View { // Construct the row view // `accessibilityDifferentiateWithoutColor` is used within the row to change colours if DWC is enabled, e.g. use different symbols instead of different colours for button images. // Add the .leftSwipeButtons, .rightSwipeButtons, and .contextMenu // Add the .confirmationDialog for when I want to ask for confirmation before deleting an item } } Now, the problems: Swipe an item row, tap one of the buttons, e.g. edit, and the list refreshes and jumps back to the top. In the console I see: ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor changed. Why did accessibilityDifferentiateWithoutColor change? The setting in Settings > Accessibility > Display & Text Size has not been changed, so why does the row's view think it changed? With a .confirmationDialog attached to the end of the ItemRow (as seen in the code above), if I swipe and tap the delete button the list refreshes and jumps back to the top again. In the console I see: ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor, _showDeleteConfirmation changed. Right, it changed for the one row that I tapped the button for. Why does every row get redrawn? I already had to shift from using the colorScheme environment variable to add new asset colours with light and dark variants to cover this, but you can't do that with DWC. Honestly, managing state in SwiftUI is a nightmare. I had zero problems until iOS 26 started removing one or two rows when I scrolled, and the fix for that - using @Statebject/@ObservedObject - has introduced multiple further annoying, mind-bending problems, and necessitated massive daily refactorings. And, of course, plenty of my time islost trying to figure out where a problem is in the code because "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"...
7
0
233
Oct ’25
List jumps back to the top
Following on from this thread: https://developer.apple.com/forums/thread/805037 my list of items is now correctly maintaining state (no more disappearing rows), but I'm now hitting a really annoying issue: Every time something changes - even just changing the dark mode of the device - the entire list of items is refreshed, and the list jumps back to the top. A simple representation: // modelData.filteredItems is either all items or some items, depending on whether the user is searching List { ForEach(modelData.filteredItems) { item in ItemRow(item: item) } } When the user isn't searching, filteredItems has everything in it. When they turn on search, I filter and sort the data in place: // Called when the user turns on search, or when the searchString or searchType changes func sortAndFilterItemsInModelData() { modelData.filteredItems.removeAll() // Remove all items from the filtered array modelData.filteredItems.append(contentsOf: modelData.allItems) // Add all items back in let searchString: String = modelData.searchString.lowercased() switch(modelData.searchType) { case 1: // Remove all items from the filtered array that don't match the search string modelData.filteredItems.removeAll(where: { !$0.name.lowercased().contains(searchString) }) ... } // Sorting switch(modelData.sortKey) { case sortKeyDate: modelData.sortAscending ? modelData.filteredItems.sort { $0.date < $1.date } : modelData.filteredItems.sort { $0.date > $1.date } // Sorts in place ... } } The method doesn't return anything because all the actions are done in place on the data, and the view should display the contents of modelData.filteredItems. If you're searching and there are, say 10 items in the list and you're at the bottom of the list, then you change the search so there are now 11 items, it jumps back to the top rather than just adding the extra ItemRow to the bottom. Yes, the data is different, but it hasn't been replaced; it has been altered in place. The biggest issue here is that you can simply change the device to/from Dark Mode - which can happen automatically at a certain time of day - and you're thrown back to the top of the list. The array of data hasn't changed, but SwiftUI treats it as though it has. There's also a section in the List that can be expanded and contracted. It shows or hides items of a certain type. When I expand it, I expect the list to stay in the same place and just show the extra rows, but again, it jumps to the top. It's a really poor user experience. Am I doing something wrong (probably, yes), or is there some other way to retain the scroll position in a List? The internet suggests switching to a LazyVStack, but I lose left/right swipe buttons and the platform-specific styling. Thanks.
9
0
213
Oct ’25
[WC] WCSession counterpart app not installed BUT IT IS!
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.
10
3
4.9k
Feb ’24
Can't pin down how to show complications properly at different stages
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...
11
2
3.9k
Mar ’23
Unable to install iOS & watchOS app to iPhone, because of intents change
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.
12
1
2.3k
May ’25