Is is possible to animate text color changing only in a part of text in iOS? - ios

I wonder if it even possible in iOS to animate changing color in only a part of the text, preferably not char by char, but pixel by pixel, like on this picture?
I know how to change text color in static with NSAttributedString and I know how to animate the whole text with CADisplayLink, but this makes me worry.
Maybe I can dive into CoreText, but I'm still not sure it is possible even with it. Any thoughts?
UPD I decided to add a video with my first results to make the question more clear:
my efforts for now (the label is overlapping)

You can quite easily achieve this using CoreAnimation possibilities.
I've added a simple demo, you play with it here (just build the project and tap anywhere to see the animation).
The logic is the following:
Create a custom subclass of UIView.
When some text is set, create two similar CATextLayers, each with the same text and frame.
Set different foregroundColor and mask for those layers. The mask of the left layer will be the left part of the view, and the mask of the right layer will be the right part.
Animate foregroundColor for those layers (simultaneously).
The code of a custom view:
class CustomTextLabel: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textLayer1: CATextLayer?
private var textLayer2: CATextLayer?
func setText(_ text: String, fontSize: CGFloat) {
// create 2 layers with the same text and size, we'll set the colors for them later
textLayer1 = createTextLayer(text, fontSize: fontSize)
textLayer2 = createTextLayer(text, fontSize: fontSize)
// estimate the frame size needed for the text layer with such text and font size
let textSize = textLayer1!.preferredFrameSize()
let w = frame.width, h = frame.height
// calculate the frame such that both layers will be in center of view
let centeredTextFrame = CGRect(x: (w-textSize.width)/2, y: (h-textSize.height)/2, width: textSize.width, height: textSize.height)
textLayer1!.frame = centeredTextFrame
textLayer2!.frame = centeredTextFrame
// set up default color for the text
textLayer1!.foregroundColor = UIColor.yellow.cgColor
textLayer2!.foregroundColor = UIColor.yellow.cgColor
// set background transparent, that's very important
textLayer1!.backgroundColor = UIColor.clear.cgColor
textLayer2!.backgroundColor = UIColor.clear.cgColor
// set up masks, such that each layer's text is visible only in its part
textLayer1!.mask = createMaskLayer(CGRect(x: 0, y: 0, width: textSize.width/2, height: textSize.height))
textLayer2!.mask = createMaskLayer(CGRect(x: textSize.width/2, y: 0, width: textSize.width/2, height: textSize.height))
layer.addSublayer(textLayer1!)
layer.addSublayer(textLayer2!)
}
private var finishColor1: UIColor = .black, finishColor2: UIColor = .black
func animateText(leftPartColor1: UIColor, leftPartColor2: UIColor, rightPartColor1: UIColor, rightPartColor2: UIColor) {
finishColor1 = leftPartColor2
finishColor2 = rightPartColor2
if let layer1 = textLayer1, let layer2 = textLayer2 {
CATransaction.begin()
let animation1 = CABasicAnimation(keyPath: "foregroundColor")
animation1.fromValue = leftPartColor1.cgColor
animation1.toValue = leftPartColor2.cgColor
animation1.duration = 3.0
layer1.add(animation1, forKey: "animation1")
let animation2 = CABasicAnimation(keyPath: "foregroundColor")
animation2.fromValue = rightPartColor1.cgColor
animation2.toValue = rightPartColor2.cgColor
animation2.duration = 3.0
layer2.add(animation2, forKey: "animation2")
CATransaction.setCompletionBlock {
self.textLayer1?.foregroundColor = self.finishColor1.cgColor
self.textLayer2?.foregroundColor = self.finishColor2.cgColor
}
CATransaction.commit()
}
}
private func createTextLayer(_ text: String, fontSize: CGFloat) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.string = text
textLayer.fontSize = fontSize // TODO: also set font name
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
private func createMaskLayer(_ holeRect: CGRect) -> CAShapeLayer {
let layer = CAShapeLayer()
let path = CGMutablePath()
path.addRect(holeRect)
path.addRect(bounds)
layer.path = path
layer.fillRule = CAShapeLayerFillRule.evenOdd
layer.opacity = 1
return layer
}
}
The calls of a custom view:
class ViewController: UIViewController {
var customLabel: CustomTextLabel!
override func viewDidLoad() {
super.viewDidLoad()
let viewW = view.frame.width, viewH = view.frame.height
let labelW: CGFloat = 200, labelH: CGFloat = 50
customLabel = CustomTextLabel(frame: CGRect(x: (viewW-labelW)/2, y: (viewH-labelH)/2, width: labelW, height: labelH))
customLabel.setText("Optimizing...", fontSize: 20)
view.addSubview(customLabel)
let tapRecogniner = UITapGestureRecognizer(target: self, action: #selector(onTap))
view.addGestureRecognizer(tapRecogniner)
}
#objc func onTap() {
customLabel.animateText(leftPartColor1: UIColor.blue,
leftPartColor2: UIColor.red,
rightPartColor1: UIColor.white,
rightPartColor2: UIColor.black)
}
}

Thanks to Olha's (#OlhaPavliuk) answer, I used two CATextLayer shapes and two CAShapeLayer masks for text layers. In draw method I just change masks frames to calculated size (bounds.width * progress value), and also change the second mask origin to a new start (bounds.width - bounds.width * progress value).
Also, it was very important to set layer.fillRule = CAShapeLayerFillRule.evenOdd while creating a mask, so that both layers became visible.
It turned out that I actually didn't need any animation code involved, because changing frames looks just ok.
In motion: https://giphy.com/gifs/LMbmlMoxY9oaWhXfO1
Full code: https://gist.github.com/joliejuly/a792c2ab8d97d304d731a4a5202f741a

Related

Masking CAGradientLayer over CALayers

In my scene I have 2 views: first holds CALayer instances (bars), another hold CAGradientLayer and placed over first one. Picture below describes current state.
But I need this gradient to be applied only to bars (CALayer) of the first view.
I haven't found any relevant information to my problem. Any help appreciated.
You have to apply a mask to the gradient. There are various ways you could approach this problem.
You could create a CAShapeLayer, set the shape layer's path to the shape of the bars, and set the gradient layer's mask to that shape layer.
Or you could get rid of the bar layer and instead use two gradient layers, one for the orange bars and the other for the gray bars. Put both gradient layers in a subview, side-by-side, and set the superview's layer mask to the shape layer. Here's how to do that.
You'll need two gradient layers and a shape layer:
#IBDesignable
class BarGraphView : UIView {
private let orangeGradientLayer = CAGradientLayer()
private let grayGradientLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
You'll also need the bar width:
private let barWidth = CGFloat(9)
At initialization time, set up the gradients and add all the sublayers:
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
backgroundColor = .black
initGradientLayer(orangeGradientLayer, with: .orange)
initGradientLayer(grayGradientLayer, with: .gray)
maskLayer.strokeColor = nil
maskLayer.fillColor = UIColor.white.cgColor
layer.mask = maskLayer
}
private func initGradientLayer(_ gradientLayer: CAGradientLayer, with color: UIColor) {
gradientLayer.colors = [ color, color, color.withAlphaComponent(0.6), color ].map({ $0.cgColor })
gradientLayer.locations = [ 0.0, 0.5, 0.5, 1.0 ]
layer.addSublayer(gradientLayer)
}
At layout time, set the frames of the gradient layers and set the mask layer's path. This requires a little work because you don't want a bar to be half orange and half gray.
override func layoutSubviews() {
super.layoutSubviews()
let barCount = ceil(bounds.size.width / barWidth)
let orangeBarCount = floor(barCount / 2)
let grayBarCount = barCount - orangeBarCount
var grayFrame = bounds
grayFrame.size.width = grayBarCount * barWidth
grayFrame.origin.x = frame.maxX - grayFrame.size.width
grayGradientLayer.frame = grayFrame
var orangeFrame = bounds
orangeFrame.size.width -= grayFrame.size.width
orangeGradientLayer.frame = orangeFrame
maskLayer.frame = bounds
maskLayer.path = barPath()
}
private func barPath() -> CGPath {
var columnBounds = self.bounds
columnBounds.origin.x = columnBounds.maxX
columnBounds.size.width = barWidth
let path = CGMutablePath()
for datum in barData.reversed() {
columnBounds.origin.x -= barWidth
let barHeight = CGFloat(datum) * columnBounds.size.height
let barRect = columnBounds.insetBy(dx: 1, dy: (columnBounds.size.height - barHeight) / 2)
path.addRoundedRect(in: barRect, cornerWidth: 2, cornerHeight: 2)
}
return path
}
let barData: [Double] = {
let count = 100
return (0 ..< count).map({ 0.5 + (1 + sin(8.0 * .pi * Double($0) / Double(count))) / 4 })
}()
}
Result:
The BarGraphView is transparent wherever there are no bars. If you want it on a dark background, put a dark view behind it, or make it a subview of a dark view:

Adding border with width to UIView show small background outside

I'm trying to add circle border to a UIView with green background, I created simple UIView subclass with borderWidth, cornerRadius and borderColor properties and I'm setting it from storyboard.
#IBDesignable
class RoundedView: UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable var borderColor: UIColor {
get {
if let color = layer.borderColor {
return UIColor(cgColor: color)
} else {
return UIColor.clear
}
}
set {
layer.borderColor = newValue.cgColor
}
}
}
But when I compile and run an app or display it in InterfaceBuilder I can see a line outside the border that is still there (and is quite visible on white background).
This RoundedView with green background, frame 10x10, corner radius = 5 is placed in corner of plain UIImageView (indicates if someone is online or not). You can see green border outside on both UIImageView and white background.
Can you please tell me what's wrong?
What you are doing is relying on the layer to draw your border and round the corners. So you are not in charge of the result. You gave it a green background, and now you are seeing the background "stick out" at the edge of the border. And in any case, rounding the corners is a really skanky and unreliable way to make a round view. To make a round view, make a round mask.
So, the way to make your badge is to take complete charge of what it is drawn: you draw a green circle in the center of a white background, and mask it all with a larger circle to make the border.
Here is a Badge view that will do precisely what you're after, with no artifact round the outside:
class Badge : UIView {
class Mask : UIView {
override init(frame:CGRect) {
super.init(frame:frame)
self.isOpaque = false
self.backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fillEllipse(in: CGRect(origin:.zero, size:rect.size))
}
}
let innerColor : UIColor
let outerColor : UIColor
let innerRadius : CGFloat
var madeMask = false
init(frame:CGRect, innerColor:UIColor, outerColor:UIColor, innerRadius:CGFloat) {
self.innerColor = innerColor
self.outerColor = outerColor
self.innerRadius = innerRadius
super.init(frame:frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.setFillColor(outerColor.cgColor)
con.fill(rect)
con.setFillColor(innerColor.cgColor)
con.fillEllipse(in: CGRect(
x: rect.midX-innerRadius, y: rect.midY-innerRadius,
width: 2*innerRadius, height: 2*innerRadius))
if !self.madeMask {
self.madeMask = true // do only once
self.mask = Mask(frame:CGRect(origin:.zero, size:rect.size))
}
}
}
I tried this with a sample setting as follows:
let v = Badge(frame: CGRect(x:100, y:100, width:16, height:16),
innerColor: .green, outerColor: .white, innerRadius: 5)
self.view.addSubview(v)
It looks fine. Adjust the parameters as desired.
I solved this by using a UIBezierPath and adding to the view's layer:
let strokePath = UIBezierPath(roundedRect: view.bounds, cornerRadius: view.frame.width / 2)
let stroke = CAShapeLayer()
stroke.frame = bounds
stroke.path = strokePath.cgPath
stroke.fillColor = .green.cgColor
stroke.lineWidth = 1.0
stroke.strokeColor = .white.cgColor
view.layer.insertSublayer(stroke, at: 2)
I solved this problem with gradients.
Just seting the backgroundColor of your circle as gradient.
let gradientLayer = CAGradientLayer()
//define colors
gradientLayer.colors = [<<your_bgc_color>>>>, <<border__bgc__color>>]
//define locations of colors as NSNumbers in range from 0.0 to 1.0
gradientLayer.locations = [0.0, 0.7]
//define frame
gradientLayer.frame = self.classView.bounds
self.classView.layer.insertSublayer(gradientLayer, at: 0)
MyImage
An easier fix might be to just mask it like this:
let mask = UIView()
mask.backgroundColor = .black
mask.frame = yourCircleView.bounds.inset(by: UIEdgeInsets(top: 0.1, left: 0.1, bottom: 0.1, right: 0.1))
mask.layer.cornerRadius = mask.height * 0.5
yourCircleView.mask = mask

Creating circle and using it as "bar graph"

I want to create a circle with an inner circle that looks like the image below. I'm having trouble with the inner circle and I don't know how to create it so it's easy to adjust percentage (like the image is showing).
So far I have this CircleGraph class which can draw the ouster circle and an inner circle which can only draw 50 %.
import Foundation
import UIKit
class CircleGraph: UIView
{
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
super.drawRect(rect)
// Outer circle
Colors().getMainColor().setFill()
let outerPath = UIBezierPath(ovalInRect: rect)
outerPath.fill()
// inner circle so far
let percentage = 0.5
UIColor.whiteColor().setFill()
let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.height/2,y: rect.height/2), radius: CGFloat(rect.height/2), startAngle: CGFloat(-M_PI_2), endAngle:CGFloat(M_PI * 2 * percentage - M_PI_2), clockwise: true)
circlePath.fill()
}
}
Can anyone assist me?
What I want is something simliar to the image below:
I would go for the easy solution and create a UIView with a UIView and UILabel as subviews. If you use something like:
// To make it round
let width = self.frame.width
self.view.layer.cornerRadius = width * 0.5
self.view.layer.masksToBounds = true
for each of the sublayers. If you have set the background colour of the UIView's background layer to something like Red and the UIView layer above to have a whiteish background colour with alpha 0.5 than you already achieve this effect.
If you do not know how to proceed with this tip ill try to provide a code sample.
-- EDIT --
Here is the code sample:
import UIKit
class CircleView: UIView {
var percentage : Int?
var transparency : CGFloat?
var bottomLayerColor : UIColor?
var middleLayerColor : UIColor?
init(frame : CGRect, percentage : Int, transparency : CGFloat, bottomLayerColor : UIColor, middleLayerColor : UIColor) {
super.init(frame : frame)
self.percentage = percentage
self.transparency = transparency
self.bottomLayerColor = bottomLayerColor
self.middleLayerColor = middleLayerColor
viewDidLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
viewDidLoad()
}
func viewDidLoad() {
let width = self.frame.width
let height = self.frame.height
let textFrame = CGRectMake(0, 0, width, height)
guard let percentage = self.percentage
else {
print("Error")
return
}
let newHeight = (CGFloat(percentage)/100.0)*height
let middleFrame = CGRectMake(0,height - newHeight, width, newHeight)
// Set Background Color
if let bottomLayerColor = self.bottomLayerColor {
self.backgroundColor = bottomLayerColor
}
// Make Bottom Layer Round
self.layer.cornerRadius = width * 0.5
self.layer.masksToBounds = true
// Create Middle Layer
let middleLayer = UIView(frame: middleFrame)
if let middleLayerColor = self.middleLayerColor {
middleLayer.backgroundColor = middleLayerColor
}
if let transparency = self.transparency {
middleLayer.alpha = transparency
}
// The Label
let percentageLayer = UILabel(frame: textFrame)
percentageLayer.textAlignment = NSTextAlignment.Center
percentageLayer.textColor = UIColor.whiteColor()
if let percentage = self.percentage {
percentageLayer.text = "\(percentage)%"
}
// Add Subviews
self.addSubview(middleLayer)
self.addSubview(percentageLayer)
}
}
To use in a View Controller:
let redColor = UIColor.redColor()
let blueColor = UIColor.blueColor()
let frame = CGRectMake(50, 50, 100, 100)
// 50% Example
let circleView = CircleView(frame: frame, percentage: 50, transparency: 0.5, bottomLayerColor: redColor, middleLayerColor: blueColor)
self.view.addSubview(circleView)
// 33% Example
let newFrame = CGRectMake(50, 150, 120, 120)
let newCircleView = CircleView(frame: newFrame, percentage: 33, transparency: 0.7, bottomLayerColor: UIColor.redColor(), middleLayerColor: UIColor.whiteColor())
self.view.addSubview(newCircleView)
This will yield something like this:

Reverse layer mask for label

How do you reverse the mask layer for a label? I have a textLabel, which I use as a mask for an imageView that contains an arbitrary image as follows:
let image = UIImage(named: "someImage")
let imageView = UIImageView(image: image!)
let textLabel = UILabel()
textLabel.frame = imageView.bounds
textLabel.text = "Some text"
imageView.layer.mask = textLabel.layer
imageView.layer.masksToBounds = true
The above makes the text in textLabel have a font colour of the imageView as in How to mask the layer of a view by the content of another view?.
How do I reverse this so as to remove the text in textLabel from the imageView?
Make a subclass of UILabel:
class InvertedMaskLabel: UILabel {
override func drawTextInRect(rect: CGRect) {
guard let gc = UIGraphicsGetCurrentContext() else { return }
CGContextSaveGState(gc)
UIColor.whiteColor().setFill()
UIRectFill(rect)
CGContextSetBlendMode(gc, .Clear)
super.drawTextInRect(rect)
CGContextRestoreGState(gc)
}
}
This subclass fills its bounds with an opaque color (white in this example, but only the alpha channel matters). Then it draws the text using the Clear blend mode, which simply sets all channels of the context back to 0, including the alpha channel.
Playground demo:
let root = UIView(frame: CGRectMake(0, 0, 400, 400))
root.backgroundColor = .blueColor()
XCPlaygroundPage.currentPage.liveView = root
let image = UIImage(named: "Kaz-256.jpg")
let imageView = UIImageView(image: image)
root.addSubview(imageView)
let label = InvertedMaskLabel()
label.text = "Label"
label.frame = imageView.bounds
label.font = .systemFontOfSize(40)
imageView.maskView = label
Result:
Since I needed to implement this recently and the syntax has changed a bit, here's a Swift 4.x Version of #RobMayoff's excellent answer. Demo/GitHub repo with Swift Playground located here.
(If you do upvote this, please also upvote his original answer as well :) )
A playground demonstrating the technique. The method drawRect inside InvertedMaskLabel has the secret sauce.
import UIKit
import PlaygroundSupport
// As per https://stackoverflow.com/questions/36758946/reverse-layer-mask-for-label
class InvertedMaskLabel: UILabel {
override func drawText(in rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
UIColor.white.setFill()
UIRectFill(rect) // fill bounds w/opaque color
context.setBlendMode(.clear)
super.drawText(in: rect) // draw text using clear blend mode, ie: set *all* channels to 0
context.restoreGState()
}
}
class TestView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .green
let image = UIImage(named: "tr")
let imageView = UIImageView(image: image)
imageview.frame = bounds
addSubview(imageView)
let label = InvertedMaskLabel()
label.text = "Teddy"
label.frame = imageView.bounds
label.font = UIFont.systemFont(ofSize: 30)
imageView.mask = label
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let testView = TestView(frame: CGRect(x: 0, y: 0, width: 400, height: 500))
PlaygroundPage.current.liveView = testView

Get color behind an object

I want to invert a button that looks like the following:
Inverting, in this case, would mean making the title blue and the button's background white. Since I do not want to hard-code it, I cannot simply set it blue. When I set the background color to white, and the title's color to clearColor, of course, the title is not readable anymore:
Is there a way to find out the color that is behind the button (which is a background image of the view currently), so I can set the title's color to that color?
Thanks in advance :)
Following on from Kjuly's answer, you should create a UIButton subclass to do this.
In order to apply your different styles, you just want to add some action listeners for the button touch events in order to re-draw your button when the state changes.
As Kjuly says, you'll then want to override drawRect in order to do your custom button drawing, depending on whether the button is pressed or not.
Something like this should achieve what you're after.
class CustomButton: UIButton {
private let paragraphStyle = NSMutableParagraphStyle()
private var titleAttributes = [String:AnyObject]()
private var isPressed = false
var highlightColor = UIColor.whiteColor() { // stroke & highlight color of the button
didSet {
setNeedsDisplay()
}
}
var cornerRadius:CGFloat = 10.0 { // corner radius of button
didSet {
setNeedsDisplay()
}
}
var strokeWidth:CGFloat = 5.0 { // stroke width of button
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: Selector("buttonWasPressed"), forControlEvents: .TouchDown)
addTarget(self, action: Selector("buttonWasReleased"), forControlEvents: .TouchUpInside)
addTarget(self, action: Selector("buttonWasReleased"), forControlEvents: .TouchDragExit)
addTarget(self, action: Selector("buttonWasReleased"), forControlEvents: .TouchCancel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func buttonWasPressed() {
isPressed = true
setNeedsDisplay() // set button to be redrawn
}
func buttonWasReleased() {
isPressed = false
setNeedsDisplay() // set button to be redrawn
}
override func drawRect(rect: CGRect) {
let size = bounds.size
let context = UIGraphicsGetCurrentContext()
if isPressed { // button pressed down
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) // path of button shape for filling
CGContextSetFillColorWithColor(context, highlightColor.CGColor) // white background of the button
path.fill() // fill path
CGContextSetBlendMode(context, .DestinationOut) // set blend mode for transparent label
} else { // button not pressed
let path = UIBezierPath(roundedRect: UIEdgeInsetsInsetRect(bounds, UIEdgeInsets(top: strokeWidth*0.5, left: strokeWidth*0.5, bottom: strokeWidth*0.5, right: strokeWidth*0.5)), cornerRadius: cornerRadius-strokeWidth*0.5) // path of button shape for stroking
CGContextSetStrokeColorWithColor(context, highlightColor.CGColor) // set stroke color
path.lineWidth = strokeWidth
path.stroke()
}
guard let label = titleLabel else {return} // title label
guard let text = label.text else {return} // text to draw
// update text attributes, add any extra attributes you want transferred here.
paragraphStyle.alignment = label.textAlignment
titleAttributes[NSFontAttributeName] = label.font
titleAttributes[NSParagraphStyleAttributeName] = paragraphStyle
titleAttributes[NSForegroundColorAttributeName] = label.textColor
let textHeight = text.sizeWithAttributes(titleAttributes).height // heigh of the text to render, with the attributes
let renderRect = CGRect(x:0, y:(size.height-textHeight)*0.5, width:size.width, height:size.height) // rect to draw the text in
text.drawInRect(renderRect, withAttributes: titleAttributes) // draw text
}
}
You can then use this button by setting the various attributes on the button's titleLabel, as well the strokeWidth and cornerRadius property. For example:
let button = CustomButton(frame: CGRect(x: 50, y: 50, width: 200, height: 75))
button.titleLabel?.text = "Let's go!"
button.titleLabel?.font = UIFont.systemFontOfSize(30)
button.titleLabel?.textAlignment = .Center
button.titleLabel?.textColor = UIColor.whiteColor()
button.cornerRadius = 15.0
button.strokeWidth = 5.0
view.addSubview(button)
You need to create a subclass of the UIButton (or just UIControl), and override the -drawRect: to do rending w/ custom blend mode (kCGBlendModeDestinationOut):
- (void)drawRect:(CGRect)rect
{
[[UIColor whiteColor] setFill]; // white background of the button
UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:5.f];
[path fill];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetBlendMode(context, kCGBlendModeDestinationOut); // Set blend mode
// u'd better to cache this attributes if u need to redraw the button frequently.
NSMutableParagraphStyle * paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.alignment = NSTextAlignmentCenter;
NSDictionary * attributes = #{NSFontAttributeName:[UIFont systemFontOfSize:20.f], NSParagraphStyleAttributeName:paragraphStyle};
[#"Let's Go" drawAtPoint:CGPointMake(0.f, 0.f) withAttributes:attributes];
CGContextRestoreGState(context);
}
The kCGBlendModeDestinationOut is the blend mode you need to set, which will just show the view below current one (R = D*(1 - Sa)), in your case, the background image.
And about blend mode:
... R, S, and D are, respectively,
premultiplied result, source, and destination colors with alpha; Ra,
Sa, and Da are the alpha components of these colors.
The Porter-Duff "source over" mode is called `kCGBlendModeNormal':
R = S + D*(1 - Sa)
Note that the Porter-Duff "XOR" mode is only titularly related to the
classical bitmap XOR operation (which is unsupported by
CoreGraphics).
If your UIButton sits in a UIView and You want to access the UIView's backgroundColor, you can do so my referencing the UIButton's superclass.
Example:
#IBOutlet weak var myButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.backgroundColor = myButton.superview?.backgroundColor
}
If you want to use the color of a Background Image you might want to have a look into CoreImage to get the average or perPixel Color of that image.

Resources