Last active
December 30, 2020 21:36
-
-
Save exorcyze/6b2938a43b2a77eb93473fb94fcd61d0 to your computer and use it in GitHub Desktop.
View layouts made easier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
override func viewWillLayoutSubviews() { | |
super.viewWillLayoutSubviews() | |
// takes up entire superview | |
myscroll.pinWidth().pinHeight() | |
// take the entire superview width, and | |
// make the height a 16/9 ratio of that | |
titleImage.pinWidth().pinHeightToWidth(by: 16/9) | |
// pin to the width of the superview with 20 padding each side | |
overviewLabel.pinWidth( lpad: 20, rpad: 20 ) | |
// stack views in this order with specified paddings between | |
vstack( 0, titleImage, 20, releaseLabel, 10, genreLabel, 20, overviewLabel ) | |
// give them the same top position | |
runtimeLabel.top = releaseLabel.top | |
// stack horizontally with a 20px left margin | |
hstack( 20, releaseLabel, runtimeLabel ) | |
// set the left margin | |
genreLabel.left = 20 | |
// auto-size the scroll view content area for the child views | |
myscroll.sizeForVerticalScrolling() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// UIView+Layout.swift | |
// Created / Copyright © : Mike Johnson, 2015 | |
// | |
import Foundation | |
import UIKit | |
extension UIView { | |
// MARK: - Positioning Properties | |
var width : CGFloat { | |
get { return self.frame.size.width } | |
set { self.frame.size.width = newValue } | |
} | |
var height : CGFloat { | |
get { return self.frame.size.height } | |
set { self.frame.size.height = newValue } | |
} | |
var left : CGFloat { | |
get { return self.frame.origin.x } | |
set { self.frame.origin.x = newValue } | |
} | |
var right : CGFloat { | |
get { return self.frame.origin.x + self.width } | |
set { self.frame.origin.x = newValue - self.width } | |
} | |
var top : CGFloat { | |
get { return self.frame.origin.y } | |
set { self.frame.origin.y = newValue } | |
} | |
var bottom : CGFloat { | |
get { return self.frame.origin.y + self.height } | |
set { self.frame.origin.y = newValue - self.height } | |
} | |
var centerX : CGFloat { | |
get { return self.left + self.width * 0.5 } | |
set { self.left = newValue - self.width * 0.5 } | |
} | |
var centerY : CGFloat { | |
get { return self.top + self.height * 0.5 } | |
set { self.top = newValue - self.height * 0.5 } | |
} | |
var size : CGSize { | |
get { return self.frame.size } | |
set { self.frame.size = newValue } | |
} | |
var origin : CGPoint { | |
get { return self.frame.origin } | |
set { self.frame.origin = newValue } | |
} | |
var topRight : CGPoint { | |
get { return CGPoint( x: self.right, y: self.top ) } | |
set { self.right = newValue.x; self.top = newValue.y } | |
} | |
var bottomRight : CGPoint { | |
get { return CGPoint( x: self.right, y: self.bottom ) } | |
set { self.right = newValue.x; self.bottom = newValue.y } | |
} | |
var bottomLeft : CGPoint { | |
get { return CGPoint( x: self.left, y: self.bottom ) } | |
set { self.left = newValue.x; self.bottom = newValue.y } | |
} | |
// MARK: - Subview Methods | |
/// Adds multiple subviews at once in the order passed. | |
/// - Returns: The originating view | |
/// | |
/// myview.addSubviews( titleView, bodyView ) | |
@discardableResult func addSubviews( _ views: UIView... ) -> UIView { | |
views.forEach{ [unowned self] in self.addSubview( $0 ) } | |
return self | |
} | |
/// Removes all subviews from this view | |
func removeAllSubviews() { | |
self.subviews.forEach{ $0.removeFromSuperview() } | |
} | |
/// Removes all subviews that match the given class | |
/// | |
/// myview.removeSubviewsByClass( UILabel ) | |
func removeSubviewsByClass<T>( c: T.Type ) { | |
self.subviews.filter{ $0 is T }.forEach{ $0.removeFromSuperview() } | |
} | |
// MARK: - Alignment Methods | |
/// Pins this view to the right side of it's parent view with | |
/// the given padding. | |
/// - Returns: The originating view | |
@discardableResult func pinRight( rpad: CGFloat ) -> UIView { | |
if let sv = self.superview { self.right = sv.width - rpad } | |
return self | |
} | |
/// Pins this view to the bottom of it's parent view with | |
/// the given padding. | |
/// - Returns: The originating view | |
@discardableResult func pinBottom( pad: CGFloat ) -> UIView { | |
if let sv = self.superview { self.bottom = sv.height - pad } | |
return self | |
} | |
/// Pins this view to the width of it's parent view. | |
/// - Parameters: | |
/// - lpad: Sets the left edge of the view if passed. Left edge is left unset if not passed. | |
/// - rpad: Used for the padding on the right of this view when pinning the width | |
/// | |
/// - Returns: The originating view | |
@discardableResult func pinWidth( lpad: CGFloat = CGFloat.greatestFiniteMagnitude, rpad: CGFloat ) -> UIView { | |
if lpad != CGFloat.greatestFiniteMagnitude { self.left = lpad } | |
if let sv = self.superview { self.width = sv.width - self.left - rpad } | |
return self | |
} | |
/// Pins this view to the width of it's parent view and sets the right | |
/// padding to match the left edge of the view. | |
/// - Returns: The originating view | |
@discardableResult func pinWidth() -> UIView { return self.pinWidth( rpad: self.left ) } | |
/// Pins this view to the height of it's parent view. | |
/// - Parameters: | |
/// - tpad: Sets the top edge of the view if passed. Top edge is left unset if not passed. | |
/// - bpad: Used for the padding on the bottom of this view when pinning the height | |
/// | |
/// - Returns: The originating view | |
@discardableResult func pinHeight( tpad: CGFloat = CGFloat.greatestFiniteMagnitude, bpad: CGFloat ) -> UIView { | |
if tpad != CGFloat.greatestFiniteMagnitude { self.top = tpad } | |
if let sv = self.superview { self.height = sv.height - self.top - bpad } | |
return self | |
} | |
/// Pins this view to the height of it's parent view and sets the bottom | |
/// padding to match the top edge of the view. | |
/// - Returns: The originating view | |
@discardableResult func pinHeight() -> UIView { return self.pinHeight( bpad: self.top ) } | |
// MARK: - View Centering | |
enum CenterDirection { | |
case horizontal | |
case vertical | |
} | |
/// Pins the center of this view relative to it's parent view. | |
/// - Parameters: | |
/// - directions: List of CenterDirection orientations to center in | |
/// | |
/// - Returns: The originating view | |
/// | |
/// mybutton.pinCenter(.horizontal, .vertical) | |
@discardableResult func pinCenter( _ directions: CenterDirection... ) -> UIView { | |
guard let sv = self.superview else { return self } | |
for item in directions { | |
if item == .horizontal { self.centerX = sv.width * 0.5 } | |
if item == .vertical { self.centerY = sv.height * 0.5 } | |
} | |
return self | |
} | |
// MARK: - Aspect Sizing | |
/// Sets the height to the given ratio of the current width. Ratio is | |
/// passed as division, so 16/9 for a 16:9 aspect ratio | |
/// | |
/// - Parameters: | |
/// - aspectRatio: Aspect ratio to maintain, as a float | |
/// | |
/// - Returns: The originating view | |
/// | |
/// myimage.pinWidth().pinHeightToWidth( by: 16/9 ) | |
@discardableResult func pinHeightToWidth( by aspectRatio: CGFloat ) -> UIView { | |
self.height = self.width.heightForWidth( by: aspectRatio ) | |
return self | |
} | |
/// Sets the width to the given ratio of the current height. Ratio is | |
/// passed as division, so 16/9 for a 16:9 aspect ratio | |
/// | |
/// - Parameters: | |
/// - aspectRatio: Aspect ratio to maintain, as a float | |
/// | |
/// - Returns: The originating view | |
/// | |
/// myimage.pinWidth().pinWidthToHeight( by: 16/9 ) | |
@discardableResult func pinWidthToHeight( by aspectRatio: CGFloat ) -> UIView { | |
self.width = self.height.widthForHeight( by: aspectRatio ) | |
return self | |
} | |
} | |
extension CGFloat { | |
/// Returns the height value to the given ratio of the current value as width. | |
/// Ratio is passed as division, so 16/9 for a 16:9 aspect ratio | |
/// | |
/// - Parameters: | |
/// - aspectRatio: Aspect ratio to maintain, as a float | |
/// | |
/// - Returns: The height value | |
/// | |
/// let myheight = myview.width.heightForWidth( by: 16/9 ) | |
func heightForWidth( by aspectRatio: CGFloat ) -> CGFloat { | |
return self / aspectRatio | |
} | |
/// Returns the width value to the given ratio of the current value as height. | |
/// Ratio is passed as division, so 16/9 for a 16:9 aspect ratio | |
/// | |
/// - Parameters: | |
/// - aspectRatio: Aspect ratio to maintain, as a float | |
/// | |
/// - Returns: The width value | |
/// | |
/// let width = myview.height.widthForHeight( by: 16/9 ) | |
func widthForHeight( by aspectRatio: CGFloat ) -> CGFloat { | |
return self * aspectRatio | |
} | |
} | |
// MARK: - Stacking Methods | |
protocol Stackable {} | |
extension UIView : Stackable {} | |
extension Int : Stackable {} | |
/// Takes combo of Int and View parameters to stack vertically. Int values | |
/// will increment the current y-position. Views will increment the current | |
/// position to the bottom of that view after positioning. | |
/// | |
/// - Returns: Bottom position of last element | |
/// | |
/// vstack( 10, UIView(), 20, UIButton(), 10, UIView(), UIView() ) | |
@discardableResult func vstack( _ viewsAndSpacing: Stackable... ) -> CGFloat { | |
var pos: CGFloat = 0 | |
for item in viewsAndSpacing { | |
// place view and update position | |
if let v = item as? UIView { | |
v.top = pos | |
pos = v.bottom | |
} | |
// update position | |
if let m = item as? Int { pos += CGFloat( integerLiteral: m ) } | |
} | |
return pos | |
} | |
/// Takes combo of Int and View parameters to stack horizontally. Int values | |
/// will increment the current x-position. Views will increment the current | |
/// position to the right of that view after positioning. | |
/// | |
/// - Returns: Right side position of last element | |
/// | |
/// hstack( 10, UIView(), 20, UIButton(), 10, UIView(), UIView() ) | |
@discardableResult func hstack( _ viewsAndSpacing: Stackable... ) -> CGFloat { | |
var pos: CGFloat = 0 | |
for item in viewsAndSpacing { | |
// place view and update position | |
if let v = item as? UIView { | |
v.left = pos | |
pos = v.right | |
} | |
// update position | |
if let m = item as? Int { pos += CGFloat( integerLiteral: m ) } | |
} | |
return pos | |
} | |
extension UIScrollView { | |
/// Sets the content size to contain all the subviews for horizontal scrolling | |
func sizeForHorizontalScrolling() { | |
let nums = self.subviews.map { $0.right } | |
self.contentSize = CGSize( width: nums.max() ?? self.width, height: self.height ) | |
} | |
/// Sets the content size to contain all the subviews for vertical scrolling | |
func sizeForVerticalScrolling() { | |
let nums = self.subviews.map { $0.bottom } | |
self.contentSize = CGSize( width: self.width, height: nums.max() ?? self.height ) | |
} | |
} | |
extension UILabel { | |
/// Sets the height of the label to contain the text given the currently set width. | |
/// | |
/// - Returns: The originating view | |
/// | |
@discardableResult func setHeightForContent() -> UILabel { | |
let labelHeight = self.sizeThatFits(CGSize(width: self.width, height: CGFloat.greatestFiniteMagnitude)).height | |
self.height = labelHeight | |
return self | |
// new way? | |
//self.frame.size = self.systemLayoutSizeFitting( CGSize( width: self.width, height: 0 ), withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow ) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment