Swift iOS Xcode中实现Material垂直步进表单,求布局示例
实现Material风格垂直步进表单的Swift示例
我明白你想要在iOS Swift项目里实现Material Design风格的垂直步进表单,官方确实没直接提供现成的实现,我来给你一个完整的布局和代码示例,你可以直接基于这个扩展。
核心思路
垂直步进表单的核心是将多个步骤内容组织在垂直滚动容器中,每个步骤包含状态指示器、标题、自定义内容区,再通过底部按钮控制步骤切换,同时用线条连接步骤来体现流程感。
1. 自定义步骤视图(StepView)
先创建一个可复用的StepView,封装每个步骤的UI元素和状态切换逻辑:
import UIKit class StepView: UIView { // 步骤指示器 private let indicatorView: UIView = { let view = UIView() view.layer.cornerRadius = 15 view.backgroundColor = .systemGray4 view.translatesAutoresizingMaskIntoConstraints = false return view }() // 步骤序号/完成图标 private let stepLabel: UILabel = { let label = UILabel() label.textColor = .white label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() // 步骤标题 private let titleLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.textColor = .label label.translatesAutoresizingMaskIntoConstraints = false return label }() // 自定义内容容器(供外部添加输入框等元素) let contentContainer: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() // 步骤间连接线条 private let lineView: UIView = { let view = UIView() view.backgroundColor = .systemGray4 view.translatesAutoresizingMaskIntoConstraints = false return view }() // 状态属性 var isCompleted: Bool = false { didSet { updateIndicatorStyle() } } var isCurrent: Bool = false { didSet { updateIndicatorStyle() } } init(stepNumber: Int, title: String) { super.init(frame: .zero) stepLabel.text = "\(stepNumber)" titleLabel.text = title setupLayout() updateIndicatorStyle() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupLayout() { addSubview(indicatorView) indicatorView.addSubview(stepLabel) addSubview(titleLabel) addSubview(contentContainer) addSubview(lineView) NSLayoutConstraint.activate([ indicatorView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), indicatorView.topAnchor.constraint(equalTo: topAnchor, constant: 16), indicatorView.widthAnchor.constraint(equalToConstant: 30), indicatorView.heightAnchor.constraint(equalToConstant: 30), stepLabel.centerXAnchor.constraint(equalTo: indicatorView.centerXAnchor), stepLabel.centerYAnchor.constraint(equalTo: indicatorView.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: indicatorView.trailingAnchor, constant: 16), titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), contentContainer.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), contentContainer.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), contentContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), contentContainer.bottomAnchor.constraint(equalTo: lineView.topAnchor, constant: -16), lineView.leadingAnchor.constraint(equalTo: indicatorView.centerXAnchor), lineView.topAnchor.constraint(equalTo: indicatorView.bottomAnchor), lineView.widthAnchor.constraint(equalToConstant: 2), lineView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } // 更新指示器和线条样式 private func updateIndicatorStyle() { if isCompleted { indicatorView.backgroundColor = .systemGreen stepLabel.text = "✓" } else if isCurrent { indicatorView.backgroundColor = .systemBlue stepLabel.textColor = .white } else { indicatorView.backgroundColor = .systemGray4 stepLabel.textColor = .white } lineView.backgroundColor = isCompleted || isCurrent ? .systemBlue : .systemGray4 } }
2. 表单控制器实现
创建控制器来管理所有步骤的布局、切换逻辑:
import UIKit class VerticalStepperFormViewController: UIViewController { private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() private let stackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 0 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() private let buttonStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = 16 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() private let backButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Back", for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium) button.isHidden = true button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) return button }() private let nextButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Next", for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium) button.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside) return button }() private var steps: [StepView] = [] private var currentStepIndex = 0 override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground setupNavigationBar() setupLayout() createSteps() updateStepStates() } private func setupNavigationBar() { title = "Vertical Stepper Form" navigationController?.navigationBar.prefersLargeTitles = true } private func setupLayout() { view.addSubview(scrollView) scrollView.addSubview(stackView) view.addSubview(buttonStackView) buttonStackView.addArrangedSubview(backButton) buttonStackView.addArrangedSubview(nextButton) NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: buttonStackView.topAnchor, constant: -16), stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), buttonStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), buttonStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), buttonStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16) ]) } // 创建示例步骤 private func createSteps() { // 步骤1:个人信息 let step1 = StepView(stepNumber: 1, title: "Personal Information") let nameField = UITextField() nameField.placeholder = "Enter your name" nameField.borderStyle = .roundedRect nameField.translatesAutoresizingMaskIntoConstraints = false step1.contentContainer.addSubview(nameField) NSLayoutConstraint.activate([ nameField.topAnchor.constraint(equalTo: step1.contentContainer.topAnchor), nameField.leadingAnchor.constraint(equalTo: step1.contentContainer.leadingAnchor), nameField.trailingAnchor.constraint(equalTo: step1.contentContainer.trailingAnchor), nameField.heightAnchor.constraint(equalToConstant: 44), nameField.bottomAnchor.constraint(equalTo: step1.contentContainer.bottomAnchor) ]) // 步骤2:联系方式 let step2 = StepView(stepNumber: 2, title: "Contact Details") let emailField = UITextField() emailField.placeholder = "Enter your email" emailField.borderStyle = .roundedRect emailField.keyboardType = .emailAddress emailField.translatesAutoresizingMaskIntoConstraints = false step2.contentContainer.addSubview(emailField) NSLayoutConstraint.activate([ emailField.topAnchor.constraint(equalTo: step2.contentContainer.topAnchor), emailField.leadingAnchor.constraint(equalTo: step2.contentContainer.leadingAnchor), emailField.trailingAnchor.constraint(equalTo: step2.contentContainer.trailingAnchor), emailField.heightAnchor.constraint(equalToConstant: 44), emailField.bottomAnchor.constraint(equalTo: step2.contentContainer.bottomAnchor) ]) // 步骤3:确认信息 let step3 = StepView(stepNumber: 3, title: "Confirm Information") let confirmLabel = UILabel() confirmLabel.text = "Please review your information before submitting." confirmLabel.numberOfLines = 0 confirmLabel.translatesAutoresizingMaskIntoConstraints = false step3.contentContainer.addSubview(confirmLabel) NSLayoutConstraint.activate([ confirmLabel.topAnchor.constraint(equalTo: step3.contentContainer.topAnchor), confirmLabel.leadingAnchor.constraint(equalTo: step3.contentContainer.leadingAnchor), confirmLabel.trailingAnchor.constraint(equalTo: step3.contentContainer.trailingAnchor), confirmLabel.bottomAnchor.constraint(equalTo: step3.contentContainer.bottomAnchor, constant: -16) ]) steps = [step1, step2, step3] steps.forEach { stackView.addArrangedSubview($0) } } // 更新所有步骤的状态 private func updateStepStates() { steps.enumerated().forEach { index, step in step.isCompleted = index < currentStepIndex step.isCurrent = index == currentStepIndex // 隐藏最后一个步骤的连接线条 if index == steps.count - 1 { step.lineView.isHidden = true } } backButton.isHidden = currentStepIndex == 0 nextButton.setTitle(currentStepIndex == steps.count - 1 ? "Submit" : "Next", for: .normal) } @objc private func backButtonTapped() { guard currentStepIndex > 0 else { return } currentStepIndex -= 1 updateStepStates() scrollToCurrentStep() } @objc private func nextButtonTapped() { if currentStepIndex == steps.count - 1 { // 这里添加表单提交逻辑 print("Form submitted successfully!") return } currentStepIndex += 1 updateStepStates() scrollToCurrentStep() } // 滚动到当前步骤位置 private func scrollToCurrentStep() { let step = steps[currentStepIndex] scrollView.scrollRectToVisible(step.frame, animated: true) } }
扩展说明
- 你可以根据需求给每个步骤添加任意UI元素,只需将元素添加到对应
StepView的contentContainer中即可。 - 样式上可以调整颜色、字体、间距等,匹配你的App设计风格。
- 如果需要表单验证,可以在
nextButtonTapped方法中添加当前步骤的验证逻辑,通过后再切换步骤。
内容的提问来源于stack exchange,提问作者Ragul Cse
相关产品推荐
相关产品推荐

