简体   繁体   English

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

[英]How do you add more rows/items to a SwiftUI form?

I'm a newbie coder and learning SwiftUI.我是一个新手编码器,正在学习 SwiftUI。

This image will show the problem I'm having:此图像将显示我遇到的问题: 在此处输入图像描述

When clicking the "Add Exercise" button, I'd like the rounded rectangle and contence to be repeated below.单击“添加练习”按钮时,我希望在下面重复圆角矩形和内容。

I'm using Firebase's Cloud Firestore to store this data.我正在使用 Firebase 的 Cloud Firestore 来存储这些数据。 How would the contence of the form be structured with the changing amounts of Exercises?随着练习数量的变化,表格的内容将如何构建?

Thanks, here's my basic code for the form setup:谢谢,这是我的表单设置基本代码:

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

You could use a ForEach and an array of Exercises.您可以使用ForEach和一组练习。
For instance, I'm gonna assume you have an Exercise model that contains something like the following:例如,我假设您有一个Exercise model,其中包含以下内容:

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

Your form component could contain the following (I removed the irrelevant parts):您的表单组件可能包含以下内容(我删除了不相关的部分):

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

Upon adding a new Exercise to your array, SwiftUI will redraw your component with the new line containing an empty Exercise .将新的Exercise添加到您的数组后,SwiftUI 将使用包含空Exercise的新行重新绘制您的组件。 Because we use indexes, we are able to get a binding for the correct Exercise in our array.因为我们使用索引,所以我们能够在我们的数组中获得正确Exercise的绑定。

To achieve the look and feel you showed in your screenshot, you can use a List with InsetGroupedListStyle :要实现您在屏幕截图中显示的外观,您可以使用带有InsetGroupedListStyleList

结果截图

Each of the sections can then be represented using a Section .然后可以使用Section来表示每个部分。 As Section allows us to insert views into the header and footer, we can insert TextField s into the headers to allow the user to enter the workout title, etc. Likewise, the button for adding a new exercise can go into the footer .由于Section允许我们在 header 和页脚中插入视图,我们可以在页眉中插入TextField以允许用户输入锻炼标题等。同样,添加新练习的按钮可以 go 到footer中。

Now, making the data model editable in-place is a bit more challenging, especially if you want to connect it to a backend service like Firebase.现在,使数据 model 可就地编辑更具挑战性,特别是如果您想将其连接到像 Firebase 这样的后端服务。 We need to convert our structs into ObservableObject s so we can bind the TextField s to them.我们需要将我们的结构体转换为ObservableObject s,以便我们可以将TextField s 绑定到它们。

This is something I showed in my article series about MakeItSo .这是我在关于MakeItSo文章系列中展示的内容。

For your app, this would look as follows:对于您的应用程序,这将如下所示:

The app该应用程序

import SwiftUI

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

The Models模型

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"),
]

The ViewModels视图模型

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

The views观点

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

How top store this in Firestore depends on your overall data model.如何将其存储在 Firestore 中取决于您的整体数据 model。 Can you confirm that in your app, you're going to deal with a whole bunch of workouts?您能否确认在您的应用程序中,您将处理一大堆锻炼?

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

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