How to use #State and #Binding parallel? - swift

My setup:
I am having a ContentView that represents a the length of a final selection (selection.count). Therefore I need a selection variable on my ContentView using a #State propertyWrapper since I want the View to get update as soon as the value changes. The selection is supposed to be made on my SelectionView therefore I am creating a Binding between my selection variables on the ContentView and SelectionView.
My Problem: The UI on my SelectionView is supposed to be updated as well when the selection variable changes but since it is using #Binding and not #State the view does not get updated. So I would need something where I can use a #State and #Binding at the same time or a #Binding which also makes the UI reload.
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
Form {
ForEach((0...9).identified(by: \.self)) { i in
Button(action: {
if self.selection.contains(i) {
self.selection = self.selection.filter { !($0 == i) }
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}
Note: If I am using #State on the SelectionView instead of #Binding it works properly (which obviously requires me to not create the binding which I want).

There's nothing wrong with your binding. It is the right thing to do and it works the way you want it to. A binding is the way you pass mutable state around in SwiftUI, and you are doing that, and it works. A change to a binding does make the view reload.
To convince yourself of that, just get rid of all the extra stuff in your example, and concentrate on the heart of the matter, the binding:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
VStack {
Button.init("Append") {
self.selection.append(1)
}
Text("Selection: \(selection.count)")
}
}
}
}
Run the example, tap the link, tap the button repeatedly. The display of selection.count is changed in both views. That's what you wanted and that's what happens.
Here's a variant on your original code that displays selection more explicitly (instead of selection.count), and you can see that the right thing is happening:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(String(describing:selection))")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
List {
ForEach(0...9, id:\.self) { i in
Button(action: {
if let ix = self.selection.firstIndex(of:i) {
self.selection.remove(at: ix)
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}

The solution to your problem is: upgrade to the latest Xcode.
I tested your code in a playground under Xcode 11 beta 4 and it worked correctly.
I tested your code in a playground under Xcode 11 beta 3 and it failed in the way you describe (I think).
The current version of Xcode 11 as of this answer is beta 5, under which your code doesn't compile because the identified(by:) modifier has been removed. When I changed your code to work under beta 5, by replacing ForEach((0...9).identified(by: \.self)) with ForEach(0...9, id: \.self), it worked correctly.
Therefore I deduce that you are still running beta 2 or beta 3. (Form wasn't available in beta 1 so I know you're not using that version.)
Please understand that, at the moment, SwiftUI is still quite buggy, and also still undergoing incompatible API changes. The bugs are unfortunate, but the API evolution is good. It's better that we suffer a few months of changes now than years of less optimal API later.
This means that you need to try to stay on the latest Xcode 11 beta unless it introduces bugs (like the Path bug in beta 5, if your app uses Path) that prevent you from making any progress.

Thanks to #robmayoff I was able to solve the problem. It was a problem with Xcode 11 Beta 3, installing the newest beta version solved the problem

Related

In NavigationView, List item appears mid-screen rather than at top when #Environment(\.presentationMode) is defined

I'm facing a weird bug where a list item within a NavigationView appears below a gap. When I scroll, it resets to correct location.
Minimum reproducible example on Xcode 14.1, iOS 16.1.2:
import SwiftUI
#main
struct Test_2022_12_03_02App: App {
var body: some Scene {
WindowGroup {
ListView()
}
}
}
struct ListView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView()) {
Text("Hello")
}
}
.navigationViewStyle(.stack)
}
}
struct DetailView: View {
// If I remove the line below, the bug disappears
#Environment(\.presentationMode) var presentationMode
var members = [1]
var body: some View {
List {
ForEach(members, id:\.self) { member in
Text("World")
}
}
}
}
This bug happens consistently on my phone, but doesn't always happen in simulators. Weirdly my friend's phone also displayed expected behavior with same iOS version.
More notes:
If I remove #Environment(\.presentationMode) var presentationMode the bug disappears. (I need this line for my full app, to be able to return to main screen from the navigation link)
The bug also disappears if in the DetailView, we don't use a List, i.e.
struct DetailView: View {
// If I remove the line below, the bug disappears
#Environment(\.presentationMode) var presentationMode
var members = [1]
var body: some View {
List {
// ForEach(members, id:\.self) { member in
Text("World")
// }
}
}
}
Two questions:
Do you know what's happening? Am I missing something in my code?
If this is a freak bug, how do I find a workaround? Note that I need both the list and the ability to return to main screen in my full app.
Thanks!
NavigationView is deprecated and so is presentationMode. There is bound to be unforeseen bugs using deprecated methods. Use navigation stack instead: https://developer.apple.com/documentation/swiftui/navigationstack

WatchOS Using ObservableObject in Conditional in View Causing Runtime Error

I use an ObservableObject to keep the state of whether a user is subscribed to my app or not, and based on the subscription status, show different views. This worked fine prior to Xcode 13 and WatchOS 8, but now this is causing a runtime error of runtime: SwiftUI: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. And, the binding does not update per the error. This occurs on both Xcode 13.1 and 13.2b2
This code below reproduces the error:
struct MultiPageView: View {
#ObservedObject var subscribed = SubscribedModel.shared
var body: some View {
if subscribed.value {
TabView {
ViewOne()
ViewTwo()
ViewThree()
ToggleView()
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView {
ViewOne()
ToggleView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}
struct ToggleView: View {
#ObservedObject var subscribed = SubscribedModel()
var body: some View {
Toggle(isOn: $subscribed.value) {
Text("Subscribed")
}
}
}
class SubscribedModel: ObservableObject {
public static let shared = SubscribedModel.shared
#Published var value: Bool = false
}
I am only listing ViewOne for brevity, but ViewTwo and ViewThree are the same with different text:
struct ViewOne: View {
var body: some View {
Text("View One")
.padding()
}
}
If you navigate to the ToggleView(), and switch the toggle, the error pops immediately. Any suggestions to fix this?
Update per #LoremIpsum comment:
struct MultiPageView: View {
#StateObject var subscribed = SubscribedModel()
var body: some View {
if subscribed.value {
TabView {
ViewOne()
ViewTwo()
ViewThree()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView {
ViewOne()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
}
}
}
struct ToggleView: View {
#Binding var subscribed: Bool
var body: some View {
Toggle(isOn: $subscribed) {
Text("Subscribed")
}
}
}
It is now switching between the TabViews, but the error still remains, and is showing up immediately. Deleted DerivedData and cleaned build folder. Any thoughts?
I will add that this same basic code is running fine on iOS 15. It is just WatchOS that is popping the error.
I was having the same issue for a long time, and this is still happening on Xcode 13.2.1.
Seems to be an issue with TabView on watchOS, because if you replace the TabView for another View the error is gone.
The solution is to use the initialiser for TabView with a selection value: init(selection:content:)
1 Define a property for selection
#State private var selection = 0
2 Update TabView
From
TabView {
// content
}
To
TabView(selection: $selection) {
// content
}
Updating your code would look like this:
struct MultiPageView: View {
#StateObject var subscribed = SubscribedModel()
#State private var selection = 0
var body: some View {
if subscribed.value {
TabView(selection: $selection) {
ViewOne()
ViewTwo()
ViewThree()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView(selection: $selection) {
ViewOne()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
}
}
}
Basically just defining a #State property for TabView.selection, and using it on both your TabViews (using separated properties would also work).

Menu not updating (SwiftUI bug?)

I'm using macOS Montery Beta 4 along with Xcode 13 Beta 4, and I think I've discovered a bug in SwiftUI.
When using a CommandGroup along with a button that is enabled/disabled based on a condition, the CommandGroup doesn't update. CommandMenu does however.
Reproduction:
Create a new SwiftUI macOS project
Paste the following code in the App file:
class Test: ObservableObject {
#Published var num = 0
}
#main
struct TestApp: App {
#StateObject private var test = Test()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(test)
}
.commands {
CommandGroup(after: .newItem) {
Button(action: {}) {
Text("Test menu item")
}
.disabled(test.num == 0)
}
}
}
}
Paste the following code in the ContentView file:
struct ContentView: View {
#EnvironmentObject var test: Test
var body: some View {
Button(action: {
test.num += 1
}) {
Text(String(test.num) + " ---------")
}
}
}
Run the app, and click on the File menu. You should see that "Test menu item" is disabled as expected.
Click on the button. This simply increments a number by 1. However, "Test menu item" is still disabled, even though test.num != 0.
The thing is, replacing the CommandGroup with CommandMenu("Test menu") fixes it.
All the app does is have a menu item that is disabled if a number is zero. Pressing the button makes that number not zero, but the menu item stays disabled.
Is anybody able to reproduce this, and is this a bug on my part?
a minor variation of #George_E answer, is this:
CommandGroup(after: .newItem) {
TestView(test: test)
}
struct TestView: View {
#ObservedObject var test: Test
var body: some View {
Button(action: {}) {
Text("Test menu item")
}
.disabled(test.num == 0)
}
}
I wouldn't expect that to be intended behaviour. However, I have found a workaround to fix this.
It's odd that onChange(of:perform:) isn't run when test.num changes, but we can still detect the publisher. So I split this into a separate view and when the view receives a new value, the button is updated.
Code:
CommandGroup(after: .newItem) {
TestView(pub: test.$num)
}
struct TestView: View {
let pub: Published<Int>.Publisher
#State private var disabled = true
var body: some View {
Button(action: {}) {
Text("Test menu item")
}
.disabled(disabled)
.onReceive(pub) { new in
disabled = new == 0
}
}
}

Why does modifying the label of a NavigationLink change which View is displayed in SwiftUI?

I have an #EnvironmentObject called word (of type Word) whose identifier property I'm using for the label of a NavigationLink in SwiftUI. For the DetailView that is linked to the NavigationLink, all I have put is this:
struct DetailView: View {
#EnvironmentObject var word: Word
var body: some View {
VStack {
Text(word.identifier)
Button(action: {
self.word.identifier += "a"
}) {
Text("Click to add an 'a' to Word's identifier")
}
}
}
}
The ContentView that leads to this DetailView looks like this (I've simplified my actual code to isolate the problem).
struct ContentView: View {
#EnvironmentObject var word: Word
var body: some View {
NavigationView {
NavigationLink(destination: DetailView()) {
Text(word.identifier)
}
}
}
}
When I tap the button on the DetailView, I'd expect it to update the DetailView with a new word.identifier that has an extra "a" appended onto it. When I tap it, however, it takes me back to the ContentView, albeit with an updated word.identifier. I can't seem to find a way to stay on my DetailView when the word.identifier being used by the ContentView's NavigationLink is modified. Also, I am running Xcode 11.3.1 and am currently unable to update, so if this is has been patched, please let me know.
Here is workaround solution
struct DetailView: View {
#EnvironmentObject var word: Word
#State private var identifier: String = ""
var body: some View {
VStack {
Text(self.identifier)
Button(action: {
self.identifier += "a"
}) {
Text("Click to add an 'a' to Word's identifier")
}
}
.onAppear {
self.identifier = self.word.identifier
}
.onDisappear {
self.word.identifier = self.identifier
}
}
}
This works as expected on iOS 13.4, assuming Word is something like:
class Word : ObservableObject {
#Published var identifier = "foo"
}

SwiftUI: Animate Cells within a Form

I am trying to animate my Form or rather the cells within it. My problem is that the following code give me a nice insertion animation but for the removal the cell is suddenly removed after am ugly looking delay.
import SwiftUI
struct ContentView: View {
#State var toggledValue = false
#State var pickedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Toggle(isOn: $toggledValue) {
Text("Toggled Value")
}
if toggledValue {
Picker(selection: $pickedValue, label: Text("Picked Value")) {
ForEach((0...5).identified(by: \.self)) {
Text("Pick Value \($0)").tag($0)
}
}
}
}
Section {
Text("Some Text")
}
}
.navigationBarTitle("Navigation Bar Title")
}
}
}
What I tried so far is to to wrap the Toggle in a withAnimation closure but this does not change anything. What makes me wondering is that the same code using List instead of Form gives me the expected Animation. Is that a bug or am I overseeing something?
This will probably work (tested in iOS 16 in a similar situation):
Add #State private var isShowingPicker = false
Replace if toggledValue by if isShowingPicker
Under .navigationBarTitle(...) add:
onChange(of: toggledValue)
{ newValue in
withAnimation { isShowingPicker = newValue }
}