Toggle a button on Navigation Bar in SwiftUI & have it change appearance - swiftui

Using SwiftUI, I would like to have the ability to change the button on my NavigationView based upon some Bool value indicating if it should be On or Off.
This would behave similar to how with UIKit you can replace a bar button item on either side of the screen to show a different button & associated action upon clicking.

I am able to get it working with the following code, but I am not certain if this is the best way to accomplish it, so am open to improvement.
import SwiftUI
struct HomeList: View {
#State var isOn = true
var body: some View {
NavigationView {
List(1 ..< 4) { index in
Text("Row \(index)")
}
.navigationBarTitle(Text(verbatim: "Title"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.isOn = !self.isOn
}, label: {
Text(self.isOn ? "On" : "Off")
})
)
}
}
}
The key pieces being:
Using the #State modifier on my isOn variable, which tells my interface to invalidate & re-render upon changes
Having my Button action modify isOn &it can also support other actions if I need
The ternary operator in my Button label that updates the Text (or an Image if I want) to reflect the correct appearance
Finally, how it appears in action:

Related

Odd first run behavior, possible Swift timing issue?

I'm finishing up online auditing of Stanford CS193P class (great class BTW) and I have an oddity on my last assignment. I have created a theme data store and I use it to select a theme (color, number of pairs of cards, emoji) and then kick off and play a matching game. That works fine. Using an edit button, the user can edit a theme and change any of the theme elements.
I run into a problem the first time I use the edit button and select a theme to edit. My code acts as if the #State myEditTheme is nil. If I force unwrap it it crashes. I have put it in a nil-coalescing option as shown, the edit window comes up with the first theme in the array. Any subsequent edit attempts work normally.
In the tap gesture function, I set the value of the #State var myEditTheme, then I set the themeEditing to true. My debug print statement indicates that the myEditTheme has been properly set. When the sheet(isPresented: $themeEditing) presents the ThemeEditor in a "sheet" view, the initial value of myEditTheme is nil.
Is there a timing issue between when I set it in the tap function and when Swift senses that themeEditing is true? The code below is obviously not functional as is, I have edited it for conciseness, only showing relevant portions.
struct ThemeManager: View {
#EnvironmentObject var store: ThemeStore // injected
#State private var editMode: EditMode = .inactive
// inject a binding to List and Edit button
#State private var myEditTheme: Theme?
#State private var themeEditing = false
// used to control .sheet presentation for theme editing
var body: some View {
NavigationView {
List {
ForEach(store.themes) { theme in
NavigationLink(destination: ContentView(viewModel: EmojiMemoryGame(theme: theme))) {
VStack(alignment: .leading) {
Text(theme.name).font(.title2)
Text(theme.emojis).lineLimit(1)
} // end VStack
.sheet(isPresented: $themeEditing) {
ThemeEditor(theme: $store.themes[myEditTheme ?? theme])
.environmentObject(store)
}
.gesture(editMode == .active ? tap(theme) : nil)
} // end NavigationLink
} // end ForEach
} // end List
.navigationTitle("Themes")
.navigationBarTitleDisplayMode(.inline) // removes large title, leaves small inline one
.toolbar {
ToolbarItem { EditButton() }
ToolbarItem(placement: .navigationBarLeading) {
newThemeButton
}
}
.environment(\.editMode, $editMode)
} // NavigationView
} // body
private func tap(_ theme:Theme) -> some Gesture {
TapGesture().onEnded {
myEditTheme = theme
print("edit theme: \(myEditTheme)")
themeEditing = true
}
}

LazyVGrid onTapGesture navigate to next screen swiftUI

I am quite new to swiftUI. I have created a grid view on tapping on which I want to go to next screen. But somehow I am not able to manage to push to next screen. I am doing like this:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(viewModel.pokemon) { pokemon in
PokemonCell(pokemon: pokemon, viewModel: viewModel)
.onTapGesture {
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
Text(pokemon.name)
}
}
}
}
}
.navigationTitle("Pokedex")
}
}
Upon doing like this, I am getting a warning stating
Result of 'NavigationLink<Label, Destination>' initializer is unused
Can someone please guide me, how to do this?
.onTapGesture adds an action to perform when the view recognizes a tap gesture. In your case you don't need to use .onTapGesture. If you want to go to another view when cell is tapped you need to write NavigationLink as below.
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
PokemonCell(pokemon: pokemon, viewModel: viewModel)
}
If you want to use .onTapGesture, another approach is creating #State for your tapped cell's pokemon and using NavigationLink's isActive binding. So when user tap the cell it will change the #State and toggle the isActive in .onTapGesture. You may need to add another Stack (ZStack etc.) for this.
NavigationView {
ZStack {
NavigationLink("", destination: PokemonDetailView(pokemon: pokemon), isActive: $isNavigationActive).hidden()
ScrollView {
// ...

Make a .contextMenu update with changes from PasteBoard

I have a contextMenu view modifier for a view like this one:
Text("Some Text")
.contextMenu {
Button(action: {
editCodes(withTappedCode: codeOnDisplay, delete: true)
}, label: {
Text("Paste")
Image(systemName: "doc.on.clipboard")
})
.disabled(!UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType]))
}
The button should only be enabled when a certain Pasteboard Type is available. However this doesn't happen.
The disabled state is set when the context menu for the Button is first shown. After this any changes to the pasteboard will not modify the disabled state, even if the menu is closed and opened again.
This seems to only to happen if the modified view is refreshed in any way.
How can I change the disabled state, for the context menu button, with the Pasteboard type?
You can listen to UIPasteboard.changedNotification to detect changes and refresh the view:
struct ContentView: View {
#State private var pasteDisabled = false
var body: some View {
Text("Some Text")
.contextMenu {
Button(action: {}) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
.disabled(pasteDisabled)
}
.onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in
pasteDisabled = !UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType])
}
}
}
(You may also want to use UIPasteboard.removedNotification).

Handling focus event changes on tvOS in SwiftUI

How do I respond to focus events on tvOS in SwiftUI?
I have the following SwiftUI view:
struct MyView: View {
var body: some View {
VStack {
Button(action: {
print("Button 1 pressed")
}) {
Text("Button 1")
}.focusable(true) { focused in
print("Button 1 focused: \(focused)")
}
Button(action: {
print("Button 2 pressed")
}) {
Text("Button 2")
}.focusable(true) { focused in
print("Button 2 focused: \(focused)")
}
}
}
Clicking either of the buttons prints out correctly. However, changing focus between the two buttons does not print anything.
This guy is doing the same thing with rows in a list & says it started working for him with the Xcode 11 GM, but I'm on 11.5 and it's definitely not working (at least not for Buttons (or Toggles - I tried those too)).
Reading the documentation, this appears to be the correct way to go about this, but it doesn't seem to actually work. Am I missing something, or is this just broken?
In case anybody else stumbles upon this question, the answer is to make your view data-focused
So, if you have a list of movies in a scrollview, you would add this to your outer view:
var myMovieList: [Movie];
#FocusState var selectedMovie: Int?;
And then somewhere in your body property:
ForEach(0..<myMovieList.count) { index in
MovieCard(myMovieList[i])
.focusable(true)
.focused($selectedMovie, equals: index)
}
.focusable() tells the OS that this element is focusable, and .focused tells it to make that view focused when the binding variable ($selected) equals the value passed to equals: ...
For example on tvOS, if you wrap this in a scrollview and press left/right, it will change the selection and update the selected index in the $selectedMovie variable; you can then use that to index into your movie list to display extra info.
Here is a more complete example that will also scale the selected view:
https://pastebin.com/jejwYxMU

How to Hide Keyboard in SwiftUI Form Containing Picker?

I have a SwiftUI Form that contains a Picker, a TextField, and a Text:
struct ContentView: View {
var body: some View {
Form {
Section {
Picker(selection: $selection, label: label) {
// Code to populate picker
}.pickerStyle(SegmentedPickerStyle())
HStack {
TextField(title, text: $text)
Text(text)
}
}
}
}
}
The code above results in the following UI:
I am able to easily select the second item in the picker, as shown below:
Below, you can see that I am able to initiate text entry by tapping on the TextField:
In order to dismiss the keyboard when the Picker value is updated, a Binding was added, which can be seen in the following code block:
Picker(selection: Binding(get: {
// Code to get selected segment
}, set: { (index) in
// Code to set selected segment
self.endEditing()
}), label: label) {
// Code to populate picker
}
The call to self.endEditing() is provided in the following method:
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
The following screenshot displays that selecting a different segment of the Picker dismisses the keyboard:
Up to this point, everything works as expected. However, I would like to dismiss the keyboard when tapping anywhere outside of the TextField since I am unable to figure out how to dismiss the keyboard when dragging the Form's containing scroll view.
I attempted to add the following implementation to dismiss the keyboard when tapping on the Form:
Form {
Section {
// Picker
HStack {
// TextField
// Text
}
}
}.onTapGesture {
self.endEditing()
}
Below, the following two screenshot displays that the TextField is able to become the first responder and display the keyboard. The keyboard is then successfully dismissed when tapping outside of the TextField:
However, the keyboard will not dismiss when attempting to select a different segment of the `Picker. In fact, I cannot select a different segment, even after the keyboard has been dismissed. I presume that a different segment cannot be selected because the tap gesture attached to the form is preventing the selection.
The following screenshot shows the result of attempting to select the second value in the Picker while the keyboard is shown and the tap gesture is implemented:
What can I do to allow selections of the Picker's segments while allowing the keyboard to be dismissed when tapping outside of the TextField?
import SwiftUI
struct ContentView: View {
#State private var tipPercentage = 2
let tipPercentages = [10, 15, 20, 25, 0]
#State var text = ""
#State var isEdited = false
var body: some View {
Form {
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
HStack {
TextField("Amount", text: $text, onEditingChanged: { isEdited in
self.isEdited = isEdited
}).keyboardType(.numberPad)
}
}
}.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: isEdited ? .all : .none)
}
}
Form's tap gesture (to finish editing by tap anywhere) is enabled only if text field isEdited == true
Once isEdited == false, your picker works as before.
You could place all of your code in an VStack{ code }, add a Spacer() to it and add the onTap to this VStack. This will allow you to dismiss the keyboard by clicking anywhere on the screen.
See code below:
import SwiftUI
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
VStack {
HStack {
TextField("Hello World", text: $text)
Spacer()
}
Spacer()
}
.background(Color.red)
.onTapGesture {
self.endEditing()
}
}
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Changing the background color of an HStack or VStack to red simplifies figuring out where the user may click to dismiss.
Copy and paste code for a ready to run example.