如何定义一个 returns 元素数组的函数,这些元素的类型可以不同,但​​都必须符合某些协议规则?

How to define a function that returns an array of elements which can vary in types but all must conform to certain protocol rules?

我发现在多个 UIViewController 中,我正在设置各种可重用视图的访问权限,以便它们可以相应地更新它们的约束。我想创建一个名为 ChallengeFlowViewController 的超类 UIViewController,以便它可以负责管理为支持轴更新的视图更新轴的调用。挑战就在这里。我如何定义一个方法或计算 属性 以便它可以被覆盖并且子类可以返回任意数量的不同视图,只要这些视图中的每一个都符合 HasViewModel 并且它们的 ViewModel 类型符合 HasAxis。

下面的实现有 axisSettables(),但是它的实现方式要求返回的所有视图都是相同的视图类型。我希望允许视图类型的差异,只要它们都符合要求即可。

另一个问题,在 viewWillLayoutSubView 方法中,我收到错误:Generic parameter 'T' could not be inferred


class ChallengeFlowViewController: UIViewController {

    /// override to ensure axises are set for views that adjust when rotated.
    /// Styles the views if they conform to
    func axisSettables<T: HasViewModel>() -> [T?] where T.ViewModel: HasAxis  {
        []
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        for view in axisSettables() {
            view?.setAxis()
        }
    }
}



protocol HasViewModel {

    associatedtype ViewModel
    var viewModel: ViewModel { get set }
    init()
}

extension HasViewModel {

    init(viewModel: ViewModel) {
        self.init()
        self.viewModel = viewModel
    }

    mutating func setAxis(
        from newAxis: NSLayoutConstraint.Axis = UIApplication.orientationAxis
    ) where ViewModel: HasAxis {
        guard viewModel.axis != newAxis else { return }
        viewModel.axis = newAxis
    }
}

extension UIApplication {

    /// This is orientation as it relates to constraints.
    enum ConstraintOrientation {
        /// portrait and portraitUpsideDown equal portrait
        case portrait

        /// landscapeleft and landscaperight equal landscape
        case landscape
    }

    /// Unfortunately `UIDevice.current.orientation` is unreliable in determining orientation.
    /// if you `print(UIDevice.current.orientation)` when the simulator or phone launches
    /// with landscape, you will find that the print value can sometimes print as portrait.
    /// if statusBarOrientation is unknown or nil the fallback is portrait, as it would be the safer orientation
    /// for people who have difficulty seeing, for example, if a view's landscape setting splits the views in half
    /// horizontally, the same would look narrow on portrait, wheras the portrait setting is more likely to span
    /// the width of the window.
    static var constraintOrientation: ConstraintOrientation {
        guard #available(iOS 13.0, *) else {
            return UIDevice.current.orientation.constraintOrientation
        }
        return statusBarOrientation == .landscapeLeft || statusBarOrientation == .landscapeRight ? .landscape : .portrait
    }

    @available(iOS 13.0, *)
    static var statusBarOrientation: UIInterfaceOrientation? {
        shared.windows.first(where: \.isKeyWindow)?.windowScene?.interfaceOrientation
    }

    static var orientationAxis: NSLayoutConstraint.Axis {
        constraintOrientation == .landscape ? .horizontal : .vertical
    }
}

protocol HasAxis {

    var axis: NSLayoutConstraint.Axis { get set }
}

extension HasAxis {
    var horizontal: Bool { axis == .horizontal }
    var vertical: Bool { axis == .vertical }
}


阻止你使用异构数组的是 HasViewModel 协议,它有关联的类型,这迫使你将它用作 axisSettables 函数的通用参数,这导致有将 return 类型声明为同构数组。

如果您希望能够使用异构数组,则需要使用与 HasViewModel 协议互补的“常规”协议。例如:

protocol AxisSettable {
    func setAxis()
}

class ChallengeFlowViewController: UIViewController {

    /// override to ensure axises are set for views that adjust when rotated.
    /// Styles the views if they conform to
    func axisSettables() -> [AxisSettable] {
        []
    }

这样你也可以将关注点分开,因为 axisSettables 不应该关心 returning 视图是否有视图模型,它应该只关心视图是否可以更新它们的轴.在实现级别,您可以保留 HasViewModel 协议,如果这对其他方面有帮助,只需添加对 AxisSettable.

的一致性

我最终使用了 HasViewModel 协议的一个可选函数。

protocol HasViewModel { 
    optional mutating func setAxis(from newAxis: NSLayoutConstraint.Axis)
}

现在我可以 return 符合 HasViewModel

的视图
/// Override me
var hasViewModelArray: [HasViewModel] {

}

然后我可以在 viewWillLayoutSubviews 方法中选择性地使用该方法。

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        hasViewModelArray.forEach(\.setAxis?())
    }