SwiftUI List with different cells per section - ios

I'm trying to create a List of questions.
I was planning to create a 'section' per question and have each row change upon the type
Now I have a lot of different types of questions.
Let's say for example:
Ask some text input
Select from a picker
Multiple select (so simply show all options)
I have this kind of setup working in 'regular' iOS
However, when trying to implement such a thing in SwiftUI, the preview keeps freeing, I can't seem to get the build working either. I don't really get any feedback from xcode.
Example code:
import SwiftUI
struct Question: Hashable, Codable, Identifiable {
var id: Int
var label: String
var type: Int
var options: [Option]?
var date: Date? = nil
}
struct Option : Hashable, Codable, Identifiable {
var id: Int
var value: String
}
struct MyList: View {
var questions: [Question] = [
Question(id: 1, label: "My first question", type: 0),
Question(id: 2, label: "My other question", type: 1, options: [Option(id: 15, value: "Yes"), Option(id: 22, value: "No")]),
Question(id: 3, label: "My last question", type: 2, options: [Option(id: 4, value: "Red"), Option(id: 5, value: "Green"), Option(id: 6, value: "Blue")])
]
var body: some View {
List {
ForEach(questions) { question in
Section(header: Text(question.label)) {
if(question.type == 0)
{
Text("type 0")
//Show text entry row
}
else if(question.type == 1)
{
Text("type 1")
//Show a picker containing all options
}
else
{
Text("type 2")
//Show rows for multiple select
//
// IF YOU UNCOMMENT THIS, IT STARTS FREEZING
//
// if let options = question.options {
// ForEach(options) { option in
// Text(option.value)
// }
// }
}
}
}
}
}
}
struct MyList_Previews: PreviewProvider {
static var previews: some View {
MyList()
}
}
Is such a thing possible in SwiftUI ?
What am I missing ?
Why is it freezing ?

Your code is confusing the Type checker. If you let it build long enough Xcode will give you an error stating that the type checker could not be run in a reasonable amount of time. I would report this as a bug to Apple, perhaps they can improve the type checker. In the mean time you can get your code to work by simplifying the expression that the ViewBuiler is trying to work through. I did it like this:
import SwiftUI
struct Question: Hashable, Codable, Identifiable {
var id: Int
var label: String
var type: Int
var options: [Option]?
var date: Date? = nil
}
struct Option : Hashable, Codable, Identifiable {
var id: Int
var value: String
}
struct Type0View : View {
let question : Question
var body : some View {
Text("type 0")
}
}
struct Type1View : View {
let question : Question
var body : some View {
Text("type 1")
}
}
struct Type2View : View {
let question : Question
var body : some View {
Text("type 1")
if let options = question.options {
ForEach(options) { option in
Text(option.value)
}
}
}
}
struct ContentView: View {
var questions: [Question] = [
Question(id: 1, label: "My first question", type: 0),
Question(id: 2, label: "My other question", type: 1, options: [Option(id: 15, value: "Yes"), Option(id: 22, value: "No")]),
Question(id: 3, label: "My last question", type: 2, options: [Option(id: 4, value: "Red"), Option(id: 5, value: "Green"), Option(id: 6, value: "Blue")])
]
var body: some View {
List {
ForEach(questions) { question in
Section(header: Text(question.label)) {
if(question.type == 0)
{
Type0View(question: question)
}
else if(question.type == 1)
{
Type1View(question: question)
}
else
{
Type2View(question: question)
}
}
}
}
}
}
struct MyList_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

How to loop HashMap style in the View in SWIFTUI

var someProtocol = [SurveyItems : [Surveys]]()
sectionLabels.forEach{ a in
var finalSurveys = [Surveys]()
surveys.forEach{ b in
if a.groupHeader == b.group_survey {
finalSurveys.append(b)
}
someProtocol[a] = finalSurveys
}
}
I wanted to use that someProtocol to dynamically display the label section and the surveys under that section.
for (Surveys, SurveyItems) in someProtocol {
Text(Surveys.sectionTitle)
for survey in SurveyItems {
Text(survey.label)
}
}
I tried ViewBuider but getting some error.
To loop and display your someProtocol dictionary in a View, try this example code:
Adjust the code for your own purpose. Note that in a SwiftUI View you need to use a ForEach not the "normal" swift for x in ... to loop over a sequence.
struct ContentView: View {
#State var someProtocol = [SurveyItems : [Surveys]]()
var body: some View {
List(Array(someProtocol.keys), id: \.self) { key in
VStack {
if let surveys = someProtocol[key] {
Text(key.title).foregroundColor(.red)
ForEach(surveys, id: \.self) { survey in
Text("survey \(survey.label)")
}
}
}
}
.onAppear {
// for testing
someProtocol[SurveyItems(id: "1", number: 1, title: "title-1")] = [Surveys(id: "s1", label: "label-1"), Surveys(id: "s2", label: "label-2")]
someProtocol[SurveyItems(id: "2", number: 2, title: "title-2")] = [Surveys(id: "s3", label: "label-3")]
}
}
}
struct SurveyItems: Identifiable, Hashable {
let id: String
let number: Int
var title: String
}
struct Surveys: Identifiable, Hashable {
let id: String
let label: String
}

How to update attributes of a struct with TextFields made in ForEach

In SwiftUI I have a list of menu items that each hold a name, price etc. There are a bunch of categories and under each are a list of items.
struct ItemList: Identifiable, Codable {
var id: Int
var name: String
var picture: String
var list: [Item]
#State var newItemName: String
}
I was looking for a way to create a TextField inside each category that would add to its array of items.
Making the TextFields through a ForEach loop was simple enough, but I got stuck trying to add a new Item using the entered text to the right category.
ForEach(menu.indices) { i in
Section(header: Text(menu[i].name)) {
ForEach(menu[i].list) { item in
Text(item.name)
}
TextField("New Type:", text: /*some kind of bindable here?*/) {
menu[i].list.append(Item(name: /*the text entered above*/))
}
}
}
I considered using #Published and Observable Object like this other question, but I need the ItemList to be a Codable struct so I couldn't figure out how to fit the answers there to my case.
TextField("New Type:", text: menu[i].$newItemName)
Anyway any ideas would be appreciated, thanks!
You just have to focus your View.
import SwiftUI
struct ExpandingMenuView: View {
#State var menu: [ItemList] = [
ItemList(name: "Milk Tea", picture: "", list: [ItemModel(name: "Classic Milk Tea"), ItemModel(name: "Taro milk tea")]),
ItemList(name: "Tea", picture: "", list: [ItemModel(name: "Black Tea"), ItemModel(name: "Green tea")]),
ItemList(name: "Coffee", picture: "", list: [])
]
var body: some View {
List{
//This particular setup is for iOS15+
ForEach($menu) { $itemList in
ItemListView(itemList: $itemList)
}
}
}
}
struct ItemListView: View {
#Binding var itemList: ItemList
#State var newItemName: String = ""
var body: some View {
Section(header: Text(itemList.name)) {
ForEach(itemList.list) { item in
Text(item.name)
}
TextField("New Type:", text: $newItemName, onCommit: {
//When the user commits add to array and clear the new item variable
itemList.list.append(ItemModel(name: newItemName))
newItemName = ""
})
}
}
}
struct ItemList: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var picture: String
var list: [ItemModel]
//#State is ONLY for SwiftUI Views
//#State var newItemName: String
}
struct ItemModel: Identifiable, Codable {
var id: UUID = UUID()
var name: String
}
struct ExpandingMenuView_Previews: PreviewProvider {
static var previews: some View {
ExpandingMenuView()
}
}
If you aren't using Xcode 13 and iOS 15+ there are many solutions in SO for Binding with array elements. Below is just one of them
ForEach(menu) { itemList in
let proxy = Binding(get: {itemList}, set: { new in
let idx = menu.firstIndex(where: {
$0.id == itemList.id
})!
menu[idx] = new
})
ItemListView(itemList: proxy)
}
Also note that using indices is considered unsafe. You can watch Demystifying SwiftUI from WWDC2021 for more details.
You can have an ObservableObject to be your data model, storing categories which then store the items.
You can then bind to these items, using Swift 5.5 syntax. This means we can write List($menu.categories) { $category in /* ... */ }. Then, when we write $category.newItem, we have a Binding<String> to the newItem property in Category.
Example:
struct ContentView: View {
#StateObject private var menu = Menu(categories: [
Category(name: "Milk Tea", items: [
Item(name: "Classic Milk Tea"),
Item(name: "Taro Milk Tea")
]),
Category(name: "Tea", items: [
Item(name: "Black Tea"),
Item(name: "Green Tea")
]),
Category(name: "Coffee", items: [
Item(name: "Black Coffee")
])
])
var body: some View {
List($menu.categories) { $category in
Section(header: Text(category.name)) {
ForEach(category.items) { item in
Text(item.name)
}
TextField("New item", text: $category.newItem, onCommit: {
guard !category.newItem.isEmpty else { return }
category.items.append(Item(name: category.newItem))
category.newItem = ""
})
}
}
}
}
class Menu: ObservableObject {
#Published var categories: [Category]
init(categories: [Category]) {
self.categories = categories
}
}
struct Category: Identifiable {
let id = UUID()
let name: String
var items: [Item]
var newItem = ""
}
struct Item: Identifiable {
let id = UUID()
let name: String
}
Result:

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: ", "))
}
}
}

How to get textField values in the list swiftui

I used the list this way
I want to send all TextField values when the submit button is clicked.
My problem is that I can't get the TextField value in the list
I also use mvvm architecture in the project
my code in the project :
My Model :
import Foundation
import SwiftUI
struct MyModel : Identifiable {
var id:Int64
var title:String
#State var value:String
}
My ViewModel :
import Foundation
import SwiftUI
class MyViewModel: ObservableObject {
#Published var items:[MyModel] = []
init() {
populateItem()
}
func populateItem(){
self.items.append(MyModel(id: 0, title: "title #1", value: ""))
self.items.append(MyModel(id: 1, title: "title #2", value: ""))
self.items.append(MyModel(id: 2, title: "title #3", value: ""))
self.items.append(MyModel(id: 3, title: "title #4", value: ""))
self.items.append(MyModel(id: 4, title: "title #5", value: ""))
self.items.append(MyModel(id: 5, title: "title #6", value: ""))
}
}
My View :
import SwiftUI
struct ContentView: View {
#ObservedObject var myViewModel = MyViewModel()
var body: some View {
VStack {
List {
ForEach(self.myViewModel.items) {item in
HStack {
Text(item.title)
TextField("value", text: item.$value)
}
}
}
Button(action: {
print(self.myViewModel.items[0].value)
}){
Text("Submit")
}.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First remove #State property wrapper from your model's value property.
And update your ForEach with the next code:
ForEach(self.myViewModel.items.indices, id:\.self) { index in
HStack {
Text(self.myViewModel.items[index].title)
TextField("value", text: Binding(
get: {
return self.myViewModel.items[index].value
},
set: { newValue in
return self.myViewModel.items[index].value = newValue
}))
}
}

SwiftUI Textfields inside Lists

I want a list with rows, with each row having 2 Textfields inside of it. Those rows should be saved in an array, so that I can use the data in an other view for further functions. If the text in the Textfield is changed, the text should be saved inside the right entry in the array.
You also can add new rows to the list via a button, which should also change the array for the rows.
The goal is to have a list of key value pairs, each one editable and those entries getting saved with the current text.
Could someone help me and/or give me hint for fixing this problem?
So far I have tried something like this:
// the array of list entries
#State var list: [KeyValue] = [KeyValue()]
// the List inside of a VStack
List(list) { entry in
KeyValueRow(list.$key, list.$value)
}
// the single item
struct KeyValue: Identifiable {
var id = UUID()
#State var key = ""
#State var value = ""
}
// one row in the list with view elements
struct KeyValueRow: View {
var keyBinding: Binding<String>
var valueBinding: Binding<String>
init(_ keyBinding: Binding<String>, _ valueBinding: Binding<String>){
self.keyBinding = keyBinding
self.valueBinding = valueBinding
}
var body: some View {
HStack() {
TextField("key", text: keyBinding)
Spacer()
TextField("value", text: valueBinding)
Spacer()
}
}
}
Also, about the button for adding new entries.
Problem is that if I do the following, my list in the view goes blank and everything is empty again
(maybe related: SwiftUI TextField inside ListView goes blank after filtering list items ?)
Button("Add", action: {
self.list.append(KeyValue())
})
I am not sure what the best practice is keep a view up to date with state in an array like this, but here is one approach to make it work.
For the models, I added a list class that conforms to Observable object, and each KeyValue item alerts it on changes:
class KeyValueList: ObservableObject {
#Published var items = [KeyValue]()
func update() {
self.objectWillChange.send()
}
func addItem() {
self.items.append(KeyValue(parent: self))
}
}
class KeyValue: Identifiable {
init(parent: KeyValueList) {
self.parent = parent
}
let id = UUID()
private let parent: KeyValueList
var key = "" {
didSet { self.parent.update() }
}
var value = "" {
didSet { self.parent.update() }
}
}
Then I was able to simply the row view to just keep a single piece of state:
struct KeyValueRow: View {
#State var item: KeyValue
var body: some View {
HStack() {
TextField("key", text: $item.key)
Spacer()
TextField("value", text: $item.value)
Spacer()
}
}
}
And for the list view:
struct TextFieldList: View {
#ObservedObject var list = KeyValueList()
var body: some View {
VStack {
List(list.items) { item in
HStack {
KeyValueRow(item: item)
Text(item.key)
}
}
Button("Add", action: {
self.list.addItem()
})
}
}
}
I just threw an extra Text in there for testing to see it update live.
I did not run into the Add button blanking the view as you described. Does this solve that issue for you as well?
Working code example for iOS 15
In SwiftUI, Apple recommends passing the binding directly into the List constructor and using a #Binding in the ViewBuilder block to iterate through with each element.
Apple recommends this approach over using the Indices to iterate over the collection since this doesn't reload the whole list every time a TextField value changes (better efficiency).
The new syntax is also back-deployable to previous releases of SwiftUI apps.
struct ContentView: View {
#State var directions: [Direction] = [
Direction(symbol: "car", color: .mint, text: "Drive to SFO"),
Direction(symbol: "airplane", color: .blue, text: "Fly to SJC"),
Direction(symbol: "tram", color: .purple, text: "Ride to Cupertino"),
Direction(symbol: "bicycle", color: .orange, text: "Bike to Apple Park"),
Direction(symbol: "figure.walk", color: .green, text: "Walk to pond"),
Direction(symbol: "lifepreserver", color: .blue, text: "Swim to the center"),
Direction(symbol: "drop", color: .indigo, text: "Dive to secret airlock"),
Direction(symbol: "tram.tunnel.fill", color: .brown, text: "Ride through underground tunnels"),
Direction(symbol: "key", color: .red, text: "Enter door code:"),
]
var body: some View {
NavigationView {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
}
}
.listStyle(.sidebar)
.navigationTitle("Secret Hideout")
}
}
}
struct Direction: Identifiable {
var id = UUID()
var symbol: String
var color: Color
var text: String
}
No need to mess up with classes, Observable, Identifiable. You can do it all with structs.
Note, that version below will do fine for insertions, but fail if you try to delete array elements:
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct ContentView: View {
#State var boolArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) { idx in
HStack() {
TextField("key", text: self.$boolArr[idx].key)
Spacer()
TextField("value", text: self.$boolArr[idx].value)
Spacer()
}
}
.navigationBarItems(leading:
Button(action: {
self.boolArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.boolArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.boolArr.removeLast() // causes "Index out of range" error
print(self.boolArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
A little trick to make it work with deletions as well.
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct KeyValueView: View {
#Binding var model: KeyValue
var body: some View {
HStack() {
TextField("Key", text: $model.key)
Spacer()
TextField("Value", text: $model.value)
Spacer()
}
}
}
struct ContentView: View {
#State var kvArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
List(kvArr.indices, id: \.self) { i in
KeyValueView(model: Binding(
get: {
return self.kvArr[i]
},
set: { (newValue) in
self.kvArr[i] = newValue
}))
}
.navigationBarItems(leading:
Button(action: {
self.kvArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.kvArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.kvArr.removeLast() // Works like a charm
print(self.kvArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Swift 5.5
This version of swift enables one line code for this by passing the bindable item directly from the array.
struct DirectionsList: View {
#Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}

Resources