drawRect throws "fatal error: unexpectedly found nil while unwrapping an Optional value" in Xcode playground - swift

Why the below code throws fatal error: unexpectedly found nil while unwrapping an Optional value when run in Xcode playground? I am not sure what is wrong with the below code. Thanks for your help. I haven't tried running it outside playground.
import UIKit
import XCPlayground
class CircularProgressView: UIView {
var progressBackgroundLayer: CAShapeLayer!
var progressLayer: CAShapeLayer!
var iconLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
convenience init() {
self.init(frame: CGRectZero)
}
func setup() {
progressBackgroundLayer = CAShapeLayer(layer: layer)
progressLayer = CAShapeLayer(layer: layer)
iconLayer = CAShapeLayer(layer: layer)
}
override func drawRect(rect: CGRect) {
progressBackgroundLayer.frame = self.bounds
progressLayer.frame = self.bounds
iconLayer.frame = self.bounds
}
}
var progressView = CircularProgressView(frame: CGRectMake(0, 0, 80, 80))
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
XCPlaygroundPage.currentPage.liveView = progressView

You shouldn't use the init(layer:) initialiser in order to create your CAShapeLayers. From the documentation (emphasis mine):
This initializer is used to create shadow copies of layers, for example, for the presentationLayer method. Using this method in any other situation will produce undefined behavior. For example, do not use this method to initialize a new layer with an existing layer’s content.
Therefore, as the behaviour of calling this initialiser is undefined in this circumstance, it's returning nil in your case – which you're then force unwrapping upon accessing, as it's an implicitly unwrapped optional.
You should instead just create your layers with init(). I would also recommend that you define your layer instances inline, and get rid of the implicitly unwrapped optionals, as they're inherently unsafe. For example:
class CircularProgressView: UIView {
let progressBackgroundLayer = CAShapeLayer()
let progressLayer = CAShapeLayer()
let iconLayer = CAShapeLayer()
...
In order to add the layers to your view, you need to use the addSublayer: method on the view's layer. For example:
func setup() {
layer.addSublayer(progressBackgroundLayer)
layer.addSublayer(progressLayer)
layer.addSublayer(iconLayer)
}
Also, drawRect: is used to draw the contents of your view, and therefore is totally the wrong place to be defining the frames of your layers (which should only occur when the view's bounds changes). You should instead consider doing this in layoutSubviews.
override func layoutSubviews() {
progressBackgroundLayer.frame = bounds
progressLayer.frame = bounds
iconLayer.frame = bounds
}
And finally, this code is pointless:
convenience init() {
self.init(frame: CGRectZero)
}
This is exactly what the UIView implementation of init does already.

Related

Why must I use a computed property, instead of a stored property, to access convertPoint()

I'm learning Swift Language by following Stanford University Course.
In lecture five, there is a demo to draw a smile face.
There is a declaration of faceCenter, the code showed as below.
var faceCenter: CGPoint {
return convertPoint(center, fromView: superview)
}
But my question is why can I use simply equal like below?
var faceCenter: CGPoint = convertPoint(center, fromView: superview)
When I did it, the system gives this error, "Extra argument "fromView" in call".
Can anyone tell me the problem?
It does not work because self is not initialised yet. Every value has to be assigned before self becomes available.
It actually tries to use self three times :
var faceCenter: CGPoint = self.convertPoint(self.center, fromView: self.superview)
Unfortunately the compiler is not really helpful with this error.
You can always make it an optional or give it a default value. Then give it the correct value in the init method.
It is possible to create a stored property while also accessing self, without assigning the value in the init method. This involves the method in faceCenterBeta. It is declared with lazy to assign a value to it when it is first read, not when the object is initialised. It also uses a closure instead of a getter to get the value.
class Test : UIView {
var faceCenter : CGPoint = CGPointZero // give a default value, give correct value in the init method
var faceCenterAlpha: CGPoint { // getter
print("getter")
return convertPoint(center, fromView: superview)
}
lazy var faceCenterBeta: CGPoint = { [unowned self] in // closure
print("closure")
return self.convertPoint(self.center, fromView: self.superview)
}()
func faceCenterDelta() -> CGPoint { // good ol' function
print("function")
return convertPoint(center, fromView: superview)
}
init() {
super.init(frame: CGRectZero)
faceCenter = convertPoint(center, fromView: superview)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let test = Test()
// executed every time, just like a function
test.faceCenterAlpha
test.faceCenterAlpha
test.faceCenterAlpha
// only executed once
test.faceCenterBeta
test.faceCenterBeta
test.faceCenterBeta

Why does CABasicAnimation try to initialize another instance of my custom CALayer?

I get this error:
fatal error: use of unimplemented initializer 'init(layer:)' for class 'MyProject.AccordionLayer'
using the following code. In my view controller:
override func viewDidLoad() {
let view = self.view as! AccordionView!
view.launchInitializationAnimations()
}
In my view:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
for var i = 0; i < self.accordions.count; ++i {
var layer = AccordionLayer(accordion: self.accordions[i])
layer.accordion = self.accordions[i]
layer.frame = CGRectMake(0, CGFloat(i) * self.getDefaultHeight(), self.frame.width, self.getDefaultHeight())
self.layer.addSublayer(layer)
layer.setNeedsDisplay()
layers.append(layer)
}
}
func launchInitializationAnimations() {
for layer in self.layer.sublayers {
var animation = CABasicAnimation(keyPath: "topX")
animation.duration = 2.0
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(200.0)
layer.addAnimation(animation, forKey: "animateTopX")
}
}
And in my subclassed CALayer
var topX : Int
init(accordion: (color: CGColorRef!, header: String, subtitle: String, image: UIImage?)!) {
// Some initializations
// ...
super.init()
}
I also implement needsDisplayForKey, drawInContext.
I have seen 2-3 other questions with the same error message but I can't really figure out how it is related to my specific case on my own.
Why is CABasicAnimation trying to instantiate a new (my custom) CALayer?
I just ran into the same issue, and I got it working after adding the init(layer:) to my custom CALayer:
override init(layer: Any) {
super.init(layer: layer)
}
I hope it helps whoever get here.
EDIT As Ben Morrow comments, in your override you also have to copy across the property values from your custom class. For an example, see his https://stackoverflow.com/a/38468678/341994.
Why is CABasicAnimation trying to instantiate a new (my custom) CALayer?
Because animation involves making a copy of your layer to act as the presentation layer.

Where to initialize in Swift?

I have a very general question about the Initialization in Swift.
Unlike in Objective C it's now possible to call the init() directly at the declaration outside of my functions:
e.g.
class ViewController: UIViewController {
let myView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
myView.frame = getFrame()
myView.backgroundColor = UIColor.redColor()
self.view.addSubview(myView)
}
func getFrame() -> CGRect {
return CGRectMake(0, 0, 100, 100)
}
}
In Objective C I would have done the initialization in my function.
But what if I want to call an Initializer with parameters which are not set yet?
e.g.
I want to init with a frame which is being calculated in a func()
class ViewController: UIViewController {
//THIS IS NOT WOKRING
let myView: UIView = UIView(frame: getFrame())
override func viewDidLoad() {
super.viewDidLoad()
myView.backgroundColor = UIColor.redColor()
self.view.addSubview(myView)
}
func getFrame() -> CGRect {
return CGRectMake(0, 0, 100, 100)
}
}
I don't wanna do my initializations at two different places in the Code. Is there any general pattern for the initializations?
So your options for initialisation in swift are numerous. With your current example you cannot use the method getFrame() yet because you do not yet have a reference to self as the ViewController has not get been initialised. Instead you could use:
let myView: UIView = UIView(frame: CGRectMake(0, 0, 100, 100))
As this does not require the reference to self. Alternatively you could lazy instantiation which will get run after self is available (this can only be used with var not let:
lazy var myView: UIView = {
return UIView(frame:self.getFrame())
}()
To answer your question, when using UIKit class where you often don't have control over their instantiation you can keep doing it the same was as you were in objective c and use implicitly unwrapped optionals (to prevent you having to use a ! or ? every time you instantiate a variable, e.g.:
var myView: UIView!
override func viewDidLoad(){
super.viewDidLoad();
myView = UIView(frame:getFrame())
}
This however does not work with let constants, as they need to be assigned either immediately or in the constructor of the object. Hope this helps.

Custom Activity Indicator with Swift

I am trying to create a custom activity indicator like one of these. There seems to be many approaches to animating the image, so I'm trying to figure out the best approach.
I found this tutorial with Photoshop + Aftereffects animation loop, but as the comment points out, it seems overly complex (and I don't own After Effects).
tldr: how can I take my existing image and animate it as a activity indicator (using either rotating/spinning animation or looping through an animation)
It's been a while since you posted this question, so I'm not sure if you've found the answer you're looking for.
You are right there are many ways to do what you are asking, and I don't think there's a "right way". I would make a UIView object which have a start and a stop method as the only public methods. I would also make it a singleton, so it's a shared object and not possible to put multiple instances on a UIViewController (imagine the mess it could make).
You can add a dataSource protocol which returns a UIViewController so the custom activity indicator could add itself to it's parent.
In the start method I would do whatever animations is needed (transform, rotate, GIF, etc.).
Here's an example:
import UIKit
protocol CustomActivityIndicatorDataSource {
func activityIndicatorParent() -> UIViewController
}
class CustomActivityIndicator: UIView {
private var activityIndicatorImageView: UIImageView
var dataSource: CustomActivityIndicatorDataSource? {
didSet {
self.setupView()
}
}
// Singleton
statice let sharedInstance = CustomActivityIndicator()
// MARK:- Initialiser
private init() {
var frame = CGRectMake(0,0,200,200)
self.activityIndicatorImageView = UIImageView(frame: frame)
self.activityIndicatorImageView.image = UIImage(named: "myImage")
super.init(frame: frame)
self.addSubview(self.activityIndicatorImageView)
self.hidden = true
}
internal required init?(coder aDecoder: NSCoder) {
self.activityIndicatorImageView = UIImageView(frame: CGRectMake(0, 0, 200, 200))
self.activityIndicatorImageView = UIImage(named: "myImage")
super.init(coder: aDecoder)
}
// MARK:- Helper methods
private func setupView() {
if self.dataSource != nil {
self.removeFromSuperview()
self.dataSource!.activityIndicatorParent().addSubview(self)
self.center = self.dataSource!.activityIndicatorParent().center
}
}
// MARK:- Animation methods
/**
Set active to true, and starts the animation
*/
func startAnimation() {
// A custom class which does thread handling. But you can use the dispatch methods.
ThreadController.performBlockOnMainQueue{
self.active = true
self.myAnimation
self.hidden = false
}
}
/**
This will set the 'active' boolean to false.
Remeber to remove the view from the superview manually
*/
func stopAnimation() {
ThreadController.performBlockOnMainQueue{
self.active = false
self.hidden = true
}
}
}
Mind that this is not fully tested and may need some tweaks to work 100%, but this is how I basically would handle it.

How to use Implicitly Unwrapped Optionals?

I'm porting an NSView to Swift and I need to use Quartz CGContextAddPath.
import Cocoa
class MYView: NSView {
init(frame: NSRect) {
super.init(frame: frame)
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor .redColor() .set()
NSRectFill(self.bounds)
var p :CGMutablePathRef = CGPathCreateMutable()
var ctx = NSGraphicsContext.currentContext().graphicsPort()
CGContextAddPath(ctx, p) // compiler rejects this line
}
}
How do you understand this error message ?
Cannot convert the expression's type 'Void' to type 'CGContext!'
The Swift signature of CGContextAddPath is:
func CGContextAddPath(context: CGContext!, path: CGPath!)
What is my error ?
When I use this:
let context = UnsafePointer<CGContext>(ctx).memory
I now have a runtime error:
Jun 3 15:57:13 xxx.x SwiftTest[57092] <Error>: CGContextAddPath: invalid context 0x7fff73bd0060. This is a serious error. This application, or a library it uses, is using an invalid context and is thereby contributing to an overall degradation of system stability and reliability. This notice is a courtesy: please fix this problem. It will become a fatal error in an upcoming update.
Here is the code that I'm currently using:
import Cocoa
class MYView: NSView {
init(frame: NSRect) {
super.init(frame: frame)
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
var p :CGMutablePathRef = CGPathCreateMutableCopy( CGPathCreateWithRoundedRect(self.bounds, 10, 10, nil))
var ctx = NSGraphicsContext.currentContext().graphicsPort()
let context = UnsafePointer<CGContext>(ctx).memory
CGContextAddPath(context, p) // compiler no longer rejects this line
var blueColor = NSColor.blueColor()
CGContextSetStrokeColorWithColor(context, NSColor.blueColor().CGColor)
CGContextSetLineWidth(context, 2)
CGContextStrokePath(context)
}
}
As of Swift 1.0
If your deployment target is 10.10 you can use the convenience method introduced with Yosemite.
let context = NSGraphicsContext.currentContext().CGContext
If you have to support 10.9 you'll have to cast the context manually as per below.
let contextPtr = NSGraphicsContext.currentContext().graphicsPort
let context = unsafeBitCast(contextPtr, CGContext.self)
Use NSGraphicsContext.currentContext().graphicsPort(). It returns void*. You have to cast it to
CGContextRef
let ctx = UnsafePointer<CGContext>(NSGraphicsContext.currentContext().‌​graphicsPort()).memo‌​ry