如何向 SwiftUI 表单添加更多行/项目?

Posted

技术标签:

【中文标题】如何向 SwiftUI 表单添加更多行/项目?【英文标题】:How do you add more rows/items to a SwiftUI form? 【发布时间】: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))


【问题讨论】:

【参考方案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-data 和 firebase.google.com/docs/firestore/manage-data/transactions)

以上是关于如何向 SwiftUI 表单添加更多行/项目?的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:我可以向 TabView 添加更多视图,然后是选项卡项吗?

如何添加“加载更多”单元格,按下该单元格时会向表格视图添加更多行?

Symfony twig 如何将类添加到表单行

SwiftUI - 如何在表单中添加“字母部分”和字母跳线?

允许用户在需要输入更多内容时向表单添加更多文本框字段

如何在 SwiftUI 中的表单段上添加填充