You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:33:16