Rightpoint / Anchorage

A collection of operators and utilities that simplify iOS layout code.
MIT License
627 stars 46 forks source link

Constructing an `AnchorPair` outside of Anchorage will be impossible #59

Closed rlwimi closed 6 years ago

rlwimi commented 6 years ago

To extend Anchorage in an application-specific way, one might create convenience methods to return an AnchorPair, make new AnchorGroupProviding conformers, or extend AnchorGroupProviding to specify new groups. These approaches start with being able to construct an AnchorPair outside of the Anchorage module.

I'm not sure this qualifies as the most "practical" example, but here is my real-world use case. I've implemented a vertical columns-based layout system in order to match the way my designer works, to support handling devices with different widths by scaling the UI rather than implementing (and designing) a responsive design or doing nothing.

So for example, I'd like to be express a layout like

titleLabel.horizontalAnchors == AnchorPair(first: columnOne.leadingAnchor, second: columnTwo.trailingAnchor)

That's pretty raw, and you might imagine a convenience method to make this

titleLabel.horizontalAnchors == anchorsSpanningColumns(.one, .two)

The AnchorPair initializer is marked internal, so I can't use it directly. I've hobbled by with a hack of replicating the internal initializer with slightly different parameter labels, intending to bring this very question in front of this group. With Swift 4.1 and SE-0189, this is now explicitly deprecated.

What is the reasoning behind disallowing AnchorPair construction outside of Anchorage? Is there a downside or complication to making its initializer public that I'm not seeing?

Obviously, I have alternatives. I can always use Anchorage as the implementation for layout convenience methods, but integration with the DSL is so much nicer than mixing it with method calls.

I'd love to hear what you think. In the event that there aren't objections – maybe this initializer was just very conservatively marked internal – I'll post a PR. In that case, I'd have some organizational questions around the Internal.swift file.

ZevEisenberg commented 6 years ago

My guess is that it wasn't marked as public because we didn't see a use case, but yours is definitely intriguing. @jvisenti should make the call, but this seems like a reasonable request to me.

jvisenti commented 6 years ago

@rlwimi @ZevEisenberg I believe AnchorPair is essentially an internal type because exposing the initializer publicly could allow for unsafe usage of the API. Consider this function:

func == <T, U>(lhs: AnchorPair<T, U>, rhs: AnchorPair<T, U>) -> ConstraintPair

If you were to construct two AnchorPair<NSLayoutAnchor, NSLayoutAnchor>, you could mix NSLayoutXAxisAnchor and NSLayoutYAxisAnchor however you wanted, which would result in a crash if you attempted to use this operator on mismatched types, i.e. trying to constraint an XAxisAnchor to a YAxisAnchor. The current API forbids this by construction.

That said, the existence of a valid use case may outweigh the pretty minor risk that comes with it. Happy to take a look at a PR if you have something put together already, and see how it could fit in.

ZevEisenberg commented 6 years ago

I was just reading through some code that I wrote in a project, and found what I believe is a valid use case:

var safeCenterAnchors: AnchorPair<NSLayoutXAxisAnchor, NSLayoutYAxisAnchor> {
    if #available(iOS 11.0, *) {
        return AnchorPair(first: safeAreaLayoutGuide.centerXAnchor, second: safeAreaLayoutGuide.centerYAnchor)
    }
    else {
        return centerAnchors
    }
}
rlwimi commented 5 years ago

Thanks, everyone! Much appreciated.