ios 在不同视图控制器之间的导航栏下显示固定进度

bksxznpy  于 2022-11-19  发布在  iOS
关注(0)|答案(1)|浏览(129)

我有一个很长的登记表,包括4个步骤(内容不相关),以下是模型:

我的问题是我需要在多个视图之间共享一个进度视图。这个视图应该有一个增长的动画。用UIKit做这个事情的正确和干净的方法是什么?我应该用这个进度创建一个自定义的导航栏吗?或者以某种方式使用子控制器?
我一直在这里搜索,但我发现的其他问题是非常古老的(像7年前),我不知道是否可以有更好的解决方案。
多谢了!

0lvr5msh

0lvr5msh1#

有多种方法可以做到这一点...
一种常见的方法是将“进度视图”设置为导航栏的标题视图--但这不会将其显示在导航栏的 * 下方 *。
因此,另一种方法是子类化UINavigationController,并添加一个“进度视图”作为子视图,然后实现willShow viewController和/或didShow viewController来更新进度。
举个简单的例子,假设我们有4个“步骤”要导航到...
我们将从定义一个“基本”视图控制器开始,它包含我们的自定义导航控制器类将使用的两个属性:

class MyBaseVC: UIViewController {

    // this will be read by ProgressNavController
    //  to calculate the "progress percentage"
    public let numSteps: Int = 4
    
    // this will be set by each MyBaseVC subclass,
    //  and will be read by ProgressNavController
    public var myStepNumber: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // maybe some stuff common to the "step" controllers
    }
    
}

然后,每个“step”控制器将是MyBaseVC的子类,并将设置其“step number”(沿着该控制器特定的任何其他内容):

class Step1VC: MyBaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myStepNumber = 1
        
        // maybe some other stuff specific to this "step"
    }
    
}
class Step2VC: MyBaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myStepNumber = 2
        
        // maybe some other stuff specific to this "step"
    }
    
}
class Step3VC: MyBaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myStepNumber = 3
        
        // maybe some other stuff specific to this "step"
    }
    
}
class Step4VC: MyBaseVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        myStepNumber = 4
        
        // maybe some other stuff specific to this "step"
    }
    
}

然后我们可以像这样设置我们的自定义导航控制器类(它实际上并不像看起来那么复杂):

class ProgressNavController: UINavigationController, UINavigationControllerDelegate {
    
    private let outerView = UIView()
    private let innerView = UIView()
    private var pctConstraint: NSLayoutConstraint!
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    private func commonInit() {
        
        // for this example, we're using a simple
        //  green view inside a red view
        //  as our "progress view"
        
        // we set it up here, but we don't add it as a subview
        //  until we navigate to a MyBaseVC
        
        // we know we're setting
        //  outerView height to 20
        //  innerView height to 12 (4-points top/bottom "padding")
        // so let's round the ends of the innerView
        innerView.layer.cornerRadius = 8.0
        
        outerView.backgroundColor = .systemRed
        innerView.backgroundColor = .systemGreen
        
        outerView.translatesAutoresizingMaskIntoConstraints = false
        innerView.translatesAutoresizingMaskIntoConstraints = false
        
        outerView.addSubview(innerView)
        
        // initialize pctConstraint
        pctConstraint = innerView.widthAnchor.constraint(equalTo: outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)

        NSLayoutConstraint.activate([
            innerView.topAnchor.constraint(equalTo: outerView.topAnchor, constant: 4.0),
            innerView.leadingAnchor.constraint(equalTo: outerView.leadingAnchor, constant: 4.0),
            innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor, constant: -4.0),
            pctConstraint,
        ])

        self.delegate = self
        
    }
    
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        
        // if the next VC to show
        //  is a MyBaseVC subclass
        if let _ = viewController as? MyBaseVC {
            
            // add the "progess view" if we're coming from a non-MyBaseVC controller
            if outerView.superview == nil {
                
                view.addSubview(outerView)
                
                let g = view.safeAreaLayoutGuide
                NSLayoutConstraint.activate([
                    
                    outerView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 4.0),
                    outerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                    outerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                    outerView.heightAnchor.constraint(equalToConstant: 20.0),
                    
                ])
                
                // .alpha to Zero so we can "fade it in"
                outerView.alpha = 0.0
                
                // we just added the progress view,
                //  so we'll let didShow "fade it in"
                //  and update the progress width
                
            } else {
                
                self.updateProgress(viewController)

            }
            
        } else {
            
            if outerView.superview != nil {
                // we want to quickly "fade-out" and remove the "progress view"
                //  if the next VC to show
                //  is NOT a MyBaseVC subclass
                UIView.animate(withDuration: 0.1, animations: {
                    self.outerView.alpha = 0.0
                }, completion: { _ in
                    self.outerView.removeFromSuperview()
                    self.pctConstraint.isActive = false
                    self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
                    self.pctConstraint.isActive = true
                })
            }
            
        }
        
    }
    
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        
        // if the VC just shown
        //  is a MyBaseVC subclass
        //  AND
        //  outerView.alpha < 1.0 (meaning it was just added)
        if let _ = viewController as? MyBaseVC, outerView.alpha < 1.0 {
            self.updateProgress(viewController)
        }
        
        // otherwise, updateProgress() is called from willShow

    }
    
    private func updateProgress(_ viewController: UIViewController) {
        
        if let vc = viewController as? MyBaseVC {
            
            // update the innerView width -- the "progress"
            let nSteps: CGFloat = CGFloat(vc.numSteps)
            let thisStep: CGFloat = CGFloat(vc.myStepNumber)
            var pct: CGFloat = .leastNonzeroMagnitude
            
            // sanity check
            //  avoid error/crash if either values are Zero
            if nSteps > 0.0, thisStep > 0.0 {
                pct = thisStep / nSteps
            }
            
            // don't exceed 100%
            pct = min(pct, 1.0)
            
            // we can't update the multiplier directly, so
            //  deactivate / update / activate
            self.pctConstraint.isActive = false
            self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: pct, constant: -8.0)
            self.pctConstraint.isActive = true
            
            // if .alpha is already 1.0, this is effectively ignored
            UIView.animate(withDuration: 0.1, animations: {
                self.outerView.alpha = 1.0
            })
            
            // animate the "bar width"
            UIView.animate(withDuration: 0.3, animations: {
                self.outerView.layoutIfNeeded()
            })

        }

    }
    
}

因此,当我们导航到新控制器时:

  • 我们检查它是否是MyBaseVC的示例
  • 如果
  • 添加进度视图(如果尚未添加)
  • 从新控制器获取步骤编号
  • 更新进度
  • 如果不是
  • 删除进度视图

我提出了一个完整的例子,你可以检查和检查在这里:https://github.com/DonMag/ProgressNavController

相关问题