How to pass arguments from one class into a UIView class? Swift - ios

I have a UIView class in my app which plots a line graph. In there, I assign my graphPoints variables like so :
var graphPoints:[Int] = [1,2,3,5,7,9]
var graphPoints2:[Int] = [1,2,3,5,7,9]
What I want to do is pass an array of Int from another class and assign those variables, but I am not sure how to do it. Initially i put all my code into one func with array [Int] as parameters and called it from another class but it stopped plotting the graph altogether. How do i do this?
Here is my UIVIew GraphPlotter class code :
import UIKit
#IBDesignable class GraphPlotter: UIView {
var graphPoints:[Int] = [1,2,3,5,7,9]
var graphPoints2:[Int] = [1,2,3,5,7,9]
//1 - the properties for the gradient
var startColor: UIColor = UIColor.redColor()
var endColor: UIColor = UIColor.greenColor()
override func drawRect(rect: CGRect) {
let width = rect.width
let height = rect.height
//set up background clipping area
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: 8.0, height: 8.0))
path.addClip()
//2 - get the current context
let context = UIGraphicsGetCurrentContext()
let colors = [startColor.CGColor, endColor.CGColor]
//3 - set up the color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
//4 - set up the color stops
let colorLocations:[CGFloat] = [0.0, 1.0]
//5 - create the gradient
let gradient = CGGradientCreateWithColors(colorSpace,
colors,
colorLocations)
//6 - draw the gradient
var startPoint = CGPoint.zero
var endPoint = CGPoint(x:0, y:self.bounds.height)
CGContextDrawLinearGradient(context,
gradient,
startPoint,
endPoint,
[])
//calculate the x point
let margin:CGFloat = 40.0
let columnXPoint = { (column:Int) -> CGFloat in
//Calculate gap between points
let spacer = (width - margin*2 - 4) /
CGFloat((self.graphPoints.count - 1))
var x:CGFloat = CGFloat(column) * spacer
x += margin + 2
return x
}
// calculate the y point
let topBorder:CGFloat = 60
let bottomBorder:CGFloat = 50
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints2.maxElement()!
let columnYPoint = { (graphPoint2:Int) -> CGFloat in
var y:CGFloat = CGFloat(graphPoint2) /
CGFloat(maxValue) * graphHeight
y = graphHeight + topBorder - y // Flip the graph
return y
}
// draw the line graph
UIColor.flatTealColor().setFill()
UIColor.flatTealColor().setStroke()
//set up the points line
let graphPath = UIBezierPath()
//go to start of line
graphPath.moveToPoint(CGPoint(x:columnXPoint(0),
y:columnYPoint(graphPoints2[0])))
//add points for each item in the graphPoints array
//at the correct (x, y) for the point
for i in 1..<graphPoints.count {
let nextPoint = CGPoint(x:columnXPoint(i),
y:columnYPoint(graphPoints2[i]))
graphPath.addLineToPoint(nextPoint)
}
//Create the clipping path for the graph gradient
//1 - save the state of the context (commented out for now)
CGContextSaveGState(context)
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
//3 - add lines to the copied path to complete the clip area
clippingPath.addLineToPoint(CGPoint(
x: columnXPoint(graphPoints.count - 1),
y:height))
clippingPath.addLineToPoint(CGPoint(
x:columnXPoint(0),
y:height))
clippingPath.closePath()
//4 - add the clipping path to the context
clippingPath.addClip()
let highestYPoint = columnYPoint(maxValue)
startPoint = CGPoint(x:margin, y: highestYPoint)
endPoint = CGPoint(x:margin, y:self.bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, [])
CGContextRestoreGState(context)
//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints2[i]))
point.x -= 5.0/2
point.y -= 5.0/2
let circle = UIBezierPath(ovalInRect:
CGRect(origin: point,
size: CGSize(width: 5.0, height: 5.0)))
circle.fill()
let label = UILabel(frame: CGRectMake(0, 0, 200, 21))
label.center = CGPointMake(160, 284)
label.textAlignment = NSTextAlignment.Center
// label.text = "I'am a test label"
self.addSubview(label)
}
//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()
//top line
linePath.moveToPoint(CGPoint(x:margin, y: topBorder))
linePath.addLineToPoint(CGPoint(x: width - margin,
y:topBorder))
//center line
linePath.moveToPoint(CGPoint(x:margin,
y: graphHeight/2 + topBorder))
linePath.addLineToPoint(CGPoint(x:width - margin,
y:graphHeight/2 + topBorder))
//bottom line
linePath.moveToPoint(CGPoint(x:margin,
y:height - bottomBorder))
linePath.addLineToPoint(CGPoint(x:width - margin,
y:height - bottomBorder))
let color = UIColor.flatTealColor()
color.setStroke()
linePath.lineWidth = 1.0
linePath.stroke()
}
}
DBController, func dosmth where I pass the array :
func dosmth(metadata: DBMetadata!) {
let documentsDirectoryPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let localFilePath = (documentsDirectoryPath as NSString).stringByAppendingPathComponent(metadata.filename)
var newarray = [Int]()
do{
let data = try String(contentsOfFile: localFilePath as String,
encoding: NSASCIIStringEncoding)
print(data)
newarray = data.characters.split(){$0 == ","}.map{
Int(String.init($0).stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()))!}
print(newarray)
}
catch let error { print(error) }
//Probably wrong
GraphPlotter().graphPoints = newarray
GraphPlotter().graphPoints2 = newarray
}

So your drawRect method is based on the two variables graphPoints and graphPoints2. Create a method whose job is to update the arrays of these two variables, and then invoke setNeedsDisplay - which will go on to redraw the view.
func plotGraphPoints(gpArray1 : [Int], andMorePoints gpArray2: [Int] ) {
print("Old Values", self.graphPoints)
self.graphPoints = gpArray1
self.graphPoints2 = gpArray2
print("New values", self.graphPoints)
self.setNeedsDisplay()
}

First, I'd set these up so that any update will redraw the view:
var graphPoints:[Int]? { didSet { setNeedsDisplay() } }
var graphPoints2:[Int]? { didSet { setNeedsDisplay() } }
Note, I made those optionals, because you generally want it to handle the absence of data with nil values rather than dummy values. This does assume, though, that you'll tweak your implementation to detect and handle these nil values, e.g., before you start drawing the lines, do a
guard graphPoints != nil && graphPoints2 != nil else { return }
But, I notice that this whole class is IBDesignable, in which case, you probably want a prepareForInterfaceBuilder that provides sample data:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
graphPoints = [1,2,3,5,7,9]
graphPoints2 = [1,2,3,5,7,9]
}
Second, your other class needs to have a reference to this custom view.
If this "other" class is the view controller and you added the custom view via IB, you would just add a #IBOutlet for the custom view to this view controller. If you added this custom view programmatically, you'd just keep a reference to it in some property after adding it to the view hierarchy. But, however you added a reference to that view, say graphView, you'd just set these properties:
graphView.graphPoints = ...
graphView.graphPoints2 = ...
If this "other" class is something other than a view controller (and in discussion, it sounds like the class in question is a controller for processing of asynchronous DropBox API), you also need to give that class some mechanism to reference the view controller (and thus the custom view). You can accomplish this by either implementing a "completion handler pattern" or a "delegate-protocol" pattern.

Related

Building a circular facepile of profile pictures in Swift: how to have the last photo tucked under the first?

I am trying to build a UIView that has a few UIImageViews arranged in a circular, overlapping manner (see image below). Let's say we have N images. Drawing out the first N - 1 is easy, just use sin/cos functions to arrange the centers of the UIImageViews around a circle. The problem is with the last image that seemingly has two z-index values! I know this is possible since kik messenger has similar group profile photos.
The best idea I have come up so far is taking the last image, split into something like "top half" and "bottom half" and assign different z-values for each. This seems doable when the image is the left-most one, but what happens if the image is the top most? In this case, I would need to split left and right instead of top and bottom.
Because of this problem, it's probably not top, left, or right, but more like a split across some imaginary axis from the center of the overall facepile through the center of the UIImageView. How would I do that?!
Below Code Will Layout UIImageView's in Circle
You would need to import SDWebImage and provide some image URLs to run the code below.
import Foundation
import UIKit
import SDWebImage
class EventDetailsFacepileView: UIView {
static let dimension: CGFloat = 66.0
static let radius: CGFloat = dimension / 1.68
private var profilePicViews: [UIImageView] = []
var profilePicURLs: [URL] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicURLs.map({ (profilePic) -> UIImageView in
let imageView = UIImageView()
imageView.sd_setImage(with: profilePic)
imageView.roundImage(imageDimension: EventDetailsFacepileView.dimension, showsBorder: true)
imageView.sd_imageTransition = .fade
return imageView
})
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
}
self.setNeedsLayout()
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
let xOffset: CGFloat = 0
let yOffset: CGFloat = 0
let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
let radius: CGFloat = EventDetailsFacepileView.radius
let angleStep: CGFloat = 2 * CGFloat(Double.pi) / CGFloat(profilePicViews.count)
var count = 0
for profilePicView in profilePicViews {
let xPos = center.x + CGFloat(cosf(Float(angleStep) * Float(count))) * (radius - xOffset)
let yPos = center.y + CGFloat(sinf(Float(angleStep) * Float(count))) * (radius - yOffset)
profilePicView.frame = CGRect(origin: CGPoint(x: xPos, y: yPos),
size: CGSize(width: EventDetailsFacepileView.dimension, height: EventDetailsFacepileView.dimension))
count += 1
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = EventDetailsFacepileView.dimension + EventDetailsFacepileView.radius
return CGSize(width: requiredSize,
height: requiredSize)
}
}
I don't think you'll have much success trying to split images to get over/under z-indexes.
One approach is to use masks to make it appear that the image views are overlapped.
The general idea would be:
subclass UIImageView
in layoutSubviews()
apply cornerRadius to layer to make the image round
get a rect from the "overlapping view"
convert that rect to local coordinates
expand that rect by the desired width of the "outline"
get an oval path from that rect
combine it with a path from self
apply it as a mask layer
Here is an example....
I was not entirely sure what your sizing calculations were doing... trying to use your EventDetailsFacepileView as-is gave me small images in the lower-right corner of the view?
So, I modified your EventDetailsFacepileView in a couple ways:
uses local images named "pro1" through "pro5" (you should be able to replace with your SDWebImage)
uses auto-layout constraints instead of explicit frames
uses MyOverlapImageView class to handle the masking
Code - no #IBOutlet connections, so just set a blank view controller to OverlapTestViewController:
class OverlapTestViewController: UIViewController {
let facePileView = MyFacePileView()
override func viewDidLoad() {
super.viewDidLoad()
facePileView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(facePileView)
facePileView.dimension = 120
let sz = facePileView.sizeThatFits(.zero)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
facePileView.widthAnchor.constraint(equalToConstant: sz.width),
facePileView.heightAnchor.constraint(equalTo: facePileView.widthAnchor),
facePileView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
facePileView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
facePileView.profilePicNames = [
"pro1", "pro2", "pro3", "pro4", "pro5"
]
}
}
class MyFacePileView: UIView {
var dimension: CGFloat = 66.0
lazy var radius: CGFloat = dimension / 1.68
private var profilePicViews: [MyOverlapImageView] = []
var profilePicNames: [String] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicNames.map({ (profilePic) -> MyOverlapImageView in
let imageView = MyOverlapImageView()
if let img = UIImage(named: profilePic) {
imageView.image = img
}
return imageView
})
// add MyOverlapImageViews to self
// and set width / height constraints
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: dimension).isActive = true
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
// start at "12 o'clock"
var curAngle: CGFloat = .pi * 1.5
// angle increment
let incAngle: CGFloat = ( 360.0 / CGFloat(self.profilePicViews.count) ) * .pi / 180.0
// calculate position for each image view
// set center constraints
self.profilePicViews.forEach { imgView in
let xPos = cos(curAngle) * radius
let yPos = sin(curAngle) * radius
imgView.centerXAnchor.constraint(equalTo: centerXAnchor, constant: xPos).isActive = true
imgView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: yPos).isActive = true
curAngle += incAngle
}
// set "overlapView" property for each image view
let n = self.profilePicViews.count
for i in (1..<n).reversed() {
self.profilePicViews[i].overlapView = self.profilePicViews[i-1]
}
self.profilePicViews[0].overlapView = self.profilePicViews[n - 1]
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = dimension * 2.0 + radius / 2.0
return CGSize(width: requiredSize,
height: requiredSize)
}
}
class MyOverlapImageView: UIImageView {
// reference to the view that is overlapping me
weak var overlapView: MyOverlapImageView?
// width of "outline"
var outlineWidth: CGFloat = 6
override func layoutSubviews() {
super.layoutSubviews()
// make image round
layer.cornerRadius = bounds.size.width * 0.5
layer.masksToBounds = true
let mask = CAShapeLayer()
if let v = overlapView {
// get bounds from overlapView
// converted to self
// inset by outlineWidth (negative numbers will make it grow)
let maskRect = v.convert(v.bounds, to: self).insetBy(dx: -outlineWidth, dy: -outlineWidth)
// oval path from mask rect
let path = UIBezierPath(ovalIn: maskRect)
// path from self bounds
let clipPath = UIBezierPath(rect: bounds)
// append paths
clipPath.append(path)
mask.path = clipPath.cgPath
mask.fillRule = .evenOdd
// apply mask
layer.mask = mask
}
}
}
Result:
(I grabbed random images by searching google for sample profile pictures)

iOS, how to continuously animate a line "running" ("marching ants" effect)?

I must admit I have no clue how to do this in iOS -
Here's some code that makes a nice dotted line:
Now, I want that line to "run" upwards:
So, every one second it will move upwards by, itemLength * 2.0.
Of course, it would wrap around top to bottom.
So, DottedVertical should just do this completely on its own.
Really, how do you do this in iOS?
It would be great if the solution is general and will "scroll" any I suppose layer or drawn thing.
In say a game engine it's trivial, you just animate the offset of the texture. Can you perhaps offset the layer, or something, in iOS?
What's the best way?
I guess you'd want to use the GPU (layer animation right?) to avoid melting the cpu.
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = UIColor.faveColor
override func draw(_ rect: CGRect) {
// say you want 8 dots, with perfect fenceposting:
let totalCount = 8 + 8 - 1
let fullHeight = bounds.size.height
let width = bounds.size.width
let itemLength = fullHeight / CGFloat(totalCount)
let beginFromTop = !lowerHalfOnly ? 0.0 : (fullHeight * 8.0 / 15.0)
let top = CGPoint(x: width/2, y: beginFromTop)
let bottom = CGPoint(x: width/2, y: fullHeight)
let path = UIBezierPath()
path.move(to: top)
path.addLine(to: bottom)
path.lineWidth = width
let dashes: [CGFloat] = [itemLength, itemLength]
path.setLineDash(dashes, count: dashes.count, phase: 0)
dotColor.setStroke()
path.stroke()
}
(Bonus - if you had a few of these on screen, they'd have to be synced of course. There'd need to be a "sync" call that starts the running animation, so you can start them all at once with a notification or other message.)
Hate to answer my own question, here's a copy and paste solution based on the Men's suggestions above!
Good one! Superb effect...
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = sfBlack6 { didSet {setup()} }
override func layoutSubviews() { setup() }
var s:CAShapeLayer? = nil
func setup() {
// say you want 8 dots, with perfect fenceposting:
- calculate exactly as in the example in the question above -
// marching ants...
if (s == nil) {
s = CAShapeLayer()
self.layer.addSublayer(s!)
}
s!.strokeColor = dotColor.cgColor
s!.fillColor = backgroundColor?.cgColor
s!.lineWidth = width
let ns = NSNumber(value: Double(itemLength))
s!.lineDashPattern = [ns, ns]
let path = CGMutablePath()
path.addLines(between: [top, bottom])
s!.path = path
let anim = CABasicAnimation(keyPath: "lineDashPhase")
anim.fromValue = 0
anim.toValue = ns + ns
anim.duration = 1.75 // seconds
anim.repeatCount = Float.greatestFiniteMagnitude
s!.add(anim, forKey: nil)
self.layer.addSublayer(s!)
}
}

How to draw UIBezierPath identical to MKPolyline in a UIView

Currently I am tracking my location on an MKMapView. My objective is to draw a bezier path identical to an MKPolyline created from tracked locations.
What I have attempted is: Store all location coordinates in a CLLocation array. Iterate over that array and store the lat/lng coordinates in a CLLocationCoordinate2D array. Then ensure the polyline is in the view of the screen to then convert all the location coordinates in CGPoints.
Current attempt:
#IBOutlet weak var bezierPathView: UIView!
var locations = [CLLocation]() // values from didUpdateLocation(_:)
func createBezierPath() {
bezierPathView.isHidden = false
var coordinates = [CLLocationCoordinate2D]()
for location in locations {
coordinates.append(location.coordinate)
}
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
fitPolylineInView(polyline: polyline)
let mapPoints = polyline.points()
var points = [CGPoint]()
for point in 0...polyline.pointCount
{
let coordinate = MKCoordinateForMapPoint(mapPoints[point])
points.append(mapView.convert(coordinate, toPointTo: polylineView))
}
print(points)
let path = UIBezierPath(points: points)
path.lineWidth = 2.0
path.lineJoinStyle = .round
let layer = CAShapeLayer(path: path, lineColor: UIColor.red, fillColor: UIColor.black)
bezierPathView.layer.addSublayer(layer)
}
extension UIBezierPath {
convenience init(points:[CGPoint])
{
self.init()
//connect every points by line.
//the first point is start point
for (index,aPoint) in points.enumerated()
{
if index == 0 {
self.move(to: aPoint)
}
else {
self.addLine(to: aPoint)
}
}
}
}
extension CAShapeLayer
{
convenience init(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor)
{
self.init()
self.path = path.cgPath
self.strokeColor = lineColor.cgColor
self.fillColor = fillColor.cgColor
self.lineWidth = path.lineWidth
self.opacity = 1
self.frame = path.bounds
}
}
I am able to output the points to the console that stored from the convert(_:) method( not sure if they are correct ). Yet the there is not output on the bezierPathView-resulting in an empty-white background-view controller.
Your extensions work fine. The problem may be in the code that adds the layer to the view (which you do not show).
I'd suggest that you simplify your project, for example use predefined array of points that definitely fit to your view. For example, for a view that is 500 pixels wide and 300 pixels high, you could use something like:
let points = [
CGPoint(x: 10, y: 10),
CGPoint(x: 490, y: 10),
CGPoint(x: 490, y: 290),
CGPoint(x: 10, y: 290),
CGPoint(x: 10, y: 10)
]
Use colors that are clearly visible, like black and yellow for your stroke and fill.
Make sure that your path is correctly added to the view, for example:
let path = UIBezierPath(points: points)
let shapeLayer = CAShapeLayer(path: path, lineColor: UIColor.blue, fillColor: UIColor.lightGray)
view.layer.addSublayer(shapeLayer)
Inspect the controller that contains the view in Xcode's Interface Builder. In the debug view hierarchy function:
this might help you, in case you haven't solved it yet.
I wanted the shape of an MKPolyline as an image without any background.
I used the code above as an inspiration and had the same troubles as you had, the route was not shown.
In fact it was kind a scaling problem I think. At least it looked like that in the playground.
Anyway, with this methods I get an image of the polylines shape.
private func createPolylineShapeAsImage() -> UIImage? {
let vw = UIView(frame: mapView.bounds)
var image : UIImage?
if let polyline = viewModel.tourPolyline {
let path = createBezierPath(mapView, polyline, to: mapView)
let layer = getShapeLayer(path: path, lineColor: UIColor.white, fillColor: .clear)
vw.layer.addSublayer(layer)
image = vw.asImage()
}
return image
}
func createBezierPath(_ mapView : MKMapView, _ polyline : MKPolyline, to view : UIView) -> UIBezierPath {
let mapPoints = polyline.points()
var points = [CGPoint]()
let max = polyline.pointCount - 1
for point in 0...max {
let coordinate = mapPoints[point].coordinate
points.append(mapView.convert(coordinate, toPointTo: view))
}
let path = UIBezierPath(points: points)
path.lineWidth = 5.0
return path
}
private func getShapeLayer(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = lineColor.cgColor
layer.fillColor = fillColor.cgColor
layer.lineWidth = path.lineWidth
layer.opacity = 1
layer.frame = path.bounds
return layer
}
And to get the image of the view use this extension
import UIKit
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(self.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}

Custom UIView: animate subLayers with delay

I want to create a custom UIView subclass representing a bunch of stars on a dark-blue sky.
Therefore, I created this view:
import UIKit
class ConstellationView: UIView {
// MARK: - Properties
#IBInspectable var numberOfStars: Int = 80
#IBInspectable var animated: Bool = false
// Private properties
private var starsToDraw = [CAShapeLayer]()
// Layers
private let starsLayer = CAShapeLayer()
// MARK: - Drawing
// override func drawRect(rect: CGRect) {
override func layoutSubviews() {
// Generate stars
drawStars(rect: self.bounds)
}
/// Generate stars
func drawStars(rect: CGRect) {
let width = rect.size.width
let height = rect.size.height
let screenBounds = UIScreen.main.bounds
// Create the stars and store them in starsToDraw array
for _ in 0 ..< numberOfStars {
let x = randomFloat() * width
let y = randomFloat() * height
// Calculate the thinness of the stars as a percentage of the screen resolution
let thin: CGFloat = max(screenBounds.width, screenBounds.height) * 0.003 * randomFloat()
let starLayer = CAShapeLayer()
starLayer.path = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: thin, height: thin)).cgPath
starLayer.fillColor = UIColor.white.cgColor
starsToDraw.append(starLayer)
}
// Define a fade animation
let appearAnimation = CABasicAnimation(keyPath: "opacity")
appearAnimation.fromValue = 0.2
appearAnimation.toValue = 1
appearAnimation.duration = 1
appearAnimation.fillMode = kCAFillModeForwards
// Add the animation to each star (if animated)
for (index, star) in starsToDraw.enumerated() {
if animated {
// Add 1 s between each animation
appearAnimation.beginTime = CACurrentMediaTime() + TimeInterval(index)
star.add(appearAnimation, forKey: nil)
}
starsLayer.insertSublayer(star, at: 0)
}
// Add the stars layer to the view layer
layer.insertSublayer(starsLayer, at: 0)
}
private func randomFloat() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UINT32_MAX)
}
}
It works quite well, here is the result:
However, I'd like to have it animated, that is, each one of the 80 stars should appear one after the other, with a 1 second delay.
I tried to increase the beginTimeof my animation, but it does not seem to do the trick.
I checked with drawRect or layoutSubviews, but there is no difference.
Could you help me ?
Thanks
PS: to reproduce my app, just create a new single view app in XCode, create a new file with this code, and set the ViewController's view as a ConstellationView, with a dark background color. Also set the animated property to true, either in Interface Builder, or in the code.
PPS: this is in Swift 3, but I think it's still comprehensible :-)
You're really close, only two things to do!
First, you need to specify the key when you add the animation to the layer.
star.add(appearAnimation, forKey: "opacity")
Second, the fill mode for the animation needs to be kCAFillModeBackwards instead of kCAFillModeForwards.
For a more detailed reference see - https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html
And here's a fun tutorial (for practice with CAAnimations!) - https://www.raywenderlich.com/102590/how-to-create-a-complex-loading-animation-in-swift
Hope this helps 😀
Full Code:
class ConstellationView: UIView {
// MARK: - Properties
#IBInspectable var numberOfStars: Int = 80
#IBInspectable var animated: Bool = true
// Private properties
private var starsToDraw = [CAShapeLayer]()
// Layers
private let starsLayer = CAShapeLayer()
override func awakeFromNib() {
super.awakeFromNib()
}
// MARK: - Drawing
override func layoutSubviews() {
// Generate stars
drawStars(rect: self.bounds)
}
/// Generate stars
func drawStars(rect: CGRect) {
let width = rect.size.width
let height = rect.size.height
let screenBounds = UIScreen.main.bounds
// Create the stars and store them in starsToDraw array
for _ in 0 ..< numberOfStars {
let x = randomFloat() * width
let y = randomFloat() * height
// Calculate the thinness of the stars as a percentage of the screen resolution
let thin: CGFloat = max(screenBounds.width, screenBounds.height) * 0.003 * randomFloat()
let starLayer = CAShapeLayer()
starLayer.path = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: thin, height: thin)).cgPath
starLayer.fillColor = UIColor.white.cgColor
starsToDraw.append(starLayer)
}
// Define a fade animation
let appearAnimation = CABasicAnimation(keyPath: "opacity")
appearAnimation.fromValue = 0.2
appearAnimation.toValue = 1
appearAnimation.duration = 1
appearAnimation.fillMode = kCAFillModeBackwards
// Add the animation to each star (if animated)
for (index, star) in starsToDraw.enumerated() {
if animated {
// Add 1 s between each animation
appearAnimation.beginTime = CACurrentMediaTime() + TimeInterval(index)
star.add(appearAnimation, forKey: "opacity")
}
starsLayer.insertSublayer(star, above: nil)
}
// Add the stars layer to the view layer
layer.insertSublayer(starsLayer, above: nil)
}
private func randomFloat() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UINT32_MAX)
}
}

moveToPoint fails after upgrading to Swift 1.2 / Xcode 6.3

I've created a sub class of a UIView in which I draw a graph. I made it a public class so I can pass it new data and have it update when needed.
It all worked perfectly until I upgraded to Xcode 6.3 / Swift 1.2. Now when that view attempts to render my app crashes.
The error I get is:
Assertion failed: (CGFloatIsValid(x) && CGFloatIsValid(y)), function void CGPathMoveToPoint(CGMutablePathRef, const
CGAffineTransform *, CGFloat, CGFloat), file Paths/CGPath.cc, line
254.
Here is my code for the class:
import UIKit
public class GraphView: UIView {
//Data from parent VC
var graphPoints = [0, 0, 0, 0, 0, 0, 0]
var keyColor = BabyMasterStyleKit.bathsBase
override public func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let width = rect.width
let height = rect.height
//calculate the x point
let margin:CGFloat = 15.0
var columnXPoint = { (column:Int) -> CGFloat in
//Calculate gap between points
let spacer = (width - margin*2 - 4) /
CGFloat((self.graphPoints.count - 1))
var x:CGFloat = CGFloat(column) * spacer
x += margin + 2
return x
}
// calculate the y point
let topBorder:CGFloat = 15
let bottomBorder:CGFloat = 15
let graphHeight = height - topBorder - bottomBorder
let maxValue = maxElement(graphPoints)
var columnYPoint = { (graphPoint:Int) -> CGFloat in
var y:CGFloat = CGFloat(graphPoint) /
CGFloat(maxValue) * graphHeight
y = graphHeight + topBorder - y // Flip the graph
return y
}
//Draw horizontal graph lines on the top of everything
var linePath = UIBezierPath()
//top line
linePath.moveToPoint(CGPoint(x:0, y: topBorder))
linePath.addLineToPoint(CGPoint(x: width,
y:topBorder))
//center line
linePath.moveToPoint(CGPoint(x:0,
y: graphHeight/2 + topBorder))
linePath.addLineToPoint(CGPoint(x:width,
y:graphHeight/2 + topBorder))
let color = UIColor.lightGrayColor()
color.setStroke()
linePath.lineWidth = 0.5
linePath.stroke()
// draw the line graph
keyColor.setFill()
keyColor.setStroke()
// set up the points line
var graphPath = UIBezierPath()
// go to start of line
graphPath.moveToPoint(CGPoint(x:columnXPoint(0), y:columnYPoint(graphPoints[0])))
// add points for each item in the graph points array
// at the correct (x, y) for the point
for i in 1..<graphPoints.count {
let nextPoint = CGPoint(x:columnXPoint(i),
y:columnYPoint(graphPoints[i]))
graphPath.addLineToPoint(nextPoint)
}
graphPath.stroke()
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints[i]))
point.x -= 5.0/2
point.y -= 5.0/2
let circle = UIBezierPath(ovalInRect:
CGRect(origin: point,
size: CGSize(width: 6.0, height: 6.0)))
circle.fill()
}
// add left and bottom borders
UIColor.lightGrayColor().setStroke()
var borderPath = UIBezierPath()
borderPath.moveToPoint(CGPoint(x:0, y:0))
borderPath.addLineToPoint(CGPoint(x:0, y:height))
borderPath.addLineToPoint(CGPoint(x:width, y:height))
borderPath.stroke()
}
}
The app fails at this line:
graphPath.moveToPoint(CGPoint(x:columnXPoint(0), y:columnYPoint(graphPoints[0])))
The array graphPoints is the data I am passing in. I have it set to all 0s as a default. In my ViewController I have a function that passes in an array of real data (7 different numbers) and then does a setNeedsDislay() to re-render the view as needed.
Thanks for your help.
When the input array is
var graphPoints = [0, 0, 0, 0, 0, 0, 0]
then
let maxValue = maxElement(graphPoints)
makes maxValue 0, so that the line in columnYpoint
var y:CGFloat = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
does a divide-by-zero and makes y a NaN. It's a special case of having all zero elements in the initial array.
I think the default graphPoints array was being ignored, so when the view first loaded it didn't have values (though it did show that it was passing a 0 into the columnYPoint function, so who knows). As a work around I just tested for y.isNAN and if it was set it to equal 0. After I did that everything worked.
edit: Just checked to see if in fact the default array for graphPoints was getting ignored, and it wasn't. I think that it just doesn't like that it was being passed in a 0 value. If anyone has an idea as to why this would be, I'd love to hear about it.

Resources