PinLayout icon indicating copy to clipboard operation
PinLayout copied to clipboard

Improve Views Concatenation Methods

Open pasp94 opened this issue 1 year ago • 3 comments

Description

This PR aims to improve to improve the behaviour of methods like below(of ... or above(of ... allowing to pin the view to the top/bottom of its superview.

Motivation and Context

Using PinLayout frequently to chain views with the following APIs below(of ..., above(of ..., or after(of ... combined with visible( ... ) method, it's easy and fast but we often find ourself facing the same problem: the view on top must always be visible. In some cases, however, it would be useful and convenient to overcome this trivial limitation by anchoring the first visible view to the superview’s edge.

The solution adopted by me and also my colleagues is to use an if else statement inside the layout method that check the visibility of the previous/next relative view:

func performLayout() {
    firstView.pin
      .top()
      .size(100)
      .hCenter()
      .marginTop(10)
    
    if firstView.isHidden {
      secondView.pin
        .top()
    } else {
      secondView.pin
        .below(of: firstView)
    }
    ...
}

With this pull request I would like to discuss a possible solution implemented as a set of methods that after checking the visibility state of his relative views, instead of causing the layout to fail, pin the view to the edge of the superview (top/bottom/left/right depending on the method used).

/// Example of a view that is attached below of the last visible `relativeViews`
/// or to `top()` if all relative views are hidden.
@discardableResult
public func topIfNotBelow(of relativeViews: [PinView], aligned: HorizontalAlign = .none) -> PinLayout {
  func context() -> String { return relativeContext("below", relativeViews, aligned) }
  return topIfNotBelow(of: relativeViews, aligned: aligned, context: context)
}

fileprivate func topIfNotBelow(of relativeViews: [PinView], aligned: HorizontalAlign, context: Context ) -> PinLayout {
  guard layoutSuperview(context) != nil else { return self }
  guard relativeViews.count > 0 else {
    setTop(0, context)
    return self
  }
  
  let anchors: [Anchor]
  switch aligned {
  case .left:   anchors = relativeViews.map({ $0.anchor.bottomLeft })
  case .center: anchors = relativeViews.map({ $0.anchor.bottomCenter })
  case .right:  anchors = relativeViews.map({ $0.anchor.bottomRight })
  case .start:  anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomLeft }) : relativeViews.map({ $0.anchor.bottomRight })
  case .end:    anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomRight }) : relativeViews.map({ $0.anchor.bottomLeft })
  case .none:   anchors = relativeViews.map({ $0.anchor.bottomLeft })
  }
  
  if let coordinates = computeCoordinates(forAnchors: anchors, context) {
    setTop(getBottomMostCoordinate(list: coordinates), context)
    applyHorizontalAlignment(aligned, coordinates: coordinates, context: context)
  }
  return self
}

Using this method as follow:

func performLayout() {
  redView.pin
    .top()
    .size(100)
    .hCenter()
    .marginTop(10)
  
  blueView.pin
    .topIfNotBelow(of: visible([redView]))
    .size(100)
    .hCenter()
    .marginTop(12)
  
  greenView.pin
    .topIfNotBelow(of: visible([redView, blueView]))
    .size(100)
    .hCenter()
    .marginTop(12)
  
  ...
}

I’ll leave here a short video to show animations as well.

https://user-images.githubusercontent.com/16403933/196270311-1e2a0587-b916-4f21-9967-539953ae2c00.mp4

More Thoughts

It could also be thought of a version of this proposal that improves the names of the methods added by me. Existing functions (such as below(of ...)) could be improved by adding a parameter that describes where the view should be pin in case there are no relative views visible (as default it will maintain the default behaviour):

enum ParentVerticaAnchor {
  case none
  
  case top
  
  case bottom
  
  case center
}

fileprivate func below(of relativeViews: [PinView], aligned: HorizontalAlign, ifNeeded anchor: ParentVerticaAnchor = .none, context: Context) -> PinLayout {
  guard layoutSuperview(context) != nil else { return self }
  
  switch anchor {
  case .none:
    guard relativeViews.count > 0 else {
      warnWontBeApplied("At least one view must be visible (i.e. UIView.isHidden != true) ", context)
      return self
    }
    
  case .top:
    guard relativeViews.count > 0 else {
      setTop(0, context)
      return self
    }
    
  case .bottom:
    guard relativeViews.count > 0 else {
      setBottom(0, context)
      return self
    }
    
  case .center:
    guard relativeViews.count > 0 else {
      setVerticalCenter(0, context)
      return self
    }
  }
  
  let anchors: [Anchor]
  switch aligned {
    case .left:   anchors = relativeViews.map({ $0.anchor.bottomLeft })
    case .center: anchors = relativeViews.map({ $0.anchor.bottomCenter })
    case .right:  anchors = relativeViews.map({ $0.anchor.bottomRight })
    case .start:  anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomLeft }) : relativeViews.map({ $0.anchor.bottomRight })
    case .end:    anchors = isLTR() ? relativeViews.map({ $0.anchor.bottomRight }) : relativeViews.map({ $0.anchor.bottomLeft })
    case .none:   anchors = relativeViews.map({ $0.anchor.bottomLeft })
  }
  
  if let coordinates = computeCoordinates(forAnchors: anchors, context) {
    setTop(getBottomMostCoordinate(list: coordinates), context)
    applyHorizontalAlignment(aligned, coordinates: coordinates, context: context)
  }
  return self
}

pasp94 avatar Oct 17 '22 20:10 pasp94