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
I use the following bit of code to snapshot a View as a UIImage, but it's causing a memory leak:
extension View {
@ViewBuilder func snapshot(trigger: Bool, onComplete: @escaping (UIImage) -> ()) -> some View {
self.modifier(SnapshotModifier(trigger: trigger, onComplete: onComplete))
}
}
fileprivate struct SnapshotModifier: ViewModifier {
var trigger: Bool
var onComplete: (UIImage) -> ()
@State private var view: UIView = .init(frame: .zero)
func body(content: Content) -> some View {
content
.background(ViewExtractor(view: view))
.compositingGroup()
.onChange(of: trigger) {
generateSnapshot()
}
}
private func generateSnapshot() {
if let superView = view.superview?.superview {
let render = UIGraphicsImageRenderer(size: superView.bounds.size)
let image = render.image { _ in
superView.drawHierarchy(in: superView.bounds, afterScreenUpdates: true)
}
onComplete(image)
}
}
}
fileprivate struct ViewExtractor: UIViewRepresentable {
var view: UIView
func makeUIView(context: Context) -> UIView {
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
// No process
}
}
Taking the snapshot is triggered like this:
struct ContentView: View {
@State private var triggerSnapshot: Bool = false
var body: some View {
Button("Press to snapshot") {
triggerSnapshot = true
}
TheViewIWantToSnapshot()
.snapshot(trigger: triggerSnapshot) { image in
// Save the image; you don't have to do anything here to get the leak.
}
}
}
I'm not the best at Instruments, and this is what the Leaks template produces. There are no method names, just memory addresses:
Is this leak in an internal iOS library, is there something wrong with Instruments, or am I missing something obvious in my code?
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.
I have a List which acts like a form where you can enter values. It uses a custom View struct SectionArea to separate the various sections and cut down on code duplication, like this:
List {
SectionArea(title: "Section One", height: 176, horizontalPadding: 0, roundCorners: (0, 0, 0, 0)) {
VStack {
Button {
print("Pressed Button One")
fullScreenViewChoice = .optionOne
showSubView.toggle()
} label: {
HStack {
Text("Button One")
Spacer()
Image(systemName: "chevron.right")
}
}
.frame(height: 40)
Divider()
Button {
print("Pressed Button Two")
fullScreenViewChoice = .optionTwo
showSubView.toggle()
} label: {
HStack {
Text("Button Two")
Spacer()
Image(systemName: "chevron.right")
}
}
.frame(height: 40)
Divider()
}
}
}
.listStyle(.plain)
.listRowSpacing(0)
It works fine, but regardless of which button I press it always acts as though both buttons have been pressed. Say I press Button One, the console will display:
Pressed Button One
Pressed Button Two
Can someone tell me where I'm going wrong here, please? Thanks!
EDIT: Actually, it doesn't look like it's because of the SectionArea struct, because I've taken the code from there and wrapped it around the content directly, rather than using that struct, and it still does it.
I've removed everything and just put this:
List {
VStack {
ButtonOne
ButtonTwo
}
}
It still presses both buttons.
iOS app with Home Screen and Lock Screen widgets written in Swift/SwiftUI. I've never been able to get widgets to work properly. It's more pronounced on Lock Screen widgets, so let's try that method first...
The app stores data in Core Data as an Event. They're read into my model and stored as WidgetEventDetails structs:
struct WidgetEventDetails: AppEntity, Identifiable, Hashable {
public var eventId: String
public var name: String
public var date: Date
public var id: String {
eventId
}
This all works absolutely fine in the iOS app, and each one is unique based on the eventId.
When I go to add a Lock Screen widget, I customise the Lock Screen, tap in the section to add a widget, and my widgets appear correctly and are selectable: (bottom right, says "1y 28w 1d")
So, I tap it and it appears in the widgets section:
But it appears as "17w 6d", which is a different event entirely. Notice how the one in the selectable widgets has changed to "15w 5d", and the one I tapped (1y 28w 1d) is nowhere to be seen.
So, I tap the one in the top row (17w 6d) to select an event, and this appears, suggesting that the event is the "Edinburgh & Glasgow 2024-02" event:
But that event is actually only a day away (1d), so that's not the one I selected at all.
I tap the list and see these events:
I select "Las Vegas 2024", which is 17w 3d away, and this is shown:
17w 6d is a different event, not Las Vegas 2024.
So, I tap it again and see this. The "Loading" text appears for ages, but occasionally does show the full list, as before:
I select "Edinburgh & Glasgow 2024-02" which is 1d away, and I see this again:
So, I resign myself to hoping it'll just figure itself out, and I tap "Done":
"17w 6d" again :(
I finish customising, and exit the customisation screen. I show the Lock Screen, and I see this:
Why doesn't this work?
Here's the code:
@main
struct WidgetExtensionBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
WidgetExtension()
}
}
struct WidgetExtension: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kWidgetKind, intent: WidgetEventIntent.self, provider: WidgetEventTimelineProvider()) { entry in
WidgetEntry(entry: entry)
.environment(modelData)
}
.configurationDisplayName(NSLocalizedString("AddingWidget_Title", comment: "Adding the Widget"))
.description(NSLocalizedString("AddingWidget_Description", comment: "Adding the Widget"))
.supportedFamilies([.accessoryCircular, .accessoryInline, .accessoryRectangular, .systemSmall, .systemMedium])
.contentMarginsDisabled()
}
}
struct WidgetEventIntent: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "AddingWidget_Title"
static let description = IntentDescription(LocalizedStringResource("AddingWidget_Description"))
@Parameter(title: LocalizedStringResource("Event"))
var event: WidgetEventDetails?
init(event: WidgetEventDetails? = nil) {
self.event = event
}
init() {}
static var parameterSummary: some ParameterSummary {
Summary {
\.$event
}
}
}
struct EventQuery: EntityQuery, Sendable {
func entities(for identifiers: [WidgetEventDetails.ID]) async throws -> [WidgetEventDetails] {
modelData.availableEvents.filter { identifiers.contains($0.id) } // availableEvents is just [WidgetEventDetails]
}
func suggestedEntities() async throws -> [WidgetEventDetails] {
return modelData.availableEvents.filter { $0.type == kEventTypeStandard }
}
}
If you think it's the TimelineProvider causing it, I can provide that code, too.
When a user swipes up to see the app switcher, I put a blocking view over my app so the data inside cannot be seen if you flick through the app switcher. I do this by checking if the scenePhase goes from .active to .inactive.
If the app goes into the background, scenePhase == .background so I trigger something that would force the user to authenticate with Face ID/Touch ID when the app is next brought to the foreground or launched.
However, this doesn't seem to work. The biometrics authentication is executed, but it just lets the user in without showing the Face ID animation. I put my finger over the sensors so it couldn't possibly be authenticating, but it just lets them in.
Here's a quick set of logs:
scenePhase == .inactive - User showed app switcher
scenePhase == .background - User swiped up fully, went to Home Screen
scenePhase == .inactive - User has tapped the app icon
scenePhase == .active - App is now active
authenticate() - Method called
authenticate(), authenticateViaBiometrics() == true - User is going to be authenticated via Face ID
// Face ID did not appear!
success = true - Result of calling `context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics` means user was authenticated successfully
error = nil - No error in the authentication policy
authenticate(), success - Method finished, user was authenticated
Here's the code:
print("authenticate(), authenticateViaBiometrics() == true - User is going to be authenticated via Face ID")
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
// Handle permission denied or error
print("authenticate(), no permission, or error")
authenticated = false
defaultsUpdateAuthenticated(false)
defaultsUpdateAuthenticateViaBiometrics(false)
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate with biometrics") { (success, error) in
DispatchQueue.main.async {
print("success = \(success)")
print("error = \(String(describing: error?.localizedDescription))")
if(success) {
print("authenticate(), success")
authenticated = true
} else {
print("authenticate(), failure")
authenticated = false
}
}
}
This happens with or without the DispatchQueue... call.
I have a content blocker that generally works correctly, but I need to block an element that has certain text in it.
For example, <span id="theId">Some text</span> is easy enough to block because I can locate the id and block that, but what if there is no id, or the id is completely random? What if it's just <span>Some text</span>? How do I block that?
Let's say this is my only content blocker rule:
[
{
"action": {
"type": "css-display-none",
"selector": ":has-text(/Some text/i)"
},
"trigger": {
"url-filter": ".*"
}
}
]
No errors are seen when the rule is loaded, so it's syntactically correct, it just doesn't block the HTML. I gather this is because :has-text() works on attributes, not contents, so it works on alt, href, aria-label etc. but not on the contents of the element itself.
How do I block Some text in my example above? Thanks!
... and yet, Apple's staff think it's appropriate to post lame responses thanking them for their interest in the forums.
Come on, guys! It's obvious spam! Mark it as such and delete it.
It's not beyond your abilities to stop people posting the word "WhatsApp" plus a phone number. You already stop us posting certain web links, so you can do this.
If you continue ignoring the spam problem, then these forums will become less and less useful.
Why should I contribute my free time to help others when you won't even help us?
Topic:
Developer Tools & Services
SubTopic:
Developer Forums
I have a List containing ItemRow views based on an ItemDetails object. The content is provided by a model which pulls it from Core Data.
When I scroll through the list one or two of the rows will disappear and reappear when I scroll back up. I have a feeling it's because the state is being lost?
Here's some relevant info (only necessary parts of the files are provided):
-- ModelData.swift:
@Observable
class ModelData {
var allItems: [ItemDetails] = coreData.getAllItems()
...
}
-- ItemDetails.swift:
struct ItemDetails: Identifiable, Hashable, Equatable {
public let id: UUID = UUID()
public var itemId: String // Also unique, but used for a different reason
...
}
-- MainApp.swift:
let modelData: ModelData = ModelData() // Created as a global
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Methods in here (and in lots of other places) use `modelData`, which is why it's a global
}
@main
struct MainApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
}
...
}
-- MainView.swift:
struct MainView: View {
var body: some View {
List {
ForEach(modelData.allItems, id: \.id) { item in
ItemRow(item)
}
}
}
}
struct ItemRow: View, Equatable {
var item: ItemDetails
var body: some View {
...
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.item == rhs.item
}
}
There's obviously more code in the app than that, but it's not relevant to the issue.
I've tried:
ItemRow(item).equatable()
Wrapping ItemRow in an EquatableView
Giving the List a unique id
Using class ModelData: ObservableObject and @StateObject for modelData
None made any difference.
I'm using iOS/iPadOS 26.0.1, and I see it on my physical iPhone 17 Pro Max and iPad Pro 11-inch M4, but I don't see it in the equivalent simulators on those versions. The Simulator also doesn't exhibit this for versions 17.5 and 18.5, and I have no physical devices on 17.5/18.5 to check.
Should I be doing as I currently am, where I create modelData as a global let so I can access it everywhere, or should I pass it through the view hierarchy as an Environment variable, like @Environment(ModelData.self) var modelData: ModelData? Bear in mind that some functions are outside of the view hierarchy and cannot access modelData if I do this. Various things like controllers that need access to values in modelData cannot get to it.
Any ideas? Thanks.