I want to create a half circular progress bar with gradient colour in iOS using swift. The minimum value of progress bar is 300 and maximum value of progress bar is 900. The current value of the progress bar is dynamic. I am attaching screenshot ans css for colour reference. Can someone please provide me a small working demo?
below is the CSS -
/* Layout Properties */
top: 103px;
left: 105px;
width: 165px;
height: 108px;
/* UI Properties */
background: transparent linear-gradient
(269deg, #32E1A0 0%, #EEED56 23%, #EFBF39 50%, #E59148 75%, #ED4D4D 100%)
0% 0% no-repeat padding-box;
opacity: 1;
you can create not same but similar progress bar with framework called
MKMagneticProgress
example code :-
import MKMagneticProgress
#IBOutlet weak var magProgress:MKMagneticProgress!
override func viewDidLoad() {
magProgress.setProgress(progress: 0.5, animated: true)
magProgress.progressShapeColor = UIColor.blue
magProgress.backgroundShapeColor = UIColor.yellow
magProgress.titleColor = UIColor.red
magProgress.percentColor = UIColor.black
magProgress.lineWidth = 10
magProgress.orientation = .top
magProgress.lineCap = .round
magProgress.title = "Title"
magProgress.percentLabelFormat = "%.2f%%"
}
i hope it will work ... :)
this could solve your problem in swiftui
import SwiftUI
struct ContentView: View {
#State var progressValue: Float = 0.3
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var degress: Double = -110
var body: some View {
VStack {
ZStack{
ProgressBar(progress: self.$progressValue)
.frame(width: 250.0, height: 250.0)
.padding(40.0).onReceive(timer) { _ in
withAnimation {
if progressValue < 0.8999996 {
progressValue += 0.0275
}
}
}
ProgressBarTriangle(progress: self.$progressValue).frame(width: 280.0, height: 290.0).rotationEffect(.degrees(degress), anchor: .bottom)
.offset(x: 0, y: -150).onReceive(timer) { input in
withAnimation(.linear(duration: 0.01).speed(200)) {
if degress < 110.0 {
degress += 10
}
print(degress)
}
}
}
Spacer()
}
}
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
ZStack {
Circle()
.trim(from: 0.3, to: 0.9)
.stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round))
.opacity(0.3)
.foregroundColor(Color.gray)
.rotationEffect(.degrees(54.5))
Circle()
.trim(from: 0.3, to: CGFloat(self.progress))
.stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round))
.fill(AngularGradient(gradient: Gradient(stops: [
.init(color: Color.init(hex: "ED4D4D"), location: 0.39000002),
.init(color: Color.init(hex: "E59148"), location: 0.48000002),
.init(color: Color.init(hex: "EFBF39"), location: 0.5999999),
.init(color: Color.init(hex: "EEED56"), location: 0.7199998),
.init(color: Color.init(hex: "32E1A0"), location: 0.8099997)]), center: .center))
.rotationEffect(.degrees(54.5))
VStack{
Text("824").font(Font.system(size: 44)).bold().foregroundColor(Color.init(hex: "314058"))
Text("Great Score!").bold().foregroundColor(Color.init(hex: "32E1A0"))
}
}
}
}
struct ProgressBarTriangle: View {
#Binding var progress: Float
var body: some View {
ZStack {
Image("triangle").resizable().frame(width: 10, height: 10, alignment: .center)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
I implemented something like your progress bar,
used this for level up - gain exp. progress.,
if you go from 300 to 1000, total bar is 1500
progressFrom: 300/1500, progressTo: 1000/1500 ..
func progressAnimation(duration: TimeInterval, progressFrom: CGFloat, progressTo: CGFloat) {
if progressFrom == 0 {
baseProgressLayer.removeFromSuperlayer()
}
let circlePath = UIBezierPath(ovalIn: CGRect(center: CGPoint(x: 93.0, y: 93.0), size: CGSize(width: 178, height: 178)))
progressLayer.path = circlePath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = getColor(progress: progressTo).cgColor
progressLayer.lineWidth = 8.0
layer.addSublayer(progressLayer)
layer.transform = CATransform3DMakeRotation(CGFloat(90 * Double.pi / 180), 0, 0, -1)
let end = CABasicAnimation(keyPath: "strokeEnd")
end.fromValue = progressFrom
end.toValue = progressTo
end.duration = duration * 0.85
end.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
end.fillMode = CAMediaTimingFillMode.forwards
end.isRemovedOnCompletion = false
let color = CABasicAnimation(keyPath: "strokeColor")
color.fromValue = getColor(progress: progressFrom).cgColor
color.toValue = getColor(progress: progressTo).cgColor
color.duration = duration * 0.85
color.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
color.fillMode = CAMediaTimingFillMode.forwards
color.isRemovedOnCompletion = false
let group = CAAnimationGroup()
group.animations = [end, color]
group.duration = duration * 0.85
progressLayer.strokeStart = progressFrom
progressLayer.strokeEnd = progressTo
progressLayer.add(group, forKey: "move")
if progressFrom != 0 {
let color2 = CABasicAnimation(keyPath: "strokeColor")
color2.fromValue = getColor(progress: progressFrom).cgColor
color2.toValue = getColor(progress: progressTo).cgColor
color2.duration = duration * 0.85
color2.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
color2.fillMode = CAMediaTimingFillMode.forwards
color2.isRemovedOnCompletion = false
baseProgressLayer.add(color2, forKey: "baseColor")
}
}
there is a extension for getting color for progress
extension CircularProgressBar {
private func getColor (progress: CGFloat) -> UIColor{
if progress >= 0 && progress <= 0.5{
return UIColor(red: CGFloat(255 / 255), green: CGFloat((0+((108-0)*progress*2)) / 255), blue: 0, alpha: 1.0)
}
else if progress > 0.5 && progress <= 1.0 {
return UIColor(red: CGFloat((255-((255-126)*(progress-0.5)*2)) / 255), green: CGFloat((108+((211-108)*(progress-0.5)*2)) / 255), blue: CGFloat((0+((33-0)*(progress-0.5)*2)) / 255), alpha: 1.0)
}
else {
return UIColor.green
}
}
}
Related
I am trying to replicate a component written in UIKit using SwiftUI
It is a 'shadowed card container' that looks like this -
You see the shadow effect has an offset and height value to it
Trying to create this in SwiftUI I cannot get it quite right -
The UIKit view is handled via an extension -
public extension UIView {
func addShadow(offset: CGSize = .init(width: 0, height: 3), color: UIColor = .init(red: 0, green: 0, blue: 0, alpha: 0.16), radius: CGFloat = 2, opacity: Float = 1) {
layer.masksToBounds = false
layer.shadowOffset = offset
layer.shadowColor = color.cgColor
layer.shadowRadius = radius
layer.shadowOpacity = opacity
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
}
}
On a view -
public class CardContainerView: UIView {
private var didlLayoutSubviews = false
public override init(frame: CGRect) {
super.init(frame: frame)
configureUI()
}
required init?(coder: NSCoder) {
return nil
}
public override func layoutSubviews() {
super.layoutSubviews()
guard !didlLayoutSubviews else { return }
didlLayoutSubviews = true
addShadow(
offset: .init(width: 0, height: 3),
color: .init(red: 0, green: 0, blue: 0, alpha: 0.16),
radius: 2
)
}
}
private extension CardContainerView {
func configureUI() {
layer.cornerRadius = 12
clipsToBounds = true
}
}
I have tried this in SwiftUI and come up with -
public struct CardView<Content>: View where Content: View {
private var content: () -> Content
public init(#ViewBuilder _ content: #escaping () -> Content) {
self.content = content
}
public var body: some View {
VStack(content: content)
.padding()
.frame(maxWidth: .infinity, minHeight: 100, alignment: .top)
.background(Color(.tertiarySystemBackground))
.cornerRadius(12)
.shadow(
color: Color(.init(red: 0, green: 0, blue: 0, alpha: 0.16)),
radius: 2
)
}
}
I cannot understand how to apply the height and offset values to the shadow in SwiftUI
You can apply the offset values to the x and y values of the shadow.
VStack(content: content)
.padding()
.frame(maxWidth: .infinity, minHeight: 100, alignment: .top)
.background(Color(.tertiarySystemBackground))
.cornerRadius(12)
.shadow(
color: Color(.init(red: 0, green: 0, blue: 0, alpha: 0.16)),
radius: 2,
y: 3 // This should give you the same effect
)
Heading ##I'm trying to learn the charts and having trouble
Adding consistent space between the slices.
Start the animation in sequence.
The reason, I didn't want the separator as a separate arch is to have both the edges rounded. Adding a separator as another layer overlaps the rounded corners.
Any help or pointers is highly appreciated.
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor)
let pieDataToDisplay = [dataItem(color: .red, percentage: 10),
dataItem(color: .blue, percentage: 20),
dataItem(color: .green, percentage: 25),
dataItem(color: .yellow, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 2.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//Removing the below line works but will not handle touch events :(
shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
// //Calucate percentage based on given values
// public func calculateAngle(percentageVal:Double)-> CGFloat {
// return CGFloat((percentageVal / 100) * 360)
// let val = CGFloat (percentageVal / 100.0)
// return val * 2 * CGFloat.pi
// }
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
var totalSeperatorSpace = Double(items.count) * separatorSpace
var totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return $0 + $1.percentage }
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
angle.0 = angleToStart
angle.1 = endAngle
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle
//print(angle)
}
return angleList
}
}
let container = UIView()
container.frame.size = CGSize(width: 360, height: 360)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 215, height: 215))
m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
UPDATED :
Updated the code to include proper spacing irrespective of single/multiple items on the chart with equal distribution of total spacing, based on a suggestion from #jaferAli
Open Issue: Handling tap gesture on the layer so I can perform custom actions based on the category selected.
Screen 2
UPDATED CODE:
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor, percent: CGFloat)
let pieDataToDisplay = [
dataItem(color: hexStringToUIColor(hex: "#E61628"), percentage: 10),
dataItem(color: hexStringToUIColor(hex: "#50B7FB"), percentage: 20),
dataItem(color: hexStringToUIColor(hex: "#38BE72"), percentage: 25),
dataItem(color: hexStringToUIColor(hex: "#FFAA4C"), percentage: 15),
dataItem(color: hexStringToUIColor(hex: "#B6BE33"), percentage: 30)
]
let pieDataToDisplayWhite = [dataItem(color: .white, percentage: 10),
dataItem(color: .white, percentage: 20),
dataItem(color: .white, percentage: 25),
dataItem(color: .white, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 5.0 { didSet { setNeedsLayout() } }
private var sliceThickness: CGFloat = 10.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem, percent:i.percent)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage / 50)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage / 50)
}
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(seperatorSpace)) { return $0 + $1.percentage }
let spacing = CGFloat(seperatorSpace ) / CGFloat (totalSum)
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * CGFloat.pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angle.3 = item.percentage
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
}
extension USBCircleChart {
#objc func handleTap() {
print("getting tap action")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let loca = touch?.location(in: self) else { return }
let point = self.convert(loca, from: nil)
guard let sublayers = self.layer.sublayers as? [CAShapeLayer] else { return }
for layer in sublayers {
print("checking paths \(point) \(loca) \(layer.path) \n")
if let path = layer.path, path.contains(point) {
print(layer)
}
}
}
}
let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
//m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.center = container.center
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
You need to calculate spacing and then add and Subtract it from start and end angle .. so update your calcualteStartAndEndAngle Method with this one
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return $0 + $1.percentage }
let spacing = CGFloat( totalSeperatorSpace + 1 ) / totalSum
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
It will Result this Animation
and if you want linear Animation then change your animation method
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage)
}
And if you want to learn more you can see this framework RingPieChart
The problem was
1. Re - Calculate the percentages by keeping the spacing percentage.
that is,
//This is to recalculate the percentage by adding the total spacing percentage.
/// Example : The percenatge of each category is recalculated - for instance , lets assume Apple - 60 %,
/// Android - 40 %, now we add Samsung as 10 %, which equates to 110%, To correct this
/// Apple 60 * (100- Samsung) / 100 = 54 %, Android = 36 %, which totals to 100 %.
///
/// - Parameter buffer: total spacing between the splices.
func updatedPercentage(with buffer: CGFloat ) -> CGFloat {
return percentage * (100 - buffer) / 100
}
Once this is done, the total categories + spacings will equate to 100 %.
The only problem left is, for very smaller percentage categories (lesser than spacing percentage), the start angle will be greater than end angle. This is because we are subtracting the spacing from end angle.
there are two options to correct,
a. flip the angles.
if angle.start > angle.end {
let start = angle.start
angle.start = angle.end
angle.end = start
}
b. draw it anti clock wise in Beizer path , only for that slice.
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: angle.start, endAngle: angle.end, clockwise: **angle.start < angle.end**)
this should solve all the problems, i will upload my findings on a GIT repo and publish the link here.
var emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: self.view.frame.size.width / 2, y: -10)
emitter.emitterShape = kCAEmitterLayerLine
emitter.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
emitter.emitterCells = generateEmitterCells()
self.view.layer.addSublayer(emitter)
here, CAEmitterLayer covers my view... the content of self.view not visible..
Ref. code : https://oktapodi.github.io/2017/05/08/particle-effects-in-swift-using-caemitterlayer.html
I want to set this animation on my view.
I don't know if I understand you correct, but if this is the effect you are looking for:
Then you need to:
Add a "container view" for your your emitter to live in
Create an outlet for that view
set clipsToBounds to true for your container view
Here is my ViewController which produced the above screenshot
import UIKit
enum Colors {
static let red = UIColor(red: 1.0, green: 0.0, blue: 77.0/255.0, alpha: 1.0)
static let blue = UIColor.blue
static let green = UIColor(red: 35.0/255.0 , green: 233/255, blue: 173/255.0, alpha: 1.0)
static let yellow = UIColor(red: 1, green: 209/255, blue: 77.0/255.0, alpha: 1.0)
}
enum Images {
static let box = UIImage(named: "Box")!
static let triangle = UIImage(named: "Triangle")!
static let circle = UIImage(named: "Circle")!
static let swirl = UIImage(named: "Spiral")!
}
class ViewController: UIViewController {
#IBOutlet weak var emitterContainer: UIView!
var emitter = CAEmitterLayer()
var colors:[UIColor] = [
Colors.red,
Colors.blue,
Colors.green,
Colors.yellow
]
var images:[UIImage] = [
Images.box,
Images.triangle,
Images.circle,
Images.swirl
]
var velocities:[Int] = [
100,
90,
150,
200
]
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
view.backgroundColor = UIColor.red
emitter.emitterPosition = CGPoint(x: emitterContainer.frame.size.width / 2, y: -10)
emitter.emitterShape = kCAEmitterLayerLine
emitter.emitterSize = CGSize(width: emitterContainer.frame.size.width, height: 2.0)
emitter.emitterCells = generateEmitterCells()
emitterContainer.layer.addSublayer(emitter)
emitterContainer.clipsToBounds = true
}
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 14.0
cell.lifetimeRange = 0
cell.velocity = CGFloat(getRandomVelocity())
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.color = getNextColor(i: index)
cell.contents = getNextImage(i: index)
cell.scaleRange = 0.25
cell.scale = 0.1
cells.append(cell)
}
return cells
}
private func getRandomVelocity() -> Int {
return velocities[getRandomNumber()]
}
private func getRandomNumber() -> Int {
return Int(arc4random_uniform(4))
}
private func getNextColor(i:Int) -> CGColor {
if i <= 4 {
return colors[0].cgColor
} else if i <= 8 {
return colors[1].cgColor
} else if i <= 12 {
return colors[2].cgColor
} else {
return colors[3].cgColor
}
}
private func getNextImage(i:Int) -> CGImage {
return images[i % 4].cgImage!
}
}
Hope that helps you.
It is working fine check output of my simulator. Background images are added from storyboard and blue color is done by code. Still working fine.
OUTPUT:
You can fix your problem by changing the way you add the layer right now your adding it on top of everything which sometimes hide other layers and view objects.
change
self.view.layer.addSublayer(emitter)
To
self.view.layer.insertSublayer(emitter, at: 0)
Hello change emitterPosition X of each like below:-
let emitter1 = Emitter.getEmitter(with: #imageLiteral(resourceName: "img_ribbon_4"), directionInRadian: (180 * (.pi/180)), velocity: 50)
emitter1.emitterPosition = CGPoint(x: self.view.frame.width / 3 , y: 0)
emitter1.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
self.view.layer.addSublayer(emitter1)
let emitter2 = Emitter.getEmitter(with: #imageLiteral(resourceName: "img_ribbon_6"), directionInRadian: (180 * (.pi/180)), velocity: 80)
emitter2.emitterPosition = CGPoint(x: self.view.frame.width / 2, y: 0)
emitter2.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
self.view.layer.addSublayer(emitter2)
I hope it will help you,
thank you.
I apologize if the title is ambiguous but currently, I have a UICollectionView of images. I recently discovered a way to find the dominant colors of a UIImage using DominantColor. With this, I'm trying to change the UICollectionView's background color as the user scrolls from one UIImage cell to the next, i.e. the background color will slowly fade from imageX's dominant color to imageX+1's dominant color.
However, I've spent days on this and am having a very difficult time in changing the background color opacity of the UICollectionView as the cell begins to scroll to the next/prev image.
My UI:
Background code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as UICollectionViewCell
let frame: CGRect = CGRect(x: 0, y: 0, width: imageView.frame.size.width, height: imageView.frame.size.height)
let subView: UIImageView = UIImageView(frame: frame)
subView.image = imagesArray[indexPath.row].theImage
subView.contentMode = UIViewContentMode.scaleAspectFit
cell.addSubview(subView)
return cell
}
// The colorsArray will contain a dominant color for each image in the CollectionView
func dominantColorOfImages()
{
for index in 0..<imagesArray.count
{
if let image = imagesArray[index].theImage
{
let colors: [UIColor] = image.dominantColors()
colorsArray[index] = colors[index]
}
}
// Default dominant background color for first image in CollectionView
imagesCollectionView.backgroundColor = colorsArray[0]
imagesCollectionView.reloadData()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
{
let pageWidth: CGFloat = scrollView.bounds.size.width
let currentPage: Int = Int( floor( (scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
currentImagePage = currentPage
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let pageWidth: CGFloat = scrollView.bounds.size.width
let currentPage: Int = Int( floor( (scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1)
// Horizontal
let maximumHorizontalOffset: CGFloat = scrollView.contentSize.width - scrollView.frame.size.width
let currentHorizontalOffset: CGFloat = scrollView.contentOffset.x
// This is the offset of how far the right side edge of the current image has pass the end of the left screen (i.e. when the user scrolls the image from the right to the left to get to the next image)
let pageOffset: CGFloat = ( (pageWidth * CGFloat(currentImagePage) ) + (pageWidth / 2) ) / maximumHorizontalOffset
if percentageHorizontalOffset < pageOffset
{
imagesCollectionView.backgroundColor = fadeFromColorToColor(fromColor: colorsArray[currentImagePage], toColor: colorsArray[currentImagePage + 1], withPercentage: percentageHorizontalOffset * CGFloat(colorsArray.count - 1 ) )
}
else
{
imagesCollectionView.backgroundColor = fadeFromColorToColor(fromColor: colorsArray[currentImagePage], toColor: colorsArray[currentImagePage + 1], withPercentage: (percentageHorizontalOffset - pageOffset) * CGFloat(colorsArray.count - 1) ))
}
}
func fadeFromColorToColor(fromColor: UIColor, toColor: UIColor, withPercentage: CGFloat) -> UIColor
{
var fromRed: CGFloat = 0.0
var fromGreen: CGFloat = 0.0
var fromBlue: CGFloat = 0.0
var fromAlpha: CGFloat = 0.0
// Get the RGBA values from the colours
fromColor.getRed(&fromRed, green: &fromGreen, blue: &fromBlue, alpha: &fromAlpha)
var toRed: CGFloat = 0.0
var toGreen: CGFloat = 0.0
var toBlue: CGFloat = 0.0
var toAlpha: CGFloat = 0.0
toColor.getRed(&toRed, green: &toGreen, blue: &toBlue, alpha: &toAlpha)
// Calculate the actual RGBA values of the fade colour
let red = (toRed - fromRed) * withPercentage + fromRed;
let green = (toGreen - fromGreen) * withPercentage + fromGreen;
let blue = (toBlue - fromBlue) * withPercentage + fromBlue;
let alpha = (toAlpha - fromAlpha) * withPercentage + fromAlpha;
// Return the fade colour
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
This is following the same answer as provided here: Changing between colors based on UIScrollView's contentOffset, except pageOffset is hard-coded to be 0.5 since that question deals with 2 static pages while I'm dealing with colorsArray.count - 1 pages, so I had to re-calculate the pageOffset differently.
The issue that I'm having is within withPercentage in scrollViewDidScroll.
The color fades fine as I start scrolling from Image1->Image2. But then right as soon Image2 stops, the background color jumps immediately to the color that is suppose to be in Image3 and enters the else statement.
To show example, here's the color opacity from image1->image2 output running on an iPhone 7 simulator:
scrollView.contentSize.width =1125.0
scrollView.frame.size.width =375.0
maximumHorizontalOffset =750.0
currentHorizontalOffset =110.5
pageOffset = 0.5
currentImagePage = 0
percentageHorizontalOffset = 0.147333333333333
IN IF
withPercentage in IF = 0.294666666666667
...
scrollView.contentSize.width =1125.0
scrollView.frame.size.width =375.0
maximumHorizontalOffset =750.0
currentHorizontalOffset =269.0
pageOffset = 0.5
currentImagePage = 0
percentageHorizontalOffset = 0.358666666666667
IN IF
withPercentage in IF = 0.717333333333333
...
scrollView.contentSize.width =1125.0
scrollView.frame.size.width =375.0
maximumHorizontalOffset =750.0
currentHorizontalOffset =374.5
pageOffset = 0.5
currentImagePage = 0
percentageHorizontalOffset = 0.499333333333333
IN IF
withPercentage in IF = 0.998666666666667
scrollView.contentSize.width =1125.0
scrollView.frame.size.width =375.0
maximumHorizontalOffset =750.0
currentHorizontalOffset =375.0
pageOffset = 0.5
currentImagePage = 0
percentageHorizontalOffset = 0.5
IN ELSE
withPercentage in ELSE = 0.0
This is where I know my calculations are off, but I'm racking my brain on how to achieve this.
It is hard to accurately read your code examples, as some vars/identifiers are not defined within the example text, e.g. percentageHorizontalOffset
What if you change the row in the else clause of the scrollViewDidScroll func to:
imagesCollectionView.backgroundColor = fadeFromColorToColor(fromColor: colorsArray[currentImagePage], toColor: colorsArray[currentImagePage], withPercentage: (percentageHorizontalOffset - pageOffset) * CGFloat(colorsArray.count - 1) ))
In fadeFromColorToColor if the percent is negative then swap toColor and fromColor and turn percent positive.
Here's some code ready to be pasted into a playground. Open an assistant editor to play with the collection view. Is that what you are looking for?
import UIKit
import PlaygroundSupport
extension UIColor {
func components() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
}
class Controller: UICollectionViewController {
// some test data instead of images:
let pages = [#colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1), #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1), #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1), #colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1), #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1), #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)]
// these should be your significant colors from the images:
let colorsArray = [#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1), #colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1), #colorLiteral(red: 0.9098039269, green: 0.4784313738, blue: 0.6431372762, alpha: 1), #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1), #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1), #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1)]
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
collectionView?.contentInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = pages[indexPath.item]
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentHorizontalOffset = collectionView!.contentOffset.x + collectionView!.contentInset.left
let pageWidth = scrollView.bounds.size.width
let page = Int(max(0, floor(currentHorizontalOffset / pageWidth)))
// Don't change the background color when scrolling past the last page:
guard page < pages.count - 1 else {
return
}
let partialPageVisibleWidth = currentHorizontalOffset - CGFloat(page) * pageWidth
let ratio = max(0, partialPageVisibleWidth / pageWidth)
let leftColor = colorsArray[page]
let rightColor = colorsArray[page + 1]
scrollView.backgroundColor = blend(leftColor, rightColor, ratio: ratio)
}
/**
Blends two colors by the given ratio
- returns: `lhs` if t == 0; `rhs` if t == 1; otherwise a blend of `lhs` and `rhs` by using a linear interpolation function
*/
func blend(_ lhs: UIColor, _ rhs: UIColor, ratio t: CGFloat) -> UIColor
{
let (r1, g1, b1, a1) = lhs.components()
let (r2, g2, b2, a2) = rhs.components()
// Interpolation function:
func f(_ from: CGFloat, _ to: CGFloat) -> CGFloat {
return from * (1 - t) + to * t
}
// create a new color by interpolating two colors:
return UIColor(
red: f(r1, r2),
green: f(g1, g2),
blue: f(b1, b2),
alpha: f(a2, a2)
)
}
}
// Test code:
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 280, height: 400)
layout.minimumLineSpacing = 40
let controller = Controller(collectionViewLayout: layout)
controller.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
PlaygroundPage.current.liveView = controller.view
Is there is a simple way to implement a color picker popover in swift? Are there any built-in libraries or UI elements that I could leverage for this purpose? I saw some color pickers written in objective-c, but they were several years old and I was wondering if there was something more recent.
Here's one I made which is as simple as it gets. It's just a lightweight UIView that allows you to specify the element size in case you want blocked regions (elementSize > 1). It draws itself in interface builder so you can set element size and see the consequences. Just set one of your views in interface builder to this class and then set yourself as a delegate. It will tell you when someone either taps or drags on it and the uicolor at that location. It will draw itself to its own bounds and there's no need for anything other than this class, no image required.
Element size=1 (Default)
Element size=10
internal protocol HSBColorPickerDelegate : NSObjectProtocol {
func HSBColorColorPickerTouched(sender:HSBColorPicker, color:UIColor, point:CGPoint, state:UIGestureRecognizerState)
}
#IBDesignable
class HSBColorPicker : UIView {
weak internal var delegate: HSBColorPickerDelegate?
let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3
#IBInspectable var elementSize: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
private func initialize() {
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
touchGesture.minimumPressDuration = 0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
for y : CGFloat in stride(from: 0.0 ,to: rect.height, by: elementSize) {
var saturation = y < rect.height / 2.0 ? CGFloat(2 * y) / rect.height : 2.0 * CGFloat(rect.height - y) / rect.height
saturation = CGFloat(powf(Float(saturation), y < rect.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = y < rect.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect.height - y) / rect.height
for x : CGFloat in stride(from: 0.0 ,to: rect.width, by: elementSize) {
let hue = x / rect.width
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y:y, width:elementSize,height:elementSize))
}
}
}
func getColorAtPoint(point:CGPoint) -> UIColor {
let roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
y:elementSize * CGFloat(Int(point.y / elementSize)))
var saturation = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(2 * roundedPoint.y) / self.bounds.height
: 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
saturation = CGFloat(powf(Float(saturation), roundedPoint.y < self.bounds.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
let hue = roundedPoint.x / self.bounds.width
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
}
func getPointForColor(color:UIColor) -> CGPoint {
var hue: CGFloat = 0.0
var saturation: CGFloat = 0.0
var brightness: CGFloat = 0.0
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil);
var yPos:CGFloat = 0
let halfHeight = (self.bounds.height / 2)
if (brightness >= 0.99) {
let percentageY = powf(Float(saturation), 1.0 / saturationExponentTop)
yPos = CGFloat(percentageY) * halfHeight
} else {
//use brightness to get Y
yPos = halfHeight + halfHeight * (1.0 - brightness)
}
let xPos = hue * self.bounds.width
return CGPoint(x: xPos, y: yPos)
}
#objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.began) {
let point = gestureRecognizer.location(in: self)
let color = getColorAtPoint(point: point)
self.delegate?.HSBColorColorPickerTouched(sender: self, color: color, point: point, state:gestureRecognizer.state)
}
}
}
I went ahead and wrote a simple color picker popover in Swift. Hopefully it will help someone else out.
https://github.com/EthanStrider/ColorPickerExample
Based on Joel Teply code (Swift 4), with gray bar on top:
import UIKit
class ColorPickerView : UIView {
var onColorDidChange: ((_ color: UIColor) -> ())?
let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3
let grayPaletteHeightFactor: CGFloat = 0.1
var rect_grayPalette = CGRect.zero
var rect_mainPalette = CGRect.zero
// adjustable
var elementSize: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
touchGesture.minimumPressDuration = 0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
rect_grayPalette = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * grayPaletteHeightFactor)
rect_mainPalette = CGRect(x: 0, y: rect_grayPalette.maxY,
width: rect.width, height: rect.height - rect_grayPalette.height)
// gray palette
for y in stride(from: CGFloat(0), to: rect_grayPalette.height, by: elementSize) {
for x in stride(from: (0 as CGFloat), to: rect_grayPalette.width, by: elementSize) {
let hue = x / rect_grayPalette.width
let color = UIColor(white: hue, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y:y, width:elementSize, height:elementSize))
}
}
// main palette
for y in stride(from: CGFloat(0), to: rect_mainPalette.height, by: elementSize) {
var saturation = y < rect_mainPalette.height / 2.0 ? CGFloat(2 * y) / rect_mainPalette.height : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
saturation = CGFloat(powf(Float(saturation), y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
for x in stride(from: (0 as CGFloat), to: rect_mainPalette.width, by: elementSize) {
let hue = x / rect_mainPalette.width
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y: y + rect_mainPalette.origin.y,
width: elementSize, height: elementSize))
}
}
}
func getColorAtPoint(point: CGPoint) -> UIColor
{
var roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
y:elementSize * CGFloat(Int(point.y / elementSize)))
let hue = roundedPoint.x / self.bounds.width
// main palette
if rect_mainPalette.contains(point)
{
// offset point, because rect_mainPalette.origin.y is not 0
roundedPoint.y -= rect_mainPalette.origin.y
var saturation = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(2 * roundedPoint.y) / rect_mainPalette.height
: 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
saturation = CGFloat(powf(Float(saturation), roundedPoint.y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
}
// gray palette
else{
return UIColor(white: hue, alpha: 1.0)
}
}
#objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
let point = gestureRecognizer.location(in: self)
let color = getColorAtPoint(point: point)
self.onColorDidChange?(color)
}
}
Usage:
let colorPickerView = ColorPickerView()
colorPickerView.onColorDidChange = { [weak self] color in
DispatchQueue.main.async {
// use picked color for your needs here...
self?.view.backgroundColor = color
}
}
// add it to some view and set constraints
...
Swift 3.0 version of #joel-teply's answer:
internal protocol HSBColorPickerDelegate : NSObjectProtocol {
func HSBColorColorPickerTouched(sender:HSBColorPicker, color:UIColor, point:CGPoint, state:UIGestureRecognizerState)
}
#IBDesignable
class HSBColorPicker : UIView {
weak internal var delegate: HSBColorPickerDelegate?
let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3
#IBInspectable var elementSize: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
private func initialize() {
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
touchGesture.minimumPressDuration = 0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
for y in stride(from: (0 as CGFloat), to: rect.height, by: elementSize) {
var saturation = y < rect.height / 2.0 ? CGFloat(2 * y) / rect.height : 2.0 * CGFloat(rect.height - y) / rect.height
saturation = CGFloat(powf(Float(saturation), y < rect.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = y < rect.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect.height - y) / rect.height
for x in stride(from: (0 as CGFloat), to: rect.width, by: elementSize) {
let hue = x / rect.width
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y:y, width:elementSize,height:elementSize))
}
}
}
func getColorAtPoint(point:CGPoint) -> UIColor {
let roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
y:elementSize * CGFloat(Int(point.y / elementSize)))
var saturation = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(2 * roundedPoint.y) / self.bounds.height
: 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
saturation = CGFloat(powf(Float(saturation), roundedPoint.y < self.bounds.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
let hue = roundedPoint.x / self.bounds.width
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
}
func getPointForColor(color:UIColor) -> CGPoint {
var hue:CGFloat=0;
var saturation:CGFloat=0;
var brightness:CGFloat=0;
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil);
var yPos:CGFloat = 0
let halfHeight = (self.bounds.height / 2)
if (brightness >= 0.99) {
let percentageY = powf(Float(saturation), 1.0 / saturationExponentTop)
yPos = CGFloat(percentageY) * halfHeight
} else {
//use brightness to get Y
yPos = halfHeight + halfHeight * (1.0 - brightness)
}
let xPos = hue * self.bounds.width
return CGPoint(x: xPos, y: yPos)
}
func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
let point = gestureRecognizer.location(in: self)
let color = getColorAtPoint(point: point)
self.delegate?.HSBColorColorPickerTouched(sender: self, color: color, point: point, state:gestureRecognizer.state)
}
}
With iOS 14 Apple has now implemented a standard UIColorPickerViewController and associated UIColorWell that is a color swatch that automatically brings up the UIColorPicker to choose a color.
You can test the ColorPicker by creating a Swift App project with Xcode 12 or later, targeting iOS 14+ and then try this simple code:
import SwiftUI
struct ContentView: View {
#State private var bgColor = Color.white
var body: some View {
VStack {
ColorPicker("Set the background color",
selection: $bgColor,
supportsOpacity: true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(bgColor)
}
}
Change supportsOpacity to false to get rid of the opacity slider and only allow fully opaque colors.
ColorPicker showing two different modes of selection:
ColorPicker without alpha:
Thanks for the starting point.
I took it from there and wrote a complte Color PickerViewController with a custom UIView and some drawing code.
I made the custom UIView #IBDesignable so it can be rendered in InterfaceBuilder.
https://github.com/Christian1313/iOS_Swift_ColorPicker
I quickly added code to implement a color picker for a ViewController using Apples color picker released in IOS 14. Make sure your deployment info is at least IOS 14.0 or better. See https://developer.apple.com/documentation/uikit/uicolorpickerviewcontroller for reference.
Add the color picker delegate to the class, and declare a color picker object :
class ViewController: UIViewController, UIColorPickerViewControllerDelegate {
let colorPicker = UIColorPickerViewController()
at the very end of viewDidLoad, I set the colorPicker delegate to self
colorPicker.delegate = self
} // ends viewDidLoad
I use a single color picker to choose colors for several different objects. When I present the color picker to the user, I set a boolean flag to true to indicate the reason why the color picker is being displayed.
resetAllColorChangeFlags() // First make sure all the booleans are false for robust design
changingScreenBackgroundColor = true
present(colorPicker, animated: true, completion: nil)
I added the APIs to handle colorPickerViewControllerDidSelectColor and colorPickerViewControllerDidFinish
colorPickerViewControllerDidSelectColor checks the boolean flags, and sets a color property for the appropriate object. If the color is being applied to a border color in a layer, cgColor is used.
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
if changingScreenBackgroundColor
{
self.view.backgroundColor = viewController.selectedColor
}
if addingATintCircle
{
pointerToThisTintView.backgroundColor = viewController.selectedColor
}
if changingTextBackgroundColor
{
pointerToTextObjectSelected.backgroundColor = viewController.selectedColor
}
if changingTextColor
{
pointerToTextObjectSelected.textColor = viewController.selectedColor
}
if changingTextBorderColor
{
pointerToTextObjectSelected.layer.borderColor = viewController.selectedColor.cgColor
}
if changingPhotoBorderColor
{
pointerToPhotoObjectSelected.layer.borderColor = viewController.selectedColor.cgColor
}
if changingCameraBorderColor {
cameraView.layer.borderColor = viewController.selectedColor.cgColor
}
} // ends colorPickerViewControllerDidSelectColor
colorPickerViewControllerDidFinish is used just to reset all of my booleans flags that indicate why the color picker has been presented to the user.
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController)
{
resetAllColorChangeFlags()
} // ends colorPickerViewControllerDidFinish
Here is my reset routine :
func resetAllColorChangeFlags()
{
changingFunTextColor = false
changingFunTextFirstColorForGradient = false
changingFunTextSecondColorForGradient = false
changingScreenBackgroundColor = false
changingTextBackgroundColor = false
changingTextColor = false
changingTextBorderColor = false
changingPhotoBorderColor = false
changingCameraBorderColor = false
addingATintToAPhotoObject = false
addingATintCircle = false
addingAColorForGradients = false
} // ends resetAllColorChangeFlags
ColorPickerViewImage
Based on Christian1313 answer, I added darker colors:
#IBDesignable final public class SwiftColorView: UIView {
weak var colorSelectedDelegate: ColorDelegate?
#IBInspectable public var numColorsX:Int = 10 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable public var numColorsY:Int = 18 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable public var coloredBorderWidth:Int = 10 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable public var showGridLines:Bool = false {
didSet {
setNeedsDisplay()
}
}
weak var delegate: SwiftColorPickerDataSource?
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
colorSelectedDelegate?.setStroke(color: colorAtPoint(point: location))
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
colorSelectedDelegate?.setStroke(color: colorAtPoint(point: location))
}
public override func draw(_ rect: CGRect) {
super.draw(rect)
let lineColor = UIColor.gray
let pS = patternSize()
let w = pS.w
let h = pS.h
for y in 0..<numColorsY
{
for x in 0..<numColorsX
{
let path = UIBezierPath()
let start = CGPoint(x: CGFloat(x)*w+CGFloat(coloredBorderWidth), y: CGFloat(y)*h+CGFloat(coloredBorderWidth))
path.move(to: start);
path.addLine(to: CGPoint(x: start.x+w, y: start.y))
path.addLine(to: CGPoint(x: start.x+w, y: start.y+h))
path.addLine(to: CGPoint(x: start.x, y: start.y+h))
path.addLine(to: start)
path.lineWidth = 0.25
colorForRectAt(x: x,y:y).setFill();
if (showGridLines)
{
lineColor.setStroke()
}
else
{
colorForRectAt(x: x, y: y).setStroke();
}
path.fill();
path.stroke();
}
}
}
private func colorForRectAt(x: Int, y: Int) -> UIColor
{
if let ds = delegate {
return ds.colorForPalletIndex(x: x, y: y, numXStripes: numColorsX, numYStripes: numColorsY)
} else {
var hue:CGFloat = CGFloat(x) / CGFloat(numColorsX)
var fillColor = UIColor.white
if (y==0)
{
if (x==(numColorsX-1))
{
hue = 1.0;
}
fillColor = UIColor(white: hue, alpha: 1.0);
}
else
{
if y < numColorsY / 2 {
//dark
let length = numColorsY / 2
let brightness: CGFloat = CGFloat(y) / CGFloat(length)
fillColor = UIColor(hue: hue, saturation: 1.0, brightness: brightness, alpha: 1.0)
} else if y == numColorsY / 2 {
// normal
fillColor = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
} else {
// light
let length = numColorsY / 2 - 1
let offset = y - length - 1
let sat:CGFloat = CGFloat(1.0) - CGFloat(offset) / CGFloat(length + 1)
print("sat", sat)
fillColor = UIColor(hue: hue, saturation: sat, brightness: 1.0, alpha: 1.0)
}
}
return fillColor
}
}
func colorAtPoint(point: CGPoint) -> UIColor
{
let pS = patternSize()
let w = pS.w
let h = pS.h
let x = (point.x-CGFloat(coloredBorderWidth))/w
let y = (point.y-CGFloat(coloredBorderWidth))/h
return colorForRectAt(x: Int(x), y:Int(y))
}
private func patternSize() -> (w: CGFloat, h:CGFloat)
{
let width = self.bounds.width-CGFloat(2*coloredBorderWidth)
let height = self.bounds.height-CGFloat(2*coloredBorderWidth)
let w = width/CGFloat(numColorsX)
let h = height/CGFloat(numColorsY)
return (w,h)
}
public override func prepareForInterfaceBuilder()
{
print("Compiled and run for IB")
}
}
using Michael Ros answer,
If you want to use this view by objective-c viewcontroller you can simply create a new swift file called ColorPickerView and add a uiview to your viewcontroller on the storyboard and select ColorPickerView as it's class name. then make your view controller a notification observer of name #"colorIsPicked"
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateColor) name:#"colorIsPicked" object:nil];
Code for ColorPickerView.swift
class ColorPickerView : UIView {
#objc public lazy var onColorDidChange: ((_ color: UIColor) -> ()) = {
//send a notification for the caller view to update its elements if necessery
NotificationCenter.default.post(name: Notification.Name("colorIsPicked"), object: nil)
}
let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3
let grayPaletteHeightFactor: CGFloat = 0.1
var rect_grayPalette = CGRect.zero
var rect_mainPalette = CGRect.zero
// adjustable
var elementSize: CGFloat = 10.0 {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
touchGesture.minimumPressDuration = 0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
rect_grayPalette = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * grayPaletteHeightFactor)
rect_mainPalette = CGRect(x: 0, y: rect_grayPalette.maxY,
width: rect.width, height: rect.height - rect_grayPalette.height)
// gray palette
for y in stride(from: CGFloat(0), to: rect_grayPalette.height, by: elementSize) {
for x in stride(from: (0 as CGFloat), to: rect_grayPalette.width, by: elementSize) {
let hue = x / rect_grayPalette.width
let color = UIColor(white: hue, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y:y, width:elementSize, height:elementSize))
}
}
// main palette
for y in stride(from: CGFloat(0), to: rect_mainPalette.height, by: elementSize) {
var saturation = y < rect_mainPalette.height / 2.0 ? CGFloat(2 * y) / rect_mainPalette.height : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
saturation = CGFloat(powf(Float(saturation), y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
for x in stride(from: (0 as CGFloat), to: rect_mainPalette.width, by: elementSize) {
let hue = x / rect_mainPalette.width
let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
context!.setFillColor(color.cgColor)
context!.fill(CGRect(x:x, y: y + rect_mainPalette.origin.y,
width: elementSize, height: elementSize))
}
}
}
func getColorAtPoint(point: CGPoint) -> UIColor
{
var roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
y:elementSize * CGFloat(Int(point.y / elementSize)))
let hue = roundedPoint.x / self.bounds.width
// main palette
if rect_mainPalette.contains(point)
{
// offset point, because rect_mainPalette.origin.y is not 0
roundedPoint.y -= rect_mainPalette.origin.y
var saturation = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(2 * roundedPoint.y) / rect_mainPalette.height
: 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
saturation = CGFloat(powf(Float(saturation), roundedPoint.y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
let brightness = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
}
// gray palette
else{
return UIColor(white: hue, alpha: 1.0)
}
}
#objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
let point = gestureRecognizer.location(in: self)
let color = getColorAtPoint(point: point)
self.onColorDidChange(color)
}
}