SwiftUI:如何遍历可绑定对象的数组?

时间:2019-07-23 10:20:07

标签: swift binding swiftui

我正在尝试学习SwiftUI,以及绑定如何工作。

我有可以运行的代码,其中显示了项目列表。点击一个项目后,对该项目的绑定将传递给子视图:

struct ProjectsView: View {
  @ObjectBinding var state: AppState
  @State var projectName: String = ""

  var body: some View {
    NavigationView {
      List {
        ForEach(0..<state.projects.count) { index in
          NavigationLink(destination: ProjectView(project: self.$state.projects[index])) {
            Text(self.state.projects[index].title)
          }
        }
      }
      .navigationBarTitle("Projects")
    }
  }
}

子视图,我在其中使用绑定对项目进行变异:

struct ProjectView: View {
  @Binding var project: Project
  @State var projectName: String = ""

  var body: some View {
    VStack {
      Text(project.title)
      TextField(
        $projectName,
        placeholder: Text("Change project name"),
        onCommit: {
          self.project.title = self.projectName
          self.projectName = ""
      })
      .padding()
    }
  }
}

但是,我宁愿在不使用索引的情况下遍历project数组(因为我想学习,并且更容易阅读),但是不确定如何将绑定传递给单个项目。我这样尝试过,但是后来我无法访问project.title,因为它是绑定,而不是字符串。

ForEach($state.projects) { project in
  NavigationLink(destination: ProjectView(project: project)) {
    Text(project.title)
  }
}

我该如何实现?

4 个答案:

答案 0 :(得分:1)

我发现这可行: 在您的AppState中,添加项目时,请注意其更改:

import Combine

class AppState: ObservableObject {
  @Published var projects: [Project]
  var futures = Set<AnyCancellable>()

  func addProject(project: Project) {
    project.objectWillChange
      .sink {_ in
        self.objectWillChange.send()
      }
     .store(in: &futures)
  }

  ...
}

如果您需要在外部视图中为项目var创建绑定,请按照以下步骤操作:

func titleBinding(forProject project: Project) -> Binding<String> {
    Binding {
        project.title
    } set: { newValue in
        project.title = newValue
    }
}

不过,如果要将项目传递到另一个视图中,则不需要它。

答案 1 :(得分:0)

我对您遇到的相同问题有些困惑,我找到了部分解决方案。但是首先我要指出,用ForEach(0..<state.projects.count) { index in ... }遍历索引不是一个好主意,因为index是一个Int,不符合Identifiable。因此,当阵列发生变化时,UI不会更新,并且您会在控制台中看到警告。

我的解决方案在创建state.projects并使用ProjectView获取项目元素的可绑定形式时直接访问firstIndex(of:)数组。这有点棘手,但据我所知,它使其更像SwiftUI-y。

ForEach(state.projects) { project in
  NavigationLink(destination: ProjectView(project: self.$state.projects[self.state.projects.firstIndex(of: project)!]))) {
    Text(project.title)
  }
}

答案 2 :(得分:0)

不幸的是,目前看来这还不可能。实现此目的的唯一方法如下:

ForEach(state.projects.indices) { index in
    NavigationLink(destination: ProjectView(project: state.projects[index])) {
        Text(state.projects[index].title)
    }
} 

注意:我没有编译这段代码。这只是给您如何做的手势。即使用Index类型而不是Int类型的use和index。

答案 3 :(得分:0)

注意:我使用Xcode 11.2,@ ObjectBinding已被废弃(因此您需要更新以验证以下代码)。

我询问了模型,因为它可能对方法很重要。对于类似的功能,我更喜欢Combine的ObservableObject,因此模型不是引用值类型。

这是我针对您的情况进行调整的方法。它不完全符合您的要求,因为ForEach需要一些序列,但是您尝试使用不受支持的类型来填充它。

无论如何,您可能在下面将其视为替代项(它是无索引的)。它是完整的模块,因此您可以将其粘贴到Xcode 11.2中并在预览中进行测试。希望这会有所帮助。

预览:

simple iteration preview

解决方案:

import SwiftUI
import Combine

class Project: ObservableObject, Identifiable {
    var id: String = UUID().uuidString
    @Published var title: String = ""

    init (title: String) {
        self.title = title
    }
}

class AppState: ObservableObject {
    @Published var projects: [Project] = []
    init(_ projects: [Project]) {
        self.projects = projects
    }
}

struct ProjectView: View {
  @ObservedObject var project: Project
  @State var projectName: String = ""

  var body: some View {
    VStack {
      Text(project.title)
      TextField("Change project name",
        text: $projectName,
        onCommit: {
          self.project.title = self.projectName
          self.projectName = ""
      })
      .padding()
    }
  }
}

struct ContentView: View {
    @ObservedObject var state: AppState = AppState([Project(title: "1"), Project(title: "2")])
    @State private var refreshed = false

    var body: some View {
        NavigationView {
            List {
                ForEach(state.projects) { project in
                  NavigationLink(destination: ProjectView(project: project)) {
                    // !!!  existance of .refreshed state property somewhere in ViewBuilder
                    //      is important to inavidate view, so below is just a demo
                    Text("Named: \(self.refreshed ? project.title : project.title)")
                  }
                  .onReceive(project.$title) { _ in
                        self.refreshed.toggle()
                    }
                }
            }
            .navigationBarTitle("Projects")
            .navigationBarItems(trailing: Button(action: {
                self.state.projects.append(Project(title: "Unknown"))
            }) {
                Text("New")
            })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}