4

How to change item of List in Swift ? (struct item)

@State var people: [Person] = [
        .init(firstName: "Steve",
              lastName: "Jobs", image: #imageLiteral(resourceName: "jobs"), jobTitle: "Founder of Apple"),
        .init(firstName: "Tim", lastName: "Cook", image: #imageLiteral(resourceName: "cook"), jobTitle: "Apple CEO"),
        .init(firstName: "Jony", lastName: "Ive", image: #imageLiteral(resourceName: "ive"), jobTitle: "Head of Design")
    ]

I want to pass the item of this array to another view and that view can modify item (function like class)

I try more way like :

@State Person struct

Observable Object (not working) Passthrough (not working)

this image depict my issue

3 Answers 3

3

ObservableObject should work if used correctly, the core concept of SwiftUI is having a single source of truth. That was where the Binding came in, however if you iterate trough a list, you got the value typed Person not the desired Binding<Person>. You may use indexes through the iteration and pass it to TextForm so it can get the original array.

To make a code more readable I suggest to have a viewmodel like

class PeopleViewModel: ObservableObject {

    @Published var people: [Person] = [
        .init(lastname: "Some", firstname: "Dude"),
        .init(lastname: "Other", firstname: "Dude"),
    ]
}

Which you must watch in the view using the @ObservedObject wrapper.

struct PeopleList: View {

    @ObservedObject var viewModel = PeopleViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.people.indices) { index in
                TextForm(viewModel: self.viewModel, index: index)
            }
        }
    }
}

And have the TextForm have the index end the viewmodel instance.

struct TextForm: View {

    @ObservedObject var viewModel: PeopleViewModel
    var index: Int

    var body: some View {
        VStack {
            TextField("textField", text: self.$viewModel.people[index].firstname)
            Text(self.viewModel.people[index].firstname)
        }
    }
}

If you really want to omit the viewmodel just pass the binding trough indexing.

            List(people.indices) { index in
                TextForm(item: self.$people[index])
            }
struct TextForm: View {

    @Binding var item: Person

    var body: some View {
        VStack {
            TextField("textField", text: self.$item.firstname)
            Text(self.item.firstname)
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I like the viewModel idea (very Android-esque). However, if pass my viewModel down to where I want to change it, the viewModel is changed, but if I immediately show the component that lists the contents of the model, the new data isn't there. I have to wind back up and go back down to get the updated model to display. BTW- the viewModel contents are in the Picker.
0

You have to provide a Binding. E.g:

struct MySecondView: View {
    @Binding var people: [People]    

    var body: some View { ... }

    func modifyArray() { /* Do whatever you need here */ }
}

And pass it to that view when initializing it in your other view like:

MySecondView(people: self.$people)    

1 Comment

You see the image I posted on it
0

one thing that almost no one writes about in the ObservableObject solution - it is best that the item in the array will be struct instead of class,

when a struct member (var) change it's value it is considered as a change to the containing struct, and achange to the containing struct (the array element) is considered as a change to the whole array (and therefor a change to the published object) - and this small change will cause the view to re render

so to make a short story long, here are 2 fully working code examples, one does not work (where Episode is Class) and the other is working (where Episode is Struct)

this code will not update the View as expected only because the Episode entity is defined as Class:

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        SeasonScreen()
    }
}

// model class that represents episode data
class Episode: Identifiable
{
    let id: UUID
    let name: String
    var watched: Bool
    
    init(name: String, watched: Bool)
    {
        id = UUID()
        self.name = name
        self.watched = watched
    }
}

// dummy server api that returns the episodes array
struct ServerAPI
{
    static func getAllEpisodes(showId: String, seasonId: String, completion: @escaping([Episode]) -> ())
    {
        let dummyResponse: [Episode] = [Episode(name: "Episode 1", watched: true), Episode(name: "Episode 2", watched: false)]
        completion(dummyResponse)
    }
}

// class that holds (and publish changes on) the episodes array
class SeasonEpisodes: ObservableObject
{
    @Published var episodesRow: [Episode] = []

    init()
    {
        ServerAPI.getAllEpisodes(showId: "Friends", seasonId: "Season 2", completion: { episodes in
            self.episodesRow = episodes
        })
    }
}

// main screen view that contains episodes array object and observe changes on it
struct SeasonScreen: View
{
    @ObservedObject var seasonEpisodes: SeasonEpisodes = SeasonEpisodes()
    
    var body: some View
    {
        VStack
        {
            if(self.seasonEpisodes.episodesRow.count > 0)
            {
                ScrollView(.horizontal, showsIndicators: false)
                {
                    HStack(alignment: VerticalAlignment.center, spacing: 60.0, content: {
                        ForEach(self.seasonEpisodes.episodesRow.indices) { episodeIndex in
                            EpisodeButton(seasonEpisodes: self.seasonEpisodes, episodeIndex: episodeIndex)
                                .padding(40)
                        }
                    })
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
    }
}

// ui view to represent episode button
struct EpisodeButton: View
{
    @ObservedObject var seasonEpisodes: SeasonEpisodes
    let episodeIndex: Int
    
    
    var body: some View
    {
        let watchedText = self.seasonEpisodes.episodesRow[episodeIndex].watched ? "Watched" : ""
        
        Button(action: {
            // although the change occurr on the observed object and it causes the SeasonScreen
            // to re render, and therefor re render this button view, that should receive the
            // new watched value - it doesn't work
            self.seasonEpisodes.episodesRow[episodeIndex].watched = !self.seasonEpisodes.episodesRow[episodeIndex].watched
            print("episode new watched value: \(self.seasonEpisodes.episodesRow[episodeIndex].watched)")
        }, label: {
            Text("\(self.seasonEpisodes.episodesRow[episodeIndex].name) \(watchedText)")
                .frame(width: 420, height: 224)
                .background(Color.blue)
                .foregroundColor(Color.white)
        })
            .buttonStyle(PlainButtonStyle())
            .padding()
    }
}

tapping on the buttons will produce these log lines that confirm the value is changing (although view is not re-render):

episode new watched value: false
episode new watched value: true
episode new watched value: false

and this code will work as expected and reflect the new changed value on the view just because i replaced the Episode entity from Class to Struct:

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        SeasonScreen()
    }
}

// model class that represents episode data
struct Episode: Identifiable  // <--- this is the only change to make it work
{
    let id: UUID
    let name: String
    var watched: Bool
    
    init(name: String, watched: Bool)
    {
        id = UUID()
        self.name = name
        self.watched = watched
    }
}

// dummy server api that returns the episodes array
struct ServerAPI
{
    static func getAllEpisodes(showId: String, seasonId: String, completion: @escaping([Episode]) -> ())
    {
        let dummyResponse: [Episode] = [Episode(name: "Episode 1", watched: true), Episode(name: "Episode 2", watched: false)]
        completion(dummyResponse)
    }
}

// class that holds (and publish changes on) the episodes array
class SeasonEpisodes: ObservableObject
{
    @Published var episodesRow: [Episode] = []

    init()
    {
        ServerAPI.getAllEpisodes(showId: "Friends", seasonId: "Season 2", completion: { episodes in
            self.episodesRow = episodes
        })
    }
}

// main screen view that contains episodes array object and observe changes on it
struct SeasonScreen: View
{
    @ObservedObject var seasonEpisodes: SeasonEpisodes = SeasonEpisodes()
    
    var body: some View
    {
        VStack
        {
            if(self.seasonEpisodes.episodesRow.count > 0)
            {
                ScrollView(.horizontal, showsIndicators: false)
                {
                    HStack(alignment: VerticalAlignment.center, spacing: 60.0, content: {
                        ForEach(self.seasonEpisodes.episodesRow.indices) { episodeIndex in
                            EpisodeButton(seasonEpisodes: self.seasonEpisodes, episodeIndex: episodeIndex)
                                .padding(40)
                        }
                    })
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
    }
}

// ui view to represent episode button
struct EpisodeButton: View
{
    @ObservedObject var seasonEpisodes: SeasonEpisodes
    let episodeIndex: Int
    
    
    var body: some View
    {
        let watchedText = self.seasonEpisodes.episodesRow[episodeIndex].watched ? "Watched" : ""
        
        Button(action: {
            // this time the new watched value reflects on the view
            self.seasonEpisodes.episodesRow[episodeIndex].watched = !self.seasonEpisodes.episodesRow[episodeIndex].watched
            print("episode new watched value: \(self.seasonEpisodes.episodesRow[episodeIndex].watched)")
        }, label: {
            Text("\(self.seasonEpisodes.episodesRow[episodeIndex].name) \(watchedText)")
                .frame(width: 420, height: 224)
                .background(Color.blue)
                .foregroundColor(Color.white)
        })
            .buttonStyle(PlainButtonStyle())
            .padding()
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.