CHanging #FocusState on searchable doesn't make it focused - swiftui

When the search becomes active i get a callback with .onChange(of: isFocused
but setting isFocused to false doesn't make the search inactive
struct MainSearchable: ViewModifier {
#State var searchText: String = ""
#FocusState private var isFocused: Bool
func body(content: Content) -> some View {
content
.searchable(text: $searchText, placement: .toolbar)
.searchSuggestions {
...
}
.onChange(of: isFocused, perform: { newValue in
print("FOCUSED")
})
.onSubmit(of: .search) {
...
}
.focused($isFocused)
}
}

Related

Showing ProgressView during a search

NOTE: this question is not about how to use .searchable or how to filter a List.
I am using the following view to search an external database:
struct SearchDatabaseView: View {
#Environment(\.dismiss) private var dismiss
#Environment(\.isSearching) private var isSearching: Bool
#State private var searchText: String = ""
#State private var searchResults: [Record] = []
var body: some View {
NavigationStack {
List(searchResults, id: \.self) { record in
/// display results here
}
.navigationTitle("Search Database")
.toolbar {
Button(action: {
dismiss()
}) {
Text("Done")
}
}
.overlay {
if isSearching {
ProgressView("Searching Database...")
}
}
}
.searchable(text: $searchText)
.disableAutocorrection(true)
.onSubmit(of: .search) {
searchDatabase()
}
}
}
Everything works, except the progress view is not showing. I tried putting the .overlay modifier after .onSubmit, but still it doesn't show.
What am I missing, is that not the proper use of isSearching ?
Try this approach, where two views are used (like the docs examples) to perform
the search and dismissal using dismissSearch and display the ProgressView.
This is just an example code, see the docs at: https://developer.apple.com/documentation/swiftui/managing-search-interface-activation
for more comprehensive info and examples.
struct ContentView: View {
var body: some View {
SearchDatabaseView()
}
}
struct SearchDatabaseView: View {
#State private var searchText: String = ""
var body: some View {
NavigationStack {
ListView()
.searchable(text: $searchText)
.disableAutocorrection(true)
.navigationTitle("Search Database")
.onSubmit(of: .search) {
// searchDatabase()
print("----> onSubmit: \(searchText)")
}
}
}
}
struct ListView: View {
#Environment(\.dismissSearch) private var dismissSearch
#Environment(\.isSearching) private var isSearching
#State private var searchResults: [String] = ["a-record", "b-record", "c-record", "d-record"]
var body: some View {
List(searchResults, id: \.self) { record in
Text(record)
}
.toolbar {
Button("Done") {
dismissSearch()
}
.overlay {
if isSearching {
ProgressView("Searching Database...")
}
}
}
}
}
EDIT-1:
To cater for your new question, I would do away with the isSearching thing.
Use a "normal" variable and implement a simple but effective code structure, such as in this example code:
struct SearchDatabaseView: View {
#State private var searchText: String = ""
#State private var showSearching = false
#State private var searchResults: [String] = ["a-record", "b-record", "c-record", "d-record"]
var body: some View {
NavigationStack {
List(searchResults, id: \.self) { record in
Text(record)
}
.toolbar {
Button("Done") {
showSearching = false
}
.overlay {
if showSearching {
ProgressView("Searching Database...")
}
}
.searchable(text: $searchText)
.disableAutocorrection(true)
.navigationTitle("Search Database")
.onSubmit(of: .search) {
showSearching = true
// searchDatabase()
// simulation of searchDatabase(), could also pass showSearching to it
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// .....
showSearching = false // when finished searchDatabase()
}
}
}
}
}
}

SwiftUI: Unable to update the toggle component

I have a state the decides if we need to do round up or down or nothing:
enum RoundingType: Codable {
case up
case down
}
struct ViewState {
var roundingType: RoundingType? = nil
}
Then in the toggle I simply update this flag:
#MainActor
class ViewModel: ObservableObject {
#Published var state: ViewState
func toggleRoundingType(_ roundingType: RoundingType) {
let oldRoundingType = state.roundingType
// if same rounding type, cancel it
// Otherwise, set it
let isCancel = oldRoundingType == roundingType
if isCancel {
state.roundingType = nil
} else {
state.roundingType = roundingType
}
}
}
This is my View:
struct HomeView: View {
#StateObject var viewModel: ViewModel
var body: some View {
let state = viewModel.state
HStack {
Spacer()
SUITextToggle(label: loc(.roundUp), isOn: state.roundingType == .up) { _ in
viewModel.toggleRoundingType(.up)
}
Spacer()
SUITextToggle(label: loc(.roundDown), isOn: state.roundingType == .down) { _ in
viewModel.toggleRoundingType(.down)
}
Spacer()
}
}
}
This view renders 2 toggles, and user can turn on/off the round up/down toggles.
This is my toggle implementation:
public struct SUITextToggle: View {
#State var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
isOn = newValue
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
Now I have an issue that when I turn on "Up", then turn on "Down", the "Up" button is not automatically turned off. For some reason the "Up" button is not refreshed.
EDIT:
minimal reproducible example:
import SwiftUI
import UIKit
public struct SUITextToggle: View {
#State var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
isOn = newValue
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
struct HomeView: View {
#State var isOn: Bool = true
var body: some View {
// Only one of these 2 toggles should be on
SUITextToggle(label: "Toggle 1", isOn: isOn) { _ in
isOn = !isOn
}
SUITextToggle(label: "Toggle 2", isOn: !isOn) { _ in
isOn = !isOn
}
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let hostingVC = UIHostingController(rootView: HomeView())
present(hostingVC, animated: true, completion: nil)
}
}
The issue is that there is no two-way communication that is compatible with SwiftUI. You communicate one-way on init and then use the completion handler. That does not tell the other toggle to re-render.
I made some changes to make it more SwiftUI.
import SwiftUI
enum RoundingType: String, Codable, CaseIterable, CustomStringConvertible, Identifiable {
case up
case down
//Set the description here
var description: String{
"round\(rawValue)"
}
//Make the enum Identifiable
var id: String{
rawValue
}
}
//No changes
struct ViewState {
var roundingType: RoundingType? = nil
}
#MainActor
class HomeViewModel: ObservableObject {
//Set a default value. Missing from code
#Published var state: ViewState = .init(roundingType: .up)
//Remove func
}
#available(iOS 15.0, *)
struct ToggleHomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
HStack {
Spacer()
//Iterate through all the options and provide a toggle for each option
ForEach(RoundingType.allCases){ type in
SUITextToggle(selectedType: $viewModel.state.roundingType, toggleType: type, label: type.description)
Spacer()
}
}
}
}
#available(iOS 15.0, *)
struct ToggleHomeView_Previews: PreviewProvider {
static var previews: some View {
ToggleHomeView()
}
}
#available(iOS 15.0, *)
public struct SUITextToggle: View {
//Bindng is a two-way connection
#Binding var selectedType: RoundingType?
///type that toggle represents
let toggleType: RoundingType
///proxy that uses the selectedType and toggleType to set toggle to on/off if the two variables are the same
private var binding: Binding<Bool> {
Binding<Bool> {
return selectedType == toggleType
} set: { newValue in
if newValue{
selectedType = toggleType
}else{
//if you remove the nil set a default value here
selectedType = nil
}
}
}
let label: String
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
But you can preserve most of your code if you remove the #State. This wrapper is meant to preserve its value through body's re-rendering.
#available(iOS 15.0, *)
public struct SUITextToggle: View {
var isOn: Bool
private var binding: Binding<Bool> {
Binding<Bool> {
return isOn
} set: { newValue in
onChange(newValue)
}
}
let label: String
let onChange: (Bool) -> Void
init(label: String, isOn: Bool, onChange: #escaping (Bool) -> Void) {
self.label = label
self.isOn = isOn
self.onChange = onChange
}
public var body: some View {
Toggle(label, isOn: binding)
.toggleStyle(.button)
}
}
Another "hack" that is out there is to force Views to recreate by setting the id but this causes unnecessary rendering. Efficiency issues as your app grows will become noticeable
#available(iOS 15.0, *)
struct ToggleHomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
let state = viewModel.state
HStack {
Spacer()
SUITextToggle(label: "roundUp", isOn: state.roundingType == .up) { _ in
viewModel.toggleRoundingType(.up)
}
Spacer()
SUITextToggle(label: "roundDown", isOn: state.roundingType == .down) { _ in
viewModel.toggleRoundingType(.down)
}
Spacer()
}.id(state.roundingType)
}
}

In SwiftUI how do I refer to a view to perform a property on it?

I have a SearchBar that I've added a dismiss property to. dismiss is used by the cancel button, but also might be used by the parent view when displaying a sheet. How do I define the SearchBar in the parent view to be able to reference the dismiss property?
The relevant parts of the SearchBar look like this:
struct SearchBar: View {
#Binding var text: String
#Binding var isSearching: Bool
let prompt: String
var body: some View {
...
}
var dismiss: Void {
// dismiss the keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}
I envisioned the parent SearchView to look something like this:
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
#State private var searchBar: View
var body: some View {
VStack(alignment: .leading) {
searchBar = SearchBar(text: $searchText, isSearching: $isSearching, prompt: prompt)
...
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
func showData(data: Data) {
dataShowing = data
if isSearching {
searchBar.dismiss
}
isShowingDataView = true
}
}
With the above I get the error:
"Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements"
on the searchBar definition line, and
"Type '()' cannot conform to 'View'"
on the VStack line.
dismiss should be a method on your Searchview, and you should pass a closure to SearchBar for it to call when its cancel button is tapped.
struct SearchBar: View {
#Binding var text: String
let prompt: String
let isSearching: Bool
let cancel: () -> Void
var body: some View {
HStack {
TextField(prompt, text: $text)
Button("Cancel", action: cancel)
}
.disabled(!isSearching)
}
}
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
var body: some View {
VStack(alignment: .leading) {
SearchBar(
text: $searchText,
prompt: prompt,
isSearching: $isSearching,
cancel: self.cancelSearch
)
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
private func showData(data: Data) {
dataShowing = data
cancelSearch()
isShowingDataView = true
}
private func cancelSearch() {
guard isSearching else { return }
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}

SwiftUI- passing #State variables to multiple views trouble

Im trying to make an app similar to iphones reminders app in terms of UI. I have a view where I have a list that I can add and delete items from, I also have a view for adding an item that allows me to name the item and select some options, and I have another view for when an item in the list is selected and I want to be able to show the name and options I made but it doesn't display. The code for the list view
struct DotView: View {
enum ActiveSheet: String, Identifiable {
case SwiftUIView, EditView
var id: String {
return self.rawValue
}
}
#EnvironmentObject var listViewModel: ListViewModel
#AppStorage("dotApiKey") var selectedDotApi: String = ""
#State var dotName:String = ""
#State var dotNumber:String = ""
#State var selection:String = ""
#State var triggerSelection:String = ""
#State var searchText:String = ""
#State var plugUsername:String = ""
#State var plugPassword:String = ""
#State var toggleNotification:Bool
#State var activeSheet : ActiveSheet? = nil
#Binding var show : Bool
var body: some View {
ZStack{
HStack {
EditButton()
.padding(.leading)
Spacer()
Button(action: {
self.activeSheet = .SwiftUIView
}, label: {
Text("Add")
})
.padding(.trailing)
}
ZStack {
List {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
Button(action: { self.activeSheet = .EditView }, label: {
DotItemListView(dotitem: dotitem)
})
}
.onDelete(perform: listViewModel.deleteItem)
.onMove(perform: listViewModel.moveItem)
.listRowBackground(Color("textBG"))
}.listStyle(PlainListStyle())
.background(Color("textBG"))
.frame(height: 300)
.cornerRadius(10)
.padding(.horizontal)
}
}
.sheet(item: $activeSheet){ sheet in
switch sheet {
case .SwiftUIView:
SwiftUIView(dotName: dotName, dotNumber: dotNumber, selection: selection, triggerSelection: triggerSelection, searchText: searchText, plugUsername: plugUsername, plugPassword: plugPassword, show: $show)
case .EditView:
EditView(show:$show)
}
}
When I add an item to the list it shows this in each row
struct DotItemListView:View {
let dotitem: DotItem
var body: some View{
HStack {
Text(dotitem.dotName)
Spacer()
Text(dotitem.selection)
Spacer()
Text(dotitem.dotNumber)
Spacer()
}
}
}
This is how I'm adding each item to the list
struct DotItem:Equatable, Codable{
var dotId = UUID().uuidString
let dotName:String
let dotNumber:String
let selection:String
}
class ListViewModel: ObservableObject {
#Published var dotitems: [DotItem] = [] {
didSet {
saveItem()
}
}
let dotitemsKey: String = "dotitems_list"
init() {
getDotItems()
}
func getDotItems() {
guard
let data = UserDefaults.standard.data(forKey: dotitemsKey),
let savedDotItems = try? JSONDecoder().decode([DotItem].self, from: data)
else { return }
self.dotitems = savedDotItems
}
func deleteItem(indexSet: IndexSet){
dotitems.remove(atOffsets: indexSet)
}
func moveItem(from: IndexSet, to: Int){
dotitems.move(fromOffsets: from, toOffset: to)
}
func addItem(dotName: String, dotNumber: String, selection: String){
let newItem = DotItem(dotName: dotName, dotNumber: dotNumber, selection: selection)
dotitems.append(newItem)
print(newItem)
}
func saveItem() {
if let encodedData = try? JSONEncoder().encode(dotitems) {
UserDefaults.standard.set(encodedData, forKey: dotitemsKey)
}
}
}
This is the view that I'm entering the data for each item
struct SwiftUIView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var listViewModel: ListViewModel
#AppStorage("dotApiKey") var selectedDotApi: String = ""
#State var dotName:String
#State var dotNumber:String
#State var selection:String
#State var triggerSelection:String
#State var selectedColor = Color.black
#State var searchText:String
#State var plugUsername:String
#State var plugPassword:String
#ObservedObject var vm = getDeviceNames()
#State var triggerDot:Bool = false
#State var toggleOnOff:Bool = false
#State var toggleLightColor:Bool = false
#State var isSearching:Bool = false
#StateObject var camera = CameraModel()
#Binding var show : Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Info")) {
TextField("Name", text: $dotName)
TextField("Number", text: $dotNumber)
Picker(selection: $selection, label: Text("Discover Plug")) {
ForEach(vm.dataSet, id:\.self) { item in
Text(item.Device).tag(item.Device)
}
}
Toggle(isOn: $isSearching, label: {
Text("Have smart plugs?")
})
if isSearching {
HStack{
Text("Casa")
Spacer()
Button(action: {sendPlugDict()}, label: {
Text("Login")
})
}
TextField("Username", text: $plugUsername)
TextField("Password", text: $plugPassword)
}
}
Section {
Toggle(isOn: $toggleOnOff, label: {
Text("On/Off")
})
}
Section {
Toggle(isOn: $toggleLightColor, label: {
Text("Light Color")
})
if toggleLightColor {
ColorPicker("Choose Light Color", selection: $selectedColor)
}
}
Section {
if listViewModel.dotitems.isEmpty == false {
Toggle(isOn: $triggerDot, label: {
Text("Add a DOT to trigger")
})
if triggerDot {
Picker(selection: $triggerSelection, label: Text("Select DOT")) {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
DotItemListView(dotitem: dotitem)
}
}
}
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
and this is the view that I'm trying to show the data when any list item is selected which is basically the same as above except in a few places
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var listViewModel: ListViewModel
#ObservedObject var vm = getDeviceNames()
#State var dataSet = [Result]()
#State var dotName:String = ""
#State var dotNumber:String = ""
#State var selection:String = ""
#State var triggerSelection:String = ""
#State var selectedColor = Color.black
#State var searchText:String = ""
#State var plugUsername:String = ""
#State var plugPassword:String = ""
#State var triggerDot:Bool = false
#State var toggleOnOff:Bool = false
#State var toggleLightColor:Bool = false
#State var isSearching:Bool = false
#Binding var show : Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Info")) {
TextField(dotName, text: $dotName)
TextField(dotNumber, text: $dotNumber)
Picker(selection: $selection, label: Text("Discorver Plug")) {
ForEach(dataSet, id:\.self) { item in
Text(item.Device).tag(item.Device)
}
}
Toggle(isOn: $isSearching, label: {
Text("Have smart plugs?")
})
if isSearching {
HStack{
Text("Casa")
Spacer()
Button(action: {
SwiftUIView(dotName: dotName, dotNumber: dotNumber, selection: selection, triggerSelection: triggerSelection, searchText: searchText, plugUsername: plugUsername, plugPassword: plugPassword, show: $show).sendPlugDict()}, label: {
Text("Login")
})
}
TextField("Username", text: $plugUsername)
TextField("Password", text: $plugPassword)
}
}
Section {
Toggle(isOn: $toggleOnOff, label: {
Text("On/Off")
})
}
Section {
Toggle(isOn: $toggleLightColor, label: {
Text("Light Color")
})
if toggleLightColor {
ColorPicker("Choose Light Color", selection: $selectedColor)
}
}
Section {
if listViewModel.dotitems.isEmpty == false {
Toggle(isOn: $triggerDot, label: {
Text("Add a DOT to trigger")
})
if triggerDot {
Picker(selection: $triggerSelection, label: Text("Select DOT")) {
ForEach(listViewModel.dotitems, id:\.dotId){ dotitem in
DotItemListView(dotitem: dotitem)
}
}
}
}
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
I know this is a lot to go through but any help would be greatly appreciated
use #Binding on the view that you want to pass state to .
View1 :
#State var a = true
View2(a: $a)
View2 :
#Binding var a : Bool
for passing data globally use #EnvoirmentObject :
example :
#main
struct YourApp: App {
#StateObject var session = Session()
var body: some Scene {
WindowGroup {
SplashView()
.environmentObject(session)
}
}
}
Session :
class Session: ObservableObject {
/// user signin state
#Published var isSignedIn : Bool = false
}
View1 ,View2 , View3 .... :
struct SplashView: View {
#EnvironmentObject var session : Session
var body: some View {
VStack{
SignInView()
.opacity(session.isSignedIn ? 0:1)
}
.background(Color.background.ignoresSafeArea())
}
}

How to navigate out of a ActionSheet?

how to navigate out of a ActionSheet where I can only Pass a Text but not a NavigationLink?
Sample Code:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
var body: some View {
NavigationView {
Text("Test")
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [
.default(Text("How to navigate from here to HelpView???")),
])
}
}
}
}
You would need something like this:
struct DemoActionSheetNavi: View {
#State private var showingSheet = false
#State private var showingHelp = false
var body: some View {
NavigationView {
VStack {
Text("Test")
Button("Tap me") { self.showingSheet = true }
NavigationLink(destination: HelpView(isShowing: $showingHelp),
isActive: $showingHelp) {
EmptyView()
}
}
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(
title: Text("What do you want to do?"),
message: Text("There's only one choice..."),
buttons: [.cancel(),
.default(Text("Go to help")) {
self.showingSheet = false
self.showingHelp = true
}])
}
}
}
You have another state that programmatically triggers a NavigationLink (you could also do it using .sheet and modal presentation). You would also need to pass showingHelp as a #Binding to help view to be able to reset it.
struct HelpView: View {
#Binding var isShowing: Bool
var body: some View {
Text("Help view")
.onDisappear() { self.isShowing = false }
}
}