How to display saved data in TextEditor when view is reopen - swift

I am building the app that will store vertical and horizontal measurements at 12 doors.
In there, for each loop creates 12 Sections with vertical and horizontal readings TextEditors for each door. Each text editor stores values to one of the arrays.
When done entering I used UserDefaults onDissapear() to store both arrays into a dictionary.
Here's the problem: when I put loadArray(), method that supposed to overwrite vertical and horizontal reading arrays, to onAppear() and revisit the view - none of the stored values are showed( even though, it will print the stored data to console onAppear()).
also, when I revisit the view and then start entering the values then sections with stored data are populated...
Bound_test.swift.
import SwiftUI
struct Bound_test: View {
let station : String = "Tuscany"
var body: some View {
NavigationView {
VStack {
Section {
VStack(){
List {
Group {
NavigationLink(destination: Doors_test(direction: "Outbound"), label: {
Text("Out Bound")
})
NavigationLink(destination: Doors_test(direction: "Inbound"), label: {
Text("In Bound")
})
}
.padding(.horizontal,1)
.padding(.vertical,3)
}
.pickerStyle(.wheel)
}
}
}
.navigationTitle("Select direction")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct Bound_test_Previews: PreviewProvider {
static var previews: some View {
Bound_test()
}
}
Doors.swift
import SwiftUI
struct Doors_test: View {
let station : String = "Tuscany"
let direction : String
#State var vM = [String](repeating: "", count: 12) //vert reading
#State var hM = [String](repeating: "", count: 12) //horiz reading
//constructor
init(direction: String) {
self.direction = direction
}
#FocusState private var amountIsFocust: Bool
var body: some View {
Form {
//creates 12 doors automatically
ForEach(0..<12) { door in
Section {
HStack {
Text ("Vert:")
//vert reading
TextEditor(text: $vM[door])
.keyboardType(.decimalPad)
.focused($amountIsFocust)
}
HStack {
Text ("Horizontal:")
//horiz reading
TextEditor(text: $hM[door])
.keyboardType(.decimalPad)
.focused($amountIsFocust)
}
} header: {
Text ("Door # \(door+1)")
}
}
}
.onAppear{
loadArray()
}
.navigationTitle("Enter measurements")
.navigationBarTitleDisplayMode(.inline)
.toolbar{
ToolbarItemGroup(placement: .keyboard) {
Spacer() //moves Done button to the right of the screen
Button("Done") {
amountIsFocust = false
print(amountIsFocust)
}
}
}
.onDisappear {
saveArray() //save array on exit
}
}
//yhis method saves all the readings into dict.
func saveArray() {
UserDefaults.standard.set(vM, forKey: "vReadings \(direction)\(station)")
UserDefaults.standard.set(hM, forKey: "hReadings \(direction)\(station)")
}
//load saved data as an array
func loadArray() {
vM = UserDefaults.standard.object(forKey: "vReadings \(direction)\(station)") as? [String] ?? [String](repeating: "", count: 12)
hM = UserDefaults.standard.object(forKey: "hReadings \(direction)\(station)") as? [String] ?? [String](repeating: "", count: 12)
print(vM)
print(hM)
}
}
struct Doors_test_Previews: PreviewProvider {
static var previews: some View {
Doors_test(direction: "Direction")
}
}

Big thanks for suggesting to use #AppStorage. After some research my code works.
import SwiftUI
struct Doors: View {
// var goingBack : Bool = viewWillDisappear(true)
let fileName = Date.now
let station : String
let direction : String
#AppStorage var vM: [String]
#AppStorage var hM: [String]
init(station: String, direction: String) {
self.station = station
self.direction = direction
//initialize dynamic keys for AppStorage
self._vM = AppStorage(wrappedValue:[String](repeating: "", count: 12) , "Vert \(direction) \(station)")
self._hM = AppStorage(wrappedValue:[String](repeating: "", count: 12), "Horiz \(direction) \(station)")
}
#FocusState private var amountIsFocust: Bool
var body: some View {
Form {
ForEach(0..<12) { door in
Section {
HStack {
Text ("Vert:")
TextEditor(text: $vM[door])
.keyboardType(.decimalPad)
.focused($amountIsFocust)
}
HStack {
Text ("Horizontal:")
TextEditor(text: $hM[door])
.keyboardType(.decimalPad)
.focused($amountIsFocust)
}
} header: {
Text ("Door # \(door+1)")
}
}
}
.onAppear{
loadArray()
}
.navigationTitle("Enter measurements")
.navigationBarTitleDisplayMode(.inline)
.toolbar{
ToolbarItemGroup(placement: .keyboard) {
Spacer() //moves Done button to the right of the screen
Button("Done") {
amountIsFocust = false
print(amountIsFocust)
}
}
}
.onDisappear {
saveArray()
}
}
func saveArray() {
UserDefaults.standard.set(vM, forKey: "Vert \(direction) \(station)")
UserDefaults.standard.set(hM, forKey: "Horiz \(direction) \(station)")
}
func loadArray() {
vM = UserDefaults.standard.object(forKey: "Vert \(direction) \(station)") as? [String] ?? [String](repeating: "", count: 12)
hM = UserDefaults.standard.object(forKey: "Horiz \(direction) \(station)") as? [String] ?? [String](repeating: "", count: 12)
print(vM)
print(hM)
}
}
struct Doors_Previews: PreviewProvider {
static var previews: some View {
Doors(station: "Station", direction: "Direction")
}
}

Related

SwiftUI How to change Background Color of NavigationView with Form?

Is there possibility to change background color of this view?
Note: I dont want to change form sections color, i want to change background color of view.
`.background(Color.red) does not effect.
init(){
UITableView.appearance().backgroundColor = .clear
}`
I tried code above too but didnt work.
I'm waiting for your helps. Thank you.
MyView
import SwiftUI
struct TimeView: View {
#State private var input = 100.0
#State private var inputUnit: Dimension = UnitDuration.hours
#State private var outputUnit: Dimension = UnitDuration.minutes
#FocusState private var inputIsFocused: Bool
let unitTypes =
[[UnitDuration.hours, UnitDuration.minutes, UnitDuration.seconds]]
#State var selectedUnits = 0
let formatter: MeasurementFormatter
init() {
formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
formatter.unitStyle = .long
}
var result: String {
let inputLength = Measurement(value: input, unit: inputUnit)
let outputLength = inputLength.converted(to: outputUnit)
return formatter.string(from: outputLength)
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $input, format: .number)
.keyboardType(.decimalPad)
.focused($inputIsFocused)
} header: {
Text("Amount to convert")
}
Picker("Convert from", selection: $inputUnit) {
ForEach(unitTypes[selectedUnits], id: \.self) {
Text(formatter.string(from: $0).capitalized)
}
}
Picker("Convert to", selection: $outputUnit) {
ForEach(unitTypes[selectedUnits], id: \.self) {
Text(formatter.string(from: $0).capitalized)
}
}
Section {
Text(result)
} header: {
Text("Result")
}
}
.navigationTitle("Time Converter")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
inputIsFocused = false
}
}
}
.onChange(of: selectedUnits) { newSelection in
let units = unitTypes[newSelection]
inputUnit = units[0]
outputUnit = units[1]
}
}
}
}
struct TimeView_Previews: PreviewProvider {
static var previews: some View {
TimeView()
}
}
You need to hide the default background of the Form to make any background colors visible.
Add this modifier to Form: .scrollContentBackground(.hidden).
So if you're aiming to make the background red, your code will look like this:
import SwiftUI
struct ContentView: View {
#State private var input = 100.0
#State private var inputUnit: Dimension = UnitDuration.hours
#State private var outputUnit: Dimension = UnitDuration.minutes
#FocusState private var inputIsFocused: Bool
let unitTypes =
[[UnitDuration.hours, UnitDuration.minutes, UnitDuration.seconds]]
#State var selectedUnits = 0
let formatter: MeasurementFormatter
init() {
formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
formatter.unitStyle = .long
}
var result: String {
let inputLength = Measurement(value: input, unit: inputUnit)
let outputLength = inputLength.converted(to: outputUnit)
return formatter.string(from: outputLength)
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $input, format: .number)
.keyboardType(.decimalPad)
.focused($inputIsFocused)
} header: {
Text("Amount to convert")
}
Picker("Convert from", selection: $inputUnit) {
ForEach(unitTypes[selectedUnits], id: \.self) {
Text(formatter.string(from: $0).capitalized)
}
}
Picker("Convert to", selection: $outputUnit) {
ForEach(unitTypes[selectedUnits], id: \.self) {
Text(formatter.string(from: $0).capitalized)
}
}
Section {
Text(result)
} header: {
Text("Result")
}
}
.navigationTitle("Time Converter")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("Done") {
inputIsFocused = false
}
}
}
.onChange(of: selectedUnits) { newSelection in
let units = unitTypes[newSelection]
inputUnit = units[0]
outputUnit = units[1]
}
//MARK: Here's the solution
.scrollContentBackground(.hidden)
.background(Color.red)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Dynamic list from TextField's data SwiftUI

I just started to learn Swift programing language and have a question.
I'm trying to create a simple one-page application where you can add movies to a favorite list. Movies must have 2 properties: title (string, mandatory) and year (integer, mandatory). But I have a problem, I don't know how to put it in one row.
And also, how to ignore duplicate movies?
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text)
model.movies.append(newMovie)
text = ""
let newYear = Movie(title: year)
model.movies.append(newYear)
year = ""
}
}
struct MovieRow: View {
let title: String
var body: some View {
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}
Here is the solution. It will show the data in one row and also how to ignore duplicate movies to show into the list. Check the below code:
import SwiftUI
struct Movie: Identifiable {
var id = UUID()
let title: String
let year: String
}
class MoviesViewModel: ObservableObject {
#Published var movies: [Movie] = []
}
struct ContentView: View {
#State var boolValue = false
#StateObject var viewModel = MoviesViewModel()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
// Show the data in list form
List {
ForEach(viewModel.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
// Condition to check whether the data is already exit or not
boolValue = false
let newMovie = Movie(title: text, year: year)
for movie in viewModel.movies{
if ((movie.title.contains(text)) && (movie.year.contains(year))){
boolValue = true
}
}
// check if boolValue is false so the data will store into the array.
if boolValue == false{
viewModel.movies.append(newMovie)
text = ""
year = ""
}
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
// Show the data insert into the textfield
HStack{
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
Spacer()
Label (
title: { Text(year)},
icon: { Image(systemName: "calendar") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe someone will need a similar solution, here is my result:
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
var year: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text, year: year)
model.movies.append(newMovie)
text = ""
year = ""
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
Label (
title: { Text(title + " " + year)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}

Using Data from Firestore Data Class in reusable picker SwiftUI

I feel like I'm missing something really obvious and I can't seem to figure it out. I want to use a reusable picker in SwiftUI, the one I am referring to is Stewart Lynch's "Reusable-Custom-Picker" https://github.com/StewartLynch/Reusable-Custom-Picker-for-SwiftUI
I have tried multiple times to get the filter working with my Firestore data and I am able to get the picker to read the data but then I am unable to filter it.
and the reusable picker struct is
import Combine
import Firebase
import SwiftUI
struct CustomPickerView: View {
#ObservedObject var schoolData = SchoolDataStore()
var datas : SchoolDataStore
var items : [String]
#State private var filteredItems: [String] = []
#State private var filterString: String = ""
#State private var frameHeight: CGFloat = 400
#Binding var pickerField: String
#Binding var presentPicker: Bool
var body: some View {
let filterBinding = Binding<String> (
get: { filterString },
set: {
filterString = $0
if filterString != "" {
filteredItems = items.filter{$0.lowercased().contains(filterString.lowercased())}
} else {
filteredItems = items
}
setHeight()
}
)
return ZStack {
Color.black.opacity(0.4)
VStack {
VStack(alignment: .leading, spacing: 5) {
HStack {
Button(action: {
withAnimation {
presentPicker = false
}
}) {
Text("Cancel")
}
.padding(10)
Spacer()
}
.background(Color(UIColor.darkGray))
.foregroundColor(.white)
Text("Tap an entry to select it")
.font(.caption)
.padding(.leading,10)
TextField("Filter by entering text", text: filterBinding)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
List {
ForEach(schoolData.datas, id: \.id) { i in
Button(action: {
pickerField = i.name
withAnimation {
presentPicker = false
}
}) {
Text(i.name)
}
}
}
}
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(maxWidth: 400)
.padding(.horizontal,10)
.frame(height: frameHeight)
.padding(.top, 40)
Spacer()
}
}
.edgesIgnoringSafeArea(.all)
.onAppear {
filteredItems = items
setHeight()
}
}
fileprivate func setHeight() {
withAnimation {
if filteredItems.count > 5 {
frameHeight = 400
} else if filteredItems.count == 0 {
frameHeight = 130
} else {
frameHeight = CGFloat(filteredItems.count * 45 + 130)
}
}
}
}
struct CustomPickerView_Previews: PreviewProvider {
static let sampleData = ["Milk", "Apples", "Sugar", "Eggs", "Oranges", "Potatoes", "Corn", "Bread"].sorted()
static var previews: some View {
CustomPickerView(datas: SchoolDataStore(), items: sampleData, pickerField: .constant(""), presentPicker: .constant(true))
}
}
class SchoolDataStore : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as? String ?? ""
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable, Codable {
var id : String
var name : String
}
I have managed to get the data from Firestore into my picker now, but I am currently unable to filter.
When I change the values of the filteredItems into schoolData.datas I get an error about converting to string or .filter is not a member etc.
Anybody able to point me in the right direction with this please?
Kindest Regards,

Bug in animation when loading List asynchronously

I'm trying to make two List components: one of them is static and small, the second is incredibly large and dynamic. In the first List I store food categories: Alcoholic products, Soups, Cereals, etc. In the second List, the word is searched directly from the database - it can be anything: a dish or a category of dishes. Below is the code - it displays the start page. Initially, the first static and small List is located on it, as well as the Search component (Navigationview.seacrhable()). When you type a word into the search bar, the first List disappears and the second one appears. At the moment, both sheets are loaded asynchronously. This is necessary because the second sheet is really big (thousands of rows). This is where my problem begins. Sometimes, when you type a word into the search bar, a copy of this sheet appears on top of it, as shown in the image. It only happens for a fraction of a second, but it's still noticeable. The problem is most likely due to asynchronous loading, before I added it, the List was loading super slowly, but without such bugs.
My minimal reproducible example:
ContentView.sfiwt
Main List, displaying the food categories available for selection.
import SwiftUI
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State public var addScreen: Bool = true
#State private var searchByWordView: Bool = true
#State private var searchByWordCategoryView: Bool = true
#State public var gram: String = ""
#State private var selectedFood: String = ""
#State private var selectedFoodCategoryItem: String = ""
#State private var selectedFoodTemp: String = ""
#State private var selectedFoodCategoryTemp: String = ""
#State private var FoodCList: [FoodCategory] = []
#State private var FoodList: [FoodItemByName] = []
#State var foodItems: [String] = []
#MainActor
var body: some View {
NavigationView {
ZStack {
List {
if !searchByWordView {
Section {
ForEach(FoodList, id:\.self){i in
Button(action: {
selectedFoodTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}.foregroundColor(.black)
}
}
} else {
Section {
ForEach(FoodCList, id:\.self){i in
NavigationLink(destination: GetFoodCategoryItemsView(category: "\(i.name)")) {
Text("\(i.name)")
}.foregroundColor(.black)
}
}
}
}
.listStyle(.plain)
.task{
FoodCList = await FillFoodCategoryList()
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodTemp, foodItems: $foodItems)
}
}
.navigationTitle("Add the dish")
.navigationBarTitleDisplayMode(.inline)
}
.searchable(
text: $selectedFood,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFood, perform: {i in
if i.isEmpty {
searchByWordView = true
} else {
searchByWordView = false
Task {
FoodList = await GetFoodItemsByName(_name: selectedFood)
}
}
})
}
func GetFoodCategoryItemsView(category: String) -> some View {
ZStack {
List {
if !searchByWordCategoryView {
Section {
ForEach(GetFoodCategoryItems(_category: category).filter{$0.name.contains(selectedFoodCategoryItem)}, id:\.self){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
} else {
Section {
ForEach(GetFoodCategoryItems(_category: category), id:\.self){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
}
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodCategoryTemp, foodItems: $foodItems)
}
}
.searchable(
text: $selectedFoodCategoryItem,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFoodCategoryItem, perform: {i in
if i.isEmpty {
searchByWordCategoryView = true
} else {
searchByWordCategoryView = false
}
})
.listStyle(.plain)
.navigationTitle(category)
.navigationBarTitleDisplayMode(.inline)
.interactiveDismissDisabled()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddSreenView.swift
Modal window of entering grams of food consumed.
import SwiftUI
struct addSreenView: View {
#Binding var addScreen: Bool
#Binding var gram: String
#Binding var selectedFood: String
#Binding var foodItems: [String]
var body: some View {
ZStack{
Color(.black)
.opacity(0.3)
.ignoresSafeArea()
.onTapGesture{withAnimation(.linear){addScreen.toggle()}}
VStack(spacing:0){
Text("Add a dish/meal")
.padding()
Divider()
VStack(){
TextField("Weight, in gram", text: $gram)
.padding(.leading, 16)
.padding(.trailing, 16)
Rectangle()
.frame(height: 1)
.foregroundColor(.black)
.padding(.leading, 16)
.padding(.trailing, 16)
}.padding()
Divider()
HStack(){
Button(action: {
foodItems.append("\(selectedFood), \(gram) g.")
gram = ""
selectedFood = ""
addScreen.toggle()
}){
Text("Save")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.frame(maxWidth: .infinity)
Divider()
Button(action: {
gram = ""
selectedFood = ""
addScreen.toggle()
}){
Text("Cancel")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.frame(maxWidth: .infinity)
}.frame(height: 50)
}
.background(Color.white.cornerRadius(10))
.frame(maxWidth: 350)
}
}
}
GetFunction.swift
This file simulates my real SQLite database queries.
import Foundation
import SwiftUI
// Fill start pade List (all categories of dishes)
struct FoodCategory: Identifiable, Hashable {
let name: String
let id = UUID()
}
func FillFoodCategoryList() async -> [FoodCategory] {
let catList: [FoodCategory] = [FoodCategory(name: "Alcohol"),FoodCategory(name: "Soups"),FoodCategory(name: "Cereals"),FoodCategory(name: "Fish"),FoodCategory(name: "Meat")]
return catList
}
// Search by word List
struct FoodItemByName: Identifiable, Hashable {
let name: String
let id = UUID()
}
func GetFoodItemsByName(_name: String) async -> [FoodItemByName] {
var foodItemsByName: [FoodItemByName] = []
let items: [String] = ["Light beer with 11% dry matter in the original wort","Light beer with 20% of dry matter in the original wort","Dark beer with 13% dry matter content in the original wort","Dark beer, with a proportion of dry substances in the initial wort of 20%","Dry white and red wines (including champagne)","Semi-dry white and red wines (including champagne)","Semi-sweet white and red wines (including champagne)","Sweet white and red wines (including champagne)","Strong wines","Semi-dessert wines","Dessert wines","Liqueur wines","Slivyanka liqueur","Cherry liqueur","Ordinary cognac - Three stars","Vodka"]
let filteredItems = items.filter{ $0.contains("\(_name)") }
for i in filteredItems {
foodItemsByName.append(FoodItemByName(name: i))
}
return foodItemsByName
}
// List appears when you click on Alcohole in start page (by analogy with other categories of dishes)
struct FoodCategoryItem: Identifiable, Hashable {
let name: String
let id = UUID()
}
func GetFoodCategoryItems(_category: String) -> [FoodCategoryItem] {
var foodCategoryItems: [FoodCategoryItem] = []
let _alcohole: [String] = ["Light beer with 11% dry matter in the original wort","Light beer with 20% of dry matter in the original wort","Dark beer with 13% dry matter content in the original wort","Dark beer, with a proportion of dry substances in the initial wort of 20%","Dry white and red wines (including champagne)","Semi-dry white and red wines (including champagne)","Semi-sweet white and red wines (including champagne)","Sweet white and red wines (including champagne)","Strong wines","Semi-dessert wines","Dessert wines","Liqueur wines","Slivyanka liqueur","Cherry liqueur","Ordinary cognac - Three stars","Vodka"]
let _soup: [String] = ["Chicken soup","French onion soup","Tomato soup","Chicken Dumpling Soup","Beef Stew","Cream of Potato","Lobster Bisque","Chili Con Carne","Clam Chowder","Cream Of Cheddar Broccoli"]
let _cereals: [String] = ["Cinnamon Toast Crunch","Frosted Flakes","Honey Nut Cheerios","Lucky Charms","Froot Loops","Fruity Pebbles","Cap'n Crunch","Cap'n Crunch's Crunch Berries","Cocoa Puffs","Reese's Puffs"]
let _fish: [String] = ["Salmon","Tuna","Cod","Rainbow Trout","Halibut","Red Snapper","Flounder","Bass","Mahi-Mahi","Catfish"]
let _meat: [String] = ["Beef","Chicken (Food)","Lamb","Pork","Duck","Turkey","Venison","Buffalo","American Bison"]
if _category == "Alcohol"{
for i in _alcohole{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Soups" {
for i in _soup{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Cereals" {
for i in _cereals{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Fish" {
for i in _fish{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else {
for i in _meat{foodCategoryItems.append(FoodCategoryItem(name: i))}
}
return foodCategoryItems
}
Besides using id for the IDs, as mentioned in the comments, you can do some refactoring to get SwiftUI to not re-render as much of the view hierarchy and instead reuse components. For example, you have an if condition and in each you have separate Section, ForEach, etc components. Instead, you could render the content of the ForEach based on the state of the search:
func categoryItems(category: String) -> [FoodCategoryItem] {
if !searchByWordCategoryView {
return GetFoodCategoryItems(_category: category).filter{$0.name.contains(selectedFoodCategoryItem)}
} else {
return GetFoodCategoryItems(_category: category)
}
}
func GetFoodCategoryItemsView(category: String) -> some View {
ZStack {
List {
Section {
ForEach(categoryItems(category: category)){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodCategoryTemp, foodItems: $foodItems)
}
}
}
.searchable(
text: $selectedFoodCategoryItem,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFoodCategoryItem, perform: {i in
if i.isEmpty {
searchByWordCategoryView = true
} else {
searchByWordCategoryView = false
}
})
.listStyle(.plain)
.navigationTitle(category)
.navigationBarTitleDisplayMode(.inline)
.interactiveDismissDisabled()
}

SwiftUI picker separate texts for selected item and selection view

I have a Picker embedded in a Form inside a NavigationView. I'd like to have a separate text for the chosen item in the main View and a more detailed descriptions when choosing items in the picker View.
This is what I tried so far:
struct Item {
let abbr: String
let desc: String
}
struct ContentView: View {
#State private var selectedIndex = 0
let items: [Item] = [
Item(abbr: "AA", desc: "aaaaa"),
Item(abbr: "BB", desc: "bbbbb"),
Item(abbr: "CC", desc: "ccccc"),
]
var body: some View {
NavigationView {
Form {
picker
}
}
}
var picker: some View {
Picker(selection: $selectedIndex, label: Text("Chosen item")) {
ForEach(0..<items.count) { index in
Group {
if self.selectedIndex == index {
Text(self.items[index].abbr)
} else {
Text(self.items[index].desc)
}
}
.tag(index)
}
.id(UUID())
}
}
}
Current solution
This is the picker in the main view:
And this is the selection view:
The problem is that with this solution in the selection view there is "BB" instead of "bbbbb".
This occurs because the "BB" text in both screens is produced by the very same Text view.
Expected result
The picker in the main view:
And in the selection view:
Is it possible in SwiftUI to have separate texts (views) for both screens?
Possible solution without a Picker
As mention in my comment, there is not yet a solution for a native implementation with the SwiftUI Picker. Instead, you can do it with SwiftUI Elements especially with a NavigationLink. Here is a sample code:
struct Item {
let abbr: String
let desc: String
}
struct ContentView: View {
#State private var selectedIndex = 0
let items: [Item] = [
Item(abbr: "AA", desc: "aaaaa"),
Item(abbr: "BB", desc: "bbbbb"),
Item(abbr: "CC", desc: "ccccc"),
]
var body: some View {
NavigationView {
Form {
NavigationLink(destination: (
DetailSelectionView(items: items, selectedItem: $selectedIndex)
), label: {
HStack {
Text("Chosen item")
Spacer()
Text(self.items[selectedIndex].abbr).foregroundColor(Color.gray)
}
})
}
}
}
}
struct DetailSelectionView: View {
var items: [Item]
#Binding var selectedItem: Int
var body: some View {
Form {
ForEach(0..<items.count) { index in
HStack {
Text(self.items[index].desc)
Spacer()
if self.selectedItem == index {
Image(systemName: "checkmark").foregroundColor(Color.blue)
}
}
.onTapGesture {
self.selectedItem = index
}
}
}
}
}
If there are any improvements feel free to edit the code snippet.
Expanding on JonasDeichelmann's answer I created my own picker:
struct CustomPicker<Item>: View where Item: Hashable {
#State var isLinkActive = false
#Binding var selection: Int
let title: String
let items: [Item]
let shortText: KeyPath<Item, String>
let longText: KeyPath<Item, String>
var body: some View {
NavigationLink(destination: selectionView, isActive: $isLinkActive, label: {
HStack {
Text(title)
Spacer()
Text(items[selection][keyPath: shortText])
.foregroundColor(Color.gray)
}
})
}
var selectionView: some View {
Form {
ForEach(0 ..< items.count) { index in
Button(action: {
self.selection = index
self.isLinkActive = false
}) {
HStack {
Text(self.items[index][keyPath: self.longText])
Spacer()
if self.selection == index {
Image(systemName: "checkmark")
.foregroundColor(Color.blue)
}
}
.contentShape(Rectangle())
.foregroundColor(.primary)
}
}
}
}
}
Then we have to make Item conform to Hashable:
struct Item: Hashable { ... }
And we can use it like this:
struct ContentView: View {
#State private var selectedIndex = 0
let items: [Item] = [
Item(abbr: "AA", desc: "aaaaa"),
Item(abbr: "BB", desc: "bbbbb"),
Item(abbr: "CC", desc: "ccccc"),
]
var body: some View {
NavigationView {
Form {
CustomPicker(selection: $selectedIndex, title: "Item", items: items,
shortText: \Item.abbr, longText: \Item.desc)
}
}
}
}
Note: Currently the picker's layout cannot be changed. If needed it can be made more generic using eg. #ViewBuilder.
I've had another try at a custom split picker.
Implementation
First, we need a struct as we'll use different items for selection, main screen and picker screen.
public struct PickerItem<
Selection: Hashable & LosslessStringConvertible,
Short: Hashable & LosslessStringConvertible,
Long: Hashable & LosslessStringConvertible
>: Hashable {
public let selection: Selection
public let short: Short
public let long: Long
public init(selection: Selection, short: Short, long: Long) {
self.selection = selection
self.short = short
self.long = long
}
}
Then, we create a custom view with an inner NavigationLink to simulate the behaviour of a Picker:
public struct SplitPicker<
Label: View,
Selection: Hashable & LosslessStringConvertible,
ShortValue: Hashable & LosslessStringConvertible,
LongValue: Hashable & LosslessStringConvertible
>: View {
public typealias Item = PickerItem<Selection, ShortValue, LongValue>
#State private var isLinkActive = false
#Binding private var selection: Selection
private let items: [Item]
private var showMultiLabels: Bool
private let label: () -> Label
public init(
selection: Binding<Selection>,
items: [Item],
showMultiLabels: Bool = false,
label: #escaping () -> Label
) {
self._selection = selection
self.items = items
self.showMultiLabels = showMultiLabels
self.label = label
}
public var body: some View {
NavigationLink(destination: selectionView, isActive: $isLinkActive) {
HStack {
label()
Spacer()
if let selectedItem = selectedItem {
Text(String(selectedItem.short))
.foregroundColor(Color.secondary)
}
}
}
}
}
private extension SplitPicker {
var selectedItem: Item? {
items.first { selection == $0.selection }
}
}
private extension SplitPicker {
var selectionView: some View {
Form {
ForEach(items, id: \.self) { item in
itemView(item: item)
}
}
}
}
private extension SplitPicker {
func itemView(item: Item) -> some View {
Button(action: {
selection = item.selection
isLinkActive = false
}) {
HStack {
if showMultiLabels {
itemMultiLabelView(item: item)
} else {
itemLabelView(item: item)
}
Spacer()
if item == selectedItem {
Image(systemName: "checkmark")
.font(Font.body.weight(.semibold))
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
}
}
}
private extension SplitPicker {
func itemLabelView(item: Item) -> some View {
HStack {
Text(String(item.long))
.foregroundColor(.primary)
Spacer()
}
}
}
private extension SplitPicker {
func itemMultiLabelView(item: Item) -> some View {
HStack {
HStack {
Text(String(item.short))
.foregroundColor(.primary)
Spacer()
}
.frame(maxWidth: 50)
Text(String(item.long))
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
Demo
struct ContentView: View {
#State private var selection = 2
let items = (1...5)
.map {
PickerItem(
selection: $0,
short: String($0),
long: "Long text of: \($0)"
)
}
var body: some View {
NavigationView {
Form {
Text("Selected index: \(selection)")
SplitPicker(selection: $selection, items: items) {
Text("Split picker")
}
}
}
}
}