SwiftUI enum String, Codable, Hashable Error - swiftui

import SwiftUI
import Foundation
import Combine
import CoreLocation
struct structuralCodes: Hashable, Codable, Identifiable {
var id = UUID()
var index: Int
var title: String
var icon: String
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable, Hashable {
case asce = "ASCE"
case aisc = "AISC"
}
var imageName: String
var image: Image {
Image(imageName)
}
}
var structuralcodes = [
structuralCodes(index: 1, title: "ASCE", icon: "wind", isFeatured: true, category: "ASCE", imageName: "ASCE"),
structuralCodes(index: 2, title: "AISC", icon: "wind", isFeatured: false, category: "AISC", imageName: "AISC"),
]
import SwiftUI
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var choosecodes: [structuralCodes] = structuralcodes
var features: [structuralCodes] {
structuralcodes.filter { $0.isFeatured }
}
var categories: [String: [structuralCodes]] {
Dictionary(
grouping: choosecodes,
by: { $0.category.rawValue }
)
}
} //: ModelData
I get an error in my var structuralcodes on the part
(category: "ASCE") //Error Here under the first quote
(category: "AISC") //Error Here under the first quote
The error reads:
Cannot convert value of type 'String' to expected argument type
'structuralCodes.Category
I thought when I had enum for String, it would save the case under var Category since I inputed a String. I have tried uppercase and lowercase, and get this error no matter what.

You can try this:
var structuralcodes = [
structuralCodes(index: 1, title: "ASCE", icon: "wind", isFeatured: true, category: structuralCodes.Category(rawValue: "ASCE")!, imageName: "ASCE"),
structuralCodes(index: 2, title: "AISC", icon: "wind", isFeatured: false, category: structuralCodes.Category(rawValue: "AISC")!, imageName: "AISC"),
]
All Categorys are Strings, but not all Strings are Categorys, that's why you need to jump through the hoops here. And force unwrapping is safe here, because you're clearly assigning a valid rawValue

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

Unable to use combine with TextField

I'm trying to pass data to TextField using combine. by creating a data model and using observableObject, but when I use it in textField it shows me the error. Cannot convert value of type 'String' to expected argument type 'Binding< String >'. I'm unable to understand it.
dataModel
struct People: Identifiable {
var id = UUID()
var name: String
var amount: String
}
let peopleData = [
People(name: "A",amount: ""),
People(name: "B",amount: ""),
People(name: "C",amount: "")
]
ObservableObject
import Combine
class PeopleAllData: ObservableObject{
#Published var peopleStore: [People] = peopleData
}
TextField
#ObservedObject var store = PeopleAllData()
List{
ForEach(store.peopleStore){ item in
HStack {
TextField("person Name", text: item.name) //Error:- Cannot convert value of type 'String' to expected argument type 'Binding<String>'
Button(action: {}) {
Image(systemName: "minus.circle")
.foregroundColor(.red)
}
}
}
}
.frame(width: screen.width, height: screen.height)
You need Binding to array element via ObservedObject, like below
ForEach(store.peopleStore.indices, id: \.self){ i in
HStack {
TextField("person Name", text: $store.peopleStore[i].name)

SwiftUI: Index out of range when deleting TextField() but not Text()

The code below throws an index out of range error when deleting TextField() but not when deleting Text().
Here is the full error: Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
Section{
// Text(infoList[item].title) //<-- this doesn't throw error when deleted
TextField(infoList[item].title, text: $infoList[item].title)
}
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}
#Asperi's response about creating a dynamic container was correct. I just had to tweak a bit to make it compatible with Identifiable. Final code below:
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
EditorView(container: self.$infoList, index: item, text: infoList[item].title)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}
struct EditorView : View {
var container: Binding<[Information]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = Information(id: UUID(), title: text)
})
}
}