Skip to content

Instantly share code, notes, and snippets.

@joseprl89
Last active June 22, 2019 20:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joseprl89/cf3ddcd2a25c2f2bb41c85ceeb78c079 to your computer and use it in GitHub Desktop.
Save joseprl89/cf3ddcd2a25c2f2bb41c85ceeb78c079 to your computer and use it in GitHub Desktop.
Rating view in Xcode10.1 playground
//: 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