Showing ProgressView during a search - swiftui

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

Related

Swift: NavigationLink calling destination's init method multiple times

I have two classes SubmitPhoneView and VerifyPhoneView.
For some reason, I noticed that whenever I input a digit into the textfield of SubmitPhoneView, it calls the init method of VerifyPhoneView. I want it to only be called once (when I press the continue button on SubmitPhoneView)
Why would this be?
Class SubmitPhoneView:
import SwiftUI
import Firebase
struct SubmitPhoneView: View {
#State private var phoneNumber: String = ""
#State private var verificationID : String = ""
#State private var presentMe = false
var body: some View {
ZStack {
Text("My number is")
HStack(spacing: 20){
Text("+1")
TextField("Enter phone number", text: $phoneNumber)
.keyboardType(.numberPad)
}
VStack {
NavigationLink(destination: VerifyPhoneView(phoneNumber: $phoneNumber.wrappedValue, verificationID: $verificationID.wrappedValue), isActive: $presentMe) { EmptyView() }
Button(action: {
self.submitPhoneNumber()
self.presentMe = true
}) {
Text("Continue")
}
}
}
func submitPhoneNumber() {
PhoneAuthProvider.provider().verifyPhoneNumber("+1" + phoneNumber, uiDelegate: nil) { (verificationID, error) in
if error != nil {
print(error.debugDescription)
return
}
else {
self.verificationID = verificationID!
}
}
}
}
Class VerifyPhoneView:
import SwiftUI
import Firebase
struct VerifyPhoneView: View {
private var phoneNumber: String
#State private var verificationID: String
#State private var verificationCode: String = ""
#State private var loginSuccesful: Bool = false
#EnvironmentObject var ls: LoginStatus
#EnvironmentObject var currentUser: CurrentUser
init(phoneNumber: String, verificationID: String) {
print("the init method was called for VerifyPhoneView")
self.phoneNumber = phoneNumber
_verificationID = State(initialValue: verificationID)
print(self.verificationID)
}
var body: some View {
ZStack {
Text("My code is")
TextField("Enter code", text: $verificationCode)
Button(action: {
self.submitVerificationCode()
}) {
Text("Continue")
}
}
}
func submitPhoneNumber() {
// doesn't matter
}
func submitVerificationCode() {
// doesn't matter
}
}
}
Use DeferView, as below
VStack {
NavigationLink(destination: DeferView {
VerifyPhoneView(phoneNumber: $phoneNumber.wrappedValue, verificationID: $verificationID.wrappedValue)
}, isActive: $presentMe) { EmptyView() }
Button(action: {
self.submitPhoneNumber()
self.presentMe = true
}) {
Text("Continue")
}

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

How to delete multiple rows from List in SwiftUI?

I took an example from this question: How does one enable selections in SwiftUI's List and edited the code to be able to delete rows one by one. But I don't know how to delete multiple rows from list.
Could you help me, please?
var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]
struct ContentView : View {
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(selection: $selectKeeper){
ForEach(demoData, id: \.self) { name in
Text(name)
}
.onDelete(perform: delete)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
func delete(at offsets: IndexSet) {
demoData.remove(atOffsets: offsets)
}
}
solution from SwiftUI how to perform action when EditMode changes?
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
#State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationBarTitle(Text("Demo"))
.navigationBarItems(
leading: editButton,
trailing: addDelButton
)
.environment(\.editMode, self.$editMode)
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}

SwiftUI how to perform action when EditMode changes?

I'd like to perform an action when the EditMode changes.
Specifically, in edit mode, the user can select some items to delete. He normally presses the trash button afterwards. But he may also press Done. When he later presses Edit again, the items that were selected previously are still selected. I would like all items to be cleared.
struct ContentView: View {
#State var isEditMode: EditMode = .inactive
#State var selection = Set<UUID>()
var items = [Item(), Item(), Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationBarTitle(Text("Demo"))
.navigationBarItems(
leading: EditButton(),
trailing: addDelButton
)
.environment(\.editMode, self.$isEditMode)
}
}
private var addDelButton: some View {
if isEditMode == .inactive {
return Button(action: reset) {
Image(systemName: "plus")
}
} else {
return Button(action: reset) {
Image(systemName: "trash")
}
}
}
private func reset() {
selection = Set<UUID>()
}
}
Definition of Item:
struct Item: Identifiable {
let id = UUID()
let title: String
static var i = 0
init() {
self.title = "\(Item.i)"
Item.i += 1
}
}
UPDATED for iOS 15.
This solution catches 2 birds with one stone:
The entire view redraws itself when editMode is toggle
A specific action can be performed upon activation/inactivation of editMode
Hopes this helps someone else.
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
#State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle(Text("Demo"))
.environment(\.editMode, self.$editMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
editButton
}
ToolbarItem(placement: .navigationBarTrailing) {
addDelButton
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
I was trying forever, to clear List selections when the user exited editMode. For me, the cleanest way I've found to react to a change of editMode:
Make sure to reference the #Environment variable:
#Environment(\.editMode) var editMode
Add a computed property in the view to monitor the state:
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
Then use the .onChange(of:perform:) method:
.onChange(of: self.isEditing) { value in
if value == false {
// do something
} else {
// something else
}
}
All together:
struct ContentView: View {
#Environment(\.editMode) var editMode
#State private var selections: [String] = []
#State private var colors: ["Red", "Yellow", "Blue"]
private var isEditing: Bool {
if editMode?.wrappedValue.isEditing == true {
return true
}
return false
}
var body: some View {
List(selection: $selections) {
ForEach(colors, id: \.self) { color in
Text("Color")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
.onChange(of: isEditing) { value in
if value == false {
selection.removeAll()
}
}
}
}
In case someone want to use SwiftUI's EditButton() instead of custom a Button and still want to perform action when isEditing status changes
You can use View extension
extension View {
func onChangeEditMode(editMode: EditMode?, perform: #escaping (EditMode?)->()) -> some View {
ZStack {
Text(String(describing: editMode))
.opacity(0)
.onChange(of: editMode, perform: perform)
self
}
}
}
Then you can use it like this
struct TestEditModeView: View {
#Environment(\.editMode) var editMode
#State private var editModeDescription: String = "nil"
var body: some View {
VStack {
Text(editModeDescription)
EditButton()
}
.onChangeEditMode(editMode: editMode?.wrappedValue) {
editModeDescription = String(describing: $0)
}
}
}

Dismiss sheet SwiftUI

I'm trying to implement a dismiss button for my modal sheet as follows:
struct TestView: View {
#Environment(\.isPresented) var present
var body: some View {
Button("return") {
self.present?.value = false
}
}
}
struct DataTest : View {
#State var showModal: Bool = false
var modal: some View {
TestView()
}
var body: some View {
Button("Present") {
self.showModal = true
}.sheet(isPresented: $showModal) {
self.modal
}
}
}
But the return button when tapped does nothing. When the modal is displayed the following appears in the console:
[WindowServer] display_timer_callback: unexpected state (now:5fbd2efe5da4 < expected:5fbd2ff58e89)
If you force unwrap present you find that it is nil
How can I dismiss .sheet programmatically?
iOS 15+
Starting from iOS 15 we can use DismissAction that can be accessed as #Environment(\.dismiss).
There's no more need to use presentationMode.wrappedValue.dismiss().
struct SheetView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
Text("Sheet")
.toolbar {
Button("Done") {
dismiss()
}
}
}
}
}
Use presentationMode from the #Environment.
Beta 6
struct SomeView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Text("Ohay!")
Button("Close") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
For me, beta 4 broke this method - using the Environment variable isPresented - of using a dismiss button. Here's what I do nowadays:
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}) {
Text("Show Modal")
}
.sheet(
isPresented: $showingModal,
content: { ModalPopup(showingModal: self.$showingModal) }
)
}
}
And in your modal view:
struct ModalPopup : View {
#Binding var showingModal:Bool
var body: some View {
Button(action: {
self.showingModal = false
}) {
Text("Dismiss").frame(height: 60)
}
}
}
Apple recommend (in WWDC 2020 Data Essentials in SwiftUI) using #State and #Binding for this. They also place the isEditorPresented boolean and the sheet's data in the same EditorConfig struct that is declared using #State so it can be mutated, as follows:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
let title: String
}
struct EditorConfig {
var isEditorPresented = false
var title = ""
var needsSave = false
mutating func present() {
isEditorPresented = true
title = ""
needsSave = false
}
mutating func dismiss(save: Bool = false) {
isEditorPresented = false
needsSave = save
}
}
struct ContentView: View {
#State var items = [Item]()
#State private var editorConfig = EditorConfig()
var body: some View {
NavigationView {
Form {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: presentEditor) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $editorConfig.isEditorPresented, onDismiss: {
if(editorConfig.needsSave) {
items.append(Item(title: editorConfig.title))
}
}) {
EditorView(editorConfig: $editorConfig)
}
}
}
func presentEditor() {
editorConfig.present()
}
}
struct EditorView: View {
#Binding var editorConfig: EditorConfig
var body: some View {
NavigationView {
Form {
TextField("Title", text:$editorConfig.title)
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(action: save) {
Text("Save")
}
.disabled(editorConfig.title.count == 0)
}
ToolbarItem(placement: .cancellationAction) {
Button(action: dismiss) {
Text("Dismiss")
}
}
}
}
}
func save() {
editorConfig.dismiss(save: true)
}
func dismiss() {
editorConfig.dismiss()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(items: [Item(title: "Banana"), Item(title: "Orange")])
}
}