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