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 }
}
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
.