SwiftUI: How to use a binding variable inside a ListView - swiftui

I'm trying to use a ListView to show TextFields where the user can input a name using a binding variable. But I'm getting an error.
This coding showing an single person works:
import SwiftUI
struct Person {
var firstName: String
var lastName: String
}
class People: ObservableObject {
#Published var people = [
Person(firstName: "Joe", lastName: "Smith"),
Person(firstName: "George", lastName: "Todd"),
Person(firstName: "Emily", lastName: "Wallace"),
Person(firstName: "Jeffrey", lastName: "Dodd"),
Person(firstName: "Donny", lastName: "Joseph"),
Person(firstName: "Mark", lastName: "Ryan"),
Person(firstName: "Cindy", lastName: "Geller"),
Person(firstName: "Teresa", lastName: "Kelly"),
]
}
struct Basic: View {
#ObservedObject var people = People()
var body: some View {
VStack {
Text("My name is \(people.people[1].firstName) \(people.people[1].lastName)")
HStack {
TextField("First name", text: $people.people[1].firstName)
TextField("Last name", text: $people.people[1].lastName)
}
}
.padding()
}
}
But when I try to make this coding into a list, I get errors. Here is the coding I'm using:
struct Person {
var firstName: String
var lastName: String
}
class People: ObservableObject {
#Published var people = [
Person(firstName: "Joe", lastName: "Smith"),
Person(firstName: "George", lastName: "Todd"),
Person(firstName: "Emily", lastName: "Wallace"),
Person(firstName: "Jeffrey", lastName: "Dodd"),
Person(firstName: "Donny", lastName: "Joseph"),
Person(firstName: "Mark", lastName: "Ryan"),
Person(firstName: "Cindy", lastName: "Geller"),
Person(firstName: "Teresa", lastName: "Kelly"),
]
}
struct InListView: View {
#ObservedObject var people = People()
var body: some View {
List(people.people, id:\.firstName) { name in
VStack {
Text("My name is \(name.firstName) \(name.lastName)")
HStack {
TextField("First name", text: $name.firstName)
TextField("Last name", text: $name.lastName)
}
}
.padding()
}
}
}
The errors occur on the two TextFields. The error says "Cannot find '$name' in scope".
How can I use a binding variable inside a ListView?
Thank you.

Here's a possible solution
struct InListView: View {
#ObservedObject var people = People()
var body: some View {
List(0..<people.people.count, id:\.self) { i in
VStack {
Text("My name is \(people.people[i].firstName) \(people.people[i].lastName)")
HStack {
TextField("first name", text: $people.people[i].firstName)
TextField("first name", text: $people.people[i].lastName)
}
}
.padding()
}
}
}

Related

IOS 16 Table sort by tableColumn where keyPath is a Bool

I am working with the new Table struct in SwiftUI made available to iOS in iOS 16. I have a simple model
struct City: Identifiable {
var name: String
var country: String
var population: Int
var isCapital: Bool = false
var id: String {
name
}
static var sample: [City] {
[
City(name: "Vancouver", country: "Canada", population: 2632000),
...
...
]
}
}
And a simple View that displays the table and I am trying to sort by columns
struct CountryTableView: View {
#State private var sampleCities = City.sample.sorted(using: KeyPathComparator(\.name))
#State private var sortOrder = [KeyPathComparator(\City.name)]
var body: some View {
Table(sampleCities, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Capital") { city in
HStack {
Spacer()
Text(city.isCapital ? "🌟" : "")
Spacer()
}
}
.width(60)
TableColumn("Country", value: \.country)
TableColumn("Population", value: \.population) { city in
Text("\(city.population)")
}
}
.onChange(of: sortOrder) { newOrder in
sampleCities.sort(using: newOrder)
}
}
}
The above works well. I can sort on both of the String and Int KeyPaths. However,if I try to add a value for the Boolean keypath
TableColumn("Capital", value: \.isCapital) { city in
I get this error
Referencing initializer 'init(_:value:content:)' on 'TableColumn' requires the types 'KeyPathComparator<City>' and 'SortDescriptor<City>' be equivalent
and this one
Referencing initializer 'init(_:value:content:)' on 'TableColumn' requires that 'City' inherit from 'NSObject'
This makes no sense to me as I can sort on String and Int KeyPaths but not the Boolean one.
Can anyone shed some light?
The docs (https://developer.apple.com/documentation/swiftui/tablecolumn/) do not mention using Bool for value in columns (lots of Int), but
you could try this approach, using a computed property, works for me:
struct City: Identifiable {
var name: String
var country: String
var population: Int
var isCapital: Bool = false
var id: String {
name
}
var isCapitalString: String { // <-- here
isCapital ? "🌟" : ""
}
static var sample: [City] {
[
City(name: "Vancouver", country: "Canada", population: 2632000),
City(name: "Tokyo", country: "Japan", population: 1632000, isCapital: true),
City(name: "Sydney", country: "Australia", population: 32000)
]
}
}
struct ContentView: View {
#State private var sampleCities = City.sample.sorted(using: KeyPathComparator(\.name))
#State private var sortOrder = [KeyPathComparator(\City.name)]
var body: some View {
Table(sampleCities, sortOrder: $sortOrder) {
TableColumn("Name", value: \.name)
TableColumn("Capital", value: \.isCapitalString) { city in // <-- here
Text(city.isCapitalString)
}.width(60)
TableColumn("Country", value: \.country)
TableColumn("Population", value: \.population) { city in
Text("\(city.population)")
}
}
.onChange(of: sortOrder) { newOrder in
sampleCities.sort(using: newOrder)
}
}
}

SwiftUI: Quiz, Toggle categories on/off and filter questions based on category status

I'm building a quiz app where the user should be able to select which categories they want to have, and then I'd like to filter the questions based on whether the category isActive.
I figure solving both would be too much for one post maybe, so I'm focusing on toggling the categories. I have tried creating a updateCategory function in my ViewModel but I can only tap on the first category. If I tap on any other category, only the first one gets updated (changes name and icon). Anyone that can point me in the right direction?
Here's my CategoryModel:
struct CategoryModel: Identifiable, Codable, Hashable {
var id: String
var icon: String
var name: String
var isActive: Bool
enum CodingKeys: String, CodingKey {
case id, icon, name, isActive
}
}
And here's my QuestionModel:
struct QuestionModel: Identifiable, Codable, Hashable {
var id = UUID().uuidString
var question: String
var category: String
var answer: Int
var options: [String]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
// Match the answer index with the correct option
func theAnswer() -> String {
return (answer >= 0 && answer < options.count) ? options[answer] : ""
}
}
And here's a part of my GameModel:
struct Game {
// Get questions from JSON file
static var getQuestions: [QuestionModel] = Bundle.main.decode("questions.json")
// Shuffled questions
let questions = getQuestions.shuffled()
// Get categories
var categories: [CategoryModel] = [
CategoryModel(id: "", icon: "music.quarternote.3", name: "Musik", isActive: true),
CategoryModel(id: "", icon: "hourglass.bottomhalf.filled", name: "Historia", isActive: false),
CategoryModel(id: "", icon: "tv.fill", name: "Film & TV", isActive: true),
CategoryModel(id: "", icon: "pawprint.fill", name: "Natur & Vetenskap", isActive: true),
CategoryModel(id: "", icon: "globe.europe.africa.fill", name: "Geografi", isActive: true),
CategoryModel(id: "", icon: "sportscourt.fill", name: "Sport", isActive: true)
]
}
Here's part of my ViewModel:
class GameVM: ObservableObject {
#Published var game = Game()
//MARK: - Category logic
var categories: [CategoryModel] {
game.categories
}
func updateCategory(category: CategoryModel) {
if let index = categories.firstIndex(where: { $0.id == category.id }) {
game.categories[index] = category.updateCompletion()
}
}
var categoryIndices: Range<Int> {
game.categories.indices
}
}
Here's my CategoryListView where I display the categories:
struct CategoryListView: View {
#StateObject var viewModel = GameVM()
var columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 8), count: 2)
var body: some View {
VStack {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(viewModel.categories, id: \.self) { category in
CategoryCardView(icon: category.icon, name: category.name, isActive: category.isActive)
.frame(maxWidth: .infinity, minHeight: 120)
.background(
category.isActive == true ? Color(UIColor.systemGray6) : Color(UIColor.black)
)
.cornerRadius(24)
.onTapGesture {
withAnimation(.spring()) {
viewModel.updateCategory(category: category)
}
}
}
}
}
.padding()
.navigationTitle("Choose Categories")
.navigationBarTitleDisplayMode(.inline)
}
}
First part
You are initialising each category with the same id "",
var categories: [CategoryModel] = [
CategoryModel(id: "", icon: "music.quarternote.3", name: "Musik", isActive: true),
CategoryModel(id: "", icon: "hourglass.bottomhalf.filled", name: "Historia", isActive: false),
CategoryModel(id: "", icon: "tv.fill", name: "Film & TV", isActive: true),
CategoryModel(id: "", icon: "pawprint.fill", name: "Natur & Vetenskap", isActive: true),
CategoryModel(id: "", icon: "globe.europe.africa.fill", name: "Geografi", isActive: true),
CategoryModel(id: "", icon: "sportscourt.fill", name: "Sport", isActive: true)
]
causing this to return index 0 every time.
if let index = categories.firstIndex(where: { $0.id == category.id }) {
Best fix would be to initialise id in CategoryModel like you have in QuestionModel
struct CategoryModel: Identifiable, Codable, Hashable {
var id = UUID().uuidString
var icon: String
var name: String
var isActive: Bool
enum CodingKeys: String, CodingKey {
case id, icon, name, isActive
}
}
Second part
One way of achieving this is to create a filteredQuestions variable inside GameVM like this
var filteredQuestions: [QuestionModel] {
let filteredCategoryNames = categories
.filter(\.isActive) // filters for the active categories
.map(\.name) // turns it into an array containing the name of each category
return questions.filter { question in
filteredCategoryNames.contains(question.category)
}
}

SwiftUI - What's the most efficient way to update the properties of a Struct that has too many properties?

Let's say I have the following model:
Model
struct Product {
var id: Int
var name: String
var price: Double
var property1: String
var property2: String
var property3: String
var property4: String
var property5: String
}
And this is my view:
View
struct ContentView: View {
#State var productId = 0
#State var productName = ""
#State var productPrice = 0.0
#State var productProperty1 = ""
#State var productProperty2 = ""
#State var productProperty3 = ""
#State var productProperty4 = ""
#State var productProperty5 = ""
var product = Product(id: 1, name: "Flour", price: 2.99, property1: "1", property2: "2", property3: "3", property4: "4", property5: "5")
var body: some View {
VStack {
Text("Product ID: \(productId)")
Text("Product Name: \(productName)")
Text("Product Price: \(productPrice)")
}
.onAppear {
productId = product.id
productName = product.name
productPrice = product.price
productProperty1 = product.property1
productProperty2 = product.property2
productProperty3 = product.property3
productProperty4 = product.property4
productProperty5 = product.property5
}
}
}
This works, but it feels too "clunky" and repetitive. Is there a better way to write this code?
I'm more concerned about the .onAppear part. I feel like there is a much better way to load my struct's properties without having to list them one by one like this.
Thanks in advance!
It looks like you are creating the #States so you can have a Binding to them.
What you can do is make product a #State variable, and then you can access the bindings through the product like:
product.name // String: regular name
$product.name // Binding<String>: binding to name
New code:
struct ContentView: View {
#State private var product = Product(
id: 1,
name: "Flour",
price: 2.99,
property1: "1",
property2: "2",
property3: "3",
property4: "4",
property5: "5"
)
var body: some View {
VStack {
Text("Product ID: \(product.id)")
Text("Product Name: \(product.name)")
Text("Product Price: \(product.price)")
TextField("Product name field", text: $product.name)
}
}
}
Result:
Like this:
struct Product {
var id: Int
var name: String
var price: Double
var property1: String
var property2: String
var property3: String
var property4: String
var property5: String
}
struct ContentView: View {
#State private var product = Product(id: 1, name: "Flour", price: 2.99, property1: "1", property2: "2", property3: "3", property4: "4", property5: "5")
var body: some View {
Text("Product ID: \(product.id)")
Text("Product Name: \(product.name)")
Text("Product Price: \(product.price)")
}
}

SwiftUI Variable that contains multiple variables

Let's say I have the following two Views:
ContentView:
struct ContentView: View {
#State var firstName = "John"
#State var middleName = "Michael"
#State var lastName = "Doe"
var body: some View {
TabView(){
ProfileScreen(firstName: $firstName, middleName: $middleName, lastName: $lastName)
.tabItem {
Image("Profile")
}.tag(0)
HomeScreen()
.tabItem {
Image("Home")
}.tag(1)
}
}}
and ProfileScreen:
struct ProfileScreen: View {
#Binding var firstName: String
#Binding var middleName: String
#Binding var lastName: String
var body: some View {
VStack {
Text(firstName)
Text(middleName)
Text(lastName)
}
}}
Is there a way for me to contain all three variables (firstName, middleName, and lastName) inside another variable (let's call it userInfo), in order to save space and time so I don't have to bind three separate variables across separate Views?
You can just wrap all properties in one struct, like below
struct ContentView: View {
#State var userInfo = UserInfo(firstName: "John", middleName: "Michael", lastName: "Doe")
var body: some View {
TabView(){
ProfileScreen(userInfo: $userInfo)
.tabItem {
Image("Profile")
}.tag(0)
HomeScreen()
.tabItem {
Image("Home")
}.tag(1)
}
}
}
struct UserInfo {
var firstName: String
var middleName: String
var lastName: String
}
struct ProfileScreen: View {
#Binding var userInfo: UserInfo
var body: some View {
VStack {
Text(userInfo.firstName)
Text(userInfo.middleName)
Text(userInfo.lastName)
}
}
}

SwiftUI - List / ForEach in combination with NavigationLink and isActive doesn't work properly

I'm trying to do a NavigationLink within a List or ForEach Loop in SwiftUI. Unfortunately I get a really weird behavior (e.g. when clicking on Leo it opens Karl, Opening Max points to Karl, too).
I've already figured out that it's related to the "isActive" attribute in the NavigationLink. Unfortunately, I need it to achieve a this behavior here: https://i.stack.imgur.com/g0BFz.gif which is also asked here SwiftUI - Nested NavigationView: Go back to root.
I also tried to work with selection and tag attribute but I wasn't able to achieve the "go back to root" mechanics.
Here's the Example:
import SwiftUI
struct Model: Equatable, Hashable {
var userId: String
var firstName: String
var lastName: String
}
struct ContentView: View {
#State var navigationViewIsActive: Bool = false
var myModelArray: [Model] = [
Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
var body: some View {
NavigationView {
List(myModelArray, id: \.self) { model in
NavigationLink(destination: secondView(firstName: model.firstName), isActive: $navigationViewIsActive){ Text(model.firstName) }
}
.listStyle(PlainListStyle())
}
}
}
struct secondView: View {
#State var firstName: String
var body: some View {
NavigationView {
Text(firstName)
.padding()
}
}
}
Thanks!
This happened because of the using of only one state navigationViewIsActive
So when you click in a navigation link , the value will change to True , and all the links will be active
The solution for this scenario is like that :
Define a new State which will hold the selected model value
You need just one NavigationLink , and make it Hidden (put it inside a VStack)
In the List use Button instead of NavigationLink
When a Button is clicked : first change the selectedModel value , than make the navigationLink active (true)
Like the code below (Tested with IOS 14) :
import SwiftUI
struct Model: Equatable, Hashable {
var userId: String
var firstName: String
var lastName: String
}
struct ContentView: View {
#State var navigationViewIsActive: Bool = false
#State var selectedModel : Model? = nil
var myModelArray: [Model] = [
Model(userId: "27e880a9-54c5-4da1-afff-05b4584b1d2f", firstName: "Leo", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Max", lastName: "Test"),
Model(userId: "1050412a-cb12-4160-b7e4-2702ab8430c3", firstName: "Karl", lastName: "Test")]
var body: some View {
NavigationView {
VStack {
VStack {
if selectedModel != nil {
NavigationLink(destination: SecondView(firstName: selectedModel!.firstName), isActive: $navigationViewIsActive){ EmptyView() }
}
}.hidden()
List(myModelArray, id: \.self) { model in
Button(action: {
self.selectedModel = model
self.navigationViewIsActive = true
}, label: {
Text(model.firstName)
})
}
.listStyle(PlainListStyle())
}
}
}
}
struct SecondView: View {
#State var firstName: String
var body: some View {
NavigationView {
Text(firstName)
.padding()
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
PS : I wrote this : How to navigate with SwiftUI , it will help you to understand the ways to navigate in swiftUI
You don't need isActive in this case, just use
List(myModelArray, id: \.self) { model in
NavigationLink(destination: secondView(firstName: model.firstName)) {
Text(model.firstName)
}
}
and you have not use NavigationView in second view in this, ie.
struct secondView: View {
var firstName: String // you don't need state here as well
var body: some View {
Text(firstName)
.padding()
}
}