SwiftUI: how to make searchbar become first responder? - swiftui

I would like to make the searchbar become first responder as soon as the view appears. Unfortunately there is no #FocusState support yet / other calls to achieve this out-of-the-box. Is there another way of achieving this?
struct SearchDemo: View {
#State var searchString : String = ""
var body: some View {
NavigationView {
List{
Text("...")
}
.searchable(text: $searchString)
}
}
}

Related

SwiftUI List with #FocusState and focus change handling

I want to use a List, #FocusState to track focus, and .onChanged(of: focus) to ensure the currently focused field is visible with ScrollViewReader. The problem is: when everything is setup together the List rebuilds constantly during scrolling making the scrolling not as smooth as it needs to be.
I found out that the List rebuilds on scrolling when I attach .onChanged(of: focus). The issue is gone if I replace List with ScrollView, but I like appearance of List, I need sections support, and I need editing capabilities (e.g. delete, move items), so I need to stick to List view.
I used Self._printChanges() in order to see what makes the body to rebuild itself when scrolling and the output was like:
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
...
And nothing was printed from the closure attached to .onChanged(of: focus). Below is the simplified example, the smoothness of scrolling is not a problem in this example, however, once the List content is more or less complex the smooth scrolling goes away and this is really due to .onChanged(of: focus) :(
Question: Are there any chances to listen for focus changes and not provoke the List to rebuild itself on scrolling?
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#FocusState var focus: Field?
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
.focused($focus, equals: .fieldId($0))
}
}
.onChange(of: focus) { _ in
print("Not printed unless focused manually")
}
}
}
if you add printChanges to the beginning of the body, you can monitor the views and see that they are being rendered by SwiftUI (all of them on each focus lost and focus gained)
...
var body: some View {
let _ = Self._printChanges() // <<< ADD THIS TO SEE RE-RENDER
...
so after allot of testing, it seams that the problem is with .onChange, once you add it SwiftUI will redraw all the Textfields,
the only BYPASS i found is to keep using the deprecated API as it works perfectly, and renders only the two textfields (the one that lost focus, and the one that gained the focus),
so the code should look this:
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
// #FocusState var focus: Field? /// NO NEED
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
// .focused($focus, equals: .fieldId($0)) /// NO NEED
}
}
// .onChange(of: focus) { _ in /// NO NEED
// print("Not printed unless focused manually") /// NO NEED
// } /// NO NEED
.focusable(true, onFocusChange: { focusNewValue in
print("Only textfileds that lost/gained focus will print this")
})
}
}
I recommend to consider separation of list row content into standalone view and use something like focus "selection" approach. Having FocusState internal of each row prevents parent view from unneeded updates (something like pre-"set up" I assume).
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#State private var inFocus: Field?
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100, id: \.self) {
ExtractedView(i: $0, inFocus: $inFocus)
}
}
.onChange(of: inFocus) { _ in
print("Not printed unless focused manually")
}
}
struct ExtractedView: View {
let i: Int
#Binding var inFocus: Field?
#State private var text: String = ""
#FocusState private var focus: Bool // << internal !!
var body: some View {
TextField("Enter the text for \(i)", text: $text)
.focused($focus)
.id(Field.fieldId(i))
.onChange(of: focus) { _ in
inFocus = .fieldId(i) // << report selection outside
}
}
}
}

Pass in default text in TextView while keeping state changes with SwiftUI

I am trying to set a default text on a TextView when the view appears, while being able to still keep track of changes to the TextView that I can then pass on to my ViewModel.
Here is a little example that looks like what I am trying to do. This does however not work, it does not update the state as I would have expected. Am I doing something wrong?
struct NoteView: View {
#State var note = ""
var noteFromOutside: String?
var body: some View {
VStack {
TextField("Write a note...", text: $note)
.onSubmit {
//Do something with the note.
}
}
.onAppear {
if let newNote = noteFromOutside {
note = newNote
}
}
}
}
struct ParentView: View {
var note = "Note"
var body: some View {
VStack {
NoteView(noteFromOutside: note)
}
}
}
Found this answer to another post which solved my problem. The key was in the #Binding and init().
https://stackoverflow.com/a/64526620/12764203

SwiftUI - TabView/NavigationLink navigation breaks when using a custom binding

I'm having trouble with what I think may be a bug, but most likely me doing something wrong.
I have a slightly complex navigation state variable in my model that I'm using for tracking/setting state between tab and sidebar presentations when multitasking on iPad. That all works fine except in tab mode, once I use a navigation link once I can't seem to use one again, whether the binding is on my tab view or navigation links in a list.
Would really appreciate any thoughts on this,
Cheers!
Example
NavigationItem.swift
enum SubNavigationItem: Hashable {
case overview, user, hobby
}
enum NavigationItem: Hashable {
case home(SubNavigationItem)
case settings
}
Model.swift
final class Model: ObservableObject {
#Published var selectedTab: NavigationItem = .home(.overview)
}
SwiftUIApp.swift
#main
struct SwiftUIApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
AppTabNavigation()
}
}
AppTabNavigation.swift
struct AppTabNavigation: View {
#EnvironmentObject private var model: Model
var body: some View {
TabView(selection: $model.selectedTab) {
NavigationView {
HomeView()
}
.tabItem {
Label("Home", systemImage: "house")
}
.tag(NavigationItem.home(.overview))
NavigationView {
Text("Settings View")
}
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(NavigationItem.settings)
}
}
}
HomeView.swift
I created a binding here because selection required an optional <NavigationItem?> not
struct HomeView: View {
#EnvironmentObject private var model: Model
var body: some View {
let binding = Binding<NavigationItem?>(
get: {
model.selectedTab
},
set: {
guard let item = $0 else { return }
model.selectedTab = item
}
)
List {
NavigationLink(
destination: Text("Users"),
tag: .home(.user),
selection: binding
) {
Text("Users")
}
NavigationLink(
destination: Text("Hobbies"),
tag: .home(.hobby),
selection: binding
) {
Text("Hobbies")
}
}
.navigationTitle("Home")
}
}
Second Attempt
I tried making the selectedTab property optional as #Lorem Ipsum suggested. Which means I can remove the binding there. But then the TabView doesn't work with the property. So I create a binding for that and have the same issue but with the tab bar!
Make the selected tab optional
#Published var selectedTab: NavigationItem? = .home(.overview)
And get rid of that makeshift binding variable. Just use the variable
$model.selectedTab
If the variable can never be nil then something is always selected IAW with that makeshift variable it will just keep the last value.

SwiftUI Toggle Switch without Binding

This may seem like an odd question but I have looked around and can't seem to find an answer for it.
I would like to create toggles in a view that are not binded to any variable. Imagine a list of toggle switches that are toggle-able but don't actually do anything.
I have tried using .constant butt as you would expect, that doesn't allow me to toggle the switch. Obviously leaving It blank throws an error.
//Can't be changed
Toggle(isOn: .constant(true)) {
Text("Checkbox")
}
//Throws an error
Toggle() {
Text("Checkbox")
}
Is there anything that can be passed in the isOn: parameter to allow for that?
Edit:
In theory I could just have a #State variable in my view and binding to the toggle and simple not used that variable anywhere else in my view. Only thing is, I do not know ahead of time how many toggles will be displayed in my view so I can't just declare a bunch of #State variables. And if I were too only create one #State variable and blind it to all of my toggles, they would all be in sync, which is not what I am looking for, I would like them to all be independent.
Below is a simplified example of the layout of my view
private var array: [String]
var body: some View {
ForEach((0..<self.array.count), id: \.self) {
Toggle("Show welcome message", isOn: *binding here*)
}
}
Thank you
You can simply create a single #State variable of type [Bool], an array containing all the toggle booleans.
Here is some example code:
struct ContentView: View {
var body: some View {
ToggleStack(count: 5)
}
}
struct ToggleStack: View {
#State private var toggles: [Bool]
private let count: Int
init(count: Int) {
self.count = count
toggles = Array(repeating: true, count: count)
}
var body: some View {
VStack {
ForEach(0 ..< count) { index in
Toggle(isOn: $toggles[index]) {
Text("Checkbox")
}
}
}
}
}
Result:

SwiftUI - How to pass data then initialise and edit data

I'm downloading data from Firebase and trying to edit it. It works, but with an issue. I am currently passing data to my EditViewModel with the .onAppear() method of my view. And reading the data from EditViewModel within my view.
class EditViewModel: ObservableObject {
#Published var title: String = ""
}
struct EditView: View {
#State var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}
I have given you the extremely short-hand version as it's much easier to follow.
However, I push to another view to select options from a list and pop back. As a result, everything is reset due to using the onAppear method. I have spent hours trying to use init() but I am struggling to get my application to even compile, getting errors in the process. I understand it's due to using the .onAppear method, but how can I use init() for this particular view/view-model?
I've search online but I've found the answers to not be useful, or different from what I wish to achieve.
Thank you.
You don't need to use State for input property - it is only for internal view usage. So as far as I understood your scenario, here is a possible solution:
struct EditView: View {
private var selected_item: ItemModel
#StateObject var editViewModel = EditViewModel()
init(selectedItem: ItemModel) {
selected_item = selectedItem
editViewModel.title = selectedItem.title
}
var body: some View {
VStack {
TextField("Name of item", text: self.$editViewModel.title)
Divider()
}.onAppear {
DispatchQueue.main.async {
editViewModel.title = selected_item.title
}
}
}
}