focusedSceneValue fixed by searchable - swift

I'm getting this odd issue where focusedSceneValue doesn't work on macOS Monterey Beta 6, but adding searchable to one of the views fixes it.
Here's how to reproduce:
First paste this code into ContentView:
struct ContentView: View {
#State var search = ""
var body: some View {
NavigationView {
Text("First")
.focusedSceneValue(\.customAction) {
print("Pressed!")
}
Text("Second")
.searchable(text: $search) // Comment this line and focusedSceneValue breaks
}
}
}
struct ActionKey: FocusedValueKey {
typealias Value = () -> Void
}
extension FocusedValues {
var customAction: (() -> Void)? {
get { self[ActionKey.self] }
set { self[ActionKey.self] = newValue }
}
}
And then paste this into your app file:
struct CustomCommands: Commands {
#FocusedValue(\.customAction) var action
var body: some Commands {
CommandGroup(before: .newItem) {
Button(action: {
action?()
}) {
Text("Press me")
}
.disabled(action == nil)
}
}
}
And add this into the body of your Scene:
.commands {
CustomCommands()
}
This just adds a menu item to File, and the menu item is enabled only if a variable named action is not nil.
action is supposed to be assigned in the focusedSceneValue line in ContentView. This should always happen, as long as the ContentView is visible.
This code only works if I add the searchable modifier to one of the views however. If I don't add it, then the menu item is permanently disabled.
Is anybody else able to reproduce this?

There seems to be a bug with implementing focusedSceneValue (SwiftUI and focusedSceneValue on macOS, what am I doing wrong?).
From what you have shared, it seems that the .searchable modifier provides a focus for a view in the scene so that the focusedSceneValue is updated when the focus changes.
In the focusedSceneValue documentation, it recommends using this method "for values that must be visible regardless of where focus is located in the active scene." Implicitly, it could be that focusedSceneValue only works when an active scene has focus somewhere within.

This appears to be fixed in Monterey 12.1 (21C52), so the .searchable workaround should no longer be needed.

Related

Why does my SwiftUI View not update on updating of an #State var?

I am having a strange issue with an #State var not updating an iOS SwiftUI view.
I have an edit screen for themes for a small game with a NavigationView with a list of game themes. When in edit mode and I select one of these themes, I open up an editor view, passing the theme as a binding to the editor view struct.
In my editor view I then have sections that allow the user to edit properties of the theme. I do not want to use bindings to the various theme properties in my edit fields because I do not want the changes to take effect immediately. Instead, I have created #State vars for each of these properties and then use bindings to these in the edit fields. That way, I give the user the option to either cancel without and changes taking effect, or select "Done" to assign the changes back to the theme via the binding.
In order to initialise the #State vars I have an onAppear block that assign the #State vars values from the respective theme properties.
The issue I am having is that when the onAppear block is executed and the vars are assigned, the relevant edit fields are not updating!
Here is a cut-down version of my code:
struct EditorView: View {
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#Binding var theme: GameTheme
#State private var name = ""
...
var body: some View {
NavigationView {
Form {
nameSection
...
}
.navigationTitle("Edit \(theme.name)")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancel)
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: saveTheme)
.disabled(!canSaveTheme)
}
}
.onAppear {
name = theme.name
...
}
}
.frame(minWidth: Constants.minViewSize.width, minHeight: Constants.minViewSize.height)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
...
}
So the view gets shown an on appearing, the #State var name does correctly get assigned the value from theme.name; however, this allocation does not cause an update of the view and the value of "name" is not entered into the TextField.
Interestingly, and I do not know if this is a good thing to do, if I wrap the contents of the onAppear block in a DispatchQueue.main.async, everything works fine!
i.e.
.onAppear {
DispatchQueue.main.async {
name = theme.name
...
}
}
Does anyone have any idea as to how, within the onAppear, I can force a view refresh? Or, why the assignment to "name" does not force an update?
Thanks.
This isn't the answer per se, but I went ahead and created a new iOS project with the following code (based on your post, but I cleaned it up a bit and came up with the missing GameTheme object myself).
It's more or less the same, and shows that your posted structure does re-render.
I'm wondering if there's more to the code we can't see in your post that could be causing this.
Are you possibly setting the name state variable anywhere else in a way that could be overriding the value on load?
import SwiftUI
#main
struct TestIOSApp: App {
#State var gameTheme: GameTheme = GameTheme(name: "A game theme")
var body: some Scene {
WindowGroup {
ContentView(theme: $gameTheme)
}
}
}
struct GameTheme {
var name:String;
}
struct ContentView: View {
#Binding var theme:GameTheme;
/// The current presentation mode of the view.
#Environment(\.presentationMode) var presentationMode
#State private var name = "DEFAULT SHOULD NOT BE DISPLAYED"
var body: some View {
NavigationView {
Form {
nameSection
}
.navigationTitle("Edit \(theme.name)")
.onAppear {
name = theme.name
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: {})
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: {})
}
}
.frame(maxWidth:.infinity, maxHeight: .infinity)
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField(LocalizedStringKey("Name"), text: $name)
}
}
}
I seem to have solved my problem with an init(). I created init(theme: Binding<GameTheme>) and then within the init assigned the theme via _theme = theme and then assigned the name via _name = State(initialValue: theme.name.wrappedValue).

SwiftUI Animation bug?

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"

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
}
}
}

SwiftUI - usage of toggles - console logs: “invalid mode 'kCFRunLoopCommonModes'” - didSet does not work

I have a general problem using toggles with SwiftUI.
Whenever I use them I get this console error:
invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.
In addition to this didSet does not print anything when I hit the toggle in the simulator.
Does anyone have an idea, or is it a SwiftUI bug?
Other related questions on StackOverflow which are some month old didn't seem to find a solution.
import SwiftUI
struct ContentView: View {
#State private var notifyCheck = false {
didSet {
print("Toggle pushed!")
}
}
var body: some View {
Toggle(isOn: $notifyCheck) {
Text("Activate?")
}
}
}
If this is a bug, I wonder what the workaround for toggles is.
It's not as if I would be the first person using toggles in iOS. ;-)
Ignore that warning, it's SwiftUI internals and does not affect anything. If you'd like submit feedback to Apple.
didSet does not work, because self here (as View struct) is immutable, and #State is just property wrapper which via non-mutating setter stores wrapped value outside of self.
Update: do something on toggle
#State private var notifyCheck = false
var body: some View {
let bindingOn = Binding<Bool> (
get: { self.notifyCheck },
set: { newValue in
self.notifyCheck = newValue
// << do anything
}
)
return Toggle(isOn: bindingOn) {
Text("Activate?")
}
}

How to use #State and #Binding parallel?

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