Accessing data in SwiftData in SwiftUI

438 views Asked by At

I'm trying to understand and learn SwiftData. I want to keep some data in my application. I wanted to use swiftData to learn SwiftData and created a model like this:

import Foundation
import SwiftData

@Model
final class SwiftDataModel {
    
    var height: Optional<Double>
    var heightUnit: Optional<String>
    var weight: Optional<Double>
    var weightUnit: Optional<String>
    var gender: Optional<String>
    var waterGoal: Optional<Double>
    var volumeUnit: Optional<String>
    var activity: Optional<Int>
    var age : Optional<Int>
    var nutritionLog : Optional<Array<Any>>
    
    
    
    init(height: Optional<Double>, heightUnit: Optional<String>, weight: Optional<Double>, weightUnit: Optional<String>, gender: Optional<String>, waterGoal: Optional<Double>, volumeUnit: Optional<String>, activity: Optional<Int>, age: Optional<Int>, nutritionLog: Optional<Array<Any>>) {
        self.height = height
        self.heightUnit = heightUnit
        self.weight = weight
        self.weightUnit = weightUnit
        self.gender = gender
        self.waterGoal = waterGoal
        self.volumeUnit = volumeUnit
        self.activity = activity
        self.age = age
        self.nutritionLog = nutritionLog
    }
}

Maybe I can also add an array like weightlog. As far as I understand swiftData keeps this data in a list. Therefore, I will always need to update index 0 instead of using context.insert(data). Is this true?

My other case is this: After the user logs in, can I check whether the user's age, weight and height information is kept in SwiftData and determine the page to be opened there?

For example :


@main
struct MyApp: App {
    
    var body: some Scene {
        WindowGroup {
            
            if (ViewModel.isLogin() && !ViewModel.userId.isEmpty ) {
                // If height, weight, and age information is available in swiftData TabBarView()
                // else IntroPageView()
                {
            } else {
                LoginView()
 
            }
    
        }
    }
}

Is such a check possible or should I use "User Deafults" or "Core Data" instead of SwiftData in my application?

I'm looking forward to your answers. Thanks in advance

2

There are 2 answers

7
Chris On

Welcome to Stack Overflow, I can see you are on your way to using SwiftData - and I will try answer your questions as best I can

Therefore, I will always need to update index 0 instead of using context.insert(data). Is this true?

I am not too sure what you mean by this. I am guessing you are asking how to update the model and add a new attribute. What you'll need to do is add a weightlog in the code to add it in. Ensure it is a optional like the rest and have a default value as null if you already have data in your application.

However, from your usage in your app, it seems like you would be better off using User Defaults - this is because you will only have one weight, height e.c.t for each user. SwiftData and CoreData could be used in this scenario if you have multiple users on the same phone - which is rare for any health application.

But, with any form of persistent data, you have to retrieve the data - then save it to a @State variable. If that @State variable changes in your view (a user types in a new height or weight), you'll need to save that value to the User Defaults again.

Once you have saved that data to the User Defaults. If the user closes the app and restarts, you can implement something like this where on app launch the init function is run and it checks if the height, weight, and age is present in the User Defaults and presents the correct view.

@main
struct MyApp: App {
    @State var userDataPresent : bool = false

    init() {
        heightPresent = UserDefaults.standard.object(forKey: "userHeight")
        weightPresent = ...
        agePresent = ...
        
        userDataPresent = heightPresent && weightPresent && agePresent 
    }

    var body: some Scene {
        WindowGroup {
            if (ViewModel.isLogin() && !ViewModel.userId.isEmpty ) {
                if (userDataPresent) {
                    TabBarView()
                } 
                else {
                    IntroPageView()
                }
            } else {
                LoginView()
            }
        }
    }
}

Then to save to User Defaults use:

userDefaults.set(180, forKey: "userHeight")
0
Atilla Ertürk On

Thank you for all the answers. It did what I wanted and I'm using it efficiently.

First, I updated my model like this:

@Model
final class UserInfoSwiftDataModel {

@Attribute(.unique) var userId : String?
var height: Double?
var heightUnit: String?
var weight: Double?
var weightUnit: String?
var gender: String?
var waterGoal: Double?
var consumedWater = [Water]()
var consumedWaterInt : Double?
var volumeUnit: String?
var activity: Int?
var age : Int?
var nutrition = [Nutrition]()


init(userId: String? = nil, height: Double? = nil, heightUnit: String? = nil, weight: Double? = nil, weightUnit: String? = nil, gender: String? = nil, waterGoal: Double? = nil, consumedWater: [Water] = [Water](), consumedWaterInt: Double? = nil, volumeUnit: String? = nil, activity: Int? = nil, age: Int? = nil, nutrition: [Nutrition] = [Nutrition]()) {
    self.userId = userId
    self.height = height
    self.heightUnit = heightUnit
    self.weight = weight
    self.weightUnit = weightUnit
    self.gender = gender
    self.waterGoal = waterGoal
    self.consumedWater = consumedWater
    self.consumedWaterInt = consumedWaterInt
    self.volumeUnit = volumeUnit
    self.activity = activity
    self.age = age
    self.nutrition = nutrition
}



}

@Model
final class Water {
var date: String
var consumed : Double
var User : UserInfoSwiftDataModel?

init(date: String, consumed: Double, User: UserInfoSwiftDataModel? = 
nil) {
    self.date = date
    self.consumed = consumed
    self.User = User
}
}

@Model
final class Nutrition{
var name: String
var calories: Double
var serving_size_g: Double
var fat_total_g: Double
var fat_saturated_g: Double
var protein_g: Double
var sodium_mg: Int
var potassium_mg: Int
var cholesterol_mg: Int
var carbohydrates_total_g: Double
var fiber_g: Double
var sugar_g: Double
var User : UserInfoSwiftDataModel?

init(name: String, calories: Double, serving_size_g: Double, 
fat_total_g: Double, fat_saturated_g: Double, protein_g: Double, 
sodium_mg: Int, potassium_mg: Int, cholesterol_mg: Int, 
carbohydrates_total_g: Double, fiber_g: Double, sugar_g: Double, User: 
UserInfoSwiftDataModel? = nil) {
    self.name = name
    self.calories = calories
    self.serving_size_g = serving_size_g
    self.fat_total_g = fat_total_g
    self.fat_saturated_g = fat_saturated_g
    self.protein_g = protein_g
    self.sodium_mg = sodium_mg
    self.potassium_mg = potassium_mg
    self.cholesterol_mg = cholesterol_mg
    self.carbohydrates_total_g = carbohydrates_total_g
    self.fiber_g = fiber_g
    self.sugar_g = sugar_g
    self.User = User
}   
}

To stop using Index, I added an id match condition to get the data of the logged in user. This way, I can access and update information in a healthy way. I used it as follows:

@Environment (\.modelContext) private var context
@Query (sort: \UserInfoSwiftDataModel.height, order: .forward) private 
var mySwiftData : [UserInfoSwiftDataModel]
var userId : String // Login/SignUp ekranından geliyor
var profile: UserInfoSwiftDataModel? {
    return mySwiftData.first { $0.userId == userId }
}

Text(" \(String(format: "%.1f", profile?.weight ?? 0)) \. 
(profile?.weightUnit ?? "kg")")

This way, I can easily get my data not only within the list, but everywhere. Thank you for giving a time.