WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Publishing changes from background threads is not allowed. Day 60 - Friendface Part1

Forums > 100 Days of SwiftUI

I finished the day 60 challenge but implemented it a little differently than Paul's solution. I had fairly the same setup, except I used a class to make an array of users which wasn't really necessary. My solution did work but it also produced the following error: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

I have no idea what that means and googling it wasn't very helpful. I changed my code to match Paul's, but can someone explain why I get this error and how to get around it, other than not using a class?

Here's all my code:

import SwiftUI

class Users: ObservableObject {
    @Published var items = [User]()
}

struct User: Codable, Identifiable {
    struct Friend: Codable, Identifiable {
        let id: UUID
        let name: String
    }
    let id: UUID
    let isActive: Bool
    let name: String
    let age: Int
    let company: String
    let email: String
    let address: String
    let about: String
    let registered: Date
    let tags: [String]
    let friends: [Friend]
}

struct ContentView: View {
    @StateObject var users = Users()

    var body: some View {
        NavigationView {
            Form {
                ForEach(users.items) { item in
                    NavigationLink {
                        DetailView(user: item)
                    } label: {
                        VStack(alignment: .leading) {
                            Text(item.name)
                            Text("Active: \(String(item.isActive))")
                                .font(.footnote)
                        }
                    }
                }
            }
            .navigationTitle("FriendFace")
            .task {
                await getData()
            }
        }
    }
    func getData() async {
        if users.items.isEmpty {
            guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else {
                print("Invalid URL")
                return
            }
            print("Valid URL")

            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                let decoder = JSONDecoder()
                decoder.dateDecodingStrategy = .iso8601
                if let decodedResponse = try? decoder.decode([User].self, from: data) {
                    users.items = decodedResponse
                    print("Decoded")
                } else {
                    print("Failed")
                }
            } catch {
                print("Invalid data")
            }
        }
    }
}

struct DetailView: View {
    let user: User

    var body: some View {
        Form {
            HStack {
                Text("Name: ")
                    .bold()
                Text(user.name)
            }
            HStack {
                Text("Active: ")
                    .bold()
                Text("\(String(user.isActive))")
            }
            HStack {
                Text("Age: ")
                    .bold()
                Text("\(user.age)")
            }
            HStack {
                Text("Company: ")
                    .bold()
                Text(user.company)
            }
            HStack {
                Text("E-mail: ")
                    .bold()
                Text(user.email)
            }
            HStack {
                Text("Address: ")
                    .bold()
                Text(user.address)
            }
            HStack {
                Text("Registered: ")
                    .bold()
                Text("\(user.registered.formatted(date: .abbreviated, time: .omitted))")
            }

            Section("About") {
                Text(user.about)
            }
            Section("Tags") {
                ScrollView(.horizontal){
                    HStack {
                        ForEach(user.tags, id: \.self) { tag in
                            Text(tag)
                        }
                    }
                }
            }
            Section("Friends") {
                ForEach(user.friends, id: \.id) { friend in
                    Text(friend.name)
                }
            }

        }
        .navigationTitle("Details")
        .navigationBarTitleDisplayMode(.inline)
    }
}

   

Hopefully this will help explain, as it is the same question - Publishing from background threads error.

   

In short, anything coming "up" from other parts of the code to update the UI have to be handed off to the UI or "main" thread so you don't end up with thread collisions, race conditions, dead locks, etc.

Anything that you're doing in background or as a result of callbacks or closures (or non-ui events) should be handed off in a GrandCentral Dispatch call.

DispatchQueue.main.async{ 
    doYourStuffHere() 
}

It's just a way to say "Hey, User Interface, here's that stuff you wanted me to get for you," then let him paint his own house, his way, and just thank you for delivering the paint.

   

Thanks for the reposnes. I was able to add

await MainActor.run {
  users.items = decodedResponse
}

and the error went away.

I don't really understand either of the articles, however, I did back peddle through them to find they are from a book called Swift Concurrency by Example, with the very first section titled: "This Stuff Is Hard". Yeah, so I'll back to the kiddie pool now.

1      

Think of it like I (probably too quickly) explained it above.

You're painting your house.

You hire a guy to bring you paint.

He starts painting and you say WHOA! No way buddy! I'm doing this job.

MainActor.run or DispatchQueue.main.async are both wasy of having him hand you the paint and he goes away. You can paint now that you have the resource you need.

Replace paint and painting the house to fit your understanding :)

   

I like Chris H's painting example. Here's another one to think about.

You are in your main thread, aka your kitchen making a salad. You have everything you need for a great salad, except for olives. While you continue to chop and prepare your salad, you send your flatmate to the corner store for some olives. She's in the background doing a fetch job, whilst you're in the kitchen doing main thread work.

She dawdles a tad and it takes some time for her to return. Soon she announces that she's back with the olives. However, you cannot use those olives while they are still in the background. She must unpack them (aka decode), and bring them to the main thread (the kitchen) before you can use them.

Anything retreived in a background thread must be sent to the kitchen (main thread) before it can be used.

In SwiftUI terms, any time you want to update the user interface, you must do this in the main thread.

In your old code you tried to update users.items as soon as the data was decoded.

if let decodedResponse = try? decoder.decode([User].self, from: data) {
    users.items = decodedResponse  // <-- you're in the background here! don't attempt to update the interface!     
    print("Decoded")
}

This code takes the unpacked olives and sends them to the kitchen where they can be added to the salad.

await MainActor.run {
  users.items = decodedResponse // <-- safely move the decoded response to the main thread.
}

1      

Thanks for the clarification. It makes more sense. I think I'll go paint my olives now.

1      

LOL nicely done!

   

So, after all my confusion with this yesterday; today's lesson was partly on MainActor: https://www.hackingwithswift.com/100/swiftui/61

After all the replies on this post, in addition to what I read today, and actually seeing how it works when two separate processes are trying to update the UI at the same time, it's making more sense.

Thanks.

   

Hacking with Swift is sponsored by Emerge

SPONSORED Optimize your app’s startup time, binary size, and overall performance using Emerge’s advanced app optimization and monitoring tools. Reliably measure app size, speed up your app's startup time with Emerge's Launch Booster, and much more. Emerge is actively used by many of the top mobile development teams in the world.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.