Swift : retrieving struct values in swiftui view is not working - swift

I am new to swift and need to define some form of Global Dictionary that I can access the content throughout my project. My understanding is that struct class can be used for that so
I created a struct and appended values to it, now I want to access each of those values in view
this is my product struct
struct Product {
let name: String
let aisleNo:Int
let location_section: Int
let location_zone: String
let productPrice: Int
}
then created a global
import Foundation
struct Global {
static var productList = [Product]()
}
this is how I append many products to Product
class SearchResult : ObservableObject {
var productList = [Product]()
//There could be hundreds of product in the array
for product in productArray {
let productName = product.productName!
let aisleNo = product.productLocation_aisle.value!
let location_section = product.productLocation_section.value!
let location_zone = product.productLocation_zone!
let productPrice = product.productPrice.value!
let product_real_id = product._id!
Global.productList.append(Product(name: productName, aisleNo: aisleNo, location_section: location_section, location_zone: location_zone, productPrice: Int(productPrice)))
}
this is my search result view where I want to display the content of the Product
struct SearchResultView: View {
var searchResults = Global.productList
var body: some View {
VStack {
List {
ForEach(model.searchResults, id: \.self) { text in
Text(text)
}
}
}
}
}
I can seem to get it to show in the searchResultView. What am doing wrong ?
I keep getting this error
Generic struct 'ForEach' requires that 'Product' conform to 'Hashable'
Initializer 'init(_:)' requires that 'Product' conform to 'StringProtocol'

You need to set "searchResults" equal to your "productList"
Right now your searchResults is EMPTY. It simply exists as an instance of your struct with no data in it.
one option is to make the variable scope global and then set your new variable = to it
self.searchResults = Global.productList
--EDIT
You are close.
Where you set your var here
var searchResults = Global.productList
it needs to be like this.
var searchResults = [Product]() // ->Creates an instance of the struct object
Then set it equal to your global array.
self.searchResults = Global.productList
ALSO you should remove you redundant variable var productList = [Product]()
Furthermore, some things to note
for product in productArray {
let productName = product.productName!
let aisleNo = product.productLocation_aisle.value!
let location_section = product.productLocation_section.value!
let location_zone = product.productLocation_zone!
let productPrice = product.productPrice.value!
let product_real_id = product._id!
Global.productList.append(Product(name: productName, aisleNo: aisleNo, location_section: location_section, location_zone: location_zone, productPrice: Int(productPrice)))
}
you are doing extra work by using all the let variables.
A better way is doing it like so.
for product in productArray {
Global.productList.append(Product(name: product.name, aisleNo: product.aisleNo, location_section: product.location_section, location_zone: product.location_zone, productPrice: Int(product.productPrice)))
}
EDIT - Hashable Erorr
try this out
struct Product: Hashable {
let name: String
let aisleNo:Int
let location_section: Int
let location_zone: String
let productPrice: Int
}

Related

Userdefaults with published enum

try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)
class PrayerTimeViewModel: ObservableObject {
#Published var lm = LocationManager()
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults.standard.set(method.params, forKey: "method")
self.getPrayerTime()
}
}
func getPrayerTime() {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
var par = method.params
par.madhab = mashab
self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}
and view.. update with AppStorage
struct MethodView: View {
#ObservedObject var model: PrayerTimeViewModel
#Environment(\.presentationMode) var presentationMode
#AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
List(CalculationMethod.allCases, id: \.self) { item in
Button(action: {
self.model.objectWillChange.send()
self.presentationMode.wrappedValue.dismiss()
self.model.method = item
method = item
}) {
HStack {
Text("\(item.rawValue)")
if model.method == item {
Image(systemName: "checkmark")
.foregroundColor(.black)
}
}
}
}
}
}
You have two issues.
First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.
Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.
Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.
I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.
Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.
This should be enough for you to understand how it works.
enum Place: String, Codable {
case first
case second
case third
case notPlaced
var someComputedProperty: String {
"Value stored: \(self.rawValue)"
}
}
class Race: ObservableObject {
#Published var place: Place = .notPlaced {
didSet {
// Store the rawValue of the enum into UserDefaults
// We can store the actual enum but that requires more code
UserDefaults.standard.setValue(place.rawValue, forKey: "method")
// Using a custom suite
// UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
}
}
init() {
// Load the value from UserDefaults if it exists
if let rawValue = UserDefaults.standard.string(forKey: "method") {
// We need to nil-coalesce here as this is a failable initializer
self.place = Place(rawValue: rawValue) ?? .notPlaced
}
// Using a custom suite
// if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
// self.place = Place(rawValue: rawValue) ?? .notPlaced
// }
}
}
struct ContentView: View {
#StateObject var race: Race = Race()
var body: some View {
VStack(spacing: 20) {
Text(race.place.someComputedProperty)
.padding(.bottom, 20)
Button("Set First") {
race.place = .first
}
Button("Set Second") {
race.place = .second
}
Button("Set Third") {
race.place = .third
}
}
}
}
Addendum:
Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.
You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults(suiteName: "method").set(method.params, forKey: "method")
self.getPrayerTime()
}
}
or if you want to use standard then just use AppStorage with default constructor, like
// use UserDefaults.standard by default
#AppStorage("method") var method: CalculationMethod = .dubai

didSet not called by Array.append() in Swift

I am following the 100 Days of SwiftUI and have reached Day 37. While doing Making changes permanent with UserDefaults, I encounter a problem with didSet.
(I am using Swift 5 with iOS 13.4)
In the example code, it writes
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
self.expenses.items.append(item)
}
})
where didSet should be called by .append().
However, in practice, the didSet is not called unless I change the above code to
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
let newItems = self.expenses.items + [item]
self.expenses.items = newItems
}
})
I also write a small test (see below) in Playground which shows that .append() works pretty well with didSet
struct Count {
var array: [Int] {
didSet {
print("struct Count - didSet() called")
}
}
}
class CountClass {
var array: [Int] {
didSet {
print("class CountClass - didSet() called")
}
}
init() {
array = [1, 2, 3]
}
}
struct Test {
var countA = Count(array: [1, 2, 3])
var countB = CountClass()
mutating func testDidSet() {
countA.array.append(4)
countB.array.append(4)
}
}
var t = Test()
t.testDidSet()
This strange behaviour really makes me wonder how didSet works. Or is this problem related to the use of #ObservedObject (which is the case of the example project)?
PS: I have downloaded the finished version from Project7 and it also has the problem.
This is a known Swift 5.2 bug: observers of wrapped properties are not called upon modification (https://bugs.swift.org/browse/SR-12089). They had known about this since January and all the same released an update breaking a bunch of production code ¯\_(ツ)_/¯
A temporary workaround is already presented in the question - property reassignment instead of modification.
I went through this project as well. Make sure your items property in the Expenses class is marked as #Published. As below;
import SwiftUI
struct ExpenseItem: Identifiable, Codable {
let id = UUID()
let name: String
let type: String
let amount: Int
}
class Expenses: ObservableObject {
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([ExpenseItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
#Published var items: [ExpenseItem] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
}
and add self.presentationMode.wrappedValue.dismiss() in your navigation bar Save button.
.navigationBarItems(trailing: Button("Save") {
if let actualAmount = Int(self.amount) {
let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
self.expenses.items.append(item)
self.presentationMode.wrappedValue.dismiss()
} else {
self.isShowingAlert = true
}
})
I finished this project last week I think, but checking the Swift GitHub page it seems there was an update on the 24th.
When I did this project everything worked fine, but now I'm on day 47 and I'm having this very problem. Maybe it's related to the Swift 5.2 update.
What's happening is that Swift is not recognizing the .append() as setting the variable. You can get around this by copying your array to a new temporary array, append the new value and then set your class array to this temporary array.
var newActivityList = [Activity]() //new temporary array
for activity in self.activityList.activities {
newActivityList.append(activity) //copy class array to temp array
}
newActivityList.append(newActivity) //append new value
self.activityList.activities = newActivityList //set class array to temp array
PS: the example above is for day 47, but the logic is the same.

How to access struct values inside of another struct in Swift 4?

I currently have
struct cellData {
var opened = Bool()
var title = String()
var iconName = String()
var sectionData = [Any]()
}
struct SectionData {
var subTitle: String
var iconName: String
}
And in another function I call:
let test = tableViewData[indexPath.section].sectionData[dataIndex]
print(test)
Which outputs:
SectionData(subTitle: "Terms", iconName: "")
How do I access the subTitle value because doing test.subTitle throws the following error:
Value of type 'Any' has no member 'subTitle'
This is because, in your line var sectionData = [Any](), you have defined the type as Any. So when you access it via tableViewData[indexPath.section], you get back the value as Any.
You should change var sectionData = [Any]() to var sectionData = [SectionData]()
Otherwise, once you get the value from tableViewData[indexPath.section], you can convert to SectionData and then access the value.
Change your sectionData array to an array of SectionData like so: var sectionData = [SectionData](). Once you do this, you'll able to access it by calling: tableViewData[indexPath.section].sectionData[dataIndex].subTitle

Looping Core Data Objects outside a Table/ Collection View

I want to loop through the objects in CoreData without using a tableView or CollectionView... But using a ViewController
I tried something like:
for var i = 0; i < numberOfExerciseItems; i++ {
let exerciseItemsfromDay = fetchedResultController.objectAtIndexPath(i) as! ExerciseItemModel
}
This obviously doesn't work since it is not of NSIndexPath type as you'd get in a table or CollectionView. Are there ways to do this outside a Table / Collection View? Thanks for the ideas in advance.
Edit
From this answer, I'd want to access an entity from an item in the items array in the loop :
let request = NSFetchRequest(entityName: "ExerciseItemModel")
let items = (try? context.executeFetchRequest(request)) as? [ExerciseItemModel] ?? []
In this loop
items.forEach {
print(items) //Displays all the objects in the console.
print(items[1].attribute //Throws the error Value of type'AnyObject' has no member 'attribute' and I cannot access an attribute from the items array . Not sure why!
}
Edit My ExerciseModel Class
import Foundation
import CoreData
#objc(ExerciseItemModel)
class ExerciseItemModel: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
extension ExerciseItemModel {
#NSManaged var exerciseType: String?
#NSManaged var exerciseName: String?
#NSManaged var durationOrSets: String?
#NSManaged var distanceOrReps: String?
#NSManaged var weight: String?
#NSManaged var backgroundImage: NSData?
#NSManaged var dayID: String?
#NSManaged var date: NSDate?
}
let request = NSFetchRequest(entityName: "ExerciseItemModel")
let items = (try? context.executeFetchRequest(request)) as? [ExerciseItemModel] ?? []
items.forEach {
// Do your stuff
}
A small example. Not exactly an answer so I'll delete it when a proper answer has been posted. This example will not show you any error messages if there are any, just keep that in mind.
Edit:
You're using forEach() incorrectly:
items.forEach {
print($0) // Print's the object in items
print($0.attribute) // Should print the attribute
}
print(items[0].attribute) // Probably same error, but it should be used outside the loop

Swift Struct Identity

I have a following struct which defines a shoppingList.
struct ShoppingList {
var shoppingListId :NSNumber
var title :String
}
let shoppingList = ShoppingList(shoppingListId: NSNumber, title: String) // I don't want to assign shoppingListId
Now, the problem is that it auto-generates the constructors which takes in the shoppingListId as parameter. Since shoppingListId is an identity or unique key I don't want to pass in with the constructor. It will be set later by the ShoppingListService after inserting shoppingList into the database.
Is this a place where class will make more sense than structures?
UPDATE:
struct ShoppingList {
var shoppingListId :NSNumber
var title :String
init(title :String) {
self.title = title
// but now I have to assign the shoppingListId here unless I make it NSNumber? nullable
}
}
let shoppingList = ShoppingList(title: "Hello World")
UPDATE 3:
let shoppingList = ShoppingList()
// THE FOLLOWING CODE WILL NOT WORK SINCE IT REQUIRES THE shoppingList to // BE var instead of let
shoppingList.shoppingListId = NSNumber(int: results.intForColumn("shoppingListId"))
shoppingList.title = results.stringForColumn("title")
shoppingLists.append(shoppingList)
If you don't want the default constructor, then make one of your own. However, it is important to keep in mind that unless your property is an Optional, it has to be initialized to some value. In this case, you will have to give shoppingListId a value, or you will need to make it an Optional. Also, I don't see any reason why a class would be better than a struct for this scenario.
struct ShoppingList {
var shoppingListId: NSNumber
var title: String
init(title: String) {
self.title = title
shoppingListId = NSNumber(integer: 0)
}
}
let newList = ShoppingList(title: "Groceries")
Update:
Just saw you updated your question. If you are not able to pick a reasonable initial value for your shoppingListId, then make it an Optional like so:
struct ShoppingList {
var shoppingListId: NSNumber?
var title: String
init(title: String) {
self.title = title
}
}
let newList = ShoppingList(title: "Groceries")
Just realize shoppingListId will be nil till you set it to something, and everywhere you use it you should bind the value in an if let.