This example is based on the latest version of Swift 6.2 for macOS.
I have several classes that cannot conform to Codable for various reasons. This, unfortunately, prevents me from using Transferable. If I serialize a class instance into a Data blob and use that for drag-and-drop, it works perfectly — the drag operation succeeds. However, the destination has no way to distinguish between different types of Data blobs.
All I’d need is a way to specify a unique identifier and type that I could reference in the drop handler to determine what kind of object I’m working with.
Being restricted to Transferable feels quite limiting when your data models can’t conform to Codable. It’s honestly frustrating. Has anyone else run into this issue? Is there a reliable workaround?
I tried creating a Transferable wrapper like this:
struct CustomObjectTransfer: Codable, Transferable
{
var data: Data
static var transferRepresentation: some TransferRepresentation
{
// Cannot use '.myGreatSettings' because Main actor–isolated static property 'myGreatSettings' cannot be referenced from a nonisolated context
CodableRepresentation(contentType: .init(exportedAs: "com.yourProject.settings"))
}
}
extension UTType
{
static let myGreatSettings: UTType = UTType("com.yourProject.settings")!
}
In my list view
.draggable ( CustomObjectTransfer(data: myObjectData) )
The UI correctly recognizes the item as draggable. If I misspell the exportedAs identifier, it even throws an error, confirming that the exported type is being recognized by the system.
However, the drop destination completely ignores this type:
.dropDestination(for: CustomObjectTransfer.self)
{ items, location in
dump(items)
return true
}isTargeted:
{ isTargeted in
myDestinationIsTargeted = isTargeted
}
If I switch to using Data.self directly — wrapping the original object data manually — everything works. I can deserialize and validate the data as needed.
The problem arises when handling multiple custom drag types. The drop target accepts any data, regardless of its intended type. You only find out the actual type after the drop occurs, during validation — which is too late. By then, isTargeted has already turned true, making the drop appear valid to the user when it actually isn’t.
Again anyone else feel this pain? Or is there a workaround that I am missing?
SwiftUI
RSS for tagProvide views, controls, and layout structures for declaring your app's user interface using SwiftUI.
Posts under SwiftUI tag
200 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi, I've created an carousel using an Tabview component and it is divided in pages, each page has 7 items. Now we wan't to load the carousel page by page, but whenever I updated the list which Tabview uses to render, it flickers because it rerenders previous elements.
I've tried to use a scroll view for this but this app is for Vision Pro and I added some 3D transformations (position/rotation) to the cards from the tabview and when I added this in the scrollview they becom unclickable.
Do you have any sugestions what can I do?
Bellow is a sample of the code, I put [items] just to suggest there is a list.
VStack (spacing: 45) {
TabView(selection: $navigationModel.carouselSelectedPage) {
let orderedArtist = [items]
let numberOfPages = Int((Double(orderedArtist.count) / Double(cardsPerPage)).rounded(.up))
if(numberOfPages != 1){
Spacer().tag(-1)
}
ForEach(0..<numberOfPages, id: \.self) { page in
LazyHStack(alignment: .top, spacing: 16){
ForEach(0..<cardsPerPage, id: \.self) { index in
if(page * cardsPerPage + index < orderedArtist.count){
let item = orderedArtist[page * cardsPerPage + index]
GeometryReader{proxy in
We have a custom implementation of what we call a “Scrollable Header” in our app. After building with Xcode 26, we’ve observed a change in behavior with the List component.
The issue can be seen in the attached GIF:
As the user scrolls up, the header is expected to collapse smoothly, and it does—until the moment the next list item becomes visible. At that point, the header collapses prematurely, without any apparent reason.
We’ve identified that this behavior occurs after the list’s data-fetching logic runs, which loads additional items as the user scrolls.
Below is the ViewModifier responsible for handling the collapsing header logic:
@available(iOS 18.0, *)
public struct L3CollapseHeaderIOS18: ViewModifier {
private let minHeight: Double = 0
private let expandedHeight: CGFloat
private let L3Height = 44.0
private let isViewVisible: Bool
@Binding private var currentHeight: CGFloat
@State private var lastOffset: ScrollOffsetInfo = ScrollOffsetInfo(offset: 0.0, offsetToBottom: 0.0, scrollableContent: 0.0)
init(currentHeight: Binding<CGFloat>, expectedHeight: CGFloat, isViewVisible: Bool) {
self._currentHeight = currentHeight
self.expandedHeight = expectedHeight
self.isViewVisible = isViewVisible
}
public func body(content: Content) -> some View {
content
.onScrollGeometryChange(for: ScrollOffsetInfo.self, of: { geometry in
if isViewVisible {
return ScrollOffsetInfo(offset: geometry.contentOffset.y, offsetToBottom: (geometry.contentSize.height) - (geometry.contentOffset.y + geometry.containerSize.height), scrollableContent: geometry.contentSize.height - geometry.containerSize.height)
} else {
return lastOffset
}
}, action: { oldValue, newValue in
if isViewVisible {
expandOrCollapseHeader(oldValue: oldValue, newValue: newValue)
}
})
}
private func expandOrCollapseHeader(oldValue: ScrollOffsetInfo, newValue: ScrollOffsetInfo) {
let oldScrollableContent = round(oldValue.scrollableContent)
let newScrollableContent = round(newValue.scrollableContent)
print("@@ scrollable content: \(newScrollableContent), old value: \(oldScrollableContent)")
if newScrollableContent != oldScrollableContent {/*abs(newValue.offset) - abs(oldValue.offset) > 80*/
return
}
let isInitialPosition = newValue.offset == 0 && lastOffset.offset == 0
let isTryingToBounceUp = newValue.offset < 0
let isTryingToBounceDown = newValue.offsetToBottom < 0
// As the header collapses, the scrollable content decreases its size
let remainingHeaderSpaceVisible = expandedHeight - currentHeight
// remainingHeaderSpaceVisible is summed to know the exact scrollableContent size
let isScrollableContentSmallToAnimate = (newValue.scrollableContent + remainingHeaderSpaceVisible) < (expandedHeight * 2 + currentHeight)
if isInitialPosition || isScrollableContentSmallToAnimate {
expandHeader(0)
return
}
let scrollDirection = scrollDirection(newValue, oldOffset: oldValue)
switch scrollDirection {
case .up(let value):
if isTryingToBounceUp {
expandHeader(0)
return
}
print("@@ will collapse with value: \(value)")
collapseHeader(value)
case .down(let value):
if isTryingToBounceDown {
collapseHeader(0)
return
}
print("@@ will expand with value: \(value)")
expandHeader(value)
case .insignificant:
return
}
}
private func expandHeader(_ value: CGFloat) {
currentHeight = min(68.0, currentHeight - value)
}
private func collapseHeader(_ value: CGFloat) {
currentHeight = max(0, currentHeight - value)
}
private func scrollDirection(_ currentOffset: ScrollOffsetInfo, oldOffset: ScrollOffsetInfo) -> ScrollDirection {
let scrollOffsetDifference = abs(currentOffset.offset) - abs(oldOffset.offset)
print("@@ currentOffset: \(currentOffset.offset), oldOffset: \(oldOffset.offset), difference: \(scrollOffsetDifference)")
let status: ScrollDirection = scrollOffsetDifference > 0
? .up(scrollOffsetDifference)
: .down(scrollOffsetDifference)
lastOffset = currentOffset
return status
}
enum ScrollDirection {
case up(CGFloat)
case down(CGFloat)
case insignificant
}
}
public struct ScrollOffsetInfo: Equatable {
public let offset: CGFloat
public let offsetToBottom: CGFloat
public let scrollableContent: CGFloat
}
public struct ScrollOffsetInfoPreferenceKey: PreferenceKey {
public static var defaultValue: ScrollOffsetInfo = ScrollOffsetInfo(offset: 0, offsetToBottom: 0, scrollableContent: 0)
public static func reduce(value: inout ScrollOffsetInfo, nextValue: () -> ScrollOffsetInfo) {
}
}
We use this ViewModifier to monitor updates to the List’s frame via the onScrollGeometryChange method.
From our investigation (see the screenshot below), it appears that onScrollGeometryChange is triggered just before the content size updates. The first event we receive corresponds to the view’s offset, and only nanoseconds later do we receive updates about the content’s scrollable height.
I’ll share the code for the List component using this modifier in the comments (it exceeded the character limit here).
Can anyone help us understand why this change in behavior occurs after building with Xcode 26?
Thanks in advance for any insights.
According to the UtilityWindow documentation:
• They receive FocusedValues from the focused main scene in an application, similar to commands in the main menu, which can be used to display information on the active content as the user focuses on different scenes.
This makes me think that any UtilityWindow, even across Scenes, should always receive @FocusedValues from the currently focused window of any scene.
However, the following code doesn't quite work:
@Observable class Info: Identifiable {
let id = UUID()
}
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView() // Inside, it uses .focusedSceneValue(info)
}
UtilityWindow("Test") {
UtilityWindowContent()
}
}
}
struct UtilityWindowContent: View {
@FocusedValue(Info.self) var info
var body: some View {
Text(info?.id ?? "<nil>") // This always shows "<nil>"
}
}
Here's some annotated screenshots of what's happening in a project:
Menu bar commands do work with the same setup:
As a workaround, attempting to use @FocusedValue in the App struct works, but as a result, the value appears to instantiate multiple times in the background, and they re-instantiate with every focus change.
The @FocusedValue variable gets the correct instance, but it's needlessly initializing others in the background.
This is on macOS 26.0 Tahoe Beta 8 (25A5349a).
Hi everyone!
I've encountered an issue when using Sheet + ScrollView on Mac Catalyst: the buttons in the toolbar appear with an abnormal gray color.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
}
.sheet(isPresented: .constant(true)) {
Sheet()
}
}
}
struct Sheet: View {
var body: some View {
NavigationStack {
ScrollView { // <-- no issue if use List
}
.toolbar {
Button(action: {}) { // <-- 👀 weird gray color
Image(systemName: "checkmark")
}
}
}
}
}
Steps to Reproduce:
On macOS 26.0 beta 9, use Xcode 26.0 beta 7 to create an iOS project and enable Mac Catalyst.
Paste the code above.
Select the Mac Catalyst scheme and run the project.
The buttons in the toolbar show a strange gray appearance.
If you change the ScrollView to a List in the code, the issue does not occur.
FB20120285
If the cell is expandable in List View, it will show slowly after deletion swipe.
the image shows how it looks like when this issue happens
and it should be easy to reproduce with this sample codes
// Model for list items
struct ListItem: Identifiable {
let id = UUID()
let title: String
let createdDate: Date
let description: String
}
struct ExpandableListView: View {
// Sample constant data
private let items: [ListItem] = [
ListItem(
title: "First Item",
createdDate: Date().addingTimeInterval(-86400 * 5),
description: "This is a detailed description for the first item. It contains two lines of text to show when expanded."
),
ListItem(
title: "Second Item",
createdDate: Date().addingTimeInterval(-86400 * 3),
description: "This is a detailed description for the second item. It provides additional information about this entry."
),
ListItem(
title: "Third Item",
createdDate: Date().addingTimeInterval(-86400 * 1),
description: "This is a detailed description for the third item. Tap to expand and see more details here."
),
ListItem(
title: "Fourth Item",
createdDate: Date(),
description: "This is a detailed description for the fourth item. It shows comprehensive information when tapped."
)
]
// Track which item is currently selected/expanded
@State private var selectedItemId: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(items) { item in
ExpandableCell(
item: item,
isExpanded: selectedItemId == item.id
) {
// Handle tap - toggle selection
withAnimation(.easeInOut(duration: 0.3)) {
if selectedItemId == item.id {
selectedItemId = nil
} else {
selectedItemId = item.id
}
}
}
}
.onDelete { indexSet in
for index in indexSet {
print("delete \(items[index].title)")
}
}
}
.navigationTitle("Items")
}
}
}
struct ExpandableCell: View {
let item: ListItem
let isExpanded: Bool
let onTap: () -> Void
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Always visible: Title and Date
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.font(.headline)
.foregroundColor(.primary)
Text(dateFormatter.string(from: item.createdDate))
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
// Chevron indicator
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
.font(.caption)
}
// Expandable: Description (2 lines)
if isExpanded {
Text(item.description)
.font(.body)
.foregroundColor(.secondary)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
.transition(.opacity.combined(with: .move(edge: .top)))
.padding(.top, 4)
}
}
.padding(.vertical, 8)
.contentShape(Rectangle())
.onTapGesture {
onTap()
}
}
}
#Preview {
ExpandableListView()
}
The scroll position is not updated when orientation changes in a ScrollView with .scrollTargetBehavior(.viewAligned) and .scrollPosition().
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .yellow, .cyan, .blue, .teal, .brown, .orange, .indigo]
@State private var selected: Int? = 0
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(0..<colors.count, id: \.self) { index in
Rectangle()
.fill(colors[index])
.containerRelativeFrame(.horizontal)
.overlay {
Text(colors[index].description)
}
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $selected)
.scrollTargetBehavior(.viewAligned)
}
}
#Preview {
ContentView()
}
@main
struct ViewAlignedScrollBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Tested on Xcode 15.3 (15E204a), iOS 17.3.1 iPhone, iOS 17.4 Simulator.
Bug report FB13685677 filed with Apple.
I'm building a Swift video editor with AVFoundation and a custom compositor. Despite setting AVVideoComposition.frameDuration to 60 FPS, I'm seeing significant frame skipping during playback.
Console Output Shows Frame Skipping
Frame #0 at 0.0 ms (fps: 60.0)
Frame #2 at 33.333333333333336 ms (fps: 60.0)
Frame #6 at 100.0 ms (fps: 60.0)
Frame #10 at 166.66666666666666 ms (fps: 60.0)
Frame #32 at 533.3333333333334 ms (fps: 60.0)
Frame #62 at 1033.3333333333335 ms (fps: 60.0)
Frame #96 at 1600.0 ms (fps: 60.0)
Instead of frames every ~16.67ms (60 FPS), I'm getting irregular intervals, sometimes 33ms, 67ms, or hundreds of milliseconds apart.
Renderer.swift (Key Parts)
@MainActor
class Renderer: ObservableObject {
@Published var playerItem: AVPlayerItem?
private let assetManager: ProjectAssetManager?
private let compositorId: String
func buildComposition() async {
// ... load mouse moves/clicks data ...
let composition = AVMutableComposition()
let videoTrack = composition.addMutableTrack(
withMediaType: .video,
preferredTrackID: kCMPersistentTrackID_Invalid
)
var currentTime = CMTime.zero
var layerInstructions: [AVMutableVideoCompositionLayerInstruction] = []
// Insert video segments
for videoURL in videoURLs {
let asset = AVAsset(url: videoURL)
let tracks = try await asset.loadTracks(withMediaType: .video)
let assetVideoTrack = tracks.first
let duration = try await asset.load(.duration)
try videoTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: duration),
of: assetVideoTrack,
at: currentTime
)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
let transform = try await assetVideoTrack.load(.preferredTransform)
layerInstruction.setTransform(transform, at: currentTime)
layerInstructions.append(layerInstruction)
currentTime = CMTimeAdd(currentTime, duration)
}
let videoComposition = AVMutableVideoComposition()
videoComposition.frameDuration = CMTime(value: 1, timescale: 60) // 60 FPS
// Set render size from first video
if let firstURL = videoURLs.first {
let firstAsset = AVAsset(url: firstURL)
let firstTrack = try await firstAsset.loadTracks(withMediaType: .video).first
let naturalSize = try await firstTrack.load(.naturalSize)
let transform = try await firstTrack.load(.preferredTransform)
videoComposition.renderSize = CGSize(
width: abs(naturalSize.applying(transform).width),
height: abs(naturalSize.applying(transform).height)
)
}
let instruction = CompositorInstruction()
instruction.timeRange = CMTimeRange(start: .zero, duration: currentTime)
instruction.layerInstructions = layerInstructions
instruction.compositorId = compositorId
videoComposition.instructions = [instruction]
videoComposition.customVideoCompositorClass = CustomVideoCompositor.self
let playerItem = AVPlayerItem(asset: composition)
playerItem.videoComposition = videoComposition
self.playerItem = playerItem
}
}
class CompositorInstruction: NSObject, AVVideoCompositionInstructionProtocol {
var timeRange: CMTimeRange = .zero
var enablePostProcessing: Bool = false
var containsTweening: Bool = false
var requiredSourceTrackIDs: [NSValue]?
var passthroughTrackID: CMPersistentTrackID = kCMPersistentTrackID_Invalid
var layerInstructions: [AVVideoCompositionLayerInstruction] = []
var compositorId: String = ""
}
class CustomVideoCompositor: NSObject, AVVideoCompositing {
var sourcePixelBufferAttributes: [String : Any]? = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]
var requiredPixelBufferAttributesForRenderContext: [String : Any] = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]
func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) {}
func startRequest(_ asyncVideoCompositionRequest: AVAsynchronousVideoCompositionRequest) {
guard let sourceTrackID = asyncVideoCompositionRequest.sourceTrackIDs.first?.int32Value,
let sourcePixelBuffer = asyncVideoCompositionRequest.sourceFrame(byTrackID: sourceTrackID),
let outputBuffer = asyncVideoCompositionRequest.renderContext.newPixelBuffer() else {
asyncVideoCompositionRequest.finish(with: NSError(domain: "VideoCompositor", code: -1))
return
}
let videoComposition = asyncVideoCompositionRequest.renderContext.videoComposition
let frameDuration = videoComposition.frameDuration
let fps = Double(frameDuration.timescale) / Double(frameDuration.value)
let compositionTime = asyncVideoCompositionRequest.compositionTime
let seconds = CMTimeGetSeconds(compositionTime)
let frameInMilliseconds = seconds * 1000
let frameNumber = Int(round(seconds * fps))
print("Frame #\(frameNumber) at \(frameInMilliseconds) ms (fps: \(fps))")
asyncVideoCompositionRequest.finish(withComposedVideoFrame: outputBuffer)
}
func cancelAllPendingVideoCompositionRequests() {}
}
VideoPlayerViewModel
@MainActor
class VideoPlayerViewModel: ObservableObject {
let player = AVPlayer()
private let renderer: Renderer
func loadVideo() async {
await renderer.buildComposition()
if let playerItem = renderer.playerItem {
player.replaceCurrentItem(with: playerItem)
}
}
}
What I've Tried
Frame skipping is consistent—exact same timestamps on every playback
Issue persists even with minimal processing (just passing through buffers)
Occurs regardless of compositor complexity
Please note that I need every frame at exact millisecond intervals for my application. Frame loss or inconsistent frameInMillisecond values are not acceptable.
I’m seeing a layout issue in SwiftUI on iOS 26 that only reproduces with specific Accessibility Motion settings.
Steps to reproduce
1. Open Settings → Accessibility → Motion.
2. Enable Reduce Motion and Prefer Cross-Fade Transitions.
3. Launch an app with a SwiftUI TextField.
4. Tap the field to show the keyboard.
5. Dismiss the keyboard (tap outside, swipe down, etc.).
Expected:
After the keyboard is dismissed, the view’s bottom safe area / layout should return to normal.
Actual:
The view continues to reserve space equal to the keyboard height — as if the keyboard were still visible. UI anchored to the safe area remains shifted upward until the view is reloaded.
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...?)
The keyboard looks dimmed/disabled on every keyboard and every keyboard type inside my app, but is still fully functional. The dimmed/disabled effect includes keyboards shown inside the WebView.
The bug only appears in iOS26 and iOS26.0.1, but not on any previous versions of iOS.
When using a TextField with axis to set to .vertical on iOS, it sets a bound selection parameter to an erroneous value. Whilst on MacOS it performs as expected.
Take the following code:
import SwiftUI
@main
struct SelectionTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@FocusState private var isFocused: Bool
@State private var text = ""
@State private var textSelection: TextSelection? = nil
var body: some View {
TextField("Label", text: $text, selection: $textSelection, axis: .vertical)
.onChange(of: textSelection, initial: true) {
if let textSelection {
print("textSelection = \(textSelection)")
} else {
print("textSelection = nil")
}
}
.focused($isFocused)
.task {
isFocused = true
}
}
}
Running this on MacOS target gives the following on the console:
textSelection = nil
textSelection = TextSelection(indices: SwiftUI.TextSelection.Indices.selection(Range(0[any]..<0[any])), affinity: SwiftUI.TextSelectionAffinity.downstream)
Running the code on iOS gives:
textSelection = TextSelection(indices: SwiftUI.TextSelection.Indices.selection(Range(1[any]..<1[any])), affinity: SwiftUI.TextSelectionAffinity.upstream)
textSelection = TextSelection(indices: SwiftUI.TextSelection.Indices.selection(Range(1[any]..<1[any])), affinity: SwiftUI.TextSelectionAffinity.upstream)
Note here the range is 1..<1 - which is incorrect.
Also of side interest this behaviour changes if you remove the axis parameter:
textSelection = nil
Am I missing something, or is this a bug?
Hello,
I am writing an audio utility, with a typical audio track player, in SwiftUI for macos 26.
My current problem is that the SwiftUI stops rendering the main window UI when the window loses focus.
This is a problem since even clicking on the app menu bar has the window loose focus, and the timer, time cursor and all animations of the audio piece stop.
All baground services, audio, timers and model continue running (even tho with some crackling on the switch). Once the window focus is re-obtained the animations continue and skip to current state.
I have read that SwiftUI optimizes macos like ios, and disables the ui run loop, but there must be a way to disable, since this obviously not the case for most mac app.
Is there a solution with either SwiftUI or involving AppKit?
In visionOS, is there a way to temporarily hide the window close/position handle at the bottom of a window?
The Safari app does this, so it must be possible.
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.
I’m really frustrated with iOS 26. It was supposed to make better use of screen space, but when you combine the navigation bar, tab bar, and search bar, they eat up way too much room.
Apple actually did a great job with the new tab bar — it’s smaller, smooth, and looks great when expanding or collapsing while scrolling. The way the search bar appears above the keyboard is also really nice.
But why did they keep the navigation bar the same height in both portrait and landscape? In landscape it takes up too much space and just looks bad. It was way better in iOS 18.
I have a custom input view in my app which is .focusable(). It behaves similar to a TextField, where it must be focused in order to be used.
This works fine on all platforms including iPad, except when when an external keyboard is connected (magic keyboard), in which case it can't be focused anymore and becomes unusable.
Is there a solution to this, or a workaround? My view is very complex, so simple solutions like replacing it with a native view isn't possible, and I must be able to pragmatically force it to focus.
Here's a very basic example replicating my issue. Non of the functionality works when a keyboard is connected:
struct FocusableTestView: View {
@FocusState private var isRectFocused: Bool
var body: some View {
VStack {
// This text field should focus the custom input when pressing return:
TextField("Enter text", text: .constant(""))
.textFieldStyle(.roundedBorder)
.onSubmit {
isRectFocused = true
}
.onKeyPress(.return) {
isRectFocused = true
return .handled
}
// This custom "input" should focus itself when tapped:
Rectangle()
.fill(isRectFocused ? Color.accentColor : Color.gray.opacity(0.3))
.frame(width: 100, height: 100)
.overlay(
Text(isRectFocused ? "Focused" : "Tap me")
)
.focusable(true, interactions: .edit)
.focused($isRectFocused)
.onTapGesture {
isRectFocused = true
print("Focused rectangle")
}
// The focus should be able to be controlled externally:
Button("Toggle Focus") {
isRectFocused.toggle()
}
.buttonStyle(.bordered)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
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.
Hello,
Whenever I put accessibility focus on an image and if image has some text in it, voiceover reads that text along with image's accessibility label.
Is there a way to programmatically turn off text recognition on images for accessibility?
I couldn't find any relevant accessibility API's that could help here.
Thanks!
Hi! I am learning Swift and UIKit for work. I am trying to automate using a pickerWheel in VisionOS, but since .adjust(toValue: ) was removed in VisionOS's API, I am absolutely struggling to find a way to set a pickerWheel to a specific value.
Currently, my solution is to calculate the amount of times I would need to increment/decrement the wheel to get from the current value to the desired value, then do so one at a time. However, this currently does not work, as .accessibilityIncrement() and .accessibilityDecrement() do not work, and .swipeUp() and .swipeDown() go too far. What can I do?
Note: I am not a frontend engineer, so while solutions may exist that involve changes to the frontend, I would much rather try and get the frontend we do have to work as is.