简体   繁体   English

如何更改 Swiftui 中的数组项

[英]How to change item of array in Swiftui

How to change item of List in Swift?如何更改 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)可观察到的 Object(不工作)直通(不工作)

this image depict my issue这张图片描述了我的问题

You have to provide a Binding .您必须提供一个Binding Eg:例如:

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)    

ObservableObject should work if used correctly, the core concept of SwiftUI is having a single source of truth.如果使用正确, ObservableObject应该可以工作,SwiftUI 的核心概念是具有单一的事实来源。 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> .这就是Binding的用武之地,但是如果您遍历一个列表,您会得到类型为Person的值,而不是所需的Binding<Person> You may use indexes through the iteration and pass it to TextForm so it can get the original array.您可以通过迭代使用索引并将其传递给TextForm以便它可以获取原始数组。

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.您必须使用@ObservedObject包装器在视图中观看。

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.并让TextForm的索引结束 viewmodel 实例。

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)
        }
    }
}

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,在 ObservableObject 解决方案中几乎没有人写过的一件事 - 数组中的项目最好是 struct 而不是 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当结构成员 (var) 更改其值时,它被视为对包含结构的更改,对包含结构(数组元素)的更改被视为对整个数组的更改(因此对已发布对象的更改) - 这个小改动会导致视图重新渲染

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)所以长话短说,这里有 2 个完全工作的代码示例,一个不工作(其中 Episode 是 Class),另一个工作(其中 Episode 是 Struct)

this code will not update the View as expected only because the Episode entity is defined as Class:此代码不会按预期更新视图,因为 Episode 实体被定义为 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:并且此代码将按预期工作并在视图上反映新的更改值,因为我将 Episode 实体从 Class 替换为 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()
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM