Here's what I want to do:
Have a SwiftUI view which changes a local State variable
On a button tap, pass that variable to some other part of my application
However, for some reason, even though I update the state variable, it doesn't get updated when it's passed to the next view.
Here's some sample code which shows the problem:
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
If you tap on "Change Number", it updates the local state to 99. But when I create another view and pass this as a parameter, it shows 1 instead of 99. What's going on?
Some things to note:
If you uncomment Text("\(number)"), it works. But this shouldn't be necessary IMO.
It also works if you make SomeView use a binding. But for my app, this won't work. My actual use case is a 'select game options' view. Then, I will create a non-SwiftUI game view and I want to pass in these options as parameters. So, I can't have bindings all the way down my gaming code just because of this bug. I want to just capture what the user enters and create a Parameters object with that data.
It also works if you make it a navigationDestination instead of a fullScreenCover. ¯\(ツ)/¯ no idea on that one...
A View is a struct, therefore its properties are immutable, so the view can not change its own properties. This is why changing the property named number from inside the body of the view needs this property to be annotated with a #State property wrapper. Thanks to Swift and SwiftUI, transparent read and write callbacks let the value being seen changed. So you must not pass number as a parameter of SomeView() when calling fullScreenCover(), but pass a reference to number, for the callbacks to be systematically called: $number. Since you are not passing an integer anymore to construct struct SomeView, the type of the property named number in this struct can not any longer be an integer, but must be a reference to an integer (namely a binding): use the #Binding annotation for this.
So, replace SomeView(number: number) by SomeView(number: $number) and let number: Int by #Binding var number: Int to do the job.
Here is the correct source code:
import SwiftUI
struct NumberView: View {
#State var number: Int = 1
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: $number)
}
}
}
}
struct SomeView: View {
#Binding var number: Int
var body: some View {
Text("\(number)")
}
}
After all that said to obtain a valid source code, their is a little trick that has not been explained up to now: if you simply replace in your source code Text("Change Number") by Text("Change Number \(number)"), without using $ reference nor #Binding keywords anywhere, you will see that the problem is also automatically solved! No need to use #binding in SomeView! This is because SwiftUI makes optimizations when building a tree of views. If it knows that the displayed view changed (not only its properties), it will compute the view with updated #State values. Adding number to the button label makes SwiftUI track changes of the number state property and it now updates its cached value to display the Text button label, therefore this new value will be correctly used to create SomeView. All of that may be considered as strange things, but is simply due to optimizations in SwiftUI. Apple does not fully explain how it implements optimizations building a tree of views, there are some informations given during WWDC events but the source code is not open. Therefore, you need to strictly follow the design pattern based on #State and #Binding to be sure that the whole thing works like it should.
All of that said again, one could argue that Apple says that you do not have to use #Binding to pass a value to a child view if this child view only wants to access the value: share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access (https://developer.apple.com/documentation/swiftui/state). This is right, but Apple says in the same article that you need to place [state] in the highest view in the view hierarchy that needs access to the value. With Apple, needing to access a value means that you need it to display the view, not only to do other computations that have no impact on the screen. This is this interpretation that lets Apple optimize the computation of the state property when it needs to update NumberView, for instance when computing the content of the Text("Change Number \(number)") line. You could find it really tricky. But there is a way to understand that: take the initial code you wrote, remove the #State in front of var number: Int = 1. To compile it, you need to move this line from inside the struct to outside, for instance at the very first line of your source file, just after the import declaration. And you will see that it works! This is because you do not need this value to display NumberView. And thus, it is perfectly legal to put the value higher, to build the view named SomeView. Be careful, here you do not want to update SomeView, so there is no border effects. But it would not work if you had to update SomeView.
Here is the code for this last trick:
import SwiftUI
// number is declared outside the views!
var number: Int = 1
struct NumberView: View {
// no more state variable named number!
// No more modification: the following code is exactly yours!
#State private var showNumber = false
var body: some View {
NavigationStack {
VStack(spacing: 40) {
// Text("\(number)")
Button {
number = 99
print(number)
} label: {
Text("Change Number")
}
Button {
showNumber = true
} label: {
Text("Show Number")
}
}
.fullScreenCover(isPresented: $showNumber) {
SomeView(number: number)
}
}
}
}
struct SomeView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
This is why you should definitely follow the #State and #Binding design pattern, taking into account that if you declare a state in a view that does not use it to display its content, you should declare this state as a #Binding in child views even if those children do not need to make changes to this state. The best way to use #State is to declare it in the highest view that needs it to display something: never forget that #State must be declared in the view that owns this variable; creating a view that owns a variable but that does not have to use it to display its content is an anti-pattern.
Since number isn't read in body, SwiftUI's dependency tracking detect it. You can give it a nudge like this:
.fullScreenCover(isPresented: $showNumber) { [number] in
Now a new closure will be created with the updated number value whenever number changes. Fyi the [number] in syntax is called a "capture list", read about it here.
Nathan Tannar gave me this explanation via another channel which I think gets to the crux of my problem. It does seem that this is a SwiftUI weirdness caused by knowing when and how it updates views based on state. Thanks Nathan!
It’s because the number isn’t “read” in the body of the view. SwiftUI is smart in that it only triggers view updates when a dependency of the view changes. Why this causes issues with the fullScreenCover modifier is because it captures an #escaping closure for the body. Which means it’s not read until the cover is presented. Since its not read the view body will not be re-evaluated when the #State changes, you can validate this by setting a breakpoint in the view body. Because the view body is not re-evaluated, the #escaping closure is never re-captured and thus it will hold a copy of the original value.
As a side note, you’ll find that once you present the cover for the first time and then dismiss, subsequent presentations will update correctly.
Arguably this seems like a SwiftUI bug, the fullScreenCover probably shouldn’t be #escaping. You can workaround by reading the number within the body, or wrapping the modifier with something like this, since here destination is not #escaping captured so the number will be read in the views body evaluation.
struct FullScreenModifier<Destination: View>: ViewModifier {
#Binding var isPresented: Bool
#ViewBuilder var destination: Destination
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented) {
destination
}
}
}
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed last month.
Improve this question
UPDATE: Let's put it very simple. This:
struct FavoriteButton: View {
#Binding var items : [Item]
var body: some View {
Button("Toggle") {
.....
}
}
}
You have the item at your disposal, not the array to pass in FavoriteButton(items: [something]). If you just create the array it will break the binding as it is the struct not the class and it will not keep the reference. Can you pass it somehow and keep the binding?
ORIGINAL QUESTION (which also explains why):
I have a List in SwiftUI. Let's say that it is bound to [Item] array and that there is a property Item.isFavorite that triggers visual change in a row. The item should be handled by the FavoriteButton that can be triggered by the single item like in swipeActions or by multiple items like in contextMenu(forSelection:).
The question is whether it is possible to do this by passing an item array to the button? My problem is that array needs to be passed as the binding so that changing of the isFavorite updates the view, and if I create a new array from the single item I seem to be unable to keep the binding.
One possible solution is to pass the item id array and then do the work in the view model (this way I don't need to keep the binding in the button). However I am particularly interested in whether it is possible to be done by passing the item array to the button and binding. I am aware that the solution with just using ids and view model might be better, but for some curiosity I am interested if this is possible (I think it should be).
EDIT: as some people asked for clarification, I'll copy it from my comment below where I have provided it:
You have a FavoriteButton view that has #Binding var items : [Item]. You only have Item (not an array) when creating the FavoriteButton at your disposal. How do you pass it as the array that keeps the binding to the original item?
This is really a most entertaining comment thread and it feels like reverse engineering code from a textual descriptions can become a new sport. Might be something for ChatGPT ;) And yes, I will delete this intro soon.
But here comes a suggestion for what I tried to understand. isFavourite can be toggled either by the new .contextMenu(forSelectionType) or the described FavoriteButton. I might be way off, just let me know :)
struct Item: Identifiable {
let id = UUID()
var name: String
var isFavorite = false
}
struct ContentView : View {
init() {
var dummy = [Item]()
for i in 1...10 {
dummy.append(Item(name: "Item \(i)"))
}
self._data = State(initialValue: dummy)
}
#State private var data: [Item]
#State private var selection: UUID?
var body: some View {
List(data, selection: $selection) { item in
HStack {
Image(systemName: "heart").opacity(item.isFavorite ? 1 : 0)
Text(item.name)
Spacer()
FavoriteButton(items: $data, selection: item.id)
}
.swipeActions {
FavoriteButton(items: $data, selection: item.id)
}
}
.contextMenu(forSelectionType: UUID.self) { indices in
FavoriteButton(items: $data, selection: indices.first)
}
}
}
struct FavoriteButton: View {
#Binding var items : [Item]
let selection: Item.ID?
var body: some View {
Button("Toggle") {
if let index = items.firstIndex(where: { $0.id == selection }) {
items[index].isFavorite.toggle()
}
}
}
}
Edit: now with FavoriteButton also used inside contextMenu.
Edit2: now with FavoriteButton also used in .swipeActions
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 months ago.
Improve this question
I want to print the name of each swiftUI but on run time if it’s possible
Im trying to print out the name of a swiftUI view like on swift type(of:) but nothing. Any idea?
try something like this, to ...get the name of a SwiftUI view programmatically:
struct MyView: View {
#State var viewName = ""
var body: some View {
Text(viewName)
.onAppear {
viewName = "\(Self.self)"
}
}
}
Or simply:
struct MyView: View {
let viewName = "\(Self.self)"
var body: some View {
Text(viewName)
}
}
Description
For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.
To read about the usage of these follow the links:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)
My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I'd like to use it in different Views and in their ViewModels?
As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.
But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.
What would be the best way to access this original object?
It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it :)
Code
What I've tried to do:
struct BaseView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack(path: $viewModel.paths) {
VStack {
Button("Page 1", action: viewModel.goToPage1)
Button("Page 2", action: viewModel.goToPage2)
}
.navigationDestination(for: String.self) { stringParam in
Page1(stringParam: stringParam)
}
.navigationDestination(for: Int.self) { intParam in
Page2(intParam: intParam)
}
}
}
}
extension BaseView {
#MainActor class ViewModel: ObservableObject {
#Published var paths = NavigationPath()
func goToPage1() {
let param = "Some random string" // gets the parameter from some calculation or async network call
paths.append(param)
}
func goToPage2() {
let param = 19 // gets the parameter from some calculation or async network call
paths.append(param)
}
}
}
struct Page1: View {
#StateObject var viewModel = ViewModel()
let stringParam: String
var body: some View {
VStack {
Button("Page 2", action: viewModel.goToPage2)
}
}
}
extension Page1 {
#MainActor class ViewModel: ObservableObject {
func goToPage2() {
// Need to add value to the original paths variable in BaseView.ViewModel
}
}
}
struct Page2: View {
#StateObject var viewModel = ViewModel()
let intParam: Int
var body: some View {
Text("\(intParam)")
}
}
extension Page2 {
#MainActor class ViewModel: ObservableObject {
}
}
There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don't even have access to the traditional view layer - it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you'll just have the same consistency problems that SwiftUI was designed to eliminate.
Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn't bother to learn it properly. Fortunately things are changing:
I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)
How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)
Stop using MVVM for SwiftUI (Apple Developer Forums)
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 10 months ago.
Improve this question
I want to show/hide progress view in swiftUI globally. I have api class for api calling but I want to show progressview when api calling.. how can I do that
class NetworkManager: NSObject {
static let shared = NetworkManager()
//MARK:- ======== FETCH API DATA =========
func fetchAPIData(url:String,
isShowHUD:Bool){
headerDic["timezone"] = TimeZone.current.identifier
if isShowHUD {
// I want to show progressview from here top of the screen.
}
**api calling code.**
}
}
}
Here if I pass isShowHUD then show progress view on top of the screen. if any error or got the response then it hide the progressview.
Please help me on that.how can I achieve in swift ui.I am in new in swift UI.
You'd need to add a ProgressView and you'd need a variable to notify when loading has finished when it's successful. Documentation on ProgressView can be found here. Without giving you the full answer he's a simple solution to add a ProgressView when doing some kind of network request.
class Network: ObservableObject {
#Published var loading = true
func fetchapidata() {
/*
Network request goes here, is successful change loading to false
*/
self.loading = false
}
}
struct ContentView: View {
#StateObject var network = Network()
var body: some View {
NavigationView {
List {
/*
Details go here
*/
}
.overlay {
if network.loading {
ProgressView("Loading")
}
}
.listStyle(.plain)
.navigationTitle("Title")
}
.navigationViewStyle(.stack)
.onAppear {
network.fetchapidata()
}
}
}