How to pass observed properties to other classes? - ios

I created an instance of UserData so that other classes can observe this instance and show the necessary information by using the username. What I am trying to do here is, when a user is logged in, different classes with user related stored properties will be updated ( i.e. by calling the api) from time to time according to the user activity in the app.
However, it shows the error 'Cannot use instance member 'userData' within property initializer; property initializers run before 'self' is available'. Any ideas how to solve this?
I am not sure how to pass the data from a single ObservedObject to another.
struct passingData: View {
#ObservedObject var userData = UserData()
#ObservedObject var images = ImageURL(userData: userData)
#ObservedObject var payment = Payment(userData: userData)
var body: some View {
VStack{
TextField("Enter userName", text: $userData.userName)
Text("url is \(images.imageURL)")
Text("Payment detail: \(payment.paymentDate)")
}
}
}
class Payment: ObservableObject{
#Published var paymentDate = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadPaymentDate()
}
func loadPaymentDate(){
self.paymentDate = "last payment date from \(userData.userName) is someDate "
}
}
class ImageURL: ObservableObject{
#Published var imageURL = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}
class UserData: ObservableObject{
#Published var userName = ""
}

Here is possible solution:
struct passingData: View {
#ObservedObject var userData: UserData
#ObservedObject var images: ImageURL
#ObservedObject var payment: Payment
init() {
let data = UserData()
self.userData = data
self.images = ImageURL(userData: data)
self.payment = Payment(userData: data)
}
// ... other code

Related

SwiftUI Pass EnvironmentObject to a class

I'm struggling with passing an variable to a class.
I have a class with different settings I store. The data is available and might be changed on different views.
class UserData: ObservableObject {
#Published var ZipCode = "DK6700"
}
The class is on my main view initialised as StateObject:
struct ContentView: View {
#StateObject var SavedData = UserData()
var body: some View {
NavigationView {
ChartView()
}
.edgesIgnoringSafeArea(.bottom)
.environmentObject(SavedData)
}
}
I call the Struct ChartView() where UserData is initialised as
#EnvironmentObject var SavedData: UserData
#ObservedObject var dataModel = DataModel()
and from the corresponding View, i can access the stored ZipCode.
So far so good.
The ChartView calls another class, where I download data in JSON format. This works as well, but I need the stored ZIP code in this class, and I can't figure out how to pass it.
Currently ZIP is hardcoded in the DataModel, and works, but it should be the stored value instead.
My DataModel():
class DataModel: ObservableObject {
#EnvironmentObject var SavedData: UserData
#MainActor #Published var Data: [MyData] = []
var ZIP:String = "3000"
#MainActor func reload() async {
let url = URL(string: "https://MyURL?zip=\(ZIP)")!
let urlSession = URLSession.shared
do {
...
} catch {
...
}
}
}
Any suggestions to get the stored ZIP code to my DataModel Class?
How about changing your function signature to
#MainActor func reload(for zipCode: String) async {
and passing it in when you call the function?

Thread 1: Fatal error: Index out of range In SwiftUI

i am trying to make a small Social Media app. the friends and friendrequest gets stored as User in different arrays. But when i want to loop the array it an shows which user send a request it first works but when i accept the user and he is remove from the Array i am getting this error "Thread 1: Fatal error: Index out of range" i know its because the loop wants to loop to a index which doesn't exist anymore but how do i fix it ?
struct FriendsView: View {
#EnvironmentObject var appUser: User
var body: some View {
List {
ForEach(0..<appUser.friendAnfrage.count) {
durchlauf in
SingleFriendView(user: appUser.friendAnfrage[durchlauf])
}
}
}
}
class User: ObservableObject{
#Published var username: String = ""
#Published var name: String = ""
var password: String = ""
#Published var email: String = ""
#Published var beschreibung: String = ""
#Published var profilBild: UIImage?
#Published var friends = [User]()
#Published var friendAnfrage = [User]()
#Published var anfrageGesendet = [User]()
#Published var feed = [SinglePostView]()
func addFriend(friend: User,appUser: User) {
friend.friendAnfrage.append(appUser)
appUser.anfrageGesendet.append(friend)
}
func newFriend(newFriend: User) {
friends.append(newFriend)
for i in 0..<friendAnfrage.count {
if friendAnfrage[i].username == newFriend.username {
friendAnfrage.remove(at: i)
}
}
}
func friendAnfrage(friend: User,appUser: User) {
appUser.friendAnfrage.append(friend)
}
func makePost(image: UIImage,appUser: User) {
feed.append(SinglePostView(bild: image, ersteller: appUser))
for i in 0..<friends.count {
friends[i].feed.append(SinglePostView(bild: image, ersteller: appUser))
}
}
}
ForEach with an index-based approach is dangerous in SwiftUI. Instead, make your model identifiable.
class User: ObservableObject, Identifiable {
var id = UUID()
//...
Then, change your loop:
ForEach(appUser.friendAnfrage) { item in
SingleFriendView(user: item)
}
Unrelated to this exact issue, but generally SwiftUI does better with using a struct for a model instead of a class. If a User in friends is updated with your current code, because it's a nested ObservableObject, your View will not get automatically updated.
User should be a struct and ForEach isn't a traditional loop, it's a View that must be supplied identifiable data, e.g.
struct FriendsView: View {
#EnvironmentObject var model: Model
var body: some View {
List {
ForEach($model.users) { $user in
SingleFriendView(user: $user)
}
}
}
}
struct User: Identifiable{
let id = UUID()
var username: String = ""
var friends: [UUID] = []
}
class Model: ObservableObject {
#Published var users: [User] = []
}

Struct initialization in SwiftUI: 'self' used before all stored properties are initialized

I'm trying to pass a Binding to my VM which is supposed to be a filter so the VM fetches objects according to the filtering passed by params.
Unfortunately, I'm not able to initialize the VM, as I'm getting the error 'self' used before all stored properties are initialized in the line where I'm initializing my VM self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
struct JobsTab: View {
#ObservedObject var jobsViewModel: JobsViewModel
#ObservedObject var categoriesViewModel: CategoriesViewModel
#StateObject var searchText: SearchText = SearchText()
#State private var isEditing: Bool
#State private var showFilter: Bool
#State private var jobFilter: JobFilter
init() {
self.categoriesViewModel = CategoriesViewModel()
self.jobFilter = JobFilter(category: nil)
self.showFilter = false
self.isEditing = false
self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
}
I think I'm initializing all the vars, and self.searchText isn't in the init block because the compiler complains that it is a get-only property.
Is there any other way to do this?
Thanks!
EDIT: Here's my VM:
class JobsViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var jobs: [Jobs] = []
#Binding var jobFilter: JobFilter
init(jobFilter: Binding<JobFilter>) {
_jobFilter = jobFilter
}
...
}
struct JobFilter {
var category: Category?
}
My idea was to have the job filter as a state in the JobsTab, and every time that state changes, the VM would try to fetch the jobs that match the JobFilter
You shouldn't create #ObservedObject values in your initializer. Doing so leads to bugs, because you'll create new instances every time the view is recreated. Either jobsViewModel and categoriesViewModel should be passed as arguments to init, or you should be using #StateObject for those properties.
But anyway, you actually asked: why can't we use $jobFilter before initializing jobsViewModel?
Let's start by simplifying the example:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
So, what's going on here? It'll help if we “de-sugar” the use of #State. The compiler transforms the declaration of jobFilter into three properties:
struct JobsTab: View {
private var _jobFilter: State<String>
var jobFilter: String {
get { _jobFilter.wrappedValue }
nonmutating set { _jobFilter.wrappedValue = newValue }
}
var $jobFilter: Binding<String> {
get { _jobFilter.projectedValue }
}
var jobFilterBinding: Binding<String>
init() {
_jobFilter = State<String>(wrappedValue: "")
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
Notice now that $jobFilter is not a stored property. It is a computed property. So accessing $jobFilter means calling its “getter”, which is a method on self. But we cannot call a method on self until self is fully initialized. That's why we get an error if we try to use $jobFilter before initializing all stored properties.
The fix is to avoid using $jobFilter. Instead, we can use _jobFilter.projectedValue directly:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = _jobFilter.projectedValue
}
var body: some View { Text("hello") }
}

How to pass data from an observed class to another?

This post is related to this post that I made. While there is no initialization error anymore now, it seems that there's one problem here: when you change the username in the textfield, the url and payment detail will not get updated still? Any idea how to solve this?
struct passingData: View {
#ObservedObject var userData: UserData
#ObservedObject var images: ImageURL
#ObservedObject var payment: Payment
init() {
let data = UserData()
self.userData = data
self.images = ImageURL(userData: data)
self.payment = Payment(userData: data)
}
var body: some View {
VStack{
TextField("Enter userName", text: $userData.userName)
Text("url is \(images.imageURL)")
Text("Payment detail: \(payment.paymentDate)")
}
}
}
class Payment: ObservableObject{
#Published var paymentDate = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadPaymentDate()
}
func loadPaymentDate(){
self.paymentDate = "last payment date from \(userData.userName) is 12.12.22 "
}
}
class ImageURL: ObservableObject{
#Published var imageURL = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}
class UserData: ObservableObject{
#Published var userName = ""
}
You cannot use #ObservedObject property wrapper in class, it is designed for View only.
Here is a demo of solution for one class. Tested with Xcode 12 / iOS 14
import Combine
class ImageURL: ObservableObject{
#Published var imageURL = ""
private var userData: UserData // << reference type
private var observer: AnyCancellable?
init(userData: UserData){
self.userData = userData
// observe changes of userName via publisher explicitly
self.observer = userData.$userName.sink(receiveValue: {[weak self] _ in
self?.loadImageURL()
})
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}

Swift Array .append() method not working in SwiftUI

I'm struggling to do a simple append in SwiftUI. Here's my code:
// This is defined in my custom view
var newClass = Class()
// This is inside a List container (I hid the Button's content because it doesn't matter)
Button(action: {
self.newClass.students.append(Student())
print(self.newClass.students) // This prints an Array with only one Student() instance - the one defined in the struct's init
})
// These are the custom structs used
struct Class: Identifiable {
var id = UUID()
#State var name = ""
#State var students: [Student] = [Student()] // Right here
}
struct Student: Identifiable {
var id = UUID()
#State var name: String = ""
}
I think it might be somehow related to the new #Struct thing, but I'm new to iOS (and Swift) development, so I'm not sure.
Let's modify model a bit...
struct Class: Identifiable {
var id = UUID()
var name = ""
var students: [Student] = [Student()]
}
struct Student: Identifiable {
var id = UUID()
var name: String = ""
}
... and instead of using #State in not intended place (because it is designed to be inside View, instead of model), let's introduce View Model layer as
class ClassViewModel: ObservableObject {
#Published var newClass = Class()
}
and now we can declare related view that behaves as expected
struct ClassView: View {
#ObservedObject var vm = ClassViewModel()
var body: some View {
Button("Add Student") {
self.vm.newClass.students.append(Student())
print(self.vm.newClass.students)
}
}
}
Output:
Test[4298:344875] [Agent] Received display message [Test.Student(id:
D1410829-F039-4D15-8440-69DEF0D55A26, name: ""), Test.Student(id:
50D45CC7-8144-49CC-88BE-598C890F2D4D, name: "")]

Resources