Last active
November 26, 2015 14:39
-
-
Save algal/406ce37c775cfd552ba2 to your computer and use it in GitHub Desktop.
CenteredTagCloudView playground code
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
// | |
// MultilineLabelCloudView.swift | |
// | |
// | |
import UIKit | |
/** | |
Presents a wrapping grid of tokens presented as pills. | |
Implements sizeThatFits, so it can communicate its | |
required height to auto layout or other consumers. | |
*/ | |
class MultilineLabelCloudView: UIView, UICollectionViewDataSource | |
{ | |
var pillBackgroundColor:UIColor = UIColor.lightGrayColor() | |
var pillTextColor:UIColor = UIColor.darkGrayColor() | |
var pillTextFontSize:CGFloat = CGFloat(9) | |
var horizontalSpacing:CGFloat = CGFloat(8) | |
var tokens:[String] = [] { didSet { | |
self.collectionView.reloadData() | |
self.collectionView.collectionViewLayout.invalidateLayout() | |
self.setNeedsLayout() | |
self.layoutIfNeeded() | |
} } | |
private weak var collectionView:UICollectionView! | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
self.setup() | |
} | |
required init(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
self.setup() | |
} | |
private func setup() { | |
self.backgroundColor = UIColor.clearColor() | |
let layout = CenteredFlowLayout() | |
layout.minimumInteritemSpacing = self.horizontalSpacing | |
let cv = UICollectionView(frame: self.bounds, collectionViewLayout: layout) | |
cv.registerClass(LabelViewCell.self, forCellWithReuseIdentifier: "LabelViewCellIdentifier") | |
cv.dataSource = self | |
layout.estimatedItemSize = CGSize(width: 60, height: 60) | |
cv.backgroundColor = self.backgroundColor | |
self.addSubview(cv) | |
fillSuperview(cv) | |
self.collectionView = cv | |
} | |
/// Returns the size that would present all its content | |
override func sizeThatFits(size: CGSize) -> CGSize { | |
return self.collectionView.contentSize | |
} | |
// MARK: UICollectionViewDataSource | |
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { | |
return 1 | |
} | |
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |
return tokens.count | |
} | |
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: | |
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { | |
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("LabelViewCellIdentifier", forIndexPath: indexPath) as! LabelViewCell | |
cell.labelView.label.text = self.tokens[indexPath.row] | |
cell.labelView.labelBackgroundColor = pillBackgroundColor | |
cell.labelView.label.font = UIFont.systemFontOfSize(self.pillTextFontSize) | |
cell.labelView.label.textColor = self.pillTextColor | |
return cell | |
} | |
} | |
class CenteredFlowLayout : UICollectionViewFlowLayout | |
{ | |
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { | |
let modifiedLayoutAttributes = self.layoutAttributesForElementsInRect(CGRectInfinite) | |
// TODO: re-implement for better perf | |
if let desiredLayAttr = modifiedLayoutAttributes?.filter({ indexPath.isEqual($0.indexPath) }).first as? UICollectionViewLayoutAttributes { | |
return desiredLayAttr | |
} | |
else | |
{ | |
NSLog("error") | |
return super.layoutAttributesForItemAtIndexPath(indexPath) | |
} | |
} | |
func modifiedLayoutAttribuets(attributes:[UICollectionViewLayoutAttributes]) -> [UICollectionViewLayoutAttributes] | |
{ | |
return attributes | |
} | |
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? | |
{ | |
typealias Row = [UICollectionViewLayoutAttributes] | |
let superAttributes = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes] | |
// partition the objs into rows | |
func inRow(lay1:UICollectionViewLayoutAttributes, row:Row) -> Bool { | |
let sameRowEpsilon:CGFloat = 1 | |
if let lay2 = row.first { | |
let absDistance = fabs(CGRectGetMidY(lay1.frame) - CGRectGetMidY(lay2.frame)) | |
return (absDistance < sameRowEpsilon) | |
} | |
return false | |
} | |
func consumeItem(var result:[Row], item:UICollectionViewLayoutAttributes) -> [Row] | |
{ | |
for (index,row) in enumerate(result) { | |
if inRow(item,row) { | |
result[index].append(item) | |
return result | |
} | |
} | |
// assert: did not match any existing rows | |
result.append([item]) | |
return result | |
} | |
let rowCollections = reduce(superAttributes, [], consumeItem) | |
// update the layout one row at a time | |
let collectionViewWidth = CGRectGetWidth(self.collectionView!.bounds) | |
for items in rowCollections { | |
let itemsInRow = items.count | |
let aggregateInterItemSpacing = self.minimumInteritemSpacing * CGFloat(itemsInRow - 1) | |
let aggregateItemWidth = items.map({CGRectGetWidth($0.frame)}).reduce(0, combine: +) | |
let alignmentWidth = aggregateItemWidth + aggregateInterItemSpacing | |
let alignmentXOffset = (collectionViewWidth - alignmentWidth) / 2.0 | |
var previousFrame = CGRectZero | |
for item in items { | |
var itemFrame = item.frame | |
if CGRectEqualToRect(previousFrame, CGRectZero) { | |
itemFrame.origin.x = alignmentXOffset | |
} else { | |
itemFrame.origin.x = CGRectGetMaxX(previousFrame) + self.minimumInteritemSpacing | |
} | |
item.frame = itemFrame | |
previousFrame = itemFrame | |
} | |
} | |
return superAttributes | |
} | |
} | |
class LabelViewCell : UICollectionViewCell | |
{ | |
private weak var labelView:LabelView! | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
self.backgroundColor = UIColor.clearColor() | |
self.contentView.backgroundColor = UIColor.clearColor() | |
let label = LabelView(frame: self.bounds) | |
self.contentView.addSubview(label) | |
fillSuperview(label) | |
self.labelView = label | |
} | |
required init(coder aDecoder:NSCoder) { | |
super.init(coder:aDecoder) | |
assertionFailure("unimplemented NSCoding initializer") | |
} | |
} | |
/** Draws a light gray pill-shaped view with an inset label showing dark gray text | |
Defines its own preferred size via auto layout. | |
*/ | |
class LabelView: UIView | |
{ | |
var labelBackgroundColor:UIColor? { | |
get { return self.backgroundColor } | |
set { | |
self.backgroundColor = newValue | |
self.label.backgroundColor = newValue | |
} | |
} | |
let label = UILabel(frame: CGRectZero) | |
let insetting = UIEdgeInsetsMake(5, 15, 5, 15) | |
override convenience init(frame:CGRect) { | |
// defaults | |
let kpillBackgroundColor:UIColor = UIColor.greenColor() | |
let kpillTextColor:UIColor = UIColor.whiteColor() | |
let kpillTextFontSize:CGFloat = CGFloat(9) | |
let kpillTextFont:UIFont = UIFont.systemFontOfSize(kpillTextFontSize) | |
self.init(frame:frame, pillBackgroundColor:kpillBackgroundColor,pillTextColor:kpillTextColor,pillTextFont:kpillTextFont) | |
} | |
init(frame:CGRect, pillBackgroundColor:UIColor,pillTextColor:UIColor,pillTextFont:UIFont) { | |
super.init(frame:frame) | |
self.setTranslatesAutoresizingMaskIntoConstraints(false) | |
self.autoresizingMask = UIViewAutoresizing.None | |
self.backgroundColor = pillBackgroundColor | |
self.clipsToBounds = true | |
label.backgroundColor = pillBackgroundColor | |
label.textColor = pillTextColor | |
label.font = pillTextFont | |
label.frame = UIEdgeInsetsInsetRect(self.bounds, self.insetting) | |
self.addSubview(label) | |
let views = ["label":label] | |
self.label.setTranslatesAutoresizingMaskIntoConstraints(false) | |
self.layoutMargins = insetting | |
["V:|-[label]-|","H:|-[label]-|"].map( { (vfl:String) -> String in | |
self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(vfl, options: .allZeros, metrics: nil, views: views)) | |
return vfl | |
}) | |
} | |
required init(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
self.label.frame = UIEdgeInsetsInsetRect(self.bounds,self.insetting) | |
self.layer.cornerRadius = self.layer.bounds.size.height / 2.0 | |
} | |
} | |
func fillSuperview(view:UIView) | |
{ | |
if let superview = view.superview { | |
view.setTranslatesAutoresizingMaskIntoConstraints(false) | |
view.frame = superview.bounds | |
superview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[v]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: ["v":view])) | |
superview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[v]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: ["v":view])) | |
} | |
} | |
let cloudView = MultilineLabelCloudView(frame: CGRect(x: 0, y: 0, width: 310, height: 50)) | |
cloudView.tokens = ["phonenumber","ssn","card","mytoken","word","another word"] | |
cloudView.backgroundColor = UIColor.yellowColor() | |
cloudView | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment