On iOS you can create a new Lock Screen that contains a bunch of emoji, and they'll get put on the screen in a repeating pattern, like this:
When you have two or more emoji they're placed in alternating patterns, like this:
How do I write something like that? I need to handle up to three emoji, and I need the canvas as large as the device's screen, and it needs to be saved as an image. Thanks!
(I've already written an emojiToImage() extension on String, but it just plonks one massive emoji in the middle of an image, so I'm doing something wrong there.)
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I just added a .systemLarge widget to my app, but I can't get Links to work. I want the user to be able to tap one of the four rows in my widget - like the EmojiRangers example - but I can't get it to work.
I watched a Developer video from WWDC20: https://developer.apple.com/videos/play/wwdc2020/10036?time=223
The guy, Izzy, 'simply' embeds an HStack in a Link, and hey presto! It all works. But that doesn't happen for me. There's clearly some code in the background that runs.
I already have .widgetURL working for .systemSmall and .systemMedium widgets, and I don't need to use Links on those two types. Those work by sending a URL to .onOpenURL { incomingURL in ... All good there, no issues.
I've wrapped each row in the large widget in a Link with the URL of something like myappurlscheme://widgetTapped/widgetId (it's the same url as that used in the small and medium widgets). I build & run. I tap a row. It doesn't act as though a row is tappable (it doesn't go slightly transparent), and just opens the app without hitting .onOpenURL or anything else. Nothing in my scene delegate is triggered. Is there a specific delegate method that gets called? Do I need to set up some awful intents?
I'm not using any sort of NavigationStack here; that model doesn't fit my app.
Any ideas? Thanks.
How would you go about handling this sort of situation?
An app has two tiers of non-consumable in-app purchases. The IAP simply unlocks a certain level of access in the app:
The first tier for $1.99 allows the user to add up to 50 things.
The second tier for $3.99 allows the user to add up to 200 things.
If the user has not bought an IAP the app will show the two tiers available for purchase.
The user then buys Tier 1 and happily goes about adding some things to the app.
The app now only shows Tier 2 available for purchase, because Tier 1 has been purchased.
A few weeks go by and they realise they need to add more than 50 things.
Would the user have to suck it up and just accept they should've paid the $3.99?
Or, could a new Tier 1.5 be added that's a kind of upgrade price of $2.00 (the difference between the two original tiers) to unlock the higher 200 things level? I doubt this would work properly, because although I can control that tier being displayed or not in the app, I cannot control it in the App Store product pages, and it would be displayed among the Tier 1 and Tier 2 levels, so people would just buy that rather than the full priced Tier 2.
How should I handle this situation? Just have the one tier (Tier 2) and make it simpler?
I'm adding my first in-app purchase to an app, and I'm finding the process incredibly frustrating.
Aside from the Apple Developer Documentation not being clear enough, and kind of glossing over the technical steps required (and their sample code being woefully inadequate), App Store Connect and the testing sandbox simply don't work as they say they do.
For example, in my app I've purchased the IAP and now I want to request a refund. I select the purchase, I choose a refund reason, and this page says, "To set up a test for approved refunds, select any refund reason on the refund request sheet, and submit the sheet. The App Store automatically approves the refund request in the testing environment."
Well, when I re-launch the app the purchase is still there. I can't request a refund again because it says this is a duplicate refund request, so it knows that the purchase has had a request, and it's supposed to have automatically refunded it, but it clearly hasn't.
So, I try clearing the purchase history via the Settings app > Developer > Sandbox Apple Account. Same thing. Purchase remains.
Try clearing purchase history in App Store Connect. Same thing.
How on Earth does anyone get an in-app purchase to work when the entire testing environment is so badly executed?
How do I get past this? The IAP is the last part of the app that needs to be implemented, and I've lost a week on this already.
I have two sandbox users in App Store Connect, as I'm trying to test in-app purchases and Family Sharing. They're set up fine; I can make purchases in the app.
The issue is that the refund request sheet in my app sometimes shows properly and lets me request a refund, but I'd say >80% of the time the sheet just shows "Cannot Connect" with a "Retry" button. Hitting that button doesn't ever result in the sheet showing the refund page.
The only fix for this is to delete the app from the device, and restart the device.
This has to be a joke, right? I need to be able to test this IAP, and the sandbox environment is useless most of the time. Why?
Anyone experiencing this sort of issue?
I'm trying to update an app of mine to have a more modern look, and the last part of it is the Action Extension in Safari.
My info.plist file has the correct NSExtension details to use a storyboard, but storyboards look so old and I'd like to use a nicer SwiftUI-based look. Is this even possible?
This is the relevant bit from the Info.plist:
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>GetURL</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
<key>NSExtensionActionWantsFullScreenPresentation</key>
<false/>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
</dict>
I see I can use NSExtensionPrincipalClass instead of NSExtensionMainStoryboard but then I get stuck.
If I remove this:
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
and replace it with this:
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ActionViewController</string>
I get this error when I run the extension:
Rejecting view controller creation request due to invalid extension storyboard or principal class: Error Domain=NSCocoaErrorDomain Code=967223 "(null)" UserInfo={Invalid Configuration=Either NSExtensionMainStoryboard or NSExtensionPrincipalClass must be specified in the extension's Info.plist file but not both.}
According to that error the two keys are mutually-exclusive, which is fine as I'm using just one of them, so why do I get this error?
Is it something to do with the actual code in ActionViewController?
I have this, and nothing here ever runs:
class ActionViewController: UIViewController {
var theUrl: String = ""
@objc override func viewDidLoad() {
super.viewDidLoad()
if let inputItem = extensionContext!.inputItems.first as? NSExtensionItem {
if let itemProvider = inputItem.attachments?.first {
itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier as String) { [unowned self] (dict, error) in
let itemDictionary = dict as! NSDictionary
let javaScriptValues = itemDictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! NSDictionary
self.theUrl = javaScriptValues["URL"] as! String
// Build the SwiftUI view, wrap it in a UIHostingController then send to the main thread to update the UI
let contentView = ActionExtensionView(theUrl: self.theUrl, clickedCancel: self.cancel, clickedDone: self.done)
let childView = UIHostingController(rootView: contentView)
self.view.addSubview(childView.view)
// Set the place where your view will be displayed
let constraints = [
childView.view.topAnchor.constraint(equalTo: view.topAnchor),
childView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
childView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
childView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
childView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
childView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
]
childView.view.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(constraints)
DispatchQueue.main.async {
self.present(childView, animated: true)
}
}
}
}
}
Apple really don't make it easy to develop for their platforms, do they?
In my content blocker I have a bunch of rules that block some content in Safari, but I want my users to be able to whiltelist a website so the blocker rules don't apply on that site.
I have something like this:
{
"action": {
"type":"css-display-none",
"selector":"a[href*='Bobbins']"
},
"trigger": {
"url-filter":".*"
}
},
{
"trigger": {
"url-filter": ".*",
"if-domain": ["*mydomain.com"]
},
"action": {
"type": "ignore-previous-rules"
}
}
I think that should block any a link where the href includes Bobbins but not if the site is mydomain.com. However, that simply doesn't work. It doesn't matter what I put in the array of domains to whitelist, it just doesn't apply it. In every case, the a link is blocked.
How do you actually whitelist a website in a Safari content blocker?
In iOS 18 the following code works to set a state variable when you hold your finger on the Text() field (well, the ScrollView()), but it doesn't work in iOS 26:
@State private var pressed: Bool = false
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.onTapGesture {} // This is required to allow the long press gesture to be recognised
.gesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
pressed = true
})
.onEnded({ _ in
pressed = false
})
)
.background(pressed ? .black.opacity(0.4) : .clear)
I've tried changing this to:
var dragGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged({ _ in self.pressed = true })
.onEnded({ _ in self.pressed = false })
}
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.gesture(dragGesture)
.background(pressed ? .black.opacity(0.4) : .clear)
And this:
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.25)
.onChanged({ _ in self.pressed = true })
.onEnded({ _ in self.pressed = false })
}
...
ScrollView {
VStack {
Text("Some text goes here")
}.frame(maxWidth: .infinity)
}
.gesture(longPress)
.background(pressed ? .black.opacity(0.4) : .clear)
Neither works.
Any ideas? Thanks.
Been at this for ages now, getting nowhere, thanks to Swift and the betas...
The iOS app schedules a local notification that has a userInfo dictionary, and one small JPEG image attachment.
Objective-C in iOS app:
content.attachments = @[[UNNotificationAttachment attachmentWithIdentifier:myIdentifier URL:[imageURL filePathURL] options:@{UNNotificationAttachmentOptionsTypeHintKey : UTTypeJPEG} error:&error]];
This works fine. The notification is correctly scheduled.
If I ignore the Watch and let the notification appear on my phone's Lock Screen, the image is there.
Going back to the Watch. The Watch app receives the notification, and the didReceive method is called in the NotificationController.
No matter what I try, I can't get the image from the notification.
NotificationController.swift: (image is sent to the NotificationView to use as the background.)
guard let attachment = notification.request.content.attachments.first
else {
print("Couldn't get the first attachment, using default")
image = Image.init(kDefaultImage)
return
}
// We get here, so we know there's an attachment
if attachment.url.startAccessingSecurityScopedResource() {
let imageData = try? Data.init(contentsOf: attachment.url)
if let imageData = imageData {
image = Image(uiImage: UIImage(data: imageData) ?? UIImage.init(imageLiteralResourceName: kDefaultImageMasked))
}
attachment.url.stopAccessingSecurityScopedResource()
} else {
// << I ALWAYS HIT THIS BIT >>
print("Couldn't access the file, using default")
image = Image.init(kDefaultImageMasked)
}
I always get told I can't access the file in the security scoped bit.
If I take out that check I get Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value because the code doesn't put anything into image. Obviously I would put the default image in there, but once the Watch app crashes the notification that's shown on the Watch shows the correct image, but it's obviously not using my NotificationView because that crashed.
How the hell do I get the image from the attachment? This was simple to do in the old WatchKit extension stuff, look:
NSArray *attachments = notification.request.content.attachments;
if(attachments.count == 1) {
[_groupAll setBackgroundImage:attachments[0]];
}
No problems there; it always worked.
Thanks.
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.
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! 🥸
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?
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.
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.