can anyone point me to a tutorial or sample code (or both!) that explains the correct way to add views and layers for iOS.
ideally in Swift not Obj C.
I have code that works when adding SKNodes by themselves, no problem, but if i add a gradient layer - i want that to be sky in the background, the Gradient covers everything else.
I've tried various forms of adding sublayers at index above, etc, but nothing seems to work. I think this is my basic understanding of CALayer and UIView. I can't find a good tutorial :(
any help appreciated.
cheers
Adam.
here is a sample.
the view fills with the gradient, which is great, but the button seems to be hidden underneath, despite trying to force it above the background layer.
import QuartzCore
class NightScene: SKScene, SKPhysicsContactDelegate {
let playButton = SKSpriteNode(imageNamed: "play")
let gradient = CAGradientLayer()
gradient.frame = view.bounds
let colorTop = UIColor(red: 0.0, green: 0.0, blue: 0.100, alpha: 1.0).CGColor
let colorMid = UIColor(red: 0.0, green: 0.0, blue: 0.450, alpha: 1.0).CGColor
let colorBottom = UIColor(red: 0.0, green: 0.0, blue: 0.260, alpha: 1.0).CGColor
let arrayColors: Array <AnyObject> = [colorTop, colorMid, colorBottom]
gradient.colors = arrayColors
gradient.locations = [ 0.0, 0.4, 1.0]
self.view.layer.insertSublayer(gradient, atIndex:0)
let sceneLayer = CALayer()
sceneLayer.frame = view.bounds
self.view.layer.insertSublayer(sceneLayer, above: gradient)
self.playButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidX(self.frame)*2.5)
self.addChild(playButton)
}
Even though this question is over a year old I am going to provide an answer after countless hours of searching myself. I found numerous similar questions with no / vague responses.
What I come to find out is that nodes are not layer backed like gradients. This results in the gradient taking over the whole UI view. Nodes are not visible even with zLocation set. This is for SpriteKit based projects.
I stumbled across a library that you can use to create gradient background. This library basically converts a gradient into a SKSpriteNode. Gradient -> Texture -> Node
https://www.github.com/braindrizzlestudio/BDGradientNode
From the developers
We wrote BDGradientNode because gradients are a bit of a pain to set up on the fly in SpriteKit. UIViews are CALayer-backed and have access to CAGradientLayer, making them simple--fairly simple--to make. (Though sweep gradients can't be done.) SKNodes are NOT layer-backed. While you can jump through some hoops to make images, and so textures, from layers that can be assigned to an SKSpriteNode (which is what we first did when we just needed a simple gradient in SpriteKit) it's both cumbersome and inflexible.
Simple Linear Background
Once the library is added your Xcode project, you can use the following to create a linear gradient background:
let color1 = UIColor(red: 0/255, green: 118/255, blue: 162/255, alpha: 1.0)
let color2 = UIColor(red: 0/255, green: 85/255, blue: 116/255, alpha: 1.0)
let colors = [color1, color2]
let blending : Float = 1.0
let startPoint = CGPoint(x: 0.5, y: 1.0)
let endPoint = CGPoint(x: 0.5, y: 0.0)
let nodeSize = CGSize(width: self.frame.width, height: self.size.height)
let texture = SKTexture(imageNamed: "dummypixel")
let myGradientNode = BDGradientNode(linearGradientWithTexture: texture, colors: colors, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: false, size: nodeSize)
myGradientNode.zPosition = 0
myGradientNode.position = CGPointMake(self.size.width/2, self.size.height/2)
I found the tutorial here very helpful:
http://www.raywenderlich.com/75270/make-game-like-candy-crush-with-swift-tutorial-part-1
It contains layers and is in Swift. I also followed a Game of Life tutorial which used the zPosition of a node to help with layering, though they were not called layers and were not set up like your gradient:
https://www.makegameswith.us/gamernews/399/create-the-game-of-life-using-swift-and-spritekit
I found that even when I removed the gradient I could not view your button in its location, but we might just be working with differently sized devices. Is your sceneLayer empty? I didn't understand the relationship between your sceneLayer and your playButton.
Related
I am working on an iOS project where I have white labels on bright images. The problem is for bright images the white labels are not showing. Here is an example:
Label not showing: https://imgur.com/hKtejHn
Label showing: https://imgur.com/Ef5qJAh
I think if I add a black gradient on all the image then the white labels will be visible. Can anyone help me as to how to implement the solution in Swift?
Thank!
If you want to add gradient on your imageView then you can just implement CAGradientLayer on your imageView.layer.
Try to change some values for your own custom look, but the code below is pretty much it.
let gradientLayer = CAGradientLayer()
gradientLayer.frame = imageView.frame
let colors = [
UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 0, blue: 0, alpha: 0).cgColor
]
gradientLayer.startPoint = CGPoint(x: 0.1, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.9, y: 0.5)
gradientLayer.colors = colors
imageView.layer.addSublayer(gradientLayer)
You can change colors, add colors, change start/end-points. You can find a lot of different CAGradientLayer-guides on youtube or google.
try this:
extension UILabel {
func lblShadow(color: UIColor , radius: CGFloat, opacity: Float){
self.textColor = color
self.layer.masksToBounds = false
self.layer.shadowRadius = radius
self.layer.shadowOpacity = opacity
self.layer.shadowOffset = CGSize(width: 1, height: 1)
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
}
}
usage:
label.lblShadow(color: UIColor.white, radius: 3, opacity: 0.75)
When I use the CAGradientLayer on iOS to set up gradient for my viewcontroller, objects that I had previously placed using my interface builder are not showing up. When I take the gradientLayer function away, these objects show up. Also, when I add these objects programatically as a subview they show up in the iOS simulator. What's going on? Here is my gradientLayer code:
func setUpGradient(){
let topColor=UIColor(red: 255/255, green: 149/255, blue: 56/255,alpha: 1)
let bottomColor=UIColor(red: 216/255, green: 57/255, blue: 177/255,
alpha: 1)
let gradientColors: [CGColor]=[topColor.cgColor,bottomColor.cgColor]
var gradientLayer = CAGradientLayer()
gradientLayer.frame = self.view.bounds
gradientLayer.colors=gradientColors
gradientLayer.startPoint=CGPoint(x: 0, y:0)
gradientLayer.endPoint=CGPoint(x: 0, y:1)
self.view.layer.addSublayer(gradientLayer)
}
The reason is simple which is the gradient layer covers all the elements when you add them in IB by this line
self.view.layer.addSublayer(gradientLayer)
so you may insert it below them like this
self.view.layer.insertSublayer(gradientLayer,at:0)
I'm new in Swift and I have a problem. my button do not set the text if I try to use the gradient. here is my code:
let gradient:CAGradientLayer = CAGradientLayer()
let colorBottom = UIColor(red: 9/255.0, green: 137/255.0, blue: 133/255.0, alpha: 1.0).CGColor
let colorTop = UIColor(red: 222.0/255.0, green: 255.0/255.0, blue: 201.0/255.0, alpha: 1.0).CGColor
gradient.colors = [colorTop, colorBottom]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = LoginButton.bounds
gradient.cornerRadius = 25.0
LoginButton.layer.addSublayer(gradient)
LoginButton.setTitle("Login", forState: UIControlState.Normal)
LoginButton.setTitleColor(UIColor.blackColor(), forState: .Normal)
self.view.addSubview(LoginButton)
Adding a sublayer works the same as adding a subview so adding the gradient layer may be covering the other layers where the text is drawn.
If you remove this line does it work?
LoginButton.layer.addSublayer(gradient)
If it does, try seeing how many layers are in LoginButton.layer.sublayers and try inserting the gradient layer behind one of those sublayers (wherever it looks the best) using one of the following methods:
-insertSublayer:above:
-insertSublayer:below:
-insertSublayer:atIndex:
This line is the problem:
LoginButton.layer.addSublayer(gradient)
Do not add any extra layers to a UIButton. It already knows how to draw itself. If you want it to have a gradient background, draw the gradient into an image and set that as the button's background image in the normal way.
Or make the button clear and put the gradient layer behind the button (though this is not as good).
I need to draw an animated linear gradient that is updated on every frame, using CADisplayLink. Drawing directly into a full-screen context with CoreGraphics is slow, I get around 40fps at full CPU load on an iPad Air.
What's the fastest way to do this?
Update
As #Kurt Revis pointed out in a comment on the question, the proper - and fastest - way to do this is to use CAGradientLayer. Add a CAGradientLayer to your view that fills its bounds, then:
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Disable implicit animations if this is called via CADisplayLink
CATransaction.begin()
CATransaction.setDisableActions(true)
// Draw to existing CAGradientLayer
gradientBackgroundLayer.colors = gradientColors
gradientBackgroundLayer.locations = gradientLocs
CATransaction.commit()
}
Old answer
After some experimentation, I ended up using CoreGraphics to draw the gradient into a 1px wide CGImage and then rely on UIImageView to do the scaling. I get solid 60fps at about 7-11% CPU load, depending on how many colors the gradient contains:
Subclass UIImageView with contentMode set to ScaleToFill, then call the following via CADisplayLink for a continuous animation.
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Create context at 1px width and display height
let gradientWidth:Double = 1
let gradientHeight:Double = Double(UIScreen.mainScreen().bounds.height)
UIGraphicsBeginImageContext(CGSize(width: gradientWidth, height: gradientHeight))
let buffer:CGContextRef = UIGraphicsGetCurrentContext()!
// Draw gradient into buffer
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorspace, gradientColors, gradientLocs)
CGContextDrawLinearGradient(
buffer,
gradient,
CGPoint(x: 0.0, y: 0.0),
CGPoint(x: 0.0, y: gradientHeight),
CGGradientDrawingOptions.DrawsAfterEndLocation
)
// Let UIImageView superclass handle scaling
image = UIImage.init(CGImage: CGBitmapContextCreateImage(buffer)!)
// Avoid memory leaks...
UIGraphicsEndImageContext()
}
Caveat: this approach will only work for horizontal or vertical linear gradients.
Are there better ways of doing this?
I have created a progress bar to be used in a tableView by creating a gradient layer. It works perfectly.
iPhone5:
In order to use the app on multiple devices, I have created the UIView in Storyboard, tagged it and added constraints.
However, when I use the app on an iPhone 6 the CALayer don't resize.
iPhone6:
I find this extremely stupid, but never mind. I have looked around and tried to understand how to solve this for months, but I have come up short. Does ANYONE know how to make CALayers resize with the UIView? Any help would be very much appreciated ! Thank you.
progressBar = cell.contentView.viewWithTag(3) as UIView!
progressBar.layer.cornerRadius = 4
progressBar.layer.masksToBounds = true
// create gradient layer
let gradient : CAGradientLayer = CAGradientLayer()
// create color array
let arrayColors: [AnyObject] = [
UIColor (red: 255/255, green: 138/255, blue: 1/255, alpha: 1).CGColor,
UIColor (red: 110/255, green: 110/255, blue: 118/255, alpha: 1).CGColor]
// set gradient frame bounds to match progressBar bounds
gradient.frame = progressBar.bounds
// set gradient's color array
gradient.colors = arrayColors
//Set progress(progressBar)
var percentageCompleted = 0.6
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.locations = [percentageCompleted, percentageCompleted]
gradient.endPoint = CGPoint(x: 1, y: 0.5)
// replace base layer with gradient layer
progressBar.layer.insertSublayer(gradient, atIndex: 0)
The default layer of a UIView does resize with its view, but sublayers don't (as you found out). One way to make this work is to create a custom view class, move the code you have in your question to it, and override layoutSublayersOfLayer where you can set the gradient layer to be the same size as the view. Because this code is now in a custom class, I also created a property percentageCompleted (instead of a local variable), and added a willSet clause so the bar's appearance is updated any time you change the percentageCompleted property.
class RDProgressView: UIView {
private let gradient : CAGradientLayer = CAGradientLayer()
var percentageCompleted: Double = 0.0 {
willSet{
gradient.locations = [newValue, newValue]
}
}
override func awakeFromNib() {
self.layer.cornerRadius = 4
self.layer.masksToBounds = true
// create color array
let arrayColors: [AnyObject] = [
UIColor (red: 255/255, green: 138/255, blue: 1/255, alpha: 1).CGColor,
UIColor (red: 110/255, green: 110/255, blue: 118/255, alpha: 1).CGColor]
// set gradient's color array
gradient.colors = arrayColors
//Set progress(progressBar)
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.locations = [percentageCompleted, percentageCompleted]
gradient.endPoint = CGPoint(x: 1, y: 0.5)
self.layer.insertSublayer(gradient, atIndex: 0)
}
override func layoutSublayersOfLayer(layer: CALayer!) {
super.layoutSublayersOfLayer(layer)
gradient.frame = self.bounds
}
}
In IB, you would change the class of your view to RDProgressView (in my example), and in cellForRowAtIndexPath, you would only need to get a reference to the view, and set its percentageCompleted property.
progressBar = cell.contentView.viewWithTag(3) as RDProgressView!
progressBar.percentageCompleted = 0.2
As an alternative to the accepted answer, you could also change the views layer class to be CAGradientLayer. The views layer will always be resized according to layout changes. You can achieve that by subclassing UIView
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
}
then set the colors
if let gradientLayer = gradientView.layer as? CAGradientLayer {
gradientLayer.colors = arrayColors
}
It's less code than adding and maintaining a sublayer, but might not suit all use cases.