Add Search Bar on the picker list SwiftUI - swift

I tap on picker it will open screen with list of element on that screen, can we add Search Bar?
I implemented Country Picker, in the country list I am showing country name and country code so on that list screen add Search bar find out country easily.
struct ContentView: View {
//All Country get from the plist with country Code and Coutnry Name.
let countyList = Country().getAllCountyInfo()
// Selected Country
#State private var selectedCountry = 0
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedCountry, label: Text("Country")) {
ForEach(countyList) { country in
HStack{
Text(country.countryName ?? "")
Spacer()
Text(country.countryCode ?? "")
}
}
}
}
.navigationBarTitle("Select country picker")
}
}
}
by run above code it will open a country list like above screen.
on the above screen (country list).
How can I add search bar to filter country data?

yes you can!
SearchBar code:
struct SearchBar: View {
#Binding var text: String
#State var isEditing: Bool = false
#ObservedObject var vc = VC()
var body: some View {
HStack {
TextField("Search Countries", text: $text)
.padding(7)
.padding(.horizontal, 25)
.background(Vc.color2)
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
UIApplication.shared.endEditing(true)
self.isEditing = false
self.text = ""
}) {
Text("Cancel")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default)
}
}
}
}
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
Picker with Search Bar:
struct locc: View {
#State var locationsSelection: [String] = //Your Array
#State var searchText = ""
#State var locationSelection: Int = 0
var body: some View {
Picker(selection: $locationSelection, label: Text("Country")) {
SearchBar(text: $searchText)
if self.searchText != "" {
ForEach(0..<locationsSelection.filter{$0.hasPrefix(searchText)}.count) {
Text(locationsSelection.filter{$0.hasPrefix(searchText)}[$0]).tag($0)
}.resignKeyboardOnDragGesture()
}else if self.searchText == "" {
ForEach(0..<locationsSelection.count) {
Text(locationsSelection[$0]).tag($0)
}.resignKeyboardOnDragGesture()
}
}
}
}
Note: I am using the locc struct in another view containing the Form.

I solve this in my blog post here: https://roddy.io/2020/09/07/add-search-bar-to-swiftui-picker/
Create the UIViewRepresentable:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.autocapitalizationType = .none
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
}
And then it's easy to embed it in your Picker:
struct FormView: View {
let countries = ["Brazil", "Canada", "Egypt", "France", "Germany", "United Kingdom"]
#State private var pickerSelection: String = ""
#State private var searchTerm: String = ""
var filteredCountries: [String] {
countries.filter {
searchTerm.isEmpty ? true : $0.lowercased().contains(searchTerm.lowercased())
}
}
var body: some View {
NavigationView {
Form {
Picker(selection: $pickerSelection, label: Text("")) {
SearchBar(text: $searchTerm, placeholder: "Search Countries")
ForEach(filteredCountries, id: \.self) { country in
Text(country).tag(country)
}
}
}
}
}
}

Related

SwitUI parent child binding: #Published in #StateObject doesn't work while #State does

I have a custom modal structure coming from this question (code below). Some property is modified in the modal view and is reflected in the source with a Binding. The catch is that when the property is coming from a #StateObject + #Published the changes are not reflected back in the modal view. It's working when using a simple #State.
Minimal example (full code):
class Model: ObservableObject {
#Published var selection: String? = nil
}
struct ParentChildBindingTestView: View {
#State private var isPresented = false
// not working with #StateObject
#StateObject private var model = Model()
// working with #State
// #State private var selection: String? = nil
var body: some View {
VStack(spacing: 20) {
Button("Show child", action: { isPresented = true })
Text("selection: \(model.selection ?? "nil")") // replace: selection
}
.modalBottom(isPresented: $isPresented, view: {
ChildView(selection: $model.selection) // replace: $selection
})
}
}
struct ChildView: View {
#Environment(\.dismissModal) var dismissModal
#Binding var selection: String?
var body: some View {
VStack {
Button("Dismiss", action: { dismissModal() })
VStack(spacing: 0) {
ForEach(["Option 1", "Option 2", "Option 3", "Option 4"], id: \.self) { choice in
Button(action: { selection = choice }) {
HStack(spacing: 12) {
Circle().fill(choice == selection ? Color.purple : Color.black)
.frame(width: 26, height: 26, alignment: .center)
Text(choice)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
.padding(50)
.background(Color.gray)
}
}
extension View {
func modalBottom<Content: View>(isPresented: Binding<Bool>, #ViewBuilder view: #escaping () -> Content) -> some View {
onChange(of: isPresented.wrappedValue) { isPresentedValue in
if isPresentedValue == true {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
else {
topMostController().dismiss(animated: false)
}
}
.onAppear {
if isPresented.wrappedValue {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
}
}
fileprivate func present<Content: View>(view: Content, dismissCallback: #escaping () -> ()) {
DispatchQueue.main.async {
let topMostController = self.topMostController()
let someView = VStack {
Spacer()
view
.environment(\.dismissModal, dismissCallback)
}
let viewController = UIHostingController(rootView: someView)
viewController.view?.backgroundColor = .clear
viewController.modalPresentationStyle = .overFullScreen
topMostController.present(viewController, animated: false, completion: nil)
}
}
}
extension View {
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.windows.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
}
private struct ModalDismissKey: EnvironmentKey {
static let defaultValue: () -> Void = {}
}
extension EnvironmentValues {
var dismissModal: () -> Void {
get { self[ModalDismissKey.self] }
set { self[ModalDismissKey.self] = newValue }
}
}
struct ParentChildBindingTestView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
ParentChildBindingTestView()
}
}
}
The changes are reflected properly when replacing my custom structure with a fullScreenCover, so the problem comes from there. But I find it surprising that it works with a #State and not with a #StateObject + #Published. I thought those were identical.
If having #StateObject is a must for your code, and your ChildView has to update the data back to its ParentView, then you can still make this works around #StateObject.
Something like this:
struct Parent: View {
#StateObject var h = Helper()
var body: some View {
TextField("edit child view", text: $h.helper)
Child(helper: $h.helper)
}
}
struct Child: View {
#Binding var helper: String
var body: some View {
Text(helper)
}
}
class Helper: ObservableObject {
#Published var helper = ""
}
I think your can get anwser here
with #State we use onChange because it uses for only current View
with #Published we use onReceive because it uses for many Views
#State should be used with #Binding
#StateObject with #ObservedObject
In your case, you would pass the model to the child view and update it's properties there.

Override the functionality of the button from another View?

I got a re-usable searchbar in a separate view that looks like this:
struct SearchBar: View {
#Binding var searchText: String
#Binding var isSearching: Bool
var body: some View {
HStack {
HStack {
TextField("Search terms here", text: $searchText)
}
.onTapGesture(perform: {
isSearching = true
})
.overlay(
HStack {
Image(systemName: "magnifyingglass")
if isSearching {
Button(action: { searchText = "" }, label: {
Image(systemName: "xmark.circle.fill")
})
}
}
)
if isSearching {
Button(action: {
isSearching = false
searchText = ""
}, label: {
Text("Cancel")
})
}
}
}
}
And I'm using the SearchBar in multiple views, like this:
SearchBar(searchText: $textFieldSearch, isSearching: $isSearching)
Is there a way to override/append the functionality of the cancel button:
Button(action: {
isSearching = false
searchText = ""
// pass more functionality here dynamically
},
label: {
Text("Cancel")
})
In some Views, I need to do some additional stuff besides clearing the searchText field and setting isSearching to false.
You can use closure.
Here I created one cancel button closure action and set it as optional.
struct SearchBar: View {
#Binding var searchText: String
#Binding var isSearching: Bool
var cancel: (() -> Void)? // <== Here
var body: some View {
HStack {
HStack {
TextField("Search terms here", text: $searchText)
}
.onTapGesture(perform: {
isSearching = true
})
.overlay(
HStack {
Image(systemName: "magnifyingglass")
if isSearching {
Button(action: { searchText = "" }, label: {
Image(systemName: "xmark.circle.fill")
})
}
}
)
if isSearching {
Button(action: {
isSearching = false
searchText = ""
cancel?() // <== Here
}, label: {
Text("Cancel")
})
}
}
}
}
Usage
SearchBar(searchText: $textFieldSearch, isSearching: $isSearching)
SearchBar(searchText: $textFieldSearch, isSearching: $isSearching) {
// Cancel Action
}
If you need addtional action then you can inject onCancel side effect like
struct SearchBar: View {
#Binding var searchText: String
#Binding var isSearching: Bool
var onCancel: () -> Void = {} // << default does nothing
...
if isSearching {
Button(action: {
isSearching = false
searchText = ""
onCancel() // << here !!
}, label: {
Text("Cancel")
})
}
and use either in default way as you did or providing side effect, like
SearchBar(searchText: $textFieldSearch, isSearching: $isSearching) {
// side effect is here
}

SwiftUI .onTapGesture issue in TableView with Sections and Rows

I have two models:
struct Category: Identifiable {
var id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
}
and:
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked = false
}
with:
class Checklist: ObservableObject {
#Published var items = [Category]()
func deleteListItem(whichElement: IndexSet) {
items.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, destination: Int) {
items.move(fromOffsets: whichElement, toOffset: destination)
}
}
I try to implement tap on row to check and uncheck cheklist item in tableView with sections and rows, but I cannot get how this can be released. My code:
struct ChecklistView: View {
#EnvironmentObject var checklist: Checklist
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}
.background(Color.white)
.onTapGesture {
if let matchingIndex =
category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].isChecked.toggle()
}
}
}
}
}
.onDelete(perform: checklist.deleteListItem)
.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
NewChecklistItemView(checklist: self.checklist)
}
}
}
I get error with this code on line with category.items[matchingIndex].isChecked.toggle():
Cannot use mutating member on immutable value: 'category' is a 'let' constant
How I can get to ChecklistItem and make it check and uncheck on tap.
import SwiftUI
//Change to class and add NSObject structs are immutable
class Category: NSObject, Identifiable {
let id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
//Now you need an init
init(title: String , number: Int, items: [ChecklistItem]) {
self.title = title
self.number = number
self.items = items
}
}
//Change to class and add NSObject structs are immutable
class ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
//Now you need an init
init(name: String) {
self.name = name
}
}
class Checklist: ObservableObject {
#Published var items = [Category]()
}
struct ChecklistView: View {
//Can be an #EnvironmentObject if the #ObservedObject comes from a higher View
#ObservedObject var checklist: Checklist = Checklist()
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
Button(action: {
print(item.isChecked.description)
item.isChecked.toggle()
//Something to trigger the view to refresh will not be necessary if using something like #FetchRequest or after you somehow notify `checklist.items` that there is a change
checklist.objectWillChange.send()
}) {
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}//HStack
//White is incompatible with Text Color in Dark Mode
.background(Color.gray)
}//Button
}//ForEach
}//Section
}//ForEach
//Methods not provided
//.onDelete(perform: checklist.deleteListItem)
//.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: {
self.newChecklistItemViewIsVisible = true
//Code to Add Samples
checklist.items.append(Category(title: "Test", number: Int.random(in: 0...100), items: [ChecklistItem(name: "Test")]))
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
//Pass as an #EnvironmentObject
NewChecklistItemView().environmentObject(checklist)
}
}
}
struct NewChecklistItemView: View {
#EnvironmentObject var checklist: Checklist
var body: some View {
Text(checklist.items.count.description)
}
}
struct ChecklistView_Previews: PreviewProvider {
static var previews: some View {
//When the #ObservedObject comes from a higher View remove comment below
ChecklistView()//.environmentObject(Checklist())
}
}
The reason you are getting that error is because structs are immutable. You should use method marked with "mutating" inside desired struct. Something like
if let matchingIndex = category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].toggleItem()
}
and inside your struct:
mutating func toggleItem() {
self.isChecked.toggle()
}
But i would recommend you to use #State instead, because what you are trying to do is straight forward related to how you represent your view. And later, when user is willing to do something with that selection you send that data to your model

SwiftUI - How to toogle all the booleans in a CoreData

I have a database with several objects with booleans as attribute. I'm looking for a function to invert all boolean objects when I press a button. I tried this function but several errors are displayed like (Value of type 'Bool' has no member 'indices') :
struct ViewList: View {
#Environment(\.managedObjectContext) var context
#State var newName: String = ""
#FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
) var list: FetchedResults<Product>
var body: some View {
VStack {
HStack {
TextField("I insert the name of the product", text: $newName)
Button(action: { self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
List {
ForEach(list, id: \.self) {
product in ViewItem(product: product)
}
}
}
}
public func add() {
let newProduct = Product(context: context)
newProduct.name = newName
do {
try context.save()
} catch {
print(error)
}
}
}
struct ViewItem: View {
#State var product: Product
#State var refresh: Bool = false
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack(alignment: .top) {
Button( action: {
self.clean()
self.product.isSelected.toggle()
}) {
if self.product.isSelected == true {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
VStack() {
Text(product.name)
if product.password != "" {
Text("Password : " + product.password)
}
Text(String(refresh)).hidden()
}
}
}
.onAppear {
self.refresh = false
}
}
}
I've been thinking about it, but I don't know how to go about it...
func clean() {
for( index ) in self.product.isSelected.indices {
self.product[index]isSelected = false
}
}
You need to create a query to flip the state of the isSelected flag. This logic is best kept out of the view system so you can use it anywhere.
You create a SelectionHandler
import CoreData
class SelectionHandler {
func clearSelection(in context: NSManagedObjectContext) {
for item in currentSelected(in: context) {
item.isSelected = false
}
}
func selectProduct(_ product: Product) {
guard let context = product.managedObjectContext else {
assertionFailure("broken !")
return
}
clearSelection(in: context)
product.isSelected = true
}
func currentSelected(in context: NSManagedObjectContext) -> [Product] {
let request = NSFetchRequest<Product>(entityName: Product.entity().name!)
let predicate = NSPredicate(format: "isSelected == YES")
request.predicate = predicate
do {
let result = try context.fetch(request)
return result
} catch {
print("fetch error =",error)
return []
}
}
}
which you can then use to select your desired product.
SelectionHandler().selectProduct(product)
As it stands your NavigationLink will do nothing because the parent list is not held in a NavigationView so you'll need to change the body of ViewList to look like this.
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Create product with name", text: $newName)
Button(action: {
self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
.padding()
List {
ForEach(list, id: \.self) { product in
ViewItem(product: product)
}
}
}
}
}
and in ViewItem , Product should be an ObservedObject so that changes are detected in the managedObject.
struct ViewItem: View {
#ObservedObject var product: Product
#State var refresh: Bool = false
var checkmarkImage: some View {
return Group {
if self.product.isSelected {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
}
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack {
checkmarkImage
Text(product.name ?? "wat")
}
}
}
}
The original Button won't play with the NavigationLink but you can simply apply the selection to onAppear in ViewDetail
struct ViewDetail: View {
#ObservedObject var product: Product
#Binding var refresh: Bool
var body: some View {
VStack {
Text("Hello, World!")
Text("Product is \(product.name ?? "wat")")
}
.onAppear {
SelectionHandler().selectProduct(self.product)
}
}
}

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}