Post

Replies

Boosts

Views

Activity

Reply to tvOS 14.6 fails trying to bind UDP/1900 (succeeds on tvOS Simulator)
I can't help with the port 1900 part — I haven't reproduced that, and Quinn's reading looks right to me: EADDRINUSE means something on the box already has it, which is a conflict rather than a permission problem. But your second question never got answered, and it has a definite answer now, so for anyone who lands here from a search: Also there seems to be no 'local networking' permission dialog in tvOS - is that also correct? Correct. tvOS has no Local Network permission at all. Apple's TN3179 "Understanding local network privacy" lists the platforms that support the feature, and the table reads: iOS yes iOS 14 iPadOS yes iPadOS 14 macOS yes macOS 15 tvOS no - visionOS yes visionOS 1 watchOS no -https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy So it isn't that the dialog is suppressed, or granted silently, or moved somewhere else in Settings. The privilege doesn't exist on tvOS or watchOS, which also means there is nothing to check and nothing to request. That technote is the 2024 rewrite of the Local Network Privacy FAQ first posted here on the forums in 2020; the table is the part worth bookmarking. The reason people keep asking is that Apple's own documentation contradicts itself, and the page you find first is the wrong one. The reference page for NSLocalNetworkUsageDescription carries an availability badge reading tvOS 14.0+, and its description reads "Any app that uses the local network, directly or indirectly, should include this description. This includes apps that use Bonjour and services implemented with Bonjour, as well as direct unicast or multicast connections to local hosts." My tvOS app makes both of those last two kinds of connection. NSBonjourServices says tvOS 14.0+ as well. Both are current as I write this. If you search the key name rather than the feature name, you get told to declare a key for a mechanism your platform doesn't have. A data point on the multicast half, since it's the same area and confirms what Quinn told you: I run SSDP-style multicast discovery in a tvOS app — joins a group, listens on a UDP port, finds a device on the LAN — with no multicast entitlement, no NSLocalNetworkUsageDescription and no NSBonjourServices in the bundle. On tvOS 27 on an Apple TV 4K it joins the group and hears both of my printers announce themselves every few seconds — 689 announcements in a 33-minute run. Nothing asks and nothing blocks. The part I'd pass on as a warning, because it cost me something. I had the purpose string declared on my tvOS target for months, harmlessly. What it did was work as evidence. I had NWConnection sometimes sitting in .waiting for a few seconds at launch, and two comments in my own code explained that as the user not having answered the Local Network prompt yet — an explanation that sat right next to a plist key appearing to confirm the prompt existed. It stood for nearly three months in the one file someone would open to decide whether that patience was still needed. And the correction is the dangerous direction, not the error. .waiting at tvOS launch is real — Wi-Fi associating, DNS settling after the device wakes — and the connection does reach .ready on its own. "tvOS has no prompt, so nothing is waiting on one" reads as an argument for deleting the timeout that bounds the wait, which is the only thing standing between an unreachable host and a connect call that never returns. The patience was right; the reason I had written down for it wasn't. So: delete the key rather than keeping it as harmless, and keep it on your iOS target, where it really is the text the user reads. Longer write-up, including a second plist key on the same target that turned out to be inert for a different reason: https://mcmizzle.com/blog/tvos-no-local-network-permission/
Topic: Programming Languages SubTopic: Swift Tags:
1w
Reply to Ventura: NWConnection returns server error in waiting state instead of failed state
I don't have a way to make .waiting become .failed, and I don't know why Ventura changed it either. Quinn's advice to file a bug is still the right thing. But there is a workaround that makes this behave, and it's small, so in case anyone is still stuck here: First, the honest caveat — your error is not my error. ECONNABORTED after a completed handshake and an HTTP 403 is a different event from what I hit, which was ECONNREFUSED on a connection that never established. I have not tested yours. What I think is the same is the shape: .waiting arrives carrying a reason that will never clear, and the framework offers you nothing to tell that apart from the reasons that will. The documentation for the case still says this today: The connection is waiting for a network path change. Connections that are waiting will indicate the reason that the connection couldn’t be established in the associated error. These errors are not fatal.https://developer.apple.com/documentation/network/nwconnection/state-swift.enum/waiting(_:) That's accurate for most of what lands in .waiting. It's what you get before a Local Network permission prompt has been answered, and while Wi-Fi and DNS settle after a device wakes, and in both of those the connection genuinely does reach .ready on its own. It is not accurate for an error where a host has already answered you. So the state is doing two jobs — "hold on, this may come good" and "the host said no" — and only the associated error separates them. The fix is to do that triage yourself: func isTerminal(_ error: NWError) -> Bool { guard case .posix(let code) = error else { return false } switch code { // A host answered. Nothing is pending. case .ECONNREFUSED, .ECONNRESET: return true // No answer, but the path is there: the host itself is not // responding, and no path change will fix that. case .EHOSTDOWN: return true // Deliberately absent: .EHOSTUNREACH, .ENETUNREACH, .ENETDOWN. // Those describe a path that isn't there yet, which is the case // the patient behavior exists for. default: return false } } case .waiting(let error): log("waiting: \(error)") guard isTerminal(error) else { return } // may still come good guard !resumed else { return } resumed = true connection.cancel() continuation.resume(throwing: error)Whether ECONNABORTED belongs in that list is your call rather than mine, and I'd lean yes for your case on the grounds that the server demonstrably answered — but I haven't tested it, so test it against a path you expect to stay patient before you ship it. One more caveat, since the April reply is on iOS: I've only tested this on tvOS, which has no Local Network prompt. On iOS, check connection.currentPath?.unsatisfiedReason for .localNetworkDenied before treating any .waiting reason as final, so you don't cancel a connection that is only waiting for the user to tap Allow. Two things that bit me implementing this, both worth knowing before you write it: .waiting fires repeatedly while NWConnection retries on its own, so if you are bridging into async with withCheckedThrowingContinuation, a bare continuation.resume in that case traps on the second pass with a continuation misuse. Hence the resumed flag above. And throwing out of the await does not stop the connection. Without the cancel() it keeps retrying on its queue behind you. On whether this still reproduces: yes, as of October 2026. I hit it on tvOS 27, on an Apple TV 4K, with ECONNREFUSED (POSIX 61) against a device whose camera port was closed. The refusal arrived in the first log line and my own 8-second connect timeout fired eight seconds later, every attempt. With the triage above, measured on the tvOS 27 simulator against a refused port, it's 8.1s to 0.0s per attempt. Worth saying what the real cost was, because it isn't obvious from an error log: nothing crashed and nothing reported a failure, so the symptom was latency, plus a timeout message that blamed the user's network. The user went and checked his Wi-Fi, which was fine. Longer write-up, with the timeout message that sent him there and a threshold I then "fixed" and had to revert: https://mcmizzle.com/blog/nwconnection-connection-refused-waiting/
Topic: App & System Services SubTopic: Core OS Tags:
2w
Reply to [tvOS] ScrollView with Text does not scroll
Your focusable element is attached correctly. The problem is that there is only one of it. On tvOS a ScrollView scrolls as a side effect of focus moving: the focus engine picks a new target, and if that target is off screen the container scrolls to bring it on. With a single focusable view there is no second target to move to, so nothing is ever revealed and the view never budges. One focusable view behaves exactly like none. That is also why your UIFocusSystem.movementDidFailNotification is firing and why -UIFocusLoggingEnabled YES is complaining — the movement really is failing, because there is nowhere to go. Apple documents the mechanism, but in UIKit rather than SwiftUI, on UIFocusItemScrollableContainer — "A type of focus item container that supports automatic scrolling of focusable content", and "The focus engine scrolls the container to keep items onscreen as they become focused." You can write SwiftUI for a long time without meeting that protocol. https://developer.apple.com/documentation/uikit/uifocusitemscrollablecontainer I don't know why the Xcode preview scrolls when the device won't — my assumption is that the preview isn't driving the focus engine, but I haven't confirmed that, so I wouldn't read much into the preview either way. I should say plainly what this does not fix: it will not make a single long Text scroll. If you need one unbroken text view, the UITextView approach further down this thread is the way to do it. What works in SwiftUI is giving the focus engine more than one place to land, which means splitting the content: ScrollView { VStack(alignment: .leading, spacing: 16) { ForEach(paragraphs, id: \.self) { para in Text(para).focusable(true) } } } Two traps I hit doing this in a shipping tvOS app, both of which cost me a round on the device. Putting .focusable(true) on the enclosing VStack instead of the rows looks like the fix and causes the bug — it collapses the whole block to one target. I had written that version myself, having correctly worked out that a scroll view with nothing focusable cannot scroll, and then not noticed that one is the same as none. And the scroll only ever travels to a view that takes focus, so anything above your topmost focusable view is unreachable once it scrolls off the top. If there is a title above the content, make it focusable too or you can go down and never come back up. The cost is a focus ring on prose when the d-pad lands on it. On balance I kept it, since that ring is the only feedback that the remote did anything. Observed on an Apple TV 4K (3rd generation) running tvOS 27, on an app that builds back to tvOS 17. Longer write-up with the two traps and a pane whose scrollability turned out to depend on StoreKit returning products: https://mcmizzle.com/blog/tvos-scrollview-wont-scroll/
Topic: UI Frameworks SubTopic: SwiftUI Tags:
3w
Reply to How can I test CloudKit User Keychain Reset?
I can't help with the trigger, and I want that out of the way first — I've never reproduced a keychain reset deliberately either, and this thread is already further along that than I got. The ADP precondition above is new to me. But there's something worth knowing before anyone tries this in the default zone, as suggested above, because there it decides whether the test can tell you anything. The documented handling in https://developer.apple.com/documentation/cloudkit/encrypting-user-data is three steps, and the first is "Delete the relevant zones." If the affected records are in the default zone of the private database, that step can't execute — the default zone is the catch-all for every record that didn't name a zone, so deleting it is either refused or far broader than the recovery you're attempting. You'd trigger the reset successfully, watch the recovery fail, and have every reason to think the trigger was still wrong. I found that by accident rather than by looking for it. Two unrelated-looking errors, both only visible against a real container on a device, both the default zone: Field 'recordName' is not marked queryable AppDefaultZone does not support getChanges call The first is the one with a thousand search results, all saying to mark recordName queryable in CloudKit Console. That works, in development — schema is per-environment, and the docs say plainly to "Promote this schema change to your production environment before deploying any app changes to the App Store that rely on the new schema." So it converts a code problem into a release-checklist problem. The second is what you hit if you switch to recordZoneChanges to avoid the index entirely, and it is documented in an easy place to miss: CKFetchRecordZoneChangesOperation's own page doesn't say which zones support it, while CKRecordZone.default() does — in terms of CKFetchRecordChangesOperation, deprecated back in iOS 10. For the operation you'd actually write the only statement I can find is in the documentation archive, on a deprecated web-services endpoint: "Fetching record changes is supported only for custom zones." Moving to a custom zone fixed all three at once, and the third is the reason it isn't a style preference — it's the only one of them that makes the recovery path above executable at all. Two smaller things for anyone landing here from a search, which you've both got right already but the error strings are worth having in one place. CKErrorUserDidResetEncryptedDataKey is a userInfo key, not a CKError.Code case — the code is zoneNotFound, and checking for it as a case doesn't compile, which is lucky. And on the testing problem itself: constructing a CKContainer for an identifier the host has no matching entitlement for traps rather than throwing, so a unit test that touches one takes the process down instead of failing. Whatever harness you end up with for this probably can't be a plain unit test. If anyone does find a reliable way to trigger the reset I'd like to know too — the recovery path is the one piece of this I can reason about but not exercise. Full write-up, with the two errors and why the console fix for the first one is a trap: https://mcmizzle.com/blog/cloudkit-default-zone-limits/
Topic: App & System Services SubTopic: iCloud Tags:
3w
Reply to Picture-In-Picture not starting with Custom Player
Adding a second cause for -1001, since this thread is the top result for it and the failure reason in the original post doesn't cover the case in the July 2023 reply. To be clear about what I can't help with: if your UIScene genuinely isn't ForegroundActive, the message in the first post is your answer and mine isn't. This is for the other case — the scene logs as UISceneActivationStateForegroundActive and startPictureInPicture() still fails. AVKit will not start Picture in Picture for a player layer that isn't on screen. Foreground is not the same test. In my app the video lives on one tab and the control that starts PiP is on another, so the layer was alive, attached to the controller, and simply not being displayed. isPictureInPicturePossible stayed true the entire time. The call produced: pip: calling startPictureInPicture() (active=false) pip: FAILED to start -- AVKitErrorDomain -1001: Failed to start picture in picture. The fix was to bring the layer on screen first and then ask, and to keep asking, because "is it laid out yet" isn't knowable up front — select the tab, then call startPictureInPicture() at 350/600/900/1200ms until isPictureInPictureActive comes back true. Verified on a physical iPhone 17. The same error turned up separately in a recovery path that built a fresh AVPlayerLayer while the app was backgrounded — a layer that has never been displayed behaves the same way, though that case is also consistent with the scene explanation so I'd not count it as independent evidence. Two things worth doing whatever your cause turns out to be: Implement pictureInPictureController(_:failedToStartPictureInPictureWithError:) if you haven't. startPictureInPicture() returns Void and that delegate method is the only place AVKit hands back an NSError. I'd been reasoning from "the flag is true and nothing happened" for a while before I implemented it. Log ns.localizedFailureReason, not just ns.localizedDescription. localizedDescription is the useless half ("Failed to start picture in picture."); the failure reason is the field carrying the diagnosis in the original post above. I logged only the description and so I can't tell you what reason, if any, AVKit attached to mine. One more thing that wasted my time and may be wasting someone's here: if you have a timeout around the start, check it isn't cutting off successes. Mine gave up at four seconds and the log showed a start landing in the same second as the giving-up, so users got told it had failed and then got a PiP window. Longer write-up with the retry ladder and the isPictureInPicturePossible part: https://mcmizzle.com/blog/pip-wont-start-avkit-1001/
Topic: Media Technologies SubTopic: Audio Tags:
Sep ’26
Reply to Background Health Store Access for Lock Screen Widgets
I don't have a way around the locked-store restriction either. There's now an explicit DTS answer confirming reads aren't permitted while locked (thread 824819, May 2026), so I've stopped hoping for one. What I can offer is the failure mode I hit while working around it, because it's easy to ship by accident and it presents as a completely different bug. A statistics query that fails because the store is encrypted reports no sum. That is the same thing it reports for a day with genuinely no samples. So if your read does something like result?.sumQuantity()?.doubleValue(for: unit) ?? 0 and binds the error parameter to _, a failed read returns a confident 0. Mine then got published to the widget with a current timestamp, so every morning the widget showed a plausible, freshly-stamped, completely wrong zero until the app was next opened. It reads exactly like a refresh bug — I lost time on timeline reloads and App Group configuration before noticing the timestamp was fresh, which meant the data wasn't stale, it was freshly wrong. What worked, given we can't read while locked: Return an optional from the read. nil for a failed query, and 0 only for HKError.errorNoData, which is HealthKit's way of saying the range really is empty. On failure, publish nothing and leave the previous values untouched. A widget showing yesterday's number is far better than one showing a wrong number today. Audit every path that writes to the widget, not just the one you fixed. A freshly-initialized model object is full of zeros too, and those are every bit as plausible as the ones a failed query produces. Carry the day the numbers describe separately from the "updated at" time. Once a write can be skipped, "when was this written" stops answering "which day is this about" — and that distinction is what lets you render a stale payload as visibly stale instead of asserting it. None of that gets data while locked. It does stop the lock from producing wrong data, which in my case was the actual user-visible bug. Longer version: https://mcmizzle.com/blog/healthkit-widget-shows-zero/
Aug ’26
Reply to Lifecycle of a tvOS 13.2 TopShelf extension?
Late to this thread, but it still happens on the tvOS 27.0 public beta (24J5325d), on an Apple TV 4K (3rd gen), with an app whose deployment target is tvOS 17. I don't think the caching angle has come up here. Something a reboot does that reinstalling doesn't: the Home screen process caches the rendered Top Shelf tile and dedupes incoming content against that cache. In my case that cache survived a changed item identifier and a full uninstall and reinstall of the app, and kept drawing a tile built from an earlier build for hours while the current code was correct. The log line that finally showed it: Skipping content update for [com.example.app] because it is unchanged If your item identifier is a constant, a corrected item is indistinguishable from the cached one, so a fix to what's in the tile can never reach the screen. Deriving the identifier from the content you're rendering closes that particular trap. (Use the content string itself, not its hashValue — Swift seeds Hashable per process, so a hash changes every launch regardless of content, which isn't what you want.) I want to be careful not to overclaim: your symptom is the extension not running at all after an Xcode relaunch, and mine was a running extension whose output wasn't reaching the screen. Those may well be different bugs. But if you're in the position of "only a reboot fixes it," it's worth checking whether the system is even accepting your content before concluding the extension is dead. Two other things that cost me an evening and are much easier to state than to discover: print() from a Top Shelf extension reaches nobody, and log stream has no device flag. os.Logger at .notice, read in Console.app with the Apple TV tethered, is the channel that works. Not .debug, which isn't persisted by default and so is missing from exactly the capture you collected. An extension can read the shared App Group container but is sandbox-denied from creating files in it — deny(1) file-write-create, surfacing as NSCocoaErrorDomain 513. The containing app has no such restriction. If you need something dynamic on the shelf, the app writes it and the extension only reads it. Full write-up if it's any use: https://mcmizzle.com/blog/tvos-top-shelf-stale-cache/
Aug ’26
Reply to tvOS 14.6 fails trying to bind UDP/1900 (succeeds on tvOS Simulator)
I can't help with the port 1900 part — I haven't reproduced that, and Quinn's reading looks right to me: EADDRINUSE means something on the box already has it, which is a conflict rather than a permission problem. But your second question never got answered, and it has a definite answer now, so for anyone who lands here from a search: Also there seems to be no 'local networking' permission dialog in tvOS - is that also correct? Correct. tvOS has no Local Network permission at all. Apple's TN3179 "Understanding local network privacy" lists the platforms that support the feature, and the table reads: iOS yes iOS 14 iPadOS yes iPadOS 14 macOS yes macOS 15 tvOS no - visionOS yes visionOS 1 watchOS no -https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy So it isn't that the dialog is suppressed, or granted silently, or moved somewhere else in Settings. The privilege doesn't exist on tvOS or watchOS, which also means there is nothing to check and nothing to request. That technote is the 2024 rewrite of the Local Network Privacy FAQ first posted here on the forums in 2020; the table is the part worth bookmarking. The reason people keep asking is that Apple's own documentation contradicts itself, and the page you find first is the wrong one. The reference page for NSLocalNetworkUsageDescription carries an availability badge reading tvOS 14.0+, and its description reads "Any app that uses the local network, directly or indirectly, should include this description. This includes apps that use Bonjour and services implemented with Bonjour, as well as direct unicast or multicast connections to local hosts." My tvOS app makes both of those last two kinds of connection. NSBonjourServices says tvOS 14.0+ as well. Both are current as I write this. If you search the key name rather than the feature name, you get told to declare a key for a mechanism your platform doesn't have. A data point on the multicast half, since it's the same area and confirms what Quinn told you: I run SSDP-style multicast discovery in a tvOS app — joins a group, listens on a UDP port, finds a device on the LAN — with no multicast entitlement, no NSLocalNetworkUsageDescription and no NSBonjourServices in the bundle. On tvOS 27 on an Apple TV 4K it joins the group and hears both of my printers announce themselves every few seconds — 689 announcements in a 33-minute run. Nothing asks and nothing blocks. The part I'd pass on as a warning, because it cost me something. I had the purpose string declared on my tvOS target for months, harmlessly. What it did was work as evidence. I had NWConnection sometimes sitting in .waiting for a few seconds at launch, and two comments in my own code explained that as the user not having answered the Local Network prompt yet — an explanation that sat right next to a plist key appearing to confirm the prompt existed. It stood for nearly three months in the one file someone would open to decide whether that patience was still needed. And the correction is the dangerous direction, not the error. .waiting at tvOS launch is real — Wi-Fi associating, DNS settling after the device wakes — and the connection does reach .ready on its own. "tvOS has no prompt, so nothing is waiting on one" reads as an argument for deleting the timeout that bounds the wait, which is the only thing standing between an unreachable host and a connect call that never returns. The patience was right; the reason I had written down for it wasn't. So: delete the key rather than keeping it as harmless, and keep it on your iOS target, where it really is the text the user reads. Longer write-up, including a second plist key on the same target that turned out to be inert for a different reason: https://mcmizzle.com/blog/tvos-no-local-network-permission/
Topic: Programming Languages SubTopic: Swift Tags:
Replies
Boosts
Views
Activity
1w
Reply to Ventura: NWConnection returns server error in waiting state instead of failed state
I don't have a way to make .waiting become .failed, and I don't know why Ventura changed it either. Quinn's advice to file a bug is still the right thing. But there is a workaround that makes this behave, and it's small, so in case anyone is still stuck here: First, the honest caveat — your error is not my error. ECONNABORTED after a completed handshake and an HTTP 403 is a different event from what I hit, which was ECONNREFUSED on a connection that never established. I have not tested yours. What I think is the same is the shape: .waiting arrives carrying a reason that will never clear, and the framework offers you nothing to tell that apart from the reasons that will. The documentation for the case still says this today: The connection is waiting for a network path change. Connections that are waiting will indicate the reason that the connection couldn’t be established in the associated error. These errors are not fatal.https://developer.apple.com/documentation/network/nwconnection/state-swift.enum/waiting(_:) That's accurate for most of what lands in .waiting. It's what you get before a Local Network permission prompt has been answered, and while Wi-Fi and DNS settle after a device wakes, and in both of those the connection genuinely does reach .ready on its own. It is not accurate for an error where a host has already answered you. So the state is doing two jobs — "hold on, this may come good" and "the host said no" — and only the associated error separates them. The fix is to do that triage yourself: func isTerminal(_ error: NWError) -> Bool { guard case .posix(let code) = error else { return false } switch code { // A host answered. Nothing is pending. case .ECONNREFUSED, .ECONNRESET: return true // No answer, but the path is there: the host itself is not // responding, and no path change will fix that. case .EHOSTDOWN: return true // Deliberately absent: .EHOSTUNREACH, .ENETUNREACH, .ENETDOWN. // Those describe a path that isn't there yet, which is the case // the patient behavior exists for. default: return false } } case .waiting(let error): log("waiting: \(error)") guard isTerminal(error) else { return } // may still come good guard !resumed else { return } resumed = true connection.cancel() continuation.resume(throwing: error)Whether ECONNABORTED belongs in that list is your call rather than mine, and I'd lean yes for your case on the grounds that the server demonstrably answered — but I haven't tested it, so test it against a path you expect to stay patient before you ship it. One more caveat, since the April reply is on iOS: I've only tested this on tvOS, which has no Local Network prompt. On iOS, check connection.currentPath?.unsatisfiedReason for .localNetworkDenied before treating any .waiting reason as final, so you don't cancel a connection that is only waiting for the user to tap Allow. Two things that bit me implementing this, both worth knowing before you write it: .waiting fires repeatedly while NWConnection retries on its own, so if you are bridging into async with withCheckedThrowingContinuation, a bare continuation.resume in that case traps on the second pass with a continuation misuse. Hence the resumed flag above. And throwing out of the await does not stop the connection. Without the cancel() it keeps retrying on its queue behind you. On whether this still reproduces: yes, as of October 2026. I hit it on tvOS 27, on an Apple TV 4K, with ECONNREFUSED (POSIX 61) against a device whose camera port was closed. The refusal arrived in the first log line and my own 8-second connect timeout fired eight seconds later, every attempt. With the triage above, measured on the tvOS 27 simulator against a refused port, it's 8.1s to 0.0s per attempt. Worth saying what the real cost was, because it isn't obvious from an error log: nothing crashed and nothing reported a failure, so the symptom was latency, plus a timeout message that blamed the user's network. The user went and checked his Wi-Fi, which was fine. Longer write-up, with the timeout message that sent him there and a threshold I then "fixed" and had to revert: https://mcmizzle.com/blog/nwconnection-connection-refused-waiting/
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
2w
Reply to [tvOS] ScrollView with Text does not scroll
Your focusable element is attached correctly. The problem is that there is only one of it. On tvOS a ScrollView scrolls as a side effect of focus moving: the focus engine picks a new target, and if that target is off screen the container scrolls to bring it on. With a single focusable view there is no second target to move to, so nothing is ever revealed and the view never budges. One focusable view behaves exactly like none. That is also why your UIFocusSystem.movementDidFailNotification is firing and why -UIFocusLoggingEnabled YES is complaining — the movement really is failing, because there is nowhere to go. Apple documents the mechanism, but in UIKit rather than SwiftUI, on UIFocusItemScrollableContainer — "A type of focus item container that supports automatic scrolling of focusable content", and "The focus engine scrolls the container to keep items onscreen as they become focused." You can write SwiftUI for a long time without meeting that protocol. https://developer.apple.com/documentation/uikit/uifocusitemscrollablecontainer I don't know why the Xcode preview scrolls when the device won't — my assumption is that the preview isn't driving the focus engine, but I haven't confirmed that, so I wouldn't read much into the preview either way. I should say plainly what this does not fix: it will not make a single long Text scroll. If you need one unbroken text view, the UITextView approach further down this thread is the way to do it. What works in SwiftUI is giving the focus engine more than one place to land, which means splitting the content: ScrollView { VStack(alignment: .leading, spacing: 16) { ForEach(paragraphs, id: \.self) { para in Text(para).focusable(true) } } } Two traps I hit doing this in a shipping tvOS app, both of which cost me a round on the device. Putting .focusable(true) on the enclosing VStack instead of the rows looks like the fix and causes the bug — it collapses the whole block to one target. I had written that version myself, having correctly worked out that a scroll view with nothing focusable cannot scroll, and then not noticed that one is the same as none. And the scroll only ever travels to a view that takes focus, so anything above your topmost focusable view is unreachable once it scrolls off the top. If there is a title above the content, make it focusable too or you can go down and never come back up. The cost is a focus ring on prose when the d-pad lands on it. On balance I kept it, since that ring is the only feedback that the remote did anything. Observed on an Apple TV 4K (3rd generation) running tvOS 27, on an app that builds back to tvOS 17. Longer write-up with the two traps and a pane whose scrollability turned out to depend on StoreKit returning products: https://mcmizzle.com/blog/tvos-scrollview-wont-scroll/
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Replies
Boosts
Views
Activity
3w
Reply to How can I test CloudKit User Keychain Reset?
I can't help with the trigger, and I want that out of the way first — I've never reproduced a keychain reset deliberately either, and this thread is already further along that than I got. The ADP precondition above is new to me. But there's something worth knowing before anyone tries this in the default zone, as suggested above, because there it decides whether the test can tell you anything. The documented handling in https://developer.apple.com/documentation/cloudkit/encrypting-user-data is three steps, and the first is "Delete the relevant zones." If the affected records are in the default zone of the private database, that step can't execute — the default zone is the catch-all for every record that didn't name a zone, so deleting it is either refused or far broader than the recovery you're attempting. You'd trigger the reset successfully, watch the recovery fail, and have every reason to think the trigger was still wrong. I found that by accident rather than by looking for it. Two unrelated-looking errors, both only visible against a real container on a device, both the default zone: Field 'recordName' is not marked queryable AppDefaultZone does not support getChanges call The first is the one with a thousand search results, all saying to mark recordName queryable in CloudKit Console. That works, in development — schema is per-environment, and the docs say plainly to "Promote this schema change to your production environment before deploying any app changes to the App Store that rely on the new schema." So it converts a code problem into a release-checklist problem. The second is what you hit if you switch to recordZoneChanges to avoid the index entirely, and it is documented in an easy place to miss: CKFetchRecordZoneChangesOperation's own page doesn't say which zones support it, while CKRecordZone.default() does — in terms of CKFetchRecordChangesOperation, deprecated back in iOS 10. For the operation you'd actually write the only statement I can find is in the documentation archive, on a deprecated web-services endpoint: "Fetching record changes is supported only for custom zones." Moving to a custom zone fixed all three at once, and the third is the reason it isn't a style preference — it's the only one of them that makes the recovery path above executable at all. Two smaller things for anyone landing here from a search, which you've both got right already but the error strings are worth having in one place. CKErrorUserDidResetEncryptedDataKey is a userInfo key, not a CKError.Code case — the code is zoneNotFound, and checking for it as a case doesn't compile, which is lucky. And on the testing problem itself: constructing a CKContainer for an identifier the host has no matching entitlement for traps rather than throwing, so a unit test that touches one takes the process down instead of failing. Whatever harness you end up with for this probably can't be a plain unit test. If anyone does find a reliable way to trigger the reset I'd like to know too — the recovery path is the one piece of this I can reason about but not exercise. Full write-up, with the two errors and why the console fix for the first one is a trap: https://mcmizzle.com/blog/cloudkit-default-zone-limits/
Topic: App & System Services SubTopic: iCloud Tags:
Replies
Boosts
Views
Activity
3w
Reply to Picture-In-Picture not starting with Custom Player
Adding a second cause for -1001, since this thread is the top result for it and the failure reason in the original post doesn't cover the case in the July 2023 reply. To be clear about what I can't help with: if your UIScene genuinely isn't ForegroundActive, the message in the first post is your answer and mine isn't. This is for the other case — the scene logs as UISceneActivationStateForegroundActive and startPictureInPicture() still fails. AVKit will not start Picture in Picture for a player layer that isn't on screen. Foreground is not the same test. In my app the video lives on one tab and the control that starts PiP is on another, so the layer was alive, attached to the controller, and simply not being displayed. isPictureInPicturePossible stayed true the entire time. The call produced: pip: calling startPictureInPicture() (active=false) pip: FAILED to start -- AVKitErrorDomain -1001: Failed to start picture in picture. The fix was to bring the layer on screen first and then ask, and to keep asking, because "is it laid out yet" isn't knowable up front — select the tab, then call startPictureInPicture() at 350/600/900/1200ms until isPictureInPictureActive comes back true. Verified on a physical iPhone 17. The same error turned up separately in a recovery path that built a fresh AVPlayerLayer while the app was backgrounded — a layer that has never been displayed behaves the same way, though that case is also consistent with the scene explanation so I'd not count it as independent evidence. Two things worth doing whatever your cause turns out to be: Implement pictureInPictureController(_:failedToStartPictureInPictureWithError:) if you haven't. startPictureInPicture() returns Void and that delegate method is the only place AVKit hands back an NSError. I'd been reasoning from "the flag is true and nothing happened" for a while before I implemented it. Log ns.localizedFailureReason, not just ns.localizedDescription. localizedDescription is the useless half ("Failed to start picture in picture."); the failure reason is the field carrying the diagnosis in the original post above. I logged only the description and so I can't tell you what reason, if any, AVKit attached to mine. One more thing that wasted my time and may be wasting someone's here: if you have a timeout around the start, check it isn't cutting off successes. Mine gave up at four seconds and the log showed a start landing in the same second as the giving-up, so users got told it had failed and then got a PiP window. Longer write-up with the retry ladder and the isPictureInPicturePossible part: https://mcmizzle.com/blog/pip-wont-start-avkit-1001/
Topic: Media Technologies SubTopic: Audio Tags:
Replies
Boosts
Views
Activity
Sep ’26
Reply to Background Health Store Access for Lock Screen Widgets
I don't have a way around the locked-store restriction either. There's now an explicit DTS answer confirming reads aren't permitted while locked (thread 824819, May 2026), so I've stopped hoping for one. What I can offer is the failure mode I hit while working around it, because it's easy to ship by accident and it presents as a completely different bug. A statistics query that fails because the store is encrypted reports no sum. That is the same thing it reports for a day with genuinely no samples. So if your read does something like result?.sumQuantity()?.doubleValue(for: unit) ?? 0 and binds the error parameter to _, a failed read returns a confident 0. Mine then got published to the widget with a current timestamp, so every morning the widget showed a plausible, freshly-stamped, completely wrong zero until the app was next opened. It reads exactly like a refresh bug — I lost time on timeline reloads and App Group configuration before noticing the timestamp was fresh, which meant the data wasn't stale, it was freshly wrong. What worked, given we can't read while locked: Return an optional from the read. nil for a failed query, and 0 only for HKError.errorNoData, which is HealthKit's way of saying the range really is empty. On failure, publish nothing and leave the previous values untouched. A widget showing yesterday's number is far better than one showing a wrong number today. Audit every path that writes to the widget, not just the one you fixed. A freshly-initialized model object is full of zeros too, and those are every bit as plausible as the ones a failed query produces. Carry the day the numbers describe separately from the "updated at" time. Once a write can be skipped, "when was this written" stops answering "which day is this about" — and that distinction is what lets you render a stale payload as visibly stale instead of asserting it. None of that gets data while locked. It does stop the lock from producing wrong data, which in my case was the actual user-visible bug. Longer version: https://mcmizzle.com/blog/healthkit-widget-shows-zero/
Replies
Boosts
Views
Activity
Aug ’26
Reply to Lifecycle of a tvOS 13.2 TopShelf extension?
Late to this thread, but it still happens on the tvOS 27.0 public beta (24J5325d), on an Apple TV 4K (3rd gen), with an app whose deployment target is tvOS 17. I don't think the caching angle has come up here. Something a reboot does that reinstalling doesn't: the Home screen process caches the rendered Top Shelf tile and dedupes incoming content against that cache. In my case that cache survived a changed item identifier and a full uninstall and reinstall of the app, and kept drawing a tile built from an earlier build for hours while the current code was correct. The log line that finally showed it: Skipping content update for [com.example.app] because it is unchanged If your item identifier is a constant, a corrected item is indistinguishable from the cached one, so a fix to what's in the tile can never reach the screen. Deriving the identifier from the content you're rendering closes that particular trap. (Use the content string itself, not its hashValue — Swift seeds Hashable per process, so a hash changes every launch regardless of content, which isn't what you want.) I want to be careful not to overclaim: your symptom is the extension not running at all after an Xcode relaunch, and mine was a running extension whose output wasn't reaching the screen. Those may well be different bugs. But if you're in the position of "only a reboot fixes it," it's worth checking whether the system is even accepting your content before concluding the extension is dead. Two other things that cost me an evening and are much easier to state than to discover: print() from a Top Shelf extension reaches nobody, and log stream has no device flag. os.Logger at .notice, read in Console.app with the Apple TV tethered, is the channel that works. Not .debug, which isn't persisted by default and so is missing from exactly the capture you collected. An extension can read the shared App Group container but is sandbox-denied from creating files in it — deny(1) file-write-create, surfacing as NSCocoaErrorDomain 513. The containing app has no such restriction. If you need something dynamic on the shelf, the app writes it and the extension only reads it. Full write-up if it's any use: https://mcmizzle.com/blog/tvos-top-shelf-stale-cache/
Replies
Boosts
Views
Activity
Aug ’26