Unable to get pixel color from an image in SwiftUI - swift

I want to design a draggable color selector view that will select the color of the present pixel. My strategy is, first, to choose the position of the pixel and then, second, get the pixel color. The position has been correctly changed while dragging the view. The color of the view has also been changed but don't understand its manner of changing. I think it doesn't returns the proper color of its pixel. Additionally, when x or y of the CGImage coordinate is less then 0, the getPixelColor() returns 'UIExtendedSRGBColorSpace 0 0 0 0'.
After a long search, I have not get any proper solution to get pixel color in SwiftUI.
The program is given below:
struct ColorSelectorView: View {
#Binding var position: CGPoint
#State private var isDragging = false
#State private var colorPicked = UIColor.black
var image: UIImage?
var colorPickedCallBack : (UIColor) -> ()
var body: some View {
colorSelectorView()
.onAppear {
colorPicked = getPixelColor(image: image, point: position) ?? .clear
}
.gesture(DragGesture()
.onChanged { value in
self.position.x += CGFloat(Int(value.location.x - value.startLocation.x))
self.position.y += CGFloat(Int(value.location.y - value.startLocation.y))
colorPicked = getPixelColor(image: image, point: position) ?? .clear
}
.onEnded({ value in
colorPickedCallBack(colorPicked)
})
)
.offset(x: position.x, y: position.y)
}
}
func getPixelColor(image: UIImage, point: CGPoint) -> UIColor? {
guard let pixelData = image.cgImage?.dataProvider?.data else { return nil }
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
let pixelInfo: Int = Int((image.size.width * point.y + point.x) * 4.0)
let i = Array(0 ... 3).map { CGFloat(data[pixelInfo + $0]) / CGFloat(255) }
return UIColor(red: i[0], green: i[1], blue: i[2], alpha: 1)
}
}

Related

Why animation is speeding up after changing a property? SwiftUI

I tried to do this Particle Animation and wanted to change the color property. Every time I do it, the animation speeds up. How can I prevent this from happening? I'm kinda new to this, so is there a better approach?
This is my Emitter
struct EmitterView: View {
private struct ParticleView: View {
let image: Image
#State private var isActive = false
let position: ParticleState<CGPoint>
let opacity: ParticleState<Double>
let rotation: ParticleState<Angle>
let scale: ParticleState<CGFloat>
var body: some View {
image
.opacity(isActive ? opacity.end : opacity.start)
.scaleEffect(isActive ? scale.end : scale.start)
.rotationEffect(isActive ? rotation.end : rotation.start)
.position(isActive ? position.end : position.start)
.onAppear{self.isActive = true}
}
}
private struct ParticleState<T> {
var start: T
var end: T
init(_ start: T, _ end: T) {
self.start = start
self.end = end
}
}
var images: [String]
var particleCount: Int
var creationPoint = UnitPoint.center
var creationRange = CGSize.zero
var colors = [Color.white]
var blendMode = BlendMode.normal
var angle = Angle.zero
var angleRange = Angle.zero
var opacity = 1.0
var opacityRange = 0.0
var opacitySpeed = 0.0
var rotation = Angle.zero
var rotationRange = Angle.zero
var rotationSpeed = Angle.zero
var scale: CGFloat = 1
var scaleRange: CGFloat = 0
var scaleSpeed: CGFloat = 0
var speed = 0.0
var speedRange = 0.0
var animation = Animation.linear.repeatForever(autoreverses: false)
var animationDelayTreshold = 0.0
var body: some View {
GeometryReader { geo in
ZStack {
ForEach(0..<self.particleCount, id: \.self) { i in
ParticleView(
image: Image(images.randomElement()!),
position: self.position(in: geo),
opacity: self.makeOpacity(),
rotation: self.makeRotation(),
scale: self.makeScale()
)
.animation(self.animation.delay(Double.random(in: 0...self.animationDelayTreshold)))
.colorMultiply(self.colors.randomElement() ?? .white)
.blendMode(self.blendMode)
}
}
}
}
private func position(in proxy: GeometryProxy) -> ParticleState<CGPoint> {
let halfCreationRangeWidth = creationRange.width / 2
let halfCreationRangeHeight = creationRange.height / 2
let creationOffsetX = CGFloat.random(in: -halfCreationRangeWidth...halfCreationRangeWidth)
let creationOffsetY = CGFloat.random(in: -halfCreationRangeHeight...halfCreationRangeHeight)
let startX = (proxy.size.width * (creationPoint.x + creationOffsetX))
let startY = (proxy.size.height * (creationPoint.y + creationOffsetY))
let start = CGPoint(x: startX, y: startY)
let halfSpeedRange = speedRange / 2
let actualSpeed = Double.random(in: speed - halfSpeedRange...speed + halfSpeedRange)
let halfAngleRange = angleRange.radians / 2
let totalRange = Double.random(in: angle.radians - halfAngleRange...angle.radians + halfAngleRange)
let finalX = cos(totalRange - .pi / 2) * actualSpeed
let finalY = sin(totalRange - .pi / 2) * actualSpeed
let end = CGPoint(x: Double(startX) + finalX, y: Double(startY) + finalY)
return ParticleState(start, end)
}
private func makeOpacity() -> ParticleState<Double> {
let halfOpacityRange = opacity / 2
let randomOpacity = Double.random(in: -halfOpacityRange...halfOpacityRange)
return ParticleState(opacity + randomOpacity, opacity + opacitySpeed + randomOpacity)
}
private func makeScale() -> ParticleState<CGFloat> {
let halfScaleRange = scaleRange / 2
let randomScale = CGFloat.random(in: -halfScaleRange...halfScaleRange)
return ParticleState(scale + randomScale, scale + scaleSpeed + randomScale)
}
private func makeRotation() -> ParticleState<Angle> {
let halfRotationRange = (rotationRange / 2).radians
let randomRotation = Double.random(in: -halfRotationRange...halfRotationRange)
let randomRotationAngle = Angle(radians: randomRotation)
return ParticleState(rotation + randomRotationAngle, rotation + rotationSpeed + randomRotationAngle)
}
mutating func makeRed() {
colors = [.red]
}
}
And this is how I implemented it
import SwiftUI
struct ContentView: View {
#State var emitter = EmitterView(images: ["spark"], particleCount: 200, creationRange: CGSize(width: 0.4, height: 0.2), colors: [.white], blendMode: .screen, angle: .degrees(0), angleRange: .degrees(360), opacityRange: 0, opacitySpeed: 15, scale: 0.5, scaleRange: 0.2, scaleSpeed: -0.2, speed: 50, speedRange: 120, animation: Animation.linear(duration: 1).repeatForever(autoreverses: false), animationDelayTreshold: 1)
var body: some View {
ZStack {
emitter
.ignoresSafeArea()
}
.background(.black)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.onTapGesture {
emitter.makeRed()
}
}
}
I also tried with transaction, but I couldn't make it work, the animation won't restart.
View should be in body, animation should be joined to corresponding trigger state.
Find below fixed parts. Tested with Xcode 13.4 / iOS 15.5
#State var colors = [Color.white] // data !!
var body: some View {
ZStack {
// view is here !!
EmitterView(images: ["spark"], particleCount: 200, creationRange: CGSize(width: 0.4, height: 0.2), colors: colors, blendMode: .screen, angle: .degrees(0), angleRange: .degrees(360), opacityRange: 0, opacitySpeed: 15, scale: 0.5, scaleRange: 0.2, scaleSpeed: -0.2, speed: 50, speedRange: 120, animation: Animation.linear(duration: 1).repeatForever(autoreverses: false), animationDelayTreshold: 1)
.ignoresSafeArea()
}
.background(.black)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.onTapGesture {
colors = [.red] // update !!
}
}
and animation where is trigger
private struct ParticleView: View {
let image: Image
#State private var isActive = false
let position: ParticleState<CGPoint>
let opacity: ParticleState<Double>
let rotation: ParticleState<Angle>
let scale: ParticleState<CGFloat>
var animation: Animation
var delayTreshold = 0.0
var body: some View {
image
.opacity(isActive ? opacity.end : opacity.start)
.scaleEffect(isActive ? scale.end : scale.start)
.rotationEffect(isActive ? rotation.end : rotation.start)
.position(isActive ? position.end : position.start)
// here is animation, depends on isActive !!
.animation(self.animation.delay(Double.random(in: 0...self.delayTreshold)), value: isActive)
.onAppear{self.isActive = true}
}
}
Complete test module is here

SwiftUI, apply drawGesture only when over Image

I'm try to apply the gesture only when the user is over the Image display not when tapped outside the image.
Any suggestion how I can do? this following code draw also when user tap outside the image.
struct ContentView: View {
#StateObject var am = AppManager()
#State var switchToTakePicture = false
#State var paths: [PathContainer] = []
#State var currentDraggingId = UUID()
#State var spikoPic = #imageLiteral(resourceName: "spiko-16.jpeg")
#State var centerOFimage = CGSize(width: 0, height: 0)
var body: some View {
GeometryReader { proxy in
ZStack {
Image(uiImage: spikoPic)
.resizable()
.scaledToFit()
.position(x: proxy.size.width/2, y: proxy.size.height/2)
.background(GeometryReader {
Color.clear.preference(key: ViewRectKey.self,
value: [$0.frame(in: .global)])
})
.gesture(drawGesture) // not correct
ForEach(paths) { container in
// draw and set the foreground color of the paths to red
container.path
.stroke(Color.red, lineWidth: 4)
}
}
.onPreferenceChange(ViewRectKey.self) { rects in
print(rects.first ?? .zero)
}
}
}
var drawGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
// The point that the gesture started from
let start = value.startLocation
// The point that the gesture ended to
let end = value.location
// the properties of the rectangle to be drawn
let rectangle: CGRect = .init(origin: end,
size: .init(width: start.x - end.x,
height: start.y - end.y))
// create a path for the rectangle
let path: Path = .init { path in
path.addRect(rectangle)
}
print("rettangolo = \(rectangle) orig \(rectangle.origin) - height \(rectangle.height) width = \(rectangle.width)")
// remove the previous rectangle that was drawen in current
// process of drawing
paths.removeAll { $0.id == currentDraggingId }
// append the new rectangle
paths.append(.init(id: currentDraggingId, path: path))
}
.onEnded { _ in
// renew the dragging id so the app know that the next
// drag gesture is drawing a completely new rectangle,
// and is not continuing the drawing of the last rectangle
currentDraggingId = .init()
}
}
}
i want no box outside

Swiftui getting an image's displaying dimensions

I'm trying to get the dimensions of a displayed image to draw bounding boxes over the text I have recognized using apple's Vision framework.
So I run the VNRecognizeTextRequest uppon the press of a button with this funcion
func readImage(image:NSImage, completionHandler:#escaping(([VNRecognizedText]?,Error?)->()), comp:#escaping((Double?,Error?)->())) {
var recognizedTexts = [VNRecognizedText]()
var rr = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let requestHandler = VNImageRequestHandler(cgImage: image.cgImage(forProposedRect: &rr, context: nil, hints: nil)!
, options: [:])
let textRequest = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { completionHandler(nil,error)
return
}
for currentObservation in observations {
let topCandidate = currentObservation.topCandidates(1)
if let recognizedText = topCandidate.first {
recognizedTexts.append(recognizedText)
}
}
completionHandler(recognizedTexts,nil)
}
textRequest.recognitionLevel = .accurate
textRequest.recognitionLanguages = ["es"]
textRequest.usesLanguageCorrection = true
textRequest.progressHandler = {(request, value, error) in
comp(value,nil)
}
try? requestHandler.perform([textRequest])
}
and compute the bounding boxes offsets using this struct and function
struct DisplayingRect:Identifiable {
var id = UUID()
var width:CGFloat = 0
var height:CGFloat = 0
var xAxis:CGFloat = 0
var yAxis:CGFloat = 0
init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
self.width = width
self.height = height
self.xAxis = xAxis
self.yAxis = yAxis
}
}
func createBoundingBoxOffSet(recognizedTexts:[VNRecognizedText], image:NSImage) -> [DisplayingRect] {
var rects = [DisplayingRect]()
let imageSize = image.size
let imageTransform = CGAffineTransform.identity.scaledBy(x: imageSize.width, y: imageSize.height)
for obs in recognizedTexts {
let observationBounds = try? obs.boundingBox(for: obs.string.startIndex..<obs.string.endIndex)
let rectangle = observationBounds?.boundingBox.applying(imageTransform)
print("Rectange: \(rectangle!)")
let width = rectangle!.width
let height = rectangle!.height
let xAxis = rectangle!.origin.x - imageSize.width / 2 + rectangle!.width / 2
let yAxis = -(rectangle!.origin.y - imageSize.height / 2 + rectangle!.height / 2)
let rect = DisplayingRect(width: width, height: height, xAxis: xAxis, yAxis: yAxis)
rects.append(rect)
}
return(rects)
}
I place the rects using this code in the ContentView
ZStack{
Image(nsImage: self.img!)
.scaledToFit()
ForEach(self.rects) { rect in
Rectangle()
.fill(Color.init(.sRGB, red: 1, green: 0, blue: 0, opacity: 0.2))
.frame(width: rect.width, height: rect.height)
.offset(x: rect.xAxis, y: rect.yAxis)
}
}
If I use the original's image dimensions I get these results
But if I add
Image(nsImage: self.img!)
.resizable()
.scaledToFit()
I get these results
Is there a way to get the image dimensions and pass them and get the proper size of the image being displayed? I also need this because I can't show the whole image sometimes and need to scale it.
Thanks a lot
I would use GeometryReader on background so it reads exactly size of image, as below
#State var imageSize: CGSize = .zero // << or initial from NSImage
...
Image(nsImage: self.img!)
.resizable()
.scaledToFit()
.background(rectReader())
// ... somewhere below
private func rectReader() -> some View {
return GeometryReader { (geometry) -> Color in
let imageSize = geometry.size
DispatchQueue.main.async {
print(">> \(imageSize)") // use image actual size in your calculations
self.imageSize = imageSize
}
return .clear
}
}
Rather than pass in the frame to every view, Apple elected to give you a separate GeometryReader view that gets its frame passed in as a parameter to its child closure.
struct Example: View {
var body: some View {
GeometryReader { geometry in
Image(systemName: "check")
.onAppear {
print(geometry.frame(in: .local))
}
}
}
}

Color animation

I've written simple animations for drawing rectangles in lines, we can treat them as a bars.
Each bar is one shape layer which has a path which animates ( size change and fill color change ).
#IBDesignable final class BarView: UIView {
lazy var pathAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
let red = UIColor(red: 249/255, green: 26/255, blue: 26/255, alpha: 1)
let orange = UIColor(red: 1, green: 167/255, blue: 463/255, alpha: 1)
let green = UIColor(red: 106/255, green: 239/255, blue: 47/255, alpha: 1)
lazy var backgroundColorAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "fillColor")
animation.duration = 1
animation.fromValue = red.cgColor
animation.byValue = orange.cgColor
animation.toValue = green.cgColor
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
#IBInspectable var spaceBetweenBars: CGFloat = 10
var numberOfBars: Int = 5
let data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
override func awakeFromNib() {
super.awakeFromNib()
initSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
func setupLayers() {
let width = bounds.width - (spaceBetweenBars * CGFloat(numberOfBars + 1)) // There is n + 1 spaces between bars.
let barWidth: CGFloat = width / CGFloat(numberOfBars)
let scalePoint: CGFloat = bounds.height / 10.0 // 10.0 - 10 points is max
guard let sublayers = layer.sublayers as? [CAShapeLayer] else { return }
for i in 0...numberOfBars - 1 {
let barHeight: CGFloat = scalePoint * data[i]
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
var xPos: CGFloat!
if i == 0 {
xPos = spaceBetweenBars
} else if i == numberOfBars - 1 {
xPos = bounds.width - (barWidth + spaceBetweenBars)
} else {
xPos = barWidth * CGFloat(i) + spaceBetweenBars * CGFloat(i) + spaceBetweenBars
}
let startPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: 0)).cgPath
let endPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: -barHeight)).cgPath
sublayers[i].path = startPath
pathAnimation.toValue = endPath
sublayers[i].removeAllAnimations()
sublayers[i].add(pathAnimation, forKey: "path")
sublayers[i].add(backgroundColorAnimation, forKey: "backgroundColor")
}
}
func initSublayers() {
for _ in 1...numberOfBars {
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
}
}
}
The size ( height ) of bar depends of the data array, each sublayers has a different height. Based on this data I've crated a scale.
PathAnimation is changing height of the bars.
BackgroundColorAnimation is changing the collors of the path. It starts from red one, goes through the orange and finish at green.
My goal is to connect backgroundColorAnimation with data array as well as it's connected with pathAnimation.
Ex. When in data array is going to be value 1.0 then the bar going to be animate only to the red color which is a derivated from a base red color which is declared as a global variable. If the value in the data array going to be ex. 4.5 then the color animation will stop close to the delcared orange color, the 5.0 limit going to be this orange color or color close to this. Value closer to 10 going to be green.
How could I connect these conditions with animation properties fromValue, byValue, toValue. Is it an algorithm for that ? Any ideas ?
You have several problems.
You're setting fillMode and isRemovedOnCompletion. This tells me, to be blunt, that you don't understand Core Animation. You need to watch WWDC 2011 Session 421: Core Animation Essentials.
You're adding more layers every time layoutSubviews is called, but not doing anything with them.
You're adding animation every time layoutSubviews runs. Do you really want to re-animate the bars when the double-height “in-call” status bar appears or disappears, or on an interface rotation? It's probably better to have a separate animateBars() method, and call it from your view controller's viewDidAppear method.
You seem to think byValue means “go through this value on the way from fromValue to toValue”, but that's not what it means. byValue is ignored in your case, because you're setting fromValue and toValue. The effects of byValue are explained in Setting Interpolation Values.
If you want to interpolate between colors, it's best to use a hue-based color space, but I believe Core Animation uses an RGB color space. So you should use a keyframe animation to specify intermediate colors that you calculate by interpolating in a hue-based color space.
Here's a rewrite of BarView that fixes all these problems:
#IBDesignable final class BarView: UIView {
#IBInspectable var spaceBetweenBars: CGFloat = 10
var data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
var maxDatum = CGFloat(10)
func animateBars() {
guard window != nil else { return }
let bounds = self.bounds
var flatteningTransform = CGAffineTransform.identity.translatedBy(x: 0, y: bounds.size.height).scaledBy(x: 1, y: 0.001)
let duration: CFTimeInterval = 1
let frames = Int((duration * 60.0).rounded(.awayFromZero))
for (datum, barLayer) in zip(data, barLayers) {
let t = datum / maxDatum
if let path = barLayer.path {
let path0 = path.copy(using: &flatteningTransform)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
pathAnimation.fromValue = path0
barLayer.add(pathAnimation, forKey: pathAnimation.keyPath)
let colors = gradient.colors(from: 0, to: t, count: frames).map({ $0.cgColor })
let colorAnimation = CAKeyframeAnimation(keyPath: "fillColor")
colorAnimation.timingFunction = pathAnimation.timingFunction
colorAnimation.duration = duration
colorAnimation.values = colors
barLayer.add(colorAnimation, forKey: colorAnimation.keyPath)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
createOrDestroyBarLayers()
let bounds = self.bounds
let barSpacing = (bounds.size.width - spaceBetweenBars) / CGFloat(data.count)
let barWidth = barSpacing - spaceBetweenBars
for ((offset: i, element: datum), barLayer) in zip(data.enumerated(), barLayers) {
let t = datum / maxDatum
let barHeight = t * bounds.size.height
barLayer.frame = bounds
let rect = CGRect(x: spaceBetweenBars + CGFloat(i) * barSpacing, y: bounds.size.height, width: barWidth, height: -barHeight)
barLayer.path = CGPath(rect: rect, transform: nil)
barLayer.fillColor = gradient.color(at: t).cgColor
}
}
private let gradient = Gradient(startColor: .red, endColor: .green)
private var barLayers = [CAShapeLayer]()
private func createOrDestroyBarLayers() {
while barLayers.count < data.count {
barLayers.append(CAShapeLayer())
layer.addSublayer(barLayers.last!)
}
while barLayers.count > data.count {
barLayers.removeLast().removeFromSuperlayer()
}
}
}
private extension UIColor {
var hsba: [CGFloat] {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return [hue, saturation, brightness, alpha]
}
}
private struct Gradient {
init(startColor: UIColor, endColor: UIColor) {
self.startColor = startColor
self.startHsba = startColor.hsba
self.endColor = endColor
self.endHsba = endColor.hsba
}
let startColor: UIColor
let endColor: UIColor
let startHsba: [CGFloat]
let endHsba: [CGFloat]
func color(at t: CGFloat) -> UIColor {
let out = zip(startHsba, endHsba).map { $0 * (1.0 - t) + $1 * t }
return UIColor(hue: out[0], saturation: out[1], brightness: out[2], alpha: out[3])
}
func colors(from t0: CGFloat, to t1: CGFloat, count: Int) -> [UIColor] {
var colors = [UIColor]()
colors.reserveCapacity(count)
for i in 0 ..< count {
let s = CGFloat(i) / CGFloat(count - 1)
let t = t0 * (1 - s) + t1 * s
colors.append(color(at: t))
}
return colors
}
}
Result:

How can I animate the properties of my custom UIView?

I frequently need to round only two corners in a view, and sometimes need to use gradients. I've found that the common solution of using a CALayerMask is detrimental to performance, so I devised my own solution overriding drawRect(rect: CGRect). It works well, providing an easy way to round some or all corners, draw a border, and use both linear and radial gradient fills, even being able to set color stops for the gradients.
Unfortunately, when I try to animate these properties with UIView.animateWithDuration, my corners, gradients, and borders don't animate. Rather, they look "stretched" in the initial state, then animate to the final state. I've read that this can be solved with CALayer animation, but I'm not quite clear on the nature of the problem. Is there a way I can solve this as the class is now? If not, when is drawRect(rect: CGRect) preferable to drawLayer(layer: CALayer, inContext ctx: CGContext)?
I'm also open to general suggestions on improving this class.
AppocalypseUI.swift (provides support functions for UI operations)
//
// AppocalypseUI.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
class AppocalypseUI: NSObject
{
/// Generates an array of CGFloat values ranging from 0.0-1.0 which represent the color stops in a gradient
class func makeLinearColorStops(numStops:Int) -> [CGFloat]
{
assert(numStops >= 2, "Must have at least two color stops.")
let stepIncrement = 1.0/Double(numStops-1)
var returnArr : [CGFloat] = []
// The first stop is always 0
returnArr += [0.0]
for i in 1 ..< numStops-1
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
returnArr += [stepFactor]
}
// The last stop is always 1
returnArr += [1.0]
// Fini
return returnArr
}
/// Returns the stop colors in an array
class func colorsAlongArray(colorArr:[UIColor], steps:Int) -> [UIColor]
{
let arrCount = colorArr.count
let stepIncrement = Double(arrCount)/Double(steps)
var returnArr : [UIColor] = []
for i in 0..<steps
{
let stepVal = stepIncrement*Double(i)
let stepFactor = CGFloat(fmod(stepVal, 1.0))
let stepIndex1 = Int(floor(stepVal/1.0))
var stepIndex2 = Int(ceil(stepVal/1.0))
if(stepIndex2 > arrCount-1)
{stepIndex2 = arrCount-1}
let color1 = colorArr[stepIndex1]
let color2 = colorArr[stepIndex2]
let color = colorByInterpolatingColors(color1, color2: color2, factor: stepFactor)
returnArr += [color]
}
return returnArr
}
/// Returns a color between two colors on a gradient
class func colorByInterpolatingColors(color1:UIColor, color2:UIColor, factor:CGFloat) -> UIColor
{
let startComponent = CGColorGetComponents(color1.CGColor)
let endComponent = CGColorGetComponents(color2.CGColor)
let startAlpha = CGColorGetAlpha(color1.CGColor)
let endAlpha = CGColorGetAlpha(color2.CGColor)
let r = startComponent[0] + (endComponent[0] - startComponent[0]) * factor
let g = startComponent[1] + (endComponent[1] - startComponent[1]) * factor
let b = startComponent[2] + (endComponent[2] - startComponent[2]) * factor
let a = startAlpha + (endAlpha - startAlpha) * factor
return UIColor(red: r, green: g, blue: b, alpha: a)
}
/* No longer needed
class func getFloatArrayFromNSNumbers(numbers:[NSNumber]) -> [CGFloat]
{
var returnArr : [CGFloat] = []
for number in numbers
{
returnArr += [CGFloat(number.floatValue)]
}
return returnArr
}
*/
/// Returns an array containing the RGBA components of an array of colors
class func getFloatArrayFromUIColors(colors:[UIColor]) -> [CGFloat]
{
var returnArr : [CGFloat] = []
for color : UIColor in colors
{
var red : CGFloat = 0.0
var green : CGFloat = 0.0
var blue : CGFloat = 0.0
var alpha : CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
/*
// This check and backup should probably be implemented later, but it seems to fail when it shouldn't...probably improper use of optionals
if(color?.getRed(&red, green: &green, blue: &blue , alpha: &alpha) == nil)
{
// If for some reason the above function call fails, try this method of getting RGBA instead
let components = CGColorGetComponents(color?.CGColor)
red = components[0]
green = components[1]
blue = components[2]
alpha = components[3]
}
*/
returnArr += [red, green, blue, alpha]
}
return returnArr
}
/// Returns a path for a rectangle with rounded corners
class func newPathForRoundedRect(rect:CGRect, radiusTL radTL:CGFloat, radiusTR radTR:CGFloat, radiusBL radBL:CGFloat, radiusBR radBR:CGFloat, edges:UIRectEdge = .All) -> CGPathRef
{
let retPath = CGPathCreateMutable()
// Convenience
let rectL = rect.origin.x
let rectR = rect.origin.x+rect.size.width
let rectT = rect.origin.y
let rectB = rect.origin.y+rect.size.height
// Starting from the top left arc, move clockwise
let p1 = CGPointMake(rectL , rectT+radTL)
let p2 = CGPointMake(rectL+radTL, rectT)
let p3 = CGPointMake(rectR-radTR, rectT)
let p4 = CGPointMake(rectR , rectT+radTR)
let p5 = CGPointMake(rectR , rectB-radBR)
let p6 = CGPointMake(rectR-radBR, rectB)
let p7 = CGPointMake(rectL+radBL, rectB)
let p8 = CGPointMake(rectL , rectB-radBL)
let c1 = CGPointMake(rect.origin.x , rect.origin.y)
let c2 = CGPointMake(rect.origin.x+rect.size.width , rect.origin.y)
let c3 = CGPointMake(rect.origin.x+rect.size.width , rect.origin.y+rect.size.height)
let c4 = CGPointMake(rect.origin.x , rect.origin.y+rect.size.height)
if(edges.contains(.All) || (edges.contains(.Left) && edges.contains(.Right) && edges.contains(.Top) && edges.contains(.Bottom)))
{
CGPathMoveToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)
CGPathCloseSubpath(retPath)
return retPath
}
if(edges.contains(.Top))
{
CGPathMoveToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
CGPathAddLineToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
}
if(edges.contains(.Right))
{
CGPathMoveToPoint(retPath, nil, p3.x, p3.y)
CGPathAddArcToPoint (retPath, nil, c2.x, c2.y, p4.x, p4.y, radTR)
CGPathAddLineToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
}
if(edges.contains(.Bottom))
{
CGPathMoveToPoint(retPath, nil, p5.x, p5.y)
CGPathAddArcToPoint (retPath, nil, c3.x, c3.y, p6.x, p6.y, radBR)
CGPathAddLineToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
}
if(edges.contains(.Left))
{
CGPathMoveToPoint(retPath, nil, p7.x, p7.y)
CGPathAddArcToPoint (retPath, nil, c4.x, c4.y, p8.x, p8.y, radBL)
CGPathAddLineToPoint(retPath, nil, p1.x, p1.y)
CGPathAddArcToPoint (retPath, nil, c1.x, c1.y, p2.x, p2.y, radTL)
}
return retPath
}
}
JFStylishView.swift
//
// JFStylishView.swift
// Soapbox
//
// Created by Joseph Falcone on 6/2/16.
// Copyright © 2016 Joseph Falcone. All rights reserved.
//
import UIKit
enum GradientType
{
case Linear
case Radial
}
private enum BackgroundFillType
{
case Solid
case Gradient
}
class JFStylishView : UIView
{
// Rounded Corners
var cornerTL : CGFloat = 0.0
var cornerTR : CGFloat = 0.0
var cornerBR : CGFloat = 0.0
var cornerBL : CGFloat = 0.0
// Border
var borderWidth : CGFloat = 4.0
var borderColor = UIColor.greenColor()
// Colors
private var trueBackgroundColor = UIColor.clearColor() // The backgroundColor property has to be clear so that the layer doesn't draw behind the clipping area, so we use this to track what the user wants
private var bgColors : [CGFloat] = [] // array of colors used in drawrect
// Gradient points
private var gradientStart = CGPointMake(0.5, 0.0)
private var gradientEnd = CGPointMake(0.5, 1.0)
private var gradientColorStops : [CGFloat] = []
// Gradient type
private var gradientType : GradientType = .Linear
// Background Mode
private var backgroundFillType : BackgroundFillType = .Solid
// var shadowLayer: CAShapeLayer! // Not ready for this yet
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
}
override func awakeFromNib()
{
super.awakeFromNib()
}
func initStylishStuff()
{
cornerTL = 0.0
}
// MARK: Color
private func getFillType() -> BackgroundFillType
{
// Rather than keeping a variable for this that gets set everywhere, we'll just use this getter to figure out what type we are using.
// Of course, if I get sloppy and don't make the unused elements empty when setting another fill parameter, this could produce a bug.
// RULES
// If using a gradient, trueBackgroundColor will be clear
// If using solid, bgColors will be empty
// If patterns are ever added, the above will be empty
if(bgColors.count == 0)
{return .Solid}
if(trueBackgroundColor == UIColor.clearColor())
{return .Gradient}
// Default
return .Solid
}
override var backgroundColor: UIColor?
{
get
{
return trueBackgroundColor
}
set
{
trueBackgroundColor = backgroundColor!
super.backgroundColor = UIColor.clearColor()
//bgColorArr = []
bgColors = []
backgroundFillType = .Solid
}
/*
// Property observer - whenever the background color is
didSet
{
bgColorArr = []
bgColors = []
// bgColorArr = [backgroundColor!]
// bgColors = AppocalypseUI.getFloatArrayFromUIColors([backgroundColor!, backgroundColor!])
}
*/
}
// // Convenient...maybe we shouldn't include this?
// func setBackgroundGradient(topColor:UIColor, bottomColor:UIColor)
// {
// bgColorArr = [topColor, bottomColor]
// bgColors = AppocalypseUI.getFloatArrayFromUIColors([topColor, bottomColor])
// }
// Default is linear, top to bottom
// startPoint, endPoint should be coordinates of 0.0-1.0
func setBackgroundGradient(colors:[UIColor], stops:[CGFloat]? = nil, startPoint:CGPoint?=nil, endPoint:CGPoint?=nil, type:GradientType = .Linear)
{
assert(colors.count > 1, "At least two colors must be specified.")
// We won't be using the backgroundColor property when drawing a gradient
trueBackgroundColor = UIColor.clearColor()
// Calculate the stops if they were not specified
var stops = stops // arguments are immutable, but we can declare a variable with the same name
if(stops == nil)
{
stops = AppocalypseUI.makeLinearColorStops(colors.count)
}
// Provide default start and end points if necessary
gradientType = type
switch type
{
case .Linear: // top to bottom
gradientStart = startPoint == nil ? CGPointZero : startPoint!
gradientEnd = endPoint == nil ? CGPointMake(0, 1.0) : endPoint!
case .Radial: // center to top
gradientStart = startPoint == nil ? CGPointMake(0.5, 0.5) : startPoint!
gradientEnd = endPoint == nil ? CGPointMake(0.5, 0) : endPoint!
}
assert(colors.count == stops?.count, "The number of colors and stops must be equal.")
//bgColorArr = colors
bgColors = AppocalypseUI.getFloatArrayFromUIColors(colors)
gradientColorStops = stops!
}
/*
override func layoutSubviews()
{
super.layoutSubviews()
if shadowLayer == nil
{
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 12).CGPath
//shadowLayer.fillColor = UIColor.whiteColor().CGColor
shadowLayer.fillColor = UIColor.clearColor().CGColor
shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2
//layer.insertSublayer(shadowLayer, atIndex: 0)
layer.insertSublayer(shadowLayer, below: nil) // also works
}
}
*/
// MARK: Drawing
// override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
//
// }
override func drawRect(rect: CGRect)
{
// Get the current context
let context = UIGraphicsGetCurrentContext()
// Make the background gradient
let baseSpace = CGColorSpaceCreateDeviceRGB();
let gradient = CGGradientCreateWithColorComponents(baseSpace, bgColors, gradientColorStops, gradientColorStops.count);
// Set the border color and stroke
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
// Fill in the background, inset by the border
let bgRect = CGRectMake(bounds.origin.x+borderWidth , bounds.origin.y+borderWidth , bounds.size.width-borderWidth*2, bounds.size.height-borderWidth*2);
let borderRect = CGRectMake(bounds.origin.x+borderWidth/2, bounds.origin.y+borderWidth/2, bounds.size.width-borderWidth , bounds.size.height-borderWidth);
let bgPath = AppocalypseUI.newPathForRoundedRect(bgRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)
let borderPath = AppocalypseUI.newPathForRoundedRect(borderRect, radiusTL: cornerTL, radiusTR: cornerTR, radiusBL: cornerBL, radiusBR: cornerBR)
CGContextStrokePath(context)
// Background
CGContextSaveGState(context); // Saves the state from before we clipped to the path
CGContextAddPath(context, bgPath);
CGContextClip(context); // Makes the background fill only the path
switch getFillType()
{
case .Gradient:
let gradientStartInPoints = CGPointMake(gradientStart.x*bounds.size.width, gradientStart.y*bounds.size.height);
let gradientEndInPoints = CGPointMake(gradientEnd.x*bounds.size.width, gradientEnd.y*bounds.size.height);
switch(gradientType)
{
case .Linear:
CGContextDrawLinearGradient(context, gradient, gradientStartInPoints, gradientEndInPoints, []); // Draw a vertical gradient
case .Radial:
// A radial gradient might not fill the layer...first, fill it with the end color
UIColor(red: bgColors[bgColors.count-4], green: bgColors[bgColors.count-3], blue: bgColors[bgColors.count-2], alpha: bgColors[bgColors.count-1]).setFill()
CGContextAddPath(context, bgPath); // Not sure why I need this...TODO: Investigate
CGContextFillPath(context)
let endRadius = hypot(gradientStartInPoints.x-gradientEndInPoints.x, gradientStartInPoints.y-gradientEndInPoints.y)
CGContextDrawRadialGradient(context, gradient, gradientStartInPoints, 0, gradientStartInPoints, endRadius, [])
}
case .Solid:
trueBackgroundColor.setFill()
CGContextFillPath(context)
}
CGContextRestoreGState(context); // Now we are no longer clipped to the path
// Border
CGContextAddPath(context, borderPath);
CGContextStrokePath(context);
}
// MARK: Convenience
func removeAllSubviews()
{
for view in subviews
{view.removeFromSuperview()}
}
}
You can create a CAGradientLayer, then add mask with only two corner radius to it. Then you can animate the transform using Core Animation, or UIView transform animation also should work if you put the layer in UIView.
CAGradientLayer * rectangleGradient = [CAGradientLayer layer];
rectangleGradient.colors = #[(id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor];
rectangleGradient.startPoint = CGPointMake(0.5, 0);
rectangleGradient.endPoint = CGPointMake(0.5, 1);
rectangleMask.path = maskPath;
////The animation
CABasicAnimation * theTransformAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
theTransformAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];;
theTransformAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2, 2, 1)];;
theTransformAnim.duration = 1;
[rectangleGradient addAnimation:theTransformAnim];