Search code examples
swiftgenericsuikitprotocols

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


I am finding that in multiple UIViewControllers I am setting the access of various reusable views so they can update their constraints accordingly. I'd like to create a SuperClass UIViewController called ChallengeFlowViewController so that it can take care of managing the calls that update the axis for views which support axis updates. Therein lies the challenge. How can I define a method or computed property so that it can be overridden and the subclasses can retur any number of different views so long as each of those views conform to HasViewModel and their ViewModel type conforms to HasAxis.

The following implementation has axisSettables(), however the way it is implemented, it requires all the views returned be the same view type. I want variance in view type to be allowed so long as all of them fit the requirements.

Another issue, in the viewWillLayoutSubView method, I'm getting the error: 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 }
}



Solution

  • What prevents you from using a heterogenous array is the HasViewModel protocol, which has associated types, which forces you to use it as a generic argument for the axisSettables function, which leads to having to declare the return type as a homogenous array.

    If you'll want to be able to use heterogenous arrays, then you'll need to use a "regular" protocol that is complementary to the HasViewModel one. For example:

    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] {
            []
        }
    

    This way you also keep the concerns separated, as axisSettables should not care if the returning views have or not a view model, it should only care if the views can update their axis. At the implementation level, you can keep the HasViewModel protocol, if that helps in other areas, and just add conformance to AxisSettable.