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
}
}
}
Related
I'm seeing onChange(of: Bool) action tried to update multiple times per frame warnings when clicking on NavigationLinks in the sidebar for a SwiftUI macOS App.
Here's what I currently have:
import SwiftUI
#main
struct BazbarApp: App {
#StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
class ModelData: ObservableObject {
#Published var myLinks = [URL(string: "https://google.com")!, URL(string: "https://apple.com")!, URL(string: "https://amazon.com")!]
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
#State private var selected: URL?
var body: some View {
NavigationView {
List(selection: $selected) {
Section(header: Text("Bookmarks")) {
ForEach(modelData.myLinks, id: \.self) { url in
NavigationLink(destination: DetailView(selected: $selected) ) {
Text(url.absoluteString)
}
.tag(url)
}
}
}
.onDeleteCommand {
if let selected = selected {
modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selected)!)
}
selected = nil
}
Text("Choose a link")
}
}
}
struct DetailView: View {
#Binding var selected: URL?
var body: some View {
if let selected = selected {
Text("Currently selected: \(selected)")
}
else {
Text("Choose a link")
}
}
}
When I alternate clicking on the second and third links in the sidebar, I eventually start seeing the aforementioned warnings in my console.
Here's a gif of what I'm referring to:
Interestingly, the warning does not appear when alternating clicks between the first and second link.
Does anyone know how to fix this?
I'm using macOS 12.2.1 & Xcode 13.2.1.
Thanks in advance
I think the issue is that both the List(selection:) and the NavigationLink are trying to update the state variable selected at once. A List(selection:) and a NavigationLink can both handle the task of navigation. The solution is to abandon one of them. You can use either to handle navigation.
Since List look good, I suggest sticking with that. The NavigationLink can then be removed. The second view under NavigationView is displayed on the right, so why not use DetailView(selected:) there. You already made the selected parameter a binding variable, so the view will update if that var changes.
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
#State private var selected: URL?
var body: some View {
NavigationView {
List(selection: $selected) {
Section(header: Text("Bookmarks")) {
ForEach(modelData.myLinks, id: \.self) { url in
Text(url.absoluteString)
.tag(url)
}
}
}
.onDeleteCommand {
if let selected = selected {
modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selected)!)
}
selected = nil
}
DetailView(selected: $selected)
}
}
}
I can recreate this problem with the simplest example I can think of so my guess is it's an internal bug in NavigationView.
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink("A", destination: Text("A"))
NavigationLink("B", destination: Text("B"))
NavigationLink("C", destination: Text("C"))
}
}
}
}
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).
I'm having this odd issue with macOS Montery 3 & Xcode 13 Beta 3, where I get this error with List's and animations:
[General] Row index 0 out of row range (numberOfRows: 0) for <SwiftUIListCoreOutlineView: 0x133885000>
Its kinda hard to explain, but heres a simple reproduction:
Minimal Reproducible Example
Create a new SwiftUI macOS application
Paste this code:
struct ContentView: View {
#State var items = ["Item"]
#ViewBuilder var mainView: some View {
if items.isEmpty {
Text("Im empty")
}
else {
List(items, id: \.self) {s in
Text(s)
}
}
}
var body: some View {
NavigationView {
mainView
.toolbar {
Button(action: {
withAnimation {
items.removeAll()
}
}) {
Image(systemName: "minus")
}
}
Text("Second")
}
}
}
Run the app, and try resizing the sidebar. (You should be able to)
Then press the minus button on the toolbar. This simply removes all the items.
Then, the resizing of the sidebar should be broken. You might also get a bunch of errors in the console.
Is anybody able to reproduce this issue, and is it a bug in SwiftUI?
got the same behavior. The code works well if I remove the "withAnimation". Or put "items.removeAll()" outside the "withAnimation"
This bug is driving me insane. Sometimes (well most of the time) presented sheet gets dismissed first time it is opened. This is happening only on a device and only the first time the app is launched. Here is how it looks on iPhone 11 running iOS 14.1 built using Xcode 12.1 (can be reproduced on iPhone 7 Plus running iOS 14.0.1 as well):
Steps in the video:
I open app
Swiftly navigate to Details view
Open Sheet
Red Sheed gets dismissed by the system???
I open sheet again and it remains on the screen as expected.
This is SwitUI App project (using UIKIt App Delegate) and deployment iOS 14. Code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#State var sheetIsPresented: Bool = false
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.toggle()
}
}.sheet(isPresented: $sheetIsPresented, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
I was able to fix this problem by removing line .navigationViewStyle(StackNavigationViewStyle()), but I need StackNavigationViewStyle in my project. Any help will be appreciated.
Updated: There is a similar post on Apple forum with sheet view acting randomly weird.
One solution that I found is to move sheet to the root view outside NavigationLink (that would be ContentView in my example), but that is not ideal solution.
I had the same problem in an app. After a great deal of research, I found that making the variable an observed object fixed the problem in SwiftUI 1, and it seems to be in SwiftUI 2. I do remember that it was an intermittent problem on an actual device, but it always happened in the simulator. I wish I could remember why, maybe when the sheet appears it resets the bound variable?, but this code fixes the problem:
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailsView()) {
Text("Open Details View")
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailsView: View {
#ObservedObject var sheetIsPresented = SheetIsPresented.shared
var body: some View {
VStack {
Button("Open") {
sheetIsPresented.value.toggle()
}
}.sheet(isPresented: $sheetIsPresented.value, content: {
SheetView()
})
}
}
struct SheetView: View {
var body: some View {
Color.red
}
}
final class SheetIsPresented: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
static let shared = SheetIsPresented()
#Published var value: Bool = false {
willSet {
objectWillChange.send()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Tested on Xcode 12.1, iOS 14.1 in simulator.
Just add your NavigationView view to bellow code line
.navigationViewStyle(StackNavigationViewStyle())
Example :
NavigationView { }.navigationViewStyle(StackNavigationViewStyle())
Problem will be solved (same issues I was faced)
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