如何向 SwiftUI 表单添加更多行/项目? 应用模型视图模型意见

问题描述

我是一名新手编码员,正在学习 SwiftUI。

图片显示我遇到的问题:

enter image description here

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

我使用 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",style: .continuous))
            
        }
        .padding(.horizontal)
        .navigationBarTitle("Add Injury Exercise")
        .navigationBarHidden(true)
    }
    .background(
        VisualEffectBlur()
            .edgesIgnoringSafeArea(.all))
}
}

解决方法

您可以使用 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 获取绑定。

,

要实现屏幕截图中显示的外观和感觉,您可以将 ListInsetGroupedListStyle 结合使用:

Screenshot of the result

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

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

这是我在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,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 中的顶部存储方式取决于您的整体数据模型。您能否确认在您的应用中,您将进行大量锻炼?