I have a countdown/up timer in a widget, and I want to format the timer to something more readable than the default that Text.init(myDate, style: .timer) provides.
The default outputs a timer for just the hours in the date range. So, for example, a timer of 1 week, 5 hours, 12 minutes and 45 seconds will appear as 173:12:45 (which is 7 * 24 + 5 = 173) - not very user-friendly.
An ideal output would be 1 week 05:12:45. Is there any way of doing that?
I've tried a number of different ways using TimeInterval, DateInterval, and modding (%) values - like modding the hours count by 168 to get a number of weeks - but they're pretty much useless when there are never a set number of days or weeks in a year due to leap years.
It would be great if you would provide actual code examples rather than saying to use a certain API, as I could very easily go down a rabbit hole like I have with TimeInterval and DateInterval. Thanks!
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I have a Home Screen widget that contains a timer counting down to a specific date. In this image you can see the date calculations:
eventDate: 25th Dec 2023 at 09:00:00
entryDate(Date.now): This is just showing you the date in the first SimpleEntry for the widget.
getTimeRemaining(entryDate): This shows the number of seconds from the entryDate to the eventDate, figures out how many days there are ("43 days"), and how many hours:mins:secs to the event time, so "10:48:52".
Then there's a second entry, entryDate2, that's one hour later, and the values are appropriately calculated.
When I create the timeline entries, I add them for:
Now (1 entry)
Now plus one hour to a week away (one entry per hour = 167 entries)
Event date (1 entry)
Event date plus one hour to a week later (one entry per hour = 167 entries)
Each SimpleEntry entry contains a dictionary with the relevant timer details, and that's what the widget uses to determine what to display in the timer.
SwiftUI lacks any useful formatting for a timer. Even the developer docs state: "Example output: 36:59:01". Who wants to see a timer with 36 hours on it? I want it to say "1 day 12:59:01", so I munge the numbers about and grab the separate parts, converting 36:59:01 into "1 day" and "12:59:01". You can see that in the image above.
When the entry date of the timeline is reached and the widget is redrawn, it uses the entry containing the dictionary saying it should display "43 days" and the countdown timer should be 10:48:52, then an hour later the dictionary says it's 43 days 9:48:52, etc.
The issue is that the widgets, even though they're supposed to have entries at each hour, will always end up displaying something like "29:17:09". The timeline has an entry at every hour, so it should be using the right values. I've checked, and the right values are in the dictionary, so why does the timer keep getting out of sync?
I could cut out a massive amount of my widget code if only Text.init(date, style: .timer) would allow some proper formatting.
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 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.
Just finalising some work on my app update, and it seems that when you go to select an item to show in a complication, when you select your app in the list, the subsequent list only shows 15 of your items.
If a user of my app has transferred 20 items to their Watch, they can't select five of them to be shown in a complication. Is that right?
If that's a hard limit then I need to be able to separate them out into bunches of 15 items, or maybe have them display under A-E, F-J etc.
Does this have to be done as a separate Widget in the WidgetBundle? And how do I do that? Given that I currently have one widget in that bundle that should show everything (20 items), how would I split it out to show an "A-E" widget with those items beginning with A...E? Do I have to have an A-E widget with its own set of data?
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! 🥸
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?)
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?
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.
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 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...
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 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?
Topic:
App Store Distribution & Marketing
SubTopic:
App Store Connect
Tags:
App Review
App Store
App Store Connect
Xcode