Hello dear developers!
Recently, I stumbled upon some really strange behavior of SwiftUI and I’m very curious why it works this way
struct ContentView: View {
@State private var title: String?
@State private var isSheetPresented: Bool = false
var body: some View {
Button("Hello, world!") {
title = "Sheet title"
isSheetPresented = true
}
.sheet(isPresented: $isSheetPresented, content: {
if let title {
Text(title)
} else {
EmptyView()
}
})
}
}
Why in this case when we tap the button and sheet comes in we go to the branch else even though we set title before isSheetPresented but it still somehow nil
But what really drive me crazy is that if we change a little bit code to this:
I just added another @State property 'number' and use it as the Button's title. In this scenario it works 😃 and Text in the sheet view appearing
struct ContentView: View {
@State private var title: String?
@State private var number = 0
@State private var isSheetPresented = false
var body: some View {
Button("\(number)") {
title = "Sheet title"
number += 0
isSheetPresented = true
}
.sheet(isPresented: $isSheetPresented, content: {
if let title {
Text(title)
} else {
EmptyView()
}
})
}
}
Is this somehow related to what happens under the hood like View Tree and Render Tree (Attribute Graph)?
Maybe because ContentView’s body doesn't capture title it cannot be stored in the Render Tree so it always would have the initial value of nil?
if there are any well-informed folks here, please help me figure out this mystery, I’d appreciate it!!!
p.s.
Don’t get me wrong. Im not interested in how to make it work. I’m interested in why this doesn’t work and what really happens under the hood that led to this result
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
Hello 👋
I ran into a SwiftUI lifecycle gotcha while debugging a List with .refreshable
I share the code used to reproduce the issue
@Observable
final class CounterModel: Identifiable {
let id: String
var title: String
var value: Int
init(id: String, title: String, value: Int = 0) {
self.id = id
self.title = title
self.value = value
}
deinit {
print("deinit", title)
}
}
@Observable
final class ObservableCountersStore {
var counters: [CounterModel] = [
.init(id: "1", title: "A"),
.init(id: "2", title: "B"),
.init(id: "3", title: "C")
]
func refresh() async {
try? await Task.sleep(nanoseconds: 300_000_000)
counters = [.init(id: "4", title: "D")]
}
}
struct ObservableCountersListView: View {
@State private var store = ObservableCountersStore()
var body: some View {
List {
ForEach(store.counters) { counter in
ObservableCounterRow(counter: counter)
}
}
.refreshable {
await store.refresh()
}
}
}
struct ObservableCounterRow: View {
let counter: CounterModel
var body: some View {
Text(counter.title)
}
}
Observation:
After calling refresh(), only some of the previous CounterModel
only one CounterModel is deallocated immediately.
Others are retained
This doesn’t look like a leak, but it made me realize that passing observable
reference types directly into List rows leads to non-deterministic object
lifetimes, especially with .refreshable.
Posting this as a gotcha — curious if this matches intended behavior
or if others have run into the same thing.
Hi everyone. Can you help me with my settings icon design. I`m trying to create circular setting button using Menu. My code here:
struct MenuView: View {
var body: some View {
Menu {
Text("Hello")
Text("How are you")
} label: {
Image(systemName: "gearshape.fill")
.clipShape(Circle())
}
.clipShape(Circle())
.padding(.top, 10)
.padding(.leading, 20)
}
}
You can see my try, this one looks wrong.
It should be like this:
Just Circle with setting image inside. Thank you an advance 😭🙏🛐
Hi Everyone. Can you help me with my settings icon design. I'm trying to create a circular settings button using Menu. My code here:
struct MenuView: View {
var body: some View {
Menu {
Text("Hello")
Text("How are you")
} label: {
Image(systemName: "gearshape.fill")
.clipShape(Circle())
}
.clipShape(Circle())
.padding(.top, 10)
.padding(.leading, 20)
}
}
You can see my try, this one looks wrong.
It should be like this:
Just Circle with setting image inside. Thank you an advance 😭🙏🛐
Hi there,
I just re-install 26.2, but I got many issues regarding to SwiftUI's components.
How can I fix it? Do I need download or install something else?
I have a desktop application that shows some real estate properties chosen by the user. The application shows those GPP locations on the map. The SwiftUI code is something like the following.
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View
ZStack {
mapView
}
}
private var mapView: some View {
Map(position: $propertyViewModel.mapPosition) {
ForEach(propertyViewModel.properties) { property in
Annotation("", coordinate: CLLocationCoordinate2D(latitude: property.lat, longitude: property.lon)) {
Button {
} label: {
VStack {
Image(systemName: "house.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 48)
.foregroundStyle(colorScheme == .light ? .white : .black)
...
}
}
.buttonStyle(.borderless)
}
}
UserAnnotation()
}
.mapControls {
MapUserLocationButton()
}
.mapControlVisibility(.visible)
.onAppear {
CLLocationManager().requestWhenInUseAuthorization()
}
}
}
The application only wants to use the CLLocationManager class so that it can show those locations on the map relative to your current GPS position. And I'm hit with two review rejections.
Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage
Issue Description
One or more purpose strings in the app do not sufficiently explain the use of protected resources. Purpose strings must clearly and completely describe the app's use of data and, in most cases, provide an example of how the data will be used.
Guideline 5.1.5 - Legal - Privacy - Location Services
The app uses location data for features that are not relevant to a user's location.
Specifically, the app is not functional when Location Services are disabled.
So I wonder if the application is even required to have 'NSLocationWhenInUseUsageDescription' and/or 'NSLocationUsageDescription'? just in order to show user's current location so that they can see property locations relative to it? The exact location privacy statement is the following.
The application needs your permission in accessing your current location so that it will appear on the map
Topic:
App & System Services
SubTopic:
Maps & Location
Tags:
MapKit
Privacy
SwiftUI
Maps and Location
My app has a collection of Cell Views. some of the views' model objects include a Player, and others do not.
I want to use drag and drop to move a player from one cell to another. I only want cells that contain a player to be valid drag sources. All cells will be valid drop destinations.
When I uncomment the .disabled line both drag and drop become disabled.
Is it possible to keep a view enabled as a dropDestination but disabled as a draggable source?
VStack {
Image("playerJersey_red")
if let player = content.player {
Text(player.name)
}
}
.draggable(content)
// .disabled(content.player == nil)
.dropDestination(for: CellContent.self) { items, location in
One thing I tried was to wrap everything in a ZStack and put a rectangle with .opacity(0.02) above the Image/Text VStack. I then left draggable modifying my VStack and moved dropDestination to the clear rectangle. This didn't work as I wasn't able to initiate a drag when tapping on the rectangle.
Any other ideas or suggestions?
thanks
inline-code
How do you achieve this effect in toolbar? Where is the documentation for this?
How to make it appear top toolbar or bottom toolbar?
Thank you!
Here is what I have now...
.toolbar {
ToolbarItem(placement: .destructiveAction) {
Button {
showConfirm = true
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
}
}
}
.confirmationDialog(
"Are you sure?",
isPresented: $showConfirm,
titleVisibility: .visible
) {
Button("Delete Item", role: .destructive) {
print("Deleted")
}
Button("Archive", role: .none) {
print("Archived")
}
Button("Cancel", role: .cancel) { }
}
}
Currently i am trying really hard to create experience like the Apple fitness app. So the main view is a single day and the user can swipe between days. The week would be displayed in the toolbar and provide a shortcut to scroll to the right day.
I had many attempts at solving this and it can work. You can create such an interface with SwiftUI. However, changing the data on every scroll makes limiting view updates hard and additionally the updates are not related to my code directly. Instruments show me long updates, but they belong to SwiftUI and all the advice i found does not apply or help.
struct ContentView: View {
@State var journey = JourneyPrototype(selection: 0)
@State var position: Int? = 0
var body: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 0) {
ForEach(journey.collection, id: \.self) { index in
Listing(index: index)
.id(index)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.scrollPosition(id: $position)
.onChange(of: position) { oldValue, newValue in
journey.selection = newValue ?? 0
journey.update()
}
.onScrollPhaseChange { oldPhase, newPhase in
if newPhase == .idle {
journey.commit()
}
}
}
}
struct Listing: View {
var index: Int
var body: some View {
List {
Section {
Text("Title")
.font(.largeTitle)
.padding()
}
Section {
Text("\(index)")
.font(.largeTitle)
.padding()
}
Section {
Text("1 ")
Text("2 ")
Text("3 ")
Text("4 ")
Text("5 ")
Text("6 ")
}
}
.containerRelativeFrame(.horizontal)
}
}
@Observable
class JourneyPrototype {
var selection: Int
var collection: [Int]
var nextUp: [Int]?
init(selection: Int) {
self.selection = selection
self.collection = [selection]
Task {
self.collection = [-2,-1,0,1,2]
}
}
func update() {
self.nextUp = [
self.selection - 2,
self.selection - 1,
selection,
self.selection + 1,
self.selection + 2
]
}
func commit() {
self.collection = self.nextUp ?? self.collection
self.nextUp = nil
}
}
#Preview {
ContentView()
}
There are some major Problem with this abstracted prototype
ScrollView has no good trigger for the update, because if i update on change of the position, it will update much more than once. Thats why i had to split calculation and applying the diff
The LazyHStack is not optimal, because there are only 5 View in the example, but using HStack breaks the scrollPosition
Each scroll updates all List, despite changing only 2 numbers in the array. AI recommended to append and remove, which does nothing about the updates.
In my actual Code i do this with Identifiable data and the Problem is the same. So the data itself is not the problem?
Please consider, this is just the rough prototype to explain the problem, i am aware that an array of Ints is not ideal here, but the problem is the same in Instruments and much shorter to post.
Why am i posting this? Scrolling through dynamic data is required for many apps, but there is no proper solution to this online. Github and Blogs are fine with showing a progress indicator and letting the user wait, some probably perform worse than this prototype. Other solutions require UIKit like using a UIPageViewController. But even using this i run in small hitches related to layout.
Important consideration, my data for the scrollview is too big to be calculated upfront. 100 years of days that are calculated for my domain logic take too long, so i have no network request, but the need to only act on a smaller window of data.
Instruments shows long update for one scroll action tested on a iPhone SE 2nd generation
ListRepresentable has 7 updates and takes 17ms
LazySubViewPlacements has 2 updates and takes 8ms
Other long updates are too verbose to include
I would be very grateful for any help.
Hi all,
I’m running into a “double update” effect in SwiftUI when using the @Observable with @State. I’m trying to understand whether this is expected behavior, a misuse on my side, or a potential bug.
Setup
I have an observable store using the Observation macro:
@Observable
class AlbumStore {
var albums: [Album] = [
Album(id: "1", title: "Album 1", author: "user1"),
Album(id: "2", title: "Album 2", author: "user1"),
Album(id: "3", title: "Album 3", author: "user1"),
Album(id: "4", title: "Album 4", author: "user1"),
Album(id: "5", title: "Album 5", author: "user1"),
Album(id: "6", title: "Album 6", author: "user1")
]
func addAlbum(_ album: Album) {
albums.insert(album, at: 0)
}
func removeAlbum(_ album: Album) {
albums.removeAll(where: { $0 == album })
}
}
In my view, I inject it via @Environment and also keep some local state:
@Environment(AlbumStore.self) var albumStore
@State private var albumToAdd: Album?
I derive a computed array that depends on both the environment store and local state:
private var filteredAlbums: [Album] {
let albums = albumStore.albums.filter { album in
if let albumToAdd {
return album.id != albumToAdd.id
} else {
return true
}
}
return albums
}
View usage
Inside a horizontal ScrollView / LazyHStack, I observe changes to filteredAlbums:
@ViewBuilder
private func carousel() -> some View {
GeometryReader { proxy in
let itemWidth: CGFloat = proxy.size.width / 3
let sideMargin = (proxy.size.width - itemWidth) / 2
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 20) {
ForEach(filteredAlbums, id: \.id) { album in
albumItem(album: album)
.frame(width: itemWidth)
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
content
.scaleEffect(phase.isIdentity ? 1.0 : 0.8)
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned(limitBehavior: .always))
.scrollPosition(id: $carouselScrollID, anchor: .center)
.contentMargins(.horizontal, sideMargin, for: .scrollContent)
.onChange(of: filteredAlbums) { old, new in
print("filteredAlbums id: \(new.map { $0.id })")
}
}
}
Triggering the update
When I add a new album, I do:
albumToAdd = newAlbum
albumStore.addAlbum(newAlbum)
Expected behavior
Since filteredAlbums explicitly filters out albumToAdd, I expect the result to remain unchanged.
Actual behavior
I consistently get two onChange callbacks, in this order:
filteredAlbums id: ["E852E42A-AAEC-4360-A6A6-A95752805E2E", "1", "2", "3", "4", "5", "6"]
filteredAlbums id: ["1", "2", "3", "4", "5", "6"]
This suggests:
The AlbumStore update (albums.insert) is observed first.
The @State update (albumToAdd) is applied later.
As a result, filteredAlbums is recomputed twice with different dependency snapshots.
On a real iPad device, this also causes a visible scroll position jump.
In the simulator, the jump is not visually observable; however, the onChange(of: filteredAlbums) callback still fires twice with the same sequence of values, indicating that the underlying state update behavior is identical.
Strange observations
This does not happen with ObservableObject
If I replace @Observable with a classic ObservableObject + @Published:
class OBAlbumStore: ObservableObject {
@Published var albums: [Album] = [
Album(id: "1", title: "Album 1", author: "user1"),
Album(id: "2", title: "Album 2", author: "user1"),
Album(id: "3", title: "Album 3", author: "user1"),
Album(id: "4", title: "Album 4", author: "user1"),
Album(id: "5", title: "Album 5", author: "user1"),
Album(id: "6", title: "Album 6", author: "user1")
]
func addAlbum(_ album: Album) {
albums.insert(album, at: 0)
}
func removeAlbum(_ album: Album) {
albums.removeAll(where: { $0 == album })
}
}
…and inject it with @EnvironmentObject, the double update disappears.
Removing GeometryReader also avoids the issue
If I remove the surrounding GeometryReader and hardcode sizes:
@ViewBuilder
private func carousel() -> some View {
// GeometryReader { proxy in
let itemWidth: CGFloat = 400
let sideMargin: CGFloat = 410
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 20) {
ForEach(filteredAlbums, id: \.id) { album in
albumItem(album: album)
.frame(width: itemWidth)
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
content
.scaleEffect(phase.isIdentity ? 1.0 : 0.8)
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned(limitBehavior: .always))
.scrollPosition(id: $carouselScrollID, anchor: .center)
.contentMargins(.horizontal, sideMargin, for: .scrollContent)
.onChange(of: filteredAlbums) { old, new in
print("filteredAlbums id: \(new.map { $0.id })")
}
// }
}
…the double onChange no longer occurs.
Questions
Is this update ordering expected when using @Observable and @State?
Does Observation intentionally propagate environment changes before local state updates?
Is GeometryReader forcing an additional evaluation pass that exposes this ordering?
Is this a known limitation / bug compared to ObservableObject?
I want to understand why this behaves differently under Observation.
Thanks in advance for any insights 🙏
Full Project Link
Hi. I'm trying to show 3D or AR content in multiple pages on an iOS app but I have found that if a RealityView is used earlier in a flow then future uses will never display the camera feed, even if those earlier uses do not use any spatial tracking.
For example, in the following simplified view, the second page realityView will not display the camera feed even though the first view does not use it not start a tracking session.
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
RealityView { content in
content.camera = .virtual
// showing model skipped for brevity
}
NavigationLink("Second Page") {
RealityView { content in
content.camera = .spatialTracking
}
}
}
}
}
}
What is the way around this so that 3D content can be displayed in multiple places in the app without preventing AR content from working?
I have also found the same problem when wrapping an ARView for use in SwiftUI.
Hi all - i'm encountering a strange issue since updating to Xcode 26.0.1.
It seems that any SwiftUI Views that have an :ObservedObject property that contains @Published properties, and use those properties inside the View, no longer update when those properties are updated when the view also has an @Environment property.
If I remove the @Environment property and any usage of it, the view updates correctly.
The specific environment property i'm using is .safeAreaInsets, like so:
@Environment(\.safeAreaInsets) private var safeAreaInsets
Is this a recognised bug in the latest iOS 26 SDK?
Thanks
I am adopting some of the new glass UI, but having to duplicate a lot of code to maintain support for previous UI systems in macOS. An example:
if #available(macOS 26.0, *) {
VStack {
/*some 40+ lines of code clipped here for brevity*/
}
.cueButtons()
.cueStyleGlass()
} else {
VStack {
/*identical 40+ lines of code clipped here for brevity*/
}
.cueButtons()
.cueStyle()
}
If I try to use conditional modifiers as indicated here:
extension View {
func cueStyle(font: Font = .system(size: 45)) -> some View {
if #available(macOS 26.0, *) {
modifier(GlassCueStyle(font: font))
} else {
modifier(CueStyle(font: font))
}
}
}
I get this error:
Conflicting arguments to generic parameter 'τ_1_0' ('ModifiedContent<Self, GlassCueStyle>' vs. 'ModifiedContent<Self, CueStyle>')
Is there a better way to do this?
I am using the native SwiftUI WebView and WebPage APIs (iOS 26+) and would like to implement file download functionality using the native SwiftUI WebView. However, I have not been able to find any APIs equivalent to WKDownload.
In WKWebView, the WKDownload API can be used to handle downloads. I am looking for a similar API or recommended approach in the native SwiftUI WebView that would allow downloading files.
If anyone has guidance or suggestions on how to implement this, I would appreciate your help.
I'm running Xcode 26.2 (17C52).
After deleting all simulators and creating new ones, SwiftUI Preview stopped working.
Xcode reports:
“Simulator [D774B214-XXXX-XXXX-XXXX-XXXXXXXX8288] failed to boot and may have crashed.”
However, when I list all available simulators using xcrun simctl list, there is no simulator with that identifier. It seems Xcode is still referencing a previously deleted simulator.
I have tried:
Deleting all simulators again
Creating new simulators
Restarting Xcode and the system
But the issue persists and Preview still fails to launch.
Has anyone encountered this issue or knows how to clear the cached simulator reference used by SwiftUI Preview?
I just found a weird bug:
If you place a Text view using .foregroundStyle(.secondary), .tertiary, or other semantic colors inside a ScrollView, and apply a Material background clipped to an UnevenRoundedRectangle, the text becomes invisible. This issue does not occur when:
The text uses .primary or explicit colors (e.g., .red, Color.blue), or
The background is clipped to a standard shape (e.g., RoundedRectangle).
A minimal reproducible example is shown below:
ScrollView{
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello World.")
.font(.system(size: 15))
.foregroundStyle(.quinary)
}
}
.padding()
.frame(height: 100)
.background(Material.regular)
.clipShape(UnevenRoundedRectangle(topLeadingRadius: 10,bottomLeadingRadius: 8,bottomTrailingRadius:8, topTrailingRadius: 8))
Hello, Developers!
While writing custom view modifier I ran into unexpected behavior of .strokeBorder modifier. The underlying content seem to be “bleeding” outside of the stroke border edges, even though they share the exact same shape for their layout.
This issue relevant for both Xcode Previews and on-device testing.
Maybe someone has experienced this issue before, I'd be glad to see your opinion on this matter.
Why there is a working animation with ScrollView + ForEach of items removal, but there is none with List?
ScrollView + ForEach:
struct ContentView: View {
@State var items: [String] = Array(1...5).map(\.description)
var body: some View {
ScrollView(.vertical) {
ForEach(items, id: \.self) { item in
Text(String(item))
.frame(maxWidth: .infinity, minHeight: 50)
.background(.gray)
.onTapGesture {
withAnimation(.linear(duration: 0.1)) {
items = items.filter { $0 != item }
}
}
}
}
}
}
List:
struct ContentView: View {
@State var items: [String] = Array(1...5).map(\.description)
var body: some View {
List(items, id: \.self) { item in
Text(String(item))
.frame(maxWidth: .infinity, minHeight: 50)
.background(.gray)
.onTapGesture {
withAnimation(.linear(duration: 0.1)) {
items = items.filter { $0 != item }
}
}
}
}
}```
I had take screenshots by following code
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
self.uiImage = window?.rootViewController?.view!.getImage(rect: rect)
View has two views. One is ImageView contains some image and overlay of image detection results with .overlay. another view is InfoView contains several info and button which above code fired. on iOS 17, I can take screenshots as I saw, but on iOS26, missing on image of ImageView. Overlay(detected rectangle) in Imageview and InfView can be taken.
How can I take screenshots as I saw on iOS26?(iPad)
I’ve notice that in Maps, some pins contain images and do not have the little triangle at the bottom of it, yet they still animate the same when clicked. How could this be achieved? I believe the name of this annotation is MKMapFeatureAnnotation.
I've tried this and it did not give the same result. I'm able to create a custom MKMarkerAnnotationView but it does not animate the same (balloon animation like the MKMapFeatureAnnotation). I was looking forward to create a custom MKMapFeatureAnnotation similar in design which would animate the same. Unfortunately, I cannot create a custom MKMapFeatureAnnotation because everything is privated