Swiftui Tabview causes issues with Textfield - swift

In SwiftUI, whenever I have a TextField inside a TabView, when I use a bluetooth barcode scanner, the Textfield never gets the correct value from the scanner, it always skips numbers. Sometimes it skips 50% of the barcode, sometimes just 1 or 2 numbers. This happens only when the TextField is inside a Tabview.
I never thought I would get stuck on something like this to be honest, maybe I am missing something very simple?
Here is a testcode:
import SwiftUI
struct Testview: View {
#State private var searchProductText = ""
#FocusState var productFieldIsFocused: Bool
var body: some View {
TabView{
HStack {
TextField("", text: $searchProductText)
.focused($productFieldIsFocused)
.opacity(0)
.onSubmit {
print(searchProductText)
searchProductText = ""
productFieldIsFocused = true
}
}.onAppear{
productFieldIsFocused = true
}
}
}
}
struct Testview_Previews: PreviewProvider {
static var previews: some View {
Testview()
}
}

Related

SwiftUI detect edit mode

I've returned to iOS development after a while and I'm rebuilding my Objective-C app from scratch in SwiftUI.
One of the things I want to do is use the default Edit Mode to allow entries in a List (backed by Core Data on CloudKit) to switch between a NavigationLink to a detail view and an edit view.
The main approach seems to be to handle it through a if statement that detects edit mode. The Apple documentation provides the following snippet for this approach on this developer page: https://developer.apple.com/documentation/swiftui/editmode
#Environment(\.editMode) private var editMode
#State private var name = "Maria Ruiz"
var body: some View {
Form {
if editMode?.wrappedValue.isEditing == true {
TextField("Name", text: $name)
} else {
Text(name)
}
}
.animation(nil, value: editMode?.wrappedValue)
.toolbar { // Assumes embedding this view in a NavigationView.
EditButton()
}
}
However, this does not work (I've embedded the snippet in a NavigationView as assumed).
Is this a bug in Xcode 13.4.1? iOS 15.5? Or am I doing something wrong?
Update1:
Based on Asperi's answer I came up with the following generic view to handle my situation:
import SwiftUI
struct EditableRow: View {
#if os(iOS)
#Environment(\.editMode) private var editMode
#endif
#State var rowView: AnyView
#State var detailView: AnyView
#State var editView: AnyView
var body: some View {
NavigationLink{
if(editMode?.wrappedValue.isEditing == true){
editView
}
else{
detailView
}
}label: {
rowView
}
}
}
struct EditableRow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
VStack {
EditButton()
EditableRow(rowView: AnyView(Text("Row")), detailView: AnyView(Text("Detail")), editView: AnyView(Text("Edit")))
}
}
}
The preview works as expected, but this works partially in my real app. When I implement this the NavigationLink works when not in Edit Mode, but doesn't do anything when in Edit Mode. I also tried putting the whole NavigationLink in the if statement but that had the same result.
Any idea why this isn't working?
Update2:
Something happens when it's inside a List. When I change the preview to this is shows the behavior I'm getting:
struct EditableRow_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
List {
EditableRow(rowView: AnyView(GroupRow(title: "Title", subTitle: "Subtitle", type: GroupType.personal)), detailView: AnyView(EntryList()), editView: AnyView(Text("Edit")))
}
.navigationBarItems(trailing:
HStack{
#if os(iOS)
EditButton()
#endif
}
)
}
}
}
Update3:
Found this answer: SwiftUI - EditMode and PresentationMode Environment
This claims the default EditButton is broken, which seems to be true. Replacing the default button with a custom one works (be sure to add a withAnimation{} block to get all the behavior from the stock button.
But it still doesn't work for my NavigationLink...
Update4:
Ok, tried passing an "isEditing" Bool to the above View, not to depend on the Environment variable being available. This works as long as the View (a ForEach within a List in my case) isn't in "Editing Mode" whatever happens at that point breaks any NavigationLink it seems.
Update5:
Basically my conclusion is that the default Edit Mode is meant to edit the "List Object" as a whole enabling moving and deleting of rows. In this mode Apple feels that editing the rows themselves isn't something you'd want to do. I can see this perspective.
If, however, you still want to enable a NavigationLink from a row in Edit Mode, this answer should help:
How to make SwiftUI NavigationLink work in edit mode?
Asperi's answer does cover why the detection doesn't work. I did find that Edit Mode detection does work better when setting the edit mode manually and not using the default EditButton, see the answer above for details.
It is on same level so environment is not visible, because it is activated for sub-views.
A possible solution is to separate dependent part into standalone view, like
Form {
InternalView()
}
.toolbar {
EditButton()
}
Tested with Xcode 13.4 / iOS 15.5
Test module on GitHub
#Asperi's answer worked well for me. However I wanted to still be able to access the editMode in the same hierarchy. As a workaround I created the following:
Usage
struct ContentView: View {
#State
private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
Form {
if editMode.isEditing == true {
Color.red
} else {
Color.blue
}
}
.editModeFix($editMode)
.toolbar {
EditButton()
}
}
}
}
Implementation
extension View {
func editModeFix(_ editMode: Binding<EditMode>) -> some View {
modifier(EditModeFixViewModifier(editMode: editMode))
}
}
private struct EditModeFixView: View {
#Environment(\.editMode)
private var editModeEnvironment
#Binding
var editMode: EditMode
var body: some View {
Color.clear
.onChange(of: editModeEnvironment?.wrappedValue) { editModeEnvironment in
if let editModeEnvironment = editModeEnvironment {
editMode = editModeEnvironment
}
}
.onChange(of: editMode) {
editModeEnvironment?.wrappedValue = $0
}
}
}
private struct EditModeFixViewModifier: ViewModifier {
#Binding
var editMode: EditMode
func body(content: Content) -> some View {
content
.overlay {
EditModeFixView(editMode: $editMode)
}
}
}
I've got it to work by using a .simultaneousGesture on the EditButton and playing with a #State wrapper.
struct EditingFix: View {
#Environment(\.editMode) var editMode
#State var showDeleteButton = false
var body: some View {
Text("hello")
.toolbar(content: {
if showDeleteButton {
ToolbarItem(placement: .navigationBarLeading, content: {
Label("Remove selected", systemImage: "trash")
.foregroundColor(.red)
})
}
ToolbarItem(placement: .navigationBarTrailing, content: {
EditButton()
.simultaneousGesture(TapGesture().onEnded({
showDeleteButton.toggle()
}))
})
})
.onChange(of: showDeleteButton, perform: { isEditing in
editMode?.wrappedValue = isEditing ? .active : .inactive
})
.animation(.default, value: editMode?.wrappedValue) // Restore the default smooth animation for list selection and others
}
I can definitly say that EditButton is not using the same EditMode environment as what we get when invoking #Environment(\.editMode) var editMode. So we have to do it all ourselves if we want to get the benefit of the EditButton. Mainly the localized Edit text that it displays in my case.
Alternatively
The above method led to some weird behavior where the EditButton editMode seemed to conflict in some situation with the #Environment(\.editMode) var editMode. I'd advise you use your own logic for editing using the reliable .environment(\.editMode, $editMode). This way you can do whatever you want with the binding that control editing.
struct EditingFix: View {
#State var editMode: EditMode = .inactive
#State var isEditing = false
var body: some View {
VStack {
if editMode.isEditing {
Text("Hello")
}
Text("World")
Button("Toggle hello", action: {
isEditing.toggle()
})
}
.environment(\.editMode, $editMode)
.onChange(of: isEditing, perform: { isEditing in
editMode = isEditing ? .active : .inactive
})
.animation(.default, value: editMode)
}
}

Show DisclosureGroup view based on state

I'm fairly new to SwiftUI, and I'm having trouble wrapping my head around the following issue I ran into. I have a button which toggles a state property, and I'd like to display a DisclosureGroup when the button's state is toggle on. For some reason, I can display any sort of view with my code below, with the exception of a DisclosureGroup:
#Binding var showing : Bool
#Binding var revealDetails : Bool
var body: some View {
if showing {
VStack {
DisclosureGroup("Monday", isExpanded: $revealDetails){
Text("7PM - 10PM").frame(height: 100)
}
.frame(width: 150)
.buttonStyle(PlainButtonStyle()).accentColor(.black)
}
}
}
}
The above code does not work when I present in my ContentView, however, the strange thing is, if I add some sort of empty view above the DisclosureGroup, it does work. So for now, I'm including a Text("") inside the VStack. Any thoughts on why this is?
I think you're not passing correct values to your bindings, i can tell you clearly after seeing your code in ContentView as you haven't attached it in the question but you can copy paste below code and customise it depending on your needs.
ContentView
import SwiftUI
struct ContentView: View {
// MARK: - PROPERTIES
#State private var showDiscloureGroup = false
#State private var showDetails = false
// MARK: - BODY
var body: some View {
VStack{
Toggle("Show Disclosure Group", isOn: $showDiscloureGroup)
Toggle("Show Details", isOn: $showDetails)
MyDiscloureGroup(showing: $showDiscloureGroup, revealDetails: $showDetails)
}//: VSTACK
.padding()
}
}
// MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MyDiscloureGroupView
import SwiftUI
struct MyDiscloureGroupView: View {
#Binding var showing : Bool
#Binding var revealDetails : Bool
var body: some View {
if showing {
VStack {
DisclosureGroup("Monday", isExpanded: $revealDetails){
Text("7PM - 10PM").frame(height: 100)
}
.frame(width: 150)
.buttonStyle(PlainButtonStyle()).accentColor(.black)
}
}
}
}
struct MyDiscloureGroup_Previews: PreviewProvider {
static var previews: some View {
MyDiscloureGroupView(showing: .constant(true), revealDetails: .constant(true))
}
}

Strange picker view hide animation in SwiftUI form

I am trying to show and hide a picker view with an animation. The code for this is pretty simple and although the code works for other views, I can't get it to work with a picker view. Hiding the picker view results in a very odd animation as you can see in the gif below.
When I delete the first or last row in the list, the animation seems correct. Replacing Form {} with a normal List {} also solves the issue. So it only happens in the "inset grouped style".
Issue:
Code:
import SwiftUI
struct ContentView: View {
static let options = ["One", "Two", "Three"]
#State private var showPicker = false
#State private var selectedOption = 0
var body: some View {
Form {
Text("First row") // Removing this row or the last row solves the issue
Button("Show Picker") {
withAnimation {
showPicker.toggle()
}
}
if showPicker {
Picker("Picker", selection: $selectedOption) {
ForEach(0 ..< ContentView.options.count) {
Text(ContentView.options[$0])
}
}.pickerStyle(InlinePickerStyle())
}
Text("Third row")
Text("Last row") // Removing this row or the first row solves the issue
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Does anyone know how to solve this issue? I guess it's a SwiftUI bug so a workaround would be very helpful as well.

Why doesn't the Text update when using #Binding?

The Working works as expected.
But using the #Binding in the NotWorking example, doesn't seem to update the Text control. Why doesn't the #Binding version work, what am I missing here?
Initial Launch:
After Typing:
struct Working: View {
//Binding from #State updates both controls
#State private var text = "working"
var body: some View {
VStack {
TextField("syncs to label...", text: $text)
Text($text.wrappedValue)
}
}
}
struct NotWorking: View {
//Using the #Binding only updates the TextField
#Binding var text: String
var body: some View {
//This does not works
VStack {
TextField("won't sync to label...", text: $text)
Text($text.wrappedValue)
}
}
}
struct Working_Previews: PreviewProvider {
#State static var text = "not working"
static var previews: some View {
VStack {
Working()
NotWorking(text: $text)
}
}
}
Static #States don't work. It's the fact that it being static means that the struct Working_Previews isn't mutated when text is changed, so it won't refresh.
We can test this by changing from a PreviewProvider to an actual View:
struct ContentView: View {
#State static var text = "not working"
var body: some View {
VStack {
Working()
NotWorking(text: ContentView.$text)
}
}
}
This code gives the following runtime message:
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.
Thanks to #George_E. I define #State in a wrapper view and display that for the preview. The WrapperView simply displays the control that I want to preview but it contains the State.
struct Working_Previews: PreviewProvider {
//Define the State in a wrapper view
struct WrapperView: View {
#State var text = "Preview is now working!!!"
var body: some View {
NotWorking(text: $text)
}
}
static var previews: some View {
VStack {
Working()
//Don't display the view that needs the #Binding
//NotWorking(text: $text)
//Use the WrapperView that has #State and displays the view I want to preview.
WrapperView()
}
}
}

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