【问题标题】:How do you add more rows/items to a SwiftUI form?如何向 SwiftUI 表单添加更多行/项目?
【发布时间】:2021-01-25 10:48:56
【问题描述】:

我是一名新手程序员,正在学习 SwiftUI。

这张图片将显示我遇到的问题:

点击“添加练习”按钮时,我希望圆角矩形和内容在下面重复。

我正在使用 Firebase 的 Cloud Firestore 来存储这些数据。随着习题数量的变化,表格的内容如何构成?

谢谢,这是我设置表单的基本代码:

    var body: some View {
    ScrollView {
        VStack (alignment: .leading, spacing: 4) {
            Text("Injury Exercises")
                .font(.largeTitle)
                .bold()
        }.frame(maxWidth: .infinity, alignment: .leading)
        .padding()
        
        VStack (spacing: 16){
            
            TextField("Workout Title (optional)", text: $text)
                .autocapitalization(.words)
                .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                .lineLimit(1)
            TextField("Add Warmup", text: $text)
                .font(.subheadline)
                .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
            
            VStack{
                TextField("Exercise Title (required)", text: $text)
                    .autocapitalization(.words)
                    .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                    .lineLimit(1)
                
                TextField("Sets, Reps, Tempo, Rest etc.", text: $text)
                    .font(.subheadline)
                    .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
            }
            .padding(8)
            .foregroundColor(Color("card4"))
            .background(Color.white).opacity(0.8)
            .clipShape(RoundedRectangle(cornerRadius: 16, style: /*@START_MENU_TOKEN@*/.continuous/*@END_MENU_TOKEN@*/))
            
            HStack {
                Image(systemName: "plus")
                Text("Exercise")
            }
            .font(.subheadline)
            .padding(8)
            .foregroundColor(Color("card4"))
            .background(Color.white).opacity(0.8)
            .clipShape(Capsule())
            
            TextField("Add Cooldown", text: $text)
                .font(.subheadline)
                .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
            
        }
        .padding(.horizontal)
        .navigationBarTitle("Add Injury Exercise")
        .navigationBarHidden(true)
    }
    .background(
        VisualEffectBlur()
            .edgesIgnoringSafeArea(.all))
}
}

【问题讨论】:

    标签: swift firebase forms google-cloud-firestore swiftui


    【解决方案1】:

    要实现您在屏幕截图中显示的外观,您可以使用ListInsetGroupedListStyle

    然后可以使用Section 表示每个部分。由于Section 允许我们在页眉和页脚中插入视图,我们可以在页眉中插入TextFields 以允许用户输入锻炼标题等。同样,添加新练习的按钮可以进入footer.

    现在,使数据模型可就地编辑更具挑战性,尤其是当您想将其连接到 Firebase 等后端服务时。我们需要将我们的结构体转换为ObservableObjects,以便我们可以将TextFields 绑定到它们。

    这是我在article series 中展示的关于MakeItSo 的内容。

    对于您的应用,如下所示:

    应用程序

    import SwiftUI
    
    @main
    struct SO65883241App: App {
      var viewModel = InjuryExercisesViewModel(exercises: sampleExercises)
      var body: some Scene {
        WindowGroup {
          InjuryExercisesScreen(viewModel: viewModel)
        }
      }
    }
    

    模型

    struct Exercise: Identifiable {
      var id = UUID().uuidString
      var title: String
      var details: String
    }
    
    let sampleExercises = [
      Exercise(title: "Deadlift", details: "1-3-2"),
      Exercise(title: "Squats", details: "3-3-2"),
      Exercise(title: "Push-ups", details: "20-10-10"),
    ]
    

    视图模型

    class InjuryExercisesViewModel: ObservableObject {
      @Published var title: String = ""
      @Published var warmUp: String = ""
      @Published var coolDown: String = ""
    
      @Published private var exercises = [Exercise]()
      @Published var exerciseViewModels = [ExerciseViewModel]()
      
      private var cancellables = Set<AnyCancellable>()
      
      init(exercises: [Exercise]) {
        $exercises.map { exercises in
          exercises.map { exercise in
            ExerciseViewModel(id: exercise.id, title: exercise.title, details: exercise.details)
          }
        }
        .assign(to: \.exerciseViewModels, on: self)
        .store(in: &cancellables)
        
        self.exercises = exercises
      }
      
      func addNewExercise() {
        exercises.append(Exercise(title: "", details: ""))
      }
    }
    
    class ExerciseViewModel: ObservableObject, Identifiable{
      var id: String
      @Published var title: String
      @Published var details: String
      
      init(id: String = UUID().uuidString, title: String, details: String) {
        self.id = id
        self.title = title
        self.details = details
      }
    }
    

    意见

    import SwiftUI
    import Combine
    
    
    struct ExerciseRow: View {
      @ObservedObject var exerciseViewModel: ExerciseViewModel
      var body: some View {
        TextField("Exercise Title (required)", text: $exerciseViewModel.title)
        TextField("Sets, Reps, Tempo, Rest, etc.", text: $exerciseViewModel.details)
      }
    }
    
    struct InjuryExercisesScreen: View {
      @Environment(\.presentationMode) var presentationMode
      @StateObject var viewModel: InjuryExercisesViewModel
      
      var addExerciseButton: some View {
        HStack {
          Spacer()
          HStack {
            Image(systemName: "plus")
            Text("Exercise")
          }
          .onTapGesture { viewModel.addNewExercise() }
          .font(.headline)
          .padding(12)
          .foregroundColor(Color(UIColor.systemPurple))
          .background(Color(UIColor.secondarySystemGroupedBackground))
          .clipShape(Capsule())
          Spacer()
        }
        .padding()
      }
      
      func dismiss() {
        self.presentationMode.wrappedValue.dismiss()
      }
      
      func save() {
        dump(self.viewModel.exerciseViewModels[0])
        self.presentationMode.wrappedValue.dismiss()
      }
      
      var cancelButton: some View {
        Button(action: dismiss) {
          Text("Cancel")
        }
      }
      
      var doneButton: some View {
        Button(action: save) {
          Text("Done")
        }
      }
      
      var body: some View {
        NavigationView {
          List {
            Section(header: TextField("Workout Title (optional)", text: $viewModel.title)) {
            }
            Section(header: TextField("Add Warmup", text: $viewModel.warmUp)) {
            }
            Section(header: Text("Exercises"), footer: addExerciseButton) {
              ForEach(viewModel.exerciseViewModels) { exerciseViewModel in
                ExerciseRow(exerciseViewModel: exerciseViewModel)
              }
            }
            Section(header: TextField("Add Cooldown", text: $viewModel.coolDown)) {
            }
          }
          .listStyle(InsetGroupedListStyle())
          .navigationTitle("Injury Exercises")
          .navigationBarItems(leading: cancelButton, trailing: doneButton)
        }
      }
    }
    
    struct InjuryExercisesScreen_Previews: PreviewProvider {
      static var viewModel = InjuryExercisesViewModel(exercises: sampleExercises)
      static var previews: some View {
        Group {
          InjuryExercisesScreen(viewModel: viewModel)
            .preferredColorScheme(.dark)
          InjuryExercisesScreen(viewModel: viewModel)
            .preferredColorScheme(.light)
          Text("Parent View")
            .sheet(isPresented: .constant(true)) {
              InjuryExercisesScreen(viewModel: viewModel)
            }
        }
      }
    }
    

    如何将其存储在 Firestore 中取决于您的整体数据模型。您能否确认在您的应用中,您将处理一大堆锻炼?

    【讨论】:

      【解决方案2】:

      您可以使用 ForEach 和一组练习。
      例如,我假设您有一个 Exercise 模型,其中包含以下内容:

      struct Exercise: Identifiable, Hashable {
        var id: String
        var title: String
        var description: String
      }
      

      您的表单组件可能包含以下内容(我删除了不相关的部分):

      struct Formmm: View {
          @State private var exercises: [Exercise]
      
          var body: some View {
              ScrollView {
                  // snip
      
                  ForEach(exercises.indices, id: \.self) { index in
                      VStack{
                          TextField("Exercise Title (required)", text: $exercises[index].title)
                              .autocapitalization(.words)
                              .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                              .lineLimit(1)
      
                          TextField("Sets, Reps, Tempo, Rest etc.", text: $exercises[index].description)
                              .font(.subheadline)
                              .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                      }
                  }
      
                  Button {
                      exercises.append(Exercise(title: "", description: ""))
                  } label: {
                      HStack {
                          Image(systemName: "plus")
                          Text("Exercise")
                      }
                      .font(.subheadline)
                      .padding(8)
                      .foregroundColor(Color("card4"))
                      .background(Color.white).opacity(0.8)
                      .clipShape(Capsule())
                  }
      
                  // snip
              }
          }
      }
      

      在向您的数组添加新的Exercise 后,SwiftUI 将使用包含空Exercise 的新行重绘您的组件。因为我们使用索引,所以我们能够在我们的数组中获得正确的Exercise 的绑定。

      【讨论】:

      • 嗨,非常有用,谢谢。我能够实现您将空“练习”添加到“练习”数组的方法。但是,我正在努力从 Firebase 保存和查询。我可以将单个“练习”添加到 Firebase 集合中,然后查询“练习”集合。我不确定如何将包含多个“练习”的数组添加到 Firebase 集合,然后查询整个数组。如果您可以对此进行扩展,将不胜感激。
      • 我以前从未使用过 Firebase,但快速浏览一下文档,我最好的猜测是您应该拥有一个 exercises 集合并获得对该集合的引用。要阅读所有练习(未过滤),您可以使用 reference.getDocuments(),要编写多个元素,您可能需要查看批处理操作(来源:firebase.google.com/docs/firestore/query-data/get-datafirebase.google.com/docs/firestore/manage-data/transactions
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多