Search bar with auto-text suggestion in text suggestion in swiftUI - ios

I have created a form in SwiftUI, now I am trying to add a search bar and make the List searchable without system built NavigationView. I was wondering is there any possible way to do so?

One easy way is you can customise TextField in SwiftUI, via text field text to filter data.
struct Sample006: View {
#State var dataSource: [String] = ["1", "2", "3"]
#State var filterData: [String] = []
#State var filterText: String = ""
var body: some View {
VStack {
TextField("Input Text", text: self.$filterText)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 20)
Form {
ForEach(filterData, id: \.self) { data in
Text(data)
}
}
}
.onAppear {
reset()
}
.onChange(of: filterText) { newValue in
guard !newValue.isEmpty else {
reset()
return
}
filterData = dataSource.filter { text in
text == newValue
}
}
}
private func reset() {
filterData = dataSource
}
}

Related

How to change picker based on text field input

I'm currently trying to change the data the picker will display based on the value in the series text field. I'm not getting the picker to show up, I'm not getting any errors but I'm getting this warning "Non-constant range: not an integer range" for both the ForEach lines below.
struct ConveyorTracks: View {
#State private var series = ""
#State private var selectedMaterial = 0
#State private var selectedWidth = 0
#State private var positRack = false
let materials8500 = ["HP", "LF", "Steel"]
let widths8500 = ["3.25", "4", "6"]
let materials882 = ["HP", "LF", "PS", "PSX"]
let widths882 = ["3.25", "4.5", "6","7.5", "10", "12"]
var materials: [String] {
if series == "8500" {
return materials8500
} else if series == "882" {
return materials882
} else {
return []
}
}
var widths: [String] {
if series == "8500" {
return widths8500
} else if series == "882" {
return widths882
} else {
return []
}
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Series:")
TextField("Enter series", text: $series)
}.padding()
HStack {
Text("Material:")
Picker("Materials", selection: $selectedMaterial) {
ForEach(materials.indices) { index in
Text(self.materials[index])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
HStack {
Text("Width:")
Picker("Widths", selection: $selectedWidth) {
ForEach(widths.indices) { index in
Text(self.widths[index])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
HStack {
Text("Positive Rack:")
Toggle("", isOn: $positRack)
}.padding()
}
}
}
struct ConveyorTrack_Previews: PreviewProvider {
static var previews: some View {
ConveyorTracks()
}
}
I would like the pickers to change based on which value is input in the series text field, for both materials and width.
Perhaps pickers isn't the best choice, I am open to any suggestions.
Thanks!
ForEach(materials.indices)
Needs to be
ForEach(materials.indices, id: \.self)
Because you are not using a compile-time constant in ForEach.
In general for fixed selections like this your code can be much simpler if you make everything enums, and make the enums Identifiable. This simplified example only shows one set of materials but you could return an array of applicable materials depending on the selected series (which could also be an enum?)
enum Material: Identifiable, CaseIterable {
case hp, lf, steel
var id: Material { self }
var title: String {
... return an appropriate title
}
}
#State var material: Material
...
Picker("Material", selection: $material) {
ForEach(Material.allCases) {
Text($0.title)
}
}

Supply any string value to swiftUI form in different for textfield

I have two view file. I have textfield. I want to supply string value to the textfield from another view
File 1 :- Place where form is created
struct ContentView: View {
#State var subjectLine: String = ""
var body: some View {
form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine
}
}
}
}
File 2 :- Place where I want to provide value to the string and that will show in the textfield UI
struct CalenderView: View {
#Binding var subjectLine: String
var body : some View {
Button(action: {
subjectLine = "Assigned default value"
}, label: {
Text("Fill textfield")
}
}
})
}
}
This is not working. Any other way we can supply value to the textfield in other view file.
As i can understand you have binding in CalenderView
that means you want to navigate there when you navigate update there.
struct ContentView: View {
#State private var subjectLine: String = ""
#State private var showingSheet: Bool = false
var body: some View {
NavigationView {
Form {
Section(header: Text(NSLocalizedString("subjectLine", comment: ""))) {
TextField("SubjectLine", text: $subjectLine)
}
}
.navigationBarItems(trailing: nextButton)
.sheet(isPresented: $showingSheet) {
CalenderView(subjectLine: $subjectLine)
}
}
}
var nextButton: some View {
Button("Next") {
showingSheet.toggle()
}
}
}
CalendarView
struct CalenderView: View {
#Binding var subjectLine: String
#Environment(\.dismiss) private var dismiss
var body: some View {
Button {
subjectLine = "Assigned default value"
dismiss()
} label: {
Text("Fill textfield")
}
}
}

How to replicate the search selection on iOS's Mail app?

I have a list with a .searchable search bar on top of it. It filters a struct that has text, but also a date.
I'm trying to have the user select whether he/she wants to search for all items containing the specific text, or search for all items with a data. I thought the way that iOS Mail did it when you hit he search bar on top is a good way (I'm open to other options tho...).
It looks like this:
So, when you tap the search field, the picker, or two buttons, or a tab selector shows up. I can't quite figure which is it. Regardless, I tried with a picker, but:
I don't know where to place it
I don't know how to keep it hidden until needed, and then hide it again.
this is the basic code:
let dateFormatter = DateFormatter()
struct LibItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("myapp") public var collectables: [LibItem] = []
init() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.collectables = self.collectables.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
var body: some View {
NavigationView {
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer() Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
And I was trying to add something like, taking into account isSearching:
#Environment(\.isSearching) var isSearching
var searchBy = [0, 1] // 0 = by text, 1 = by date
#State private var selectedSearch = 0
// Yes, I'd add the correct text to it, but I wanted to have it
// working first.
Picker("Search by", selection: $selectedColor) {
ForEach(colors, id: \.self) {
Text($0)
}
}
How do I do it? How can I replicate that search UX from Mail? Or, is there any better way to let the user chose whether search text or date that appears when the user taps on the search?
isSearching works on a sub view that has a searchable modifier attached. So, something like this would work:
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
#State private var selectedItem = 0
var body: some View {
NavigationView {
VStack {
SearchableSubview(selectedItem: $selectedItem)
List(filteredItems) { collectable in
VStack(alignment: .leading) {
Spacer()
Text(collectable.dateText).font(.caption).fontWeight(.medium).foregroundColor(.secondary)
Spacer()
Text(collectable.text).font(.body).padding(.leading).padding(.bottom, 1)
Spacer()
}
}
.listStyle(.plain)
}.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search..."
)
}
}
var filteredItems: [LibItem] {
data.collectables.filter {
searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText)
}
}
}
struct SearchableSubview : View {
#Environment(\.isSearching) private var isSearching
#Binding var selectedItem : Int
var body: some View {
if isSearching {
Picker("Search by", selection: $selectedItem) {
Text("Choice 1").tag(0)
Text("Choice 2").tag(1)
}.pickerStyle(.segmented)
}
}
}

Why does this SwiftUI List require an extra objectWillChange.send?

Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}

How to append more than one Number into Binding var

I'm new to swift and I'm trying to develop a Velocity Calculator.
Here is my Code:
struct VelocityCalc: View {
#State var velocityNumbers1 : [String] = []
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumbers1)
Button {
print("Button works")
} label: {
Text("Tap me")
}
}
}
}
What I want to develop is that the User can type in for example: 12, 14, 12, 10, ...
This Numbers needs to be sorted and so on.
Maybe someone can help me with this Issue or give me some advisory for that.
Big thanks for your help :)
I have seen answers, however what I have found out that when you enter the numbers the way you showed us on your question ex: 2, 1, 5, 9 with Space or WhiteSpace it won't work as expected so here it is a solution to overcome this problem:
#State var velocityNumbers = ""
func reorderTheArray(velocity: String) -> [String] {
let orderVelocity = velocity.components(separatedBy: ",").compactMap{
Int($0.trimmingCharacters(in: .whitespaces))
}
return orderVelocity.sorted().compactMap {
String($0)
}
}
var body: some View {
VStack {
Text("Headline")
TextField("example", text: self.$velocityNumbers)
Button(action: {
self.velocityNumbers = reorderTheArray(velocity: self.velocityNumbers).joined(separator: ",")
print(self.velocityNumbers)
}) {
Text("Reorder")
}
}
}
Now when you click the Reorder button, everything will be reordered on your textfield directly.
Try something like this,
Get the numbers as a string
Split them using separator(')
Convert them into Int and sort
struct ContentView: View {
#State var velocityNumber : String = ""
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumber)
Button {
let allNumbers = velocityNumber.split(separator: ",").compactMap {
Int($0)
}
print(allNumbers.sorted())
} label: {
Text("Tap me")
}
}
}
}
}
I would see it like this:
First i would take all numbers as a string, then split the string using the separator ",", then convert all strings to an int array and sort
struct VelocityCalc: View {
#State var velocityNumbers1 : String
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumbers1)
Button {
let velocityNumbersArray = velocityNumbers1
.components(separatedBy: ",")
.map { Int($0)! }.sorted()
print(velocityNumbersArray)
} label: {
Text("Tap me")
}
}
}
}
}
I think it makes more sense to enter one value at a time and use a separate field to display the entered values
#State var velocityNumbers : [Int] = []
#State var velocity: String = ""
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("velocity", text: $velocity)
Button {
if let value = Int(velocity) {
velocityNumbers.append(value)
velocityNumbers.sort()
}
velocity = ""
} label: {
Text("Add")
}
.keyboardShortcut(.defaultAction)
Divider()
Text(velocityNumbers.map(String.init).joined(separator: ", "))
}
}
}

Resources