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

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] = []
}

Related

How to control published property value update in viewmodel in SwiftUI?

I have a view model class with multiple #Published properties.
class AddPassaround : ObservableObject {
#Published var name: String = ""
#Published var reversed : String = ""
#Published var password: String = ""
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
init() {
}
}
Whenever any one of the #Published property is updated, I call an API. Now there is another scenario that needs to update multiple #Published properties at once programmatically. Something like this
viewModel.name = "test"
viewModel.password = "newPassword"
viewModel.oneAnotherProperty = "notUpdateAll"
Now the problem is the API is called multiple times and view is reloaded multiple times. How can I make the API to call only once in this case only. It should work normally in other cases.
SwiftUI faces the same problem you face: when you update three separate #Published properties of your ObservableObject, SwiftUI gets notified three times.
SwiftUI avoids updating the UI three times by coalescing the events. On the first notification, SwiftUI arranges to be awakened before the run loop waits for the next event. On the later notifications, SwiftUI sees that it's already arranged to be awakened and does nothing.
UIKit, AppKit, and Core Animation also coalesce display updates; this is what methods like UIView.setNeedsDisplay and CALayer.setNeedsDisplay are for.
You can use the same sort of coalescing. One way to do it is to use NotificationQueue. You can ask NotificationQueue to enqueue a notification and post it before the run loop goes to sleep, and you can ask it to coalesce queued notifications.
class AddPassaround : ObservableObject {
#Published var name: String = ""
#Published var reversed : String = ""
#Published var password: String = ""
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
private var tickets: [AnyCancellable] = []
private var notificationName: Notification.Name { .init("AddPassaround call API") }
init() {
NotificationCenter.default.publisher(for: notificationName)
.sink { [weak self] _ in self?.callAPI() }
.store(in: &tickets)
objectWillChange
.sink { [weak self] _ in self?.scheduleCallAPI() }
.store(in: &tickets)
}
private func scheduleCallAPI() {
// Arrange to callAPI soon, if I haven't already arranged it.
NotificationQueue.default.enqueue(
.init(name: notificationName),
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
}
private func callAPI() {
print("this is where you call the API")
}
}
If you only want a few of your properties to trigger an API call, you can give them willSet (or didSet) observers instead of subscribing to objectWillChange:
class AddPassaround : ObservableObject {
#Published var name: String = "" {
willSet { scheduleCallAPI() }
}
#Published var reversed : String = ""
#Published var password: String = "" {
willSet { scheduleCallAPI() }
}
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
private var tickets: [AnyCancellable] = []
private var notificationName: Notification.Name { .init("AddPassaround call API") }
init() {
NotificationCenter.default.publisher(for: notificationName)
.sink { [weak self] _ in self?.callAPI() }
.store(in: &tickets)
}
private func scheduleCallAPI() {
// Arrange to callAPI soon, if I haven't already arranged it.
NotificationQueue.default.enqueue(
.init(name: notificationName),
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
}
private func callAPI() {
print("this is where you call the API")
}
}

Binding not working as expected when button is pressed

I have a button that's supplied with data and this is used to unfollow or follow a user. What should happen is that I press the button, it changes the isFollowing property on the user, and then the text updates from unfollow to follow. However, this doesn't work. Here's my code that can be put into a playground (simplified for the purposes of this just to show the core elements):
struct User: Hashable, Identifiable {
let id = UUID()
var isFollowing: Bool
}
final class MyModel: ObservableObject {
#Published var users: [User] = [User(isFollowing: true)]
func unfollow(_ user: Binding<User>) async throws {
user.wrappedValue.isFollowing = false
}
}
struct ContainerView: View {
#EnvironmentObject private var model: MyModel
var body: some View {
UserListView(users: $model.users)
}
}
final class PagedUsers: ObservableObject {
#Published var loadedUsers: [Binding<User>] = []
#Binding var totalUsers: [User]
init(totalUsers: Binding<[User]>) {
self._totalUsers = totalUsers
let firstUser = $totalUsers.first!
loadedUsers.append(firstUser)
}
}
struct UserListView: View {
#StateObject private var pagedUsers: PagedUsers
init(users: Binding<[User]>) {
self._pagedUsers = StateObject(wrappedValue: PagedUsers(totalUsers: users))
}
var body: some View {
ForEach(pagedUsers.loadedUsers) { user in
MyView(user: user)
}
}
}
struct MyView: View {
#EnvironmentObject private var model: MyModel
#Binding var user: User
var body: some View {
Button(
action: {
Task {
do {
try await model.unfollow($user)
} catch {
print("Error!", error)
}
}
},
label: {
Text(user.isFollowing ? "Unfollow" : "Follow")
}
)
}
}
PlaygroundPage.current.setLiveView(
ContainerView()
.environmentObject(MyModel())
)
I think it's not working because of something to do with the passing of bindings, but I can't quite work out why. Possibly it's the setup of PagedUsers? However, this needs to be there because in my app code I essentially pass all the user data to it, and return "pages" of users from this, which gets added to as the user scrolls.
I don't fully understand why you need two classes for users ... why not put them in one in different #Published vars?
IMHO then you don't need any bindings at all!
Here is a working code with only one class, hopefully you can build from this:
import SwiftUI
#main
struct AppMain: App {
#StateObject private var model = MyModel()
var body: some Scene {
WindowGroup {
ContainerView()
.environmentObject(model)
}
}
}
struct User: Hashable, Identifiable {
let id = UUID()
var isFollowing: Bool
}
class MyModel: ObservableObject {
#Published var users: [User] = [User(isFollowing: true), User(isFollowing: true), User(isFollowing: true)]
func unfollow(_ user: User) async throws {
if let index = users.firstIndex(where: {$0.id == user.id}) {
self.objectWillChange.send()
users[index].isFollowing = false
}
}
}
struct ContainerView: View {
#EnvironmentObject private var model: MyModel
var body: some View {
UserListView(users: model.users)
}
}
struct UserListView: View {
let users: [User]
var body: some View {
List {
ForEach(users) { user in
MyView(user: user)
}
}
}
}
struct MyView: View {
#EnvironmentObject private var model: MyModel
var user: User
var body: some View {
Button(
action: {
Task {
do {
try await model.unfollow(user)
} catch {
print("Error!", error)
}
}
},
label: {
Text(user.isFollowing ? "Unfollow" : "Follow")
}
)
}
}

SwiftUI - Should you use `#State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}

How to pass observed properties to other classes?

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

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