CAGradientLayer, not resizing nicely, tearing on rotation - iphone

I'm trying to get my CAGradientLayers, that i'm using to create nice gradient backgrounds, to resize nicely on rotation and modal view presentation, but they will not play ball.
Here is a video I just created showing my problem: Notice the tearing on rotation.
Also please note this video was created by filming the iPhone Simulator on OS X. I have slowed down the animations in the video to highlight my issue.
Video of Problem...
Here is an Xcode project which I just created (which is the source for the app shown in the video), basically as illustrated the issue occurs on rotation and especially when views are presented modally:
Xcode Project, modally presenting views with CAGradientLayer backgrounds...
For what it's worth I understand that using:
[[self view] setBackgroundColor:[UIColor blackColor]];
does a reasonable job of making the transitions a bit more seamless and less jarring, but if you look at the video when I, whilst currently in landscape mode, modally present a view, you will see why the above code will not help.
Any ideas what I can do to sort this out?
John

When you create a layer (like your gradient layer), there's no view managing the layer (even when you add it as a sublayer of some view's layer). A standalone layer like this doesn't participate in the UIView animation system.
So when you update the frame of the gradient layer, the layer animates the change with its own default animation parameters. (This is called “implicit animation”.) These default parameters don't match the animation parameters used for interface rotation, so you get a weird result.
I didn't look at your project but it's trivial to reproduce your problem with this code:
#interface ViewController ()
#property (nonatomic, strong) CAGradientLayer *gradientLayer;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.colors = #[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view.layer addSublayer:self.gradientLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.gradientLayer.frame = self.view.bounds;
}
#end
Here's what that looks like, with slow motion enabled in the simulator:
Fortunately, this is an easy problem to fix. You need to make your gradient layer be managed by a view. You do that by creating a UIView subclass that uses a CAGradientLayer as its layer. The code is tiny:
// GradientView.h
#interface GradientView : UIView
#property (nonatomic, strong, readonly) CAGradientLayer *layer;
#end
// GradientView.m
#implementation GradientView
#dynamic layer;
+ (Class)layerClass {
return [CAGradientLayer class];
}
#end
Then you need to change your code to use GradientView instead of CAGradientLayer. Since you're using a view now instead of a layer, you can set the autoresizing mask to keep the gradient sized to its superview, so you don't have to do anything later to handle rotation:
#interface ViewController ()
#property (nonatomic, strong) GradientView *gradientView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientView = [[GradientView alloc] initWithFrame:self.view.bounds];
self.gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.gradientView.layer.colors = #[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view addSubview:self.gradientView];
}
#end
Here's the result:

The best part about #rob's answer is that the view controls the layer for you. Here is the Swift code that properly overrides the layer class and sets the gradient.
import UIKit
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
guard let theLayer = self.layer as? CAGradientLayer else {
return;
}
theLayer.colors = [UIColor.whiteColor.cgColor, UIColor.lightGrayColor.cgColor]
theLayer.locations = [0.0, 1.0]
theLayer.frame = self.bounds
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
You can then add the view in two lines wherever you want.
override func viewDidLoad() {
super.viewDidLoad()
let gradientView = GradientView(frame: self.view.bounds)
self.view.insertSubview(gradientView, atIndex: 0)
}

My swift version:
import UIKit
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
func gradientWithColors(firstColor : UIColor, _ secondColor : UIColor) {
let deviceScale = UIScreen.mainScreen().scale
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRectMake(0.0, 0.0, self.frame.size.width * deviceScale, self.frame.size.height * deviceScale)
gradientLayer.colors = [ firstColor.CGColor, secondColor.CGColor ]
self.layer.insertSublayer(gradientLayer, atIndex: 0)
}
}
Note that I also had to use the device scale to calculate the frame size - to get correct auto-sizing during orientation changes (with auto-layout).
In Interface Builder, I added a UIView and changed its class to GradientView (the class shown above).
I then created an outlet for it (myGradientView).
Finally, In the view controller I added:
override func viewDidLayoutSubviews() {
self.myGradientView.gradientWithColors(UIColor.whiteColor(), UIColor.blueColor())
}
Note that the gradient view is created in a "layoutSubviews" method, since we need a finalized frame to create the gradient layer.

It will look better when you insert this piece of code and remove the willAnimateRotationToInterfaceOrientation:duration: implementation.
- (void)viewWillLayoutSubviews
{
[[[self.view.layer sublayers] objectAtIndex:0] setFrame:self.view.bounds];
}
This is however not very elegant. In a real application you should subclass UIView to create a gradient view. In this custom view you can override layerClass so that it is backed by a gradient layer:
+ (Class)layerClass
{
return [CAGradientLayer class];
}
Also implement layoutSubviews to handle when the bounds of the view changes.
When creating this background view use autoresizing masks so that the bounds automatically adjust on interface rotations.

Complete Swift version. Set viewFrame from the viewController that owns this view in viewDidLayoutSubviews
import UIKit
class MainView: UIView {
let topColor = UIColor(red: 146.0/255.0, green: 141.0/255.0, blue: 171.0/255.0, alpha: 1.0).CGColor
let bottomColor = UIColor(red: 31.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).CGColor
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupGradient()
}
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
var viewFrame: CGRect! {
didSet {
self.bounds = viewFrame
}
}
private func setupGradient() {
gradientLayer.colors = [topColor, bottomColor]
}
}

Info
Use as one line solution
Replacing gradient when you add it to the view again (to use in reusables)
Automatically transiting
Automatically removing
Details
Swift 3.1, xCode 8.3.3
Solution
import UIKit
extension UIView {
func addGradient(colors: [UIColor], locations: [NSNumber]) {
addSubview(ViewWithGradient(addTo: self, colors: colors, locations: locations))
}
}
class ViewWithGradient: UIView {
private var gradient = CAGradientLayer()
init(addTo parentView: UIView, colors: [UIColor], locations: [NSNumber]){
super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 2))
restorationIdentifier = "__ViewWithGradient"
for subView in parentView.subviews {
if let subView = subView as? ViewWithGradient {
if subView.restorationIdentifier == restorationIdentifier {
subView.removeFromSuperview()
break
}
}
}
let cgColors = colors.map { (color) -> CGColor in
return color.cgColor
}
gradient.frame = parentView.frame
gradient.colors = cgColors
gradient.locations = locations
backgroundColor = .clear
parentView.addSubview(self)
parentView.layer.insertSublayer(gradient, at: 0)
parentView.backgroundColor = .clear
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
parentView.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let parentView = superview {
gradient.frame = parentView.bounds
}
}
override func removeFromSuperview() {
super.removeFromSuperview()
gradient.removeFromSuperlayer()
}
}
Usage
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
Using StoryBoard
ViewController
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}
StoryBoard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_17555986" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uii-31-sl9">
<rect key="frame" x="66" y="70" width="243" height="547"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uii-31-sl9" secondAttribute="bottom" constant="50" id="a7J-Hq-IIq"/>
<constraint firstAttribute="trailingMargin" secondItem="uii-31-sl9" secondAttribute="trailing" constant="50" id="i9v-hq-4tD"/>
<constraint firstItem="uii-31-sl9" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="50" id="wlO-83-8FY"/>
<constraint firstItem="uii-31-sl9" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="50" id="zb6-EH-j6p"/>
</constraints>
</view>
<connections>
<outlet property="viewWithGradient" destination="uii-31-sl9" id="FWB-7A-MaH"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
Programmatically
import UIKit
class ViewController2: UIViewController {
#IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let viewWithGradient = UIView(frame: CGRect(x: 10, y: 20, width: 30, height: 40))
view.addSubview(viewWithGradient)
viewWithGradient.translatesAutoresizingMaskIntoConstraints = false
let constant:CGFloat = 50.0
NSLayoutConstraint(item: viewWithGradient, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin
, multiplier: 1.0, constant: constant).isActive = true
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}

Another swift version - which is not using drawRect.
class UIGradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
func setGradientBackground(colors: [UIColor], startPoint: CGPoint = CGPoint(x: 0.5, y: 0), endPoint: CGPoint = CGPoint(x: 0.5, y: 1)) {
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.colors = colors.map({ (color) -> CGColor in return color.CGColor })
}
}
In controller I just call:
gradientView.setGradientBackground([UIColor.grayColor(), UIColor.whiteColor()])

Personally, I prefer to keep everything self-contained within the view subclass.
Here's my Swift implementation:
import UIKit
#IBDesignable
class GradientBackdropView: UIView {
#IBInspectable var startColor: UIColor=UIColor.whiteColor()
#IBInspectable var endColor: UIColor=UIColor.whiteColor()
#IBInspectable var intermediateColor: UIColor=UIColor.whiteColor()
var gradientLayer: CAGradientLayer?
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
super.drawRect(rect)
if gradientLayer == nil {
self.addGradientLayer(rect: rect)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: rect)
}
}
override func layoutSubviews() {
super.layoutSubviews()
if gradientLayer == nil {
self.addGradientLayer(rect: self.bounds)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: self.bounds)
}
}
func addGradientLayer(rect rect:CGRect) {
gradientLayer=CAGradientLayer()
gradientLayer?.frame=self.bounds
gradientLayer?.colors=[startColor.CGColor,intermediateColor.CGColor,endColor.CGColor]
gradientLayer?.startPoint=CGPointMake(0.0, 1.0)
gradientLayer?.endPoint=CGPointMake(0.0, 0.0)
gradientLayer?.locations=[NSNumber(float: 0.1),NSNumber(float: 0.5),NSNumber(float: 1.0)]
self.layer.insertSublayer(gradientLayer!, atIndex: 0)
gradientLayer?.transform=self.layer.transform
}
}

You can use this from storyboard, xib or code. You can change the colors dynamically later (I needed that for my case)
Adding a complete copy-pastable one here:
import UIKit
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
extension GradientView {
func setVerticalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 1))
}
func setHorizontalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0, y: 0.5), endPoint: .init(x: 1, y: 0.5))
}
func setGradientBackground(colors: [CGColor],
locations: [CGFloat],
startPoint: CGPoint,
endPoint: CGPoint) {
guard let gradientLayer = self.layer as? CAGradientLayer else {
return
}
gradientLayer.colors = colors
gradientLayer.locations = locations.map { $0 as NSNumber }
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.frame = bounds
}
}

Easy way. You can add a gradient layer each time when your view changes its size:
class YourVC: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
yourView.addObserver(self, forKeyPath: "bounds", options: [], context: nil)
}
...
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (object as? NSObject == yourView && keyPath == "bounds") {
//remove and add again your gradient layer
}
}
...

Related

Adding custom UIView to the view controller causes it to disappear in the storyboard

I created a custom UIView and then added it to a number of viewControllers. The view controller become glitchy and do not render in the storyboard. I just see the view controllers' outlines and no content. If I remove the custom view then all goes back to normal.
Can someone please take a look at my custom view code and see if I am doing anything wrong? Thank you so much.
import UIKit
#IBDesignable
class ProgressView: UIView {
#IBOutlet var contentView: UIView!
#IBInspectable var percentageProgress: CGFloat = 0.0 {
didSet {
self.setGradient()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("ProgressView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
setGradient()
}
private func setGradient() {
if let sublayers = self.contentView.layer.sublayers {
for sublayer in sublayers {
sublayer.removeFromSuperlayer()
}
}
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor(#colorLiteral(red: 0.3935321569, green: 0.3986310661, blue: 0.6819975972, alpha: 1)).cgColor, UIColor(#colorLiteral(red: 0.1705667078, green: 0.649338007, blue: 0.5616513491, alpha: 1)).cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.cornerRadius = contentView.frame.height / 2
contentView.layer.cornerRadius = contentView.frame.height / 2
self.contentView.layer.insertSublayer(gradientLayer, at: 0)
gradientLayer.frame = CGRect(x: contentView.frame.minX, y: contentView.frame.minY,
width: self.contentView.frame.maxX * (percentageProgress ?? 1.0), height:
contentView.frame.maxY)
self.contentView.setNeedsDisplay()
}
}

Custom UILabel with gradient text colour always become black

I created a custom UILabel to show grediant text but it always show the text as black...when i but the same code on a view controller it works!!
import UIKit
class GradientLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
let gredient = GradientView.init(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
gredient.bottomColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)
gredient.topColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)
setTextColorToGradient(image: imageWithView(view: gredient)!)
}
func imageWithView(view: UIView) -> UIImage? {//bet7awel uiview to image
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
func setTextColorToGradient(image: UIImage) {//beta7'od image we tekteb beha el text fe el label
UIGraphicsBeginImageContext(frame.size)
image.draw(in: bounds)
let myGradient = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
textColor = UIColor(patternImage: myGradient!)
}
}
You don’t want to rely on awakeFromNib. Furthermore, you really don’t want to do this in init, either, as you want to be able to respond to size changes (e.g. if you have constraints and the frame changes after the label is first created).
Instead, update your gradient from layoutSubviews, which is called whenever the view’s frame changes:
#IBDesignable
class GradientLabel: UILabel {
#IBInspectable var topColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1) {
didSet { setNeedsLayout() }
}
#IBInspectable var bottomColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1) {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
updateTextColor()
}
private func updateTextColor() {
let image = UIGraphicsImageRenderer(bounds: bounds).image { _ in
let gradient = GradientView(frame: bounds)
gradient.topColor = topColor
gradient.bottomColor = bottomColor
gradient.drawHierarchy(in: bounds, afterScreenUpdates: true)
}
textColor = UIColor(patternImage: image)
}
}
That results in:
Note,
I’ve used the new UIGraphicsImageRenderer rather than UIGraphicsBeginImageContext, UIGraphicsGetImageFromCurrentImageContext, and UIGraphicsEndImageContext.
Rather than building a CGRect manually, I just use bounds.
I’ve made it #IBDesignable so that I can use it directly in Interface Builder. I’ve also made the colors #IBInspectable so you can adjust the colors right in IB rather than going to code. Clearly, you only have to do this if you want to see the gradient effect rendered in IB.
I’ve made it update the gradient (a) when the label needs to be laid out again; and (b) whenever you change either of the colors.
For what it’s worth, this is the GradientView I used for the purposes of this example:
#IBDesignable
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
#IBInspectable var topColor: UIColor = .white { didSet { updateColors() } }
#IBInspectable var bottomColor: UIColor = .blue { didSet { updateColors() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
updateColors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateColors()
}
private func updateColors() {
gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor]
}
}
In this case, because we’re setting the layerClass to the gradient, all we need to do is to configure it during init, and the base layerClass will take care of responding the size changes.
Alternatively, you can just draw your gradient using CoreGraphics:
#IBDesignable
class GradientLabel: UILabel {
#IBInspectable var topColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1) {
didSet { setNeedsLayout() }
}
#IBInspectable var bottomColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1) {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
updateTextColor()
}
private func updateTextColor() {
let image = UIGraphicsImageRenderer(bounds: bounds).image { context in
let colors = [topColor.cgColor, bottomColor.cgColor]
guard let gradient = CGGradient(colorsSpace: nil, colors: colors as CFArray, locations: nil) else { return }
context.cgContext.drawLinearGradient(gradient,
start: CGPoint(x: bounds.midX, y: bounds.minY),
end: CGPoint(x: bounds.midX, y: bounds.maxY),
options: [])
}
textColor = UIColor(patternImage: image)
}
}
This achieves the same as the first example, but is potentially a tad more efficient.
You should init your label instead of awakeFromNib cause you are not using a xib for your label
class GradientLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
and an extension would be perfect to make your code reusable
extension UILabel {
func setTextColorToGradient(_ image: UIImage) {
UIGraphicsBeginImageContext(frame.size)
image.draw(in: bounds)
let myGradient = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
textColor = UIColor(patternImage: myGradient!)
}
}
usage:
label.setTextColorToGradient(UIImage(named:"someImage")!)

Enable user interaction on overlay view

I'm creating an darken overlay on top of my UITableView, I would like to highlight one of the row. I'm able to highlight it, but how can I enable user interaction (e.g. tap on the cell)?
func addFullScreenDarkOverlay(){
let viewDarkOverlay = UIView()
viewDarkOverlay.alpha = 0.5
viewDarkOverlay.backgroundColor = .black
viewDarkOverlay.frame = UIApplication.shared.keyWindow!.frame
// add dummy hole (will change it to the frame of the particular cell)
let holeFrame = CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width/2, height: viewDarkOverlay.frame.height/2)
// Do any additional setup after loading the view.
let path = CGMutablePath()
// Dark background
path.addRect(CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width, height: viewDarkOverlay.frame.height))
// White/spotlight path
path.addRect(holeFrame)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
viewDarkOverlay.layer.mask = maskLayer
viewDarkOverlay.clipsToBounds = true
UIApplication.shared.keyWindow!.addSubview(viewDarkOverlay)
}
Basically, you can disable the user interaction of the mask view, that will pass the all the touch events to the view below... however, if you just want the highlighted area to be intractable (i.e. the cell), you can create a custom UIView subclass, override the hitTest method, like so:
import UIKit
class MaskView: UIView {
var hitableArea: CGRect {
return CGRect(x: 0, y: 0, width: bounds.width * 0.5, height: bounds.height * 0.5)
}
override init(frame: CGRect) {
super.init(frame: frame)
installMaskLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
installMaskLayer()
}
private func installMaskLayer() {
backgroundColor = UIColor.black.withAlphaComponent(0.5)
let path = CGMutablePath()
// Dark background
path.addRect(bounds)
// White/spotlight path
path.addRect(hitableArea)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
layer.mask = maskLayer
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if (hitableArea.contains(point)) {
return nil
}
return self
}
}
Just create this custom view and add it to your window, and now you can only touch the cell now.
Remove the user interaction in your overlay view and you will be able to select the rows in the view below it:
viewDarkOverlay.isUserInteractionEnabled = false
If you want the selection only inside the whole you can transfer the touch from the overlay view to your parent view using a delegate like so:
protocol OverlaySelection: class{
func selected(with touch: UITouch?)
}
class OverlayView: UIView{
weak var delegate : OverlaySelection?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
delegate?.selected(with: touches.first)
}
}
then in your parent view:
func addFullScreenDarkOverlay(){
let viewDarkOverlay = OverlayView()
viewDarkOverlay.delegate = self
//other config..
}
//Delegate
func selected(with touch: UITouch?) {
if let location = touch?.location(in: self.view){
//Here you check if this point is inside the whole and if it is you select
//the row, if not just return
let indexpath = tableview.indexPathForRow(at: location)
tableview.selectRow(at: indexpath, animated: true, scrollPosition: .bottom)
}
}

MacOS: Custom NSView won't show subviews

I have a View Controller and added a Subview to its View. The class of the Subview should also have some Subviews etc. Here is my code of my custom Class:
import Foundation
import Cocoa
class PercentageShower: NSView {
let percentageBar = NSView()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
self.layer?.backgroundColor = CGColor.black
let testsubview = NSView(frame: frameRect)
testsubview.wantsLayer = true
testsubview.layer?.backgroundColor = CGColor.init(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
self.addSubview(testsubview)
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
}
}
This is the ViewControllers code:
class Basics1ViewController: NSViewController {
let percentageShower = PercentageShower()
override func viewDidLoad() {
super.viewDidLoad()
// Set screen mesurements:
let screen = NSScreen.main
let screenWidth = (screen?.frame.width)!
let screenHeight = (screen?.frame.height)!
// Set Percentage View
percentageShower.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
self.view.addSubview(percentageShower)
}
}
This is just an example, but why isn't my red colored subview shown? I can see the black colored view itself but not it's subview. Looking forward to your help!

How Can I Make A CALayer And All Its Sublayers Zoom Proportionally?

I am trying to make an NSScrollView which displays document pages. I have added a CALayer-backed NSView as the NSScrollView's documentView, and then I have added a CALayer sublayer to the documentView. When I zoom the NSScollview, the documentView correctly zooms in and out. However, the sublayers of the documentView do not scale proportionally with their containing documentView. If I do not set an autoresizingMask on the sublayers of the documentView layer, the sublayers simply fly off the screen when the documentView is zoomed. If I use the LayerWidthSizable/LayerHeightSizable options, the sublayers get way larger or smaller than they should be in reference to the documentView superlayer. Here is the code I have so far:
Here is the NSScrollview:
class LayerScrollView: NSScrollView {
var containerLayer: ContainerLayer!
override func awakeFromNib() {
documentView = ContainerLayer(frame: frame)
}
}
Here is the ContainerLayer (the documentView CALayer):
class ContainerLayer: NSView {
let documentLayer: DocumentLayer = DocumentLayer()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
autoresizesSubviews = true
wantsLayer = true
layer = CATiledLayer()
layer?.delegate = self
layer?.backgroundColor = NSColor.blueColor().CGColor
layer?.masksToBounds = true
documentLayer.frame = CGRect(x: frame.width / 4.0, y: frame.height / 4.0, width: frame.width / 2.0, height: frame.height / 2.0)
documentLayer.delegate = documentLayer
layer?.addSublayer(documentLayer)
documentLayer.autoresizingMask = CAAutoresizingMask.LayerWidthSizable | CAAutoresizingMask.LayerHeightSizable
documentLayer.setNeedsDisplay()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
CGContextSetFillColorWithColor(ctx, NSColor.redColor().CGColor)
CGContextFillRect(ctx, layer.bounds)
}
}
And finally, here is the DocumentLayer (the sublayers contained in the DocumentLayer):
class DocumentLayer: CALayer {
override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
CGContextSetFillColorWithColor(ctx, NSColor.redColor().CGColor)
CGContextFillRect(ctx, layer.bounds)
}
}
Here is a picture to illustrate the problem and my desired results:
The blue rectangle is the ContainerLayer, the red rectangle is the DocumentLayer.
I have looked through numerous tutorials and the documents and come up empty. It seems like this should be super-easy to do. What am I missing here?
UPDATE: SOLVED IT
Here is the solution, found after more hours of digging through documentation and other stuff (also, thanks to #mahaltertin for suggesting that I use borderWidth to help debug):
The LayerScrollView:
class LayerScrollView: NSScrollView {
var containerLayer: ContainerLayer!
override func awakeFromNib() {
containerLayer = ContainerLayer(frame: frame)
documentView = containerLayer
}
}
The ContainerLayer:
class ContainerLayer: NSView {
let documentLayer: DocumentLayer = DocumentLayer()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
layer = CALayer()
layer?.layoutManager = CAConstraintLayoutManager.layoutManager()
layer?.delegate = self
layer?.backgroundColor = NSColor.blueColor().CGColor
documentLayer.delegate = documentLayer
documentLayer.name = "documentLayer"
documentLayer.borderWidth = 1.0
documentLayer.backgroundColor = NSColor.redColor().CGColor
documentLayer.addConstraint(CAConstraint(attribute: CAConstraintAttribute.Width, relativeTo: "superlayer", attribute: CAConstraintAttribute.Width, scale: 0.5, offset: 0.0))
documentLayer.addConstraint(CAConstraint(attribute: CAConstraintAttribute.Height, relativeTo: "superlayer", attribute: CAConstraintAttribute.Height, scale: 0.5, offset: 0.0))
documentLayer.addConstraint(CAConstraint(attribute: CAConstraintAttribute.MidX, relativeTo: "superlayer", attribute: CAConstraintAttribute.MidX, scale: 1.0, offset: 0.0))
documentLayer.addConstraint(CAConstraint(attribute: CAConstraintAttribute.MidY, relativeTo: "superlayer", attribute: CAConstraintAttribute.MidY, scale: 1.0, offset: 0.0))
layer?.addSublayer(documentLayer)
layer?.setNeedsDisplay()
documentLayer.setNeedsDisplay()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And the DocumentLayer:
class DocumentLayer: CALayer {
override func actionForKey(event: String!) -> CAAction! {
return nil
}
}
The solution is to use constraints. The override of the actionForKey method in the DocumentLayer is to prevent the DocumentLayer from animating while it zooms. The constraints defined in the ContainerLayer specify that the DocumentLayer should be half the width/height of the ContainerLayer, and that the middle of both layers should be the same. Zooming now keeps the proportions correct.