Post

Replies

Boosts

Views

Created

32 byte NSNumber memory leak - how to fix?
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.
Topic: UI Frameworks SubTopic: UIKit Tags:
3
0
105
3w
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
232
Oct ’25
How to handle alert when deleting row from List
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...?)
3
0
188
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
212
Oct ’25
List rows disappearing when scrolling
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.
2
0
131
Oct ’25
How to handle long press on a Text() in iOS26
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.
1
0
85
Sep ’25
macOS 26 Tahoe beta 1 - Cannot login
A few days ago I installed beta 1 of macOS 26 Tahoe on a spare MacBook Pro 14-inch M4. Everything went well, and it looks okay (well, I don't like it at all, but hey-ho), and I shut it down. I started it up today, and it rejects my login password every time, then locks my account. When I click the link to "Restart and show password reset options" I'm asked for my Apple ID details, so I enter the correct email and password, then the MBP reboots and I'm back on the login screen with no indication that anything has changed. Guess I'm stuck now... FB18364657
4
1
643
Jun ’25
More spam on these forums...
... 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?
2
3
168
Apr ’25
Content blockers: ignore-previous-rules not working?
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?
Topic: Safari & Web SubTopic: General Tags:
1
0
433
Feb ’25
Can an Action Extension use SwiftUI for its UI?
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?
1
0
494
Feb ’25
Sandbox environment extremely unreliable
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?
1
1
499
Jan ’25
In-app purchases - why so frustrating?
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.
1
0
502
Jan ’25
In-app purchases fail: "Password reuse" error?
I'm trying to implement my first in-app purchase, and I've created the IAP in App Store Connect, and created a sandbox account. When I open Settings > Developer in the iPhone Simulator, there is a "Sandbox Apple Account" option at the bottom. If I click the blue "Sign In" link I'm asked for the email and password, so I enter the correct credentials for the sandbox account. The "Sign In" text goes grey for a few seconds, then it goes blue again. It never changes to show that I'm signed in. Should it? (I think so.) Do I need to sign into the Simulator's Apple Account, too? (I don't think so.) Anyway, aside from that, in my app the IAP is listed and I have a button to purchase it. When I click it I'm asked for the email and password to sign into the Apple Account. I enter the correct sandbox email and password (they are definitely correct) and I see this in the Xcode console: Purchase did not return a transaction: Error Domain=ASDErrorDomain Code=530 "(null)" UserInfo={client-environment-type=Sandbox, NSUnderlyingError=0x600000d0c7b0 {Error Domain=AMSErrorDomain Code=100 "Authentication Failed The authentication failed." UserInfo={NSMultipleUnderlyingErrorsKey=( "Error Domain=AMSErrorDomain Code=2 \"Password reuse not available for account The account state does not support password reuse.\" UserInfo={NSDebugDescription=Password reuse not available for account The account state does not support password reuse., AMSDescription=Password reuse not available for account, AMSFailureReason=The account state does not support password reuse.}", "Error Domain=AMSErrorDomain Code=0 \"Authentication Failed Encountered an unrecognized authentication failure.\" UserInfo={NSDebugDescription=Authentication Failed Encountered an unrecognized authentication failure., AMSDescription=Authentication Failed, AMSFailureReason=Encountered an unrecognized authentication failure.}" ), AMSDescription=Authentication Failed, NSDebugDescription=Authentication Failed The authentication failed., AMSFailureReason=The authentication failed.}}} Why is it talking about password reuse? AFAIK, I have only one sandbox account for this app (and none for any of my other apps), and this is the only one of my apps that has an IAP. Any ideas on how to get an IAP working? Thanks!
4
1
2.0k
Jan ’25
Handling non-consumable in-app purchase tiers
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?
1
0
375
Jan ’25
Content blocker not removing content
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!
2
1
628
Jan ’25