Code Monkey home page Code Monkey logo

square / paralayout Goto Github PK

View Code? Open in Web Editor NEW
771.0 13.0 34.0 690 KB

Paralayout is a set of simple, useful, and straightforward utilities that enable pixel-perfect layout in iOS. Your designers will love you.

Home Page: https://medium.com/square-corner-blog/introducing-paralayout-d5ac09e93fb0

License: Apache License 2.0

Ruby 0.33% Swift 98.39% Objective-C 0.34% Shell 0.38% Starlark 0.55%
layout ios typesetting flexbox pixel-perfect uikit swift

paralayout's Introduction

Paralayout

CI Status Carthage Compatibility Version License Platform

Paralayout is a set of simple, useful, and straightforward utilities that enable pixel-perfect layout in iOS. Your designers will love you.

Getting Started

CocoaPods

To install Paralayout via CocoaPods, add the following to your Podfile:

pod 'Paralayout'
Swift Package Manager

To install Paralayout via Swift Package Manager, add the following to your Package.swift:

dependencies: [
    .package(name: "Paralayout", url: "https://github.com/square/Paralayout.git", from: "1.0.0"),
]
Bazel

To install Paralayout via Bazel, add the following to your MODULE.bazel:

bazel_dep(name = "paralayout", version = "1.0.0")
Carthage

To install Paralayout via Carthage, add the following to your Cartfile:

github "Square/Paralayout"

Usage

Paralayout is a set of à la carte utilities, of which you can use as much or as little functionality as you like.

View Sizing

The first basic set of layout utilities is around sizing views. Paralayout extends UIView.sizeThatFits(_:) and UIView.sizeToFit() to include constraints, which gives you more control over how your subviews are sized.

For example, say you have a header bar you want to always be the full width of your container view, but use its ideal height (its sizeThatFits(_:)) clamped to the height of the container. You can easily achieve this in a single method call by combining the .fixedWidth and .maxHeight constraints.

headerBar.sizeToFit(
    bounds.size,
    constraints: [.fixedWidth, .maxHeight]
)

View Alignment

Once you have your subviews sized, the next basic action you often take is aligning your subviews. Paralayout provides a powerful set of alignment methods to align one view to another.

For example, you'll commonly want to align one subview to another subview with a certain amount of spacing between them. Let's say we want our first subview to be 16 pt beneath our second subview.

firstSubview.align(
    .topCenter,
    with: secondSubview,
    .bottomCenter,
    verticalOffset: 16
)

Alignments are automatically snapped to the nearest pixel, saving you from fuzzy edges on your views due to bad layout math.

There are also conveniences for aligning views to their superview, one of the most common alignment scenarios. For example, we can center a subview in our superview using this simple call:

someSubview.align(withSuperview: .center)

Paralayout also supports more complex alignment concepts for specialized use cases, such as aligning a label by its first line of text.

label.firstLineAlignmentProxy.align(
    .leftCenter,
    with: icon,
    .rightCenter,
    horizontalOffset: 8
)

View Distribution

In addition to simplifying the math for sizing and aligning views, Paralayout also builds layout abstractions on top of these simple concepts. View distribution solves the very common scenario of distributing views across a view, on either the vertical or horizontal axis.

For example, we can distribute a series of views within a container view. Here we'll put 16 pt of space between each subview and distribute any additional space equally above and below the subviews.

containerView.applyVerticalSubviewDistribution(
    [
        1.flexible,
        titleLabel,
        16.fixed,
        bodyLabel,
        16.fixed,
        actionButton,
        1.flexible,
    ]
)

AspectRatio

Working with aspect ratios has traditionally involved some easy-to-mess-up math, but Paralayout's AspectRatio type makes it a breeze. Create an aspect ratio from any size, rect, or width/height value, and use it to compute pixel-snapped frame rectangles.

videoPlayer.frame = AspectRatio.widescreen.rect(toFit: bounds, at: .topCenter, in: view)

Interpolations

Get the math right for multi-phase layout transitions and animations without tearing your hair out! Under the hood, Paralayout's Interpolation type is simply a value between 0 and 1, but it makes computing that value, and deriving a new value from it, effortless.

// Determine how far we are into the transition of collapsing the header.
let headerCollapseAmount = Interpolation(of: header.bounds.height, from: maxHeaderHeight, to: minHeaderHeight)

// The icon shrinks to half its usual size...
let avatarSize = headerCollapseAmount.interpolate(from: 80, to: 40)
avatar.bounds.size = CGSize(width: avatarSize, height: avatarSize)

// ...as it completely fades out.
avatar.alpha = headerCollapseAmount.interpolate(from: 1, to: 0)

UIFont Extensions

The extra space within a label above the "cap height" and below the "baseline" of its font is deterministic but non-obvious, especially at different scale factors. The simple LabelCapInsets value type encapsulates that logic.

Requirements

  • iOS 12.0 or later
  • Xcode 12.0 or later
  • Swift 5.0

Contributing

We’re glad you’re interested in Paralayout, and we’d love to see where you take it. Please read our contributing guidelines prior to submitting a pull request.

paralayout's People

Contributors

aliceaponasko avatar dfed avatar dnkoutso avatar efirestone avatar gizmosachin avatar jhollida24 avatar luispadron avatar nickentin avatar nightlynexus avatar pwesten avatar sedcash avatar soaurabhk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

paralayout's Issues

Add utilities for working with angles

Working with angles is a fairly common scenario when building UI, especially around things like animations and gesture-based interactions. We should add some utilities that make working with angles easier.

any update to swift 4?

i get some errors like

/Volumes/Swap/dev/_examples/Paralayout/Paralayout/Label.swift:448:34: Cannot subscript a value of type '[NSAttributedStringKey : Any]' with an index of type 'String'

Remove Hairline from the framework

Hairline and its associated utilities (e.g. hairlineWidth) don't really belong in Paralayout. The hairline concept is specific to a certain style of UI and therefore doesn't classify as a generic layout utility, which is what Paralayout is meant to be.

I propose that we remove it from Paralayout entirely, without replacement.

Build fails when building with Xcode 12

Building with Xcode 12 currently fails because the switches in UIDevice.isPhone and UIDevice.isPad are not exhaustive, since the introduction of UIUserInterfaceIdiom.mac in iOS 14.

Remove the concept of custom positioning

Paralayout currently supports custom positioning via conformance to the AlignmentPositionAdjusting protocol. This can lead to some less-than-obvious behavior for consumers, which goes against the core principles behind Paralayout. As far as I know, this feature has very little usage outside of Label, which we have already proposed removing (#19), while adding complexity to our alignment logic. I propose that we remove the concept of custom positioning from the framework entirely.

If my assumption is wrong and you are using custom positioning, please speak up. I am happy to discuss leaving it in there, either in its current form or with some changes, if it's still providing significant value.

Paralayout positioning methods don't trigger updates to `safeAreaInsets`

Paralayout's alignment methods use a UIView extension property named untransformedFrame that sets the size and origin of views using CALayer's bounds and position properties. UIView.safeAreaInsets do not get recalculated appropriately when doing this. This feels like a bug in CoreAnimation, but it causes unfortunate layout issues when using this framework.

I'm trying to work through this in https://github.com/squareup/cash-ios/pull/29720 . I'm not sure what the right approach is here. In cases where we could set frame safely because layer.transform.isIdentity == true, doing so corrects the problem. But it still exists in the scenario where the layer is transformed.

Consider better name for cases in TargetAlignmentBehavior

We introduced a TargetAlignmentBehavior enum in #72 to control how views are aligned, in order to address the issues raised around aligning to scroll views in #54. The case names right now are a bit confusing, and I think we can do better, but I'm not sure what would be the clearest naming.

Add distribution items for distributing a UILabel via its font bounds

Now that we've removed the concept of custom positioning (#20) in 1.0, it's become more difficult to distribute labels via their font bounds. We should introduce a verticalDistributionItemUsingFontBounds convenience for this that includes the appropriate top/bottom insets from the font bounds.

Some of the operators in the geometry additions have unclear behavior

Specifically, we should convert the following operator overloads into methods to improve clarity around what their function is and what type they will return:

  • -(CGPoint, CGPoint) should be converted to CGPoint.offset(to:)
  • +(CGPoint, UIOffset) should be converted to CGPoint.offset(by:)
  • +(CGRect, UIOffset) should be converted to CGRect.offset(by:)

Cannot use Label class in Storyboard

I can create instances of the Label class programmatically, and they will work fine. However, when I try to use a Label in Storyboard by changing a UILabel's class to Label, it gives me the "fatal error: init(coder:) has not been implemented" error. Can they only be used programmatically?

Converge on consistent preposition for describing alignment target

We currently use a mix of "with" and "to" when talking about alignment.

The base method and most parameterized usages use "with:"

  • align(.center, with: view, .center)
  • align(.center, withSuperviewPosition: .center)

But the superview methods use "to:"

  • alignToSuperview(.center, offset: .zero)
  • alignToSuperview(.center, inset: 42)

I feel strongly that we should converge on a single preposition to use in all of these cases, but I could be convinced to go with either one. I'm leaning towards "with," but curious is anyone else feels strongly.

Enum cases for insets and offsets should default to zero

Currently using methods like applyVerticalSubviewDistribution specifying an orthogonalAlignment requires callers to specify an inset. This could be simplified if the cases of types like HorizontalDistributionAlignment defaulted to zero.

Rename view sizing method to better align with existing UIKit names

Paralayout defines methods around view sizing that build on UIKit's sizeThatFits(_:) and sizeToFit() methods by adding constraints. These are currently named frameSize and resize to differentiate them from the UIKit methods and increase clarity between the two. While this is successful, it's also confusing how they interact with the UIKit versions given that they serve the same functionality. We should rename them to match the UIKit methods to make it clear that they serve the same function with the addition of constraints.

  • frameSize(thatFits:constraints:) -> sizeThatFits(_:constraints:)
  • resize(toFit:constraints:) -> sizeToFit(_:constraints:)
  • resize(toFitWidth:height:constraints:) -> sizeToFit(width:height:constraints:)

Add convenience method to CGRect for getting position in a rect

We currently have a Position.point(in:) method to get the point at that position in the rect. This works well when you have the position already (e.g. as a parameter), but is a bit cumbersome when you don't, since it requires the full type.

let _ = Position.center.point(in: rect)

We should add a CGRect.point(at:) convenience method that calls through to this same logic, matching the existing UIView.point(at:) method.

rect.point(at: .center)

Remove UIDevice size additions

The extensions on UIDevice (found in UIDeviceAdditions.swift) are not up-to-date with the current set of available devices, do not include any iPad sizes, and only work for portrait orientations (as tracked by #3).

As Apple introduces increasingly variable screen sizes, for example with iPad multitasking and Catalyst apps, this extensions become less useful. I propose that we remove them from the framework completely.

Sizing methods do not respect provided constraints

The UIView sizing additions (frameSize(thatFits:constraints:) and associated methods) do not currently respect the constraints specified in the method call.

To illustrate:

// Given a view that returns a size of (300, 200) for its size that fits.
view.frameSize(thatFitsWidth: 100, height: 100, constraints: .maxSize) // returns (300, 200)

Since the width and height are both smaller than the view's size that fits, that method should return (100, 100) instead.

The core of this bug is inside SizingConstraints.apply(_:). It sets the constrainedSize to the size, then takes the appropriate min/max of these two. It should instead be comparing the size provided in the outer method call with the view's sizeThatFits(_:) size.

Improve alignment behavior around scroll views

@frozendevil recently pointed out an issue with the way alignment works when using scroll views. Specifically, imagine you have a content view inside a scroll view, with a layout like this:

scrollView.alignToSuperview(.topCenter)
contentView.align(.topCenter, with: scrollView, .topCenter)

This will only work correctly when the scroll view has a content offset of zero.

I think we can fix this as part of the move to aligning via bounds and center (#49). Specifically, the alignment methods should treat the receiver view and the other view (the parameter) differently. The view being aligned (the receiver) should be aligned by the rect made from its bounds.size, center, and layer.anchorPoint to the rect of the target view (the parameter) by the rect made from its bounds.size, bounds.origin, center, and layer.anchorPoint.

Improve support for handling layout directions

Currently most of the layout utilities assume an LTR layout direction. We should extend the APIs to better handle RTL layout. Specifically, I think we need the following changes:

  • Add cases to the Position enum with leading/trailing variants for each left/right case
  • Support these new positions in the alignment methods
  • Support these new positions in AspectRatio utilities for finding a rect to fit/fill
  • Change the distribution options for orthogonal alignment to actually select the leading/trailing edge rather than always using left/right (and potentially add options for explicitly specifying left/right alignment)

Remove Label from the framework

Label doesn't really belong in Paralayout anymore. It was originally very helpful in working around some quirks in UILabel that made it difficult to align text properly to match the output of design tools. Many of those original issues have been resolved over the years, however, and so it is no longer providing much value in terms of layout. While it still provides additional functionality on top of UILabel, Paralayout is a layout framework and the functionality included should be scoped to only layout tools when possible.

If the Label functionality is still useful to external consumers, we can consider moving it to its own micro-framework. If you use it and would be interested in this, please speak up. For now I propose that we remove it from the Paralayout entirely.

Perform layout with bounds and center instead of frame

Currently we perform layout by setting views' frames. Using frame for layout has some significant disadvantages. You can read about some of the complexity behind frames here. The most notable comes from this warning on the documentation for UIView.frame:

Screen Shot 2021-01-17 at 10 39 36 PM

Instead of using frame to perform layout, we should instead set our views' bounds and center. This should have almost the same behavior as setting the frame, except in cases where the view has a non-identity transform (in which case the frame is undefined anyway). This also matches how Auto Layout behaves around transforms.

Spreading subviews does not respect specified margin

When calling spreadOutSubviews(...) with a non-zero margin, instead of applying the margin in between subviews as expected, all but the last subview are condensed by the correct amount, but are positioned with no margin between them; then the final subview is expanded to fill the remaining space (its expected size plus the combined missing margins).

No descriptions / topics

screen shot 2018-04-12 at 4 13 48 pm

Love the library, but it would be even more awesome and discoverable if the description and and topics were filled out!

Add ResultBuilder-backed API for subview distribution

Paralayout's applyVerticalSubviewDistribution and applyHorizontalSubviewDistribution methods are incredibly powerful. Adding a result builder could make the code that creates these distributions more expressive.

In the simplest of cases, utilizing a distribution builder would enable removing the need to declare an array. Taking us from:

containerView.applyVerticalSubviewDistribution(
    [
        1.flexible,
        titleLabel,
        16.fixed,
        bodyLabel,
        16.fixed,
        actionButton,
        1.flexible,
    ]
)

to:

containerView.applyVerticalSubviewDistribution {
    1.flexible
    titleLabel
    16.fixed
    bodyLabel
    16.fixed
    actionButton
    1.flexible
}

But that's just the beginning. Since result builders allow for inline if statements, they'd enable writing distribution like:

containerView.applyVerticalSubviewDistribution {
    1.flexible
    titleLabel
    if bodyLabel.hasText {
        16.fixed
        bodyLabel
    }
    16.fixed
    actionButton
    1.flexible
}

Result builders also enable inlining for loop and if-else statements, enabling consumers to inline complex distributions. The benefits here purely syntactic – there'd be zero changes required to the underlying Paralayout library. The only additions would be:

  1. A @resultbuilder public struct ViewDistributionBuilder type.
  2. A applyVerticalSubviewDistribution method that takes a @ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying] closure rather than a _ distribution: [ViewDistributionSpecifying] argument. This new method would call through to the existing applyVerticalSubviewDistribution method.
  3. A applyHorizontalSubviewDistribution method that takes a @ViewDistributionBuilder _ distribution: () -> [ViewDistributionSpecifying] closure rather than a _ distribution: [ViewDistributionSpecifying] argument. This new method would call through to the existing applyVerticalSubviewDistribution method.

Result builders were introduced in Swift 5.4, so this functionality would only be added for consumers using Swift 5.4 and above. I'd be happy to put up a PR that accomplishes this – I recently did something similar in another view layout library.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.