Last active
June 22, 2019 20:29
-
-
Save joseprl89/cf3ddcd2a25c2f2bb41c85ceeb78c079 to your computer and use it in GitHub Desktop.
Rating view in Xcode10.1 playground
This file contains 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
//: A UIKit based Playground for presenting user interface | |
import UIKit | |
import PlaygroundSupport | |
struct Star { | |
let radius: CGFloat | |
init(radius: CGFloat) { | |
self.radius = radius | |
} | |
func bezierPath(leadingPadding: CGFloat, topPadding: CGFloat) -> UIBezierPath { | |
let sides = 5 | |
let polygonPath = UIBezierPath() | |
polygonPath.move(to: pointForStarEdge(indexed: 0, leadingPadding: leadingPadding, topPadding: topPadding)) | |
for i in 1..<sides*2 { | |
polygonPath.addLine(to: pointForStarEdge(indexed: i, leadingPadding: leadingPadding, topPadding: topPadding)) | |
} | |
polygonPath.close() | |
return polygonPath | |
} | |
func pointForStarEdge(indexed i: Int, leadingPadding: CGFloat, topPadding: CGFloat, initialAngle: Double = 0) -> CGPoint { | |
if i % 2 == 0 { | |
return outerPointForStarEdge( | |
indexed: i / 2, | |
leadingPadding: leadingPadding, | |
topPadding: topPadding, | |
initialAngle: initialAngle | |
) | |
} else { | |
return innerPointForStarEdge( | |
indexed: (i / 2) - 2, | |
leadingPadding: leadingPadding, | |
topPadding: topPadding, | |
initialAngle: initialAngle | |
) | |
} | |
} | |
func outerPointForStarEdge(indexed i: Int, leadingPadding: CGFloat, topPadding: CGFloat, initialAngle: Double = 0) -> CGPoint { | |
let theta = 2.0 * Double.pi / 5.0 | |
let radians = Double(i) * theta + initialAngle | |
let x = radius * CGFloat(sin(radians)) | |
let y = radius * CGFloat(cos(radians)) | |
let starCenter = CGPoint( | |
x: radius + leadingPadding, | |
y: radius + topPadding | |
) | |
return CGPoint(x: x + starCenter.x, y: -y + starCenter.y) | |
} | |
func innerPointForStarEdge(indexed i: Int, leadingPadding: CGFloat, topPadding: CGFloat, initialAngle: Double = 0) -> CGPoint { | |
let theta = 2.0 * Double.pi / 5.0 | |
let radians = Double(i) * theta + initialAngle + Double.pi | |
print("Radians: \(radians / Double.pi) pi") | |
let x = radius * CGFloat(sin(radians)) / 2.5 | |
let y = radius * CGFloat(cos(radians)) / 2.5 | |
let starCenter = CGPoint( | |
x: radius + leadingPadding, | |
y: radius + topPadding | |
) | |
return CGPoint(x: x + starCenter.x, y: -y + starCenter.y) | |
} | |
} | |
class StarView: UIView { | |
var didTapStar: (_ currentState: State) -> () = { _ in } | |
let starLayer: CAShapeLayer = CAShapeLayer() | |
enum State { | |
case selected | |
case deselected | |
case disabled | |
var fillColor: UIColor { | |
switch self { | |
case .selected: return .orange | |
case .deselected: return .clear | |
case .disabled: return .gray | |
} | |
} | |
var strokeColor: UIColor { | |
switch self { | |
case .selected: return fillColor | |
case .deselected: return .orange | |
case .disabled: return fillColor | |
} | |
} | |
} | |
var state: State = .disabled { | |
didSet { | |
if oldValue != state { | |
transitionState(from: oldValue, to: state) | |
} | |
} | |
} | |
init() { | |
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 400)) | |
backgroundColor = UIColor.white | |
print("Creating star") | |
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(StarView.didTap))) | |
layer.addSublayer(starLayer) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("Not supported") | |
} | |
private func transitionState(from previousState: State, to state: State) { | |
layer.removeAllAnimations() | |
let sizeAnimation = CAKeyframeAnimation(keyPath: "transform") | |
sizeAnimation.values = [ | |
1, | |
1.1, | |
1.0 | |
].map { | |
CATransform3DMakeScale( | |
$0, | |
$0, | |
1.0 | |
) | |
} | |
sizeAnimation.timingFunctions = [ | |
CAMediaTimingFunction(name: .easeInEaseOut), | |
CAMediaTimingFunction(name: .easeInEaseOut), | |
CAMediaTimingFunction(name: .easeInEaseOut) | |
] | |
sizeAnimation.duration = 0.3 | |
let fillColorAnimation = CABasicAnimation(keyPath: "fillColor") | |
fillColorAnimation.fromValue = previousState.fillColor.cgColor | |
fillColorAnimation.toValue = state.fillColor.cgColor | |
fillColorAnimation.duration = 0.3 | |
let strokeColorAnimation = CABasicAnimation(keyPath: "strokeColor") | |
strokeColorAnimation.fromValue = previousState.strokeColor.cgColor | |
strokeColorAnimation.toValue = state.strokeColor.cgColor | |
strokeColorAnimation.duration = 0.3 | |
let group = CAAnimationGroup() | |
group.animations = [ | |
strokeColorAnimation, | |
fillColorAnimation, | |
sizeAnimation | |
] | |
group.fillMode = .forwards | |
group.isRemovedOnCompletion = false | |
group.duration = 0.3 | |
starLayer.add(group, forKey: "updated") | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
let rect = self.frame | |
let star = Star(radius: min(rect.height, rect.width) / 2.0) | |
let leadingPadding = max(0, (rect.width / 2) - star.radius) | |
let topPadding = max(0, (rect.height / 2) - star.radius) | |
let path = star.bezierPath( | |
leadingPadding: leadingPadding / 2, | |
topPadding: topPadding / 2 | |
).cgPath | |
starLayer.path = path | |
starLayer.strokeColor = UIColor.black.cgColor | |
starLayer.frame = path.boundingBox | |
starLayer.lineWidth = 5 | |
} | |
@objc | |
private func didTap() { | |
didTapStar(state) | |
} | |
override var intrinsicContentSize: CGSize { | |
return CGSize(width: 50, height: 50) | |
} | |
} | |
class RatingView: UIView { | |
lazy private var starViewOne = StarView() | |
lazy private var starViewTwo = StarView() | |
lazy private var starViewThree = StarView() | |
lazy private var starViewFour = StarView() | |
lazy private var starViewFive = StarView() | |
enum Selection { | |
case nothing | |
case rating(Int) | |
} | |
lazy private var starViews = [ | |
starViewOne, | |
starViewTwo, | |
starViewThree, | |
starViewFour, | |
starViewFive, | |
] | |
private(set) var selection = Selection.nothing { | |
didSet { | |
print(selection) | |
} | |
} | |
init() { | |
super.init(frame: CGRect(x: 0, y: 0, width: 400, height: 100)) | |
translatesAutoresizingMaskIntoConstraints = false | |
backgroundColor = .white | |
let stackView = UIStackView(arrangedSubviews: starViews) | |
stackView.translatesAutoresizingMaskIntoConstraints = false | |
stackView.alignment = .center | |
stackView.distribution = .fillEqually | |
stackView.axis = .horizontal | |
stackView.spacing = 32 | |
stackView.isLayoutMarginsRelativeArrangement = true | |
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 24, leading: 24, bottom: 24, trailing: 24) | |
addSubview(stackView) | |
NSLayoutConstraint.activate([ | |
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), | |
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), | |
stackView.topAnchor.constraint(equalTo: topAnchor), | |
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
]) | |
setNeedsLayout() | |
for i in 0..<starViews.count { | |
starViews[i].didTapStar = didTapStar(indexed: i) | |
} | |
} | |
func didTapStar(indexed: Int) -> (StarView.State) -> () { | |
return { state in | |
switch self.selection { | |
case .nothing: | |
self.selectUpTo(indexed) | |
case let .rating(value): | |
if value == 0 && indexed == 0 { | |
self.deselect() | |
} else { | |
self.selectUpTo(indexed) | |
} | |
} | |
} | |
} | |
private func selectUpTo(_ index: Int) { | |
selection = .rating(index) | |
for i in 0..<starViews.count { | |
starViews[i].state = i <= index ? .selected : .deselected | |
} | |
} | |
private func deselect() { | |
selection = .nothing | |
starViews.forEach { $0.state = .deselected } | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("Not available via coder") | |
} | |
} | |
// Present the view controller in the Live View window | |
let view = RatingView() | |
view.frame = CGRect(x: 0, y: 0, width: 500, height: 200) | |
PlaygroundPage.current.liveView = view |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment