Use mask to make UIButton text transparent in Swift 4 - ios

I'm having a bit of trouble figuring out how to make the UIButton title transparent such that it's color is the color of the superview's gradient background.
I've seen a thread about rendering the button as an image, but it was in objective-c with the old CG API and I'm wondering if anyone can give advice on a better way to solve this problem.
Any advice would be appreciated!
This is what I have so far:
import UIKit
import Foundation
class MainViewController: UIViewController {
let headingLabel: UILabel = {
let label = UILabel()
label.text = "Hello, World"
label.font = label.font.withSize(30)
label.textColor = .white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let continueButton: UIButton = {
let button = UIButton()
button.setTitleColor(.black, for: .normal)
button.setTitle("Continue", for: .normal)
button.layer.cornerRadius = 10
button.backgroundColor = .white
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let gradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [
UIColor(red: 96/255, green: 165/255, blue: 238/255, alpha: 1.0).cgColor,
UIColor(red: 233/255, green: 97/255, blue: 99/255, alpha: 1.0).cgColor,
]
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidAppear(_ animated: Bool) {
navigationController?.navigationBar.isHidden = true
}
func setupView() {
// root view
gradientLayer.frame = view.frame
view.layer.addSublayer(gradientLayer)
view.addSubview(headingLabel)
view.addSubview(continueButton)
// constraints
let views: [String: UIView] = [
"headingLabel": headingLabel,
"continueButton": continueButton,
"superview": view
]
var constraints: [NSLayoutConstraint] = []
let verticalHeadingLabelConstraint = NSLayoutConstraint.constraints(withVisualFormat:
"V:|-100-[headingLabel(30)]",
options: [],
metrics: nil,
views: views)
constraints += verticalHeadingLabelConstraint
let horizontalHeadingLabelConstraint = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-[headingLabel]-|",
options: .alignAllCenterX,
metrics: nil,
views: views)
constraints += horizontalHeadingLabelConstraint
let verticalContinueButtonConstraint = NSLayoutConstraint.constraints(withVisualFormat:
"V:[continueButton(50)]-100-|",
options: [],
metrics: nil,
views: views)
constraints += verticalContinueButtonConstraint
let horizontalContinueButtonConstraint = NSLayoutConstraint.constraints(withVisualFormat:
"H:|-100-[continueButton]-100-|",
options: [],
metrics: nil,
views: views)
constraints += horizontalContinueButtonConstraint
view.addConstraints(constraints)
}
}

If you want to do this is better that you think about a mask.
To do that you must write the text into a bitmap context and use it as a mask on a solid color.
If found something that can help you here.
In few steps:
You create an image context
You set white color text to be used as as a mask
You draw the text inside the context
You create the mask
You create another image based on the solid color
You clip them

If you are using the storyboard builder. There is a colored box next to text color. Press that and there is a slider called opacity.

Related

Button Not Fully Drawn

I have a button with text and an image on it. It gets set up in viewDidAppear and then in the IBAction I change the Attributed title. For some reason the button background color doesn't completely cover the button on the initial draw. It leaves a horizontal sliver of white. I found that by running my formatButton function in the IBAction subsequent button presses show a properly drawn button. But I can't get the first loaded view of the button to look right. Any ideas?
I found that by formatting in the IBAction it fixed it for future button draws but a sendAction(.touchUpInside) couldn't even fake it into fixing the draw problem. (It did change the button text like the IBAction makes it though.)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
formatButton(btn: searchTitlesButton)
formatButton(btn: searchPeopleButton)
formatButton(btn: searchCategoryButton)
searchTitlesButton.setTitle("Title", for: .normal)
searchPeopleButton.setTitle("Actor", for: .normal)
//searchCategoryButton.setTitle(categoryList[searchCategoryIndex], for: .normal)
let fullString = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named:"DownArrow")
let imageString = NSAttributedString(attachment: imageAttachment)
fullString.append(NSAttributedString(string: categoryList[searchCategoryIndex]+" "))
fullString.append(imageString)
searchCategoryButton.setAttributedTitle(fullString, for: .normal)
formatButton(btn: searchCategoryButton)
postTableView.rowHeight = CGFloat(120)
}
#IBAction func searchCategoryButton(_ sender: Any) {
if searchCategoryIndex < categoryList.count - 1 {
searchCategoryIndex += 1
} else {
searchCategoryIndex = 0
}
// Going to try and make a formatted label with a string and image of a down arrow.
let fullString = NSMutableAttributedString()
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(named:"DownArrow")
let imageString = NSAttributedString(attachment: imageAttachment)
fullString.append(NSAttributedString(string: categoryList[searchCategoryIndex]+" "))
fullString.append(imageString)
searchCategoryButton.setAttributedTitle(fullString, for: .normal)
formatButton(btn: searchCategoryButton)
}
func formatButton(btn:UIButton) {
btn.layer.cornerRadius = 5
btn.layer.borderWidth = 1
btn.layer.borderColor = UIColor.black.cgColor
btn.setTitleColor(UIColor.white, for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.bold)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = btn.bounds
let bottomColor = UIColor(red: CGFloat(25/255.0), green: CGFloat(113/255.0), blue: CGFloat(255/255.0), alpha: CGFloat(1.0))
gradientLayer.colors = [UIColor.white.cgColor, bottomColor.cgColor]
btn.layer.insertSublayer(gradientLayer, at: 0)
btn.clipsToBounds = true
}
The reason why the background gradient doesn't fully cover the button, it probably because the size of the button changes when you set the attributed title. The best way to solve this, is by creating a subclass of UIButton, so that you can update the frame of your custom gradient layer, whenever the button's bounds change. For example:
class GradientButton: UIButton {
private let gradientLayer = CAGradientLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
setTitleColor(UIColor.white, for: .normal)
titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: UIFont.Weight.bold)
let bottomColor = UIColor(red: CGFloat(25/255.0), green: CGFloat(113/255.0), blue: CGFloat(255/255.0), alpha: CGFloat(1.0))
gradientLayer.colors = [UIColor.white.cgColor, bottomColor.cgColor]
layer.insertSublayer(gradientLayer, at: 0)
clipsToBounds = true
}
override var bounds: CGRect {
didSet {
gradientLayer.frame = layer.bounds
}
}
}
Then in the storyboard of nib you can change the class of the button to GradientButton. It should now automatically apply the gradient styling, and update the frame whenever the bounds of the button change.
I hope you find this useful. Let me know if you are still having issues.

Adding a subview to a UIButton with Auto Layout

I have created a subclass of UIButton. This subclass (BubbleBtn) is responsible for adding a UIView to the button.
The view that gets added should be 6 pts from the top, left, and right while spanning half the height of the parent button.
Code:
class BubbleBtn: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
addBubbleView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addBubbleView()
}
func addBubbleView() {
setNeedsLayout()
layoutIfNeeded()
let bubbleBuffer:CGFloat = 6
let bubble = UIView(frame: CGRect(x: bubbleBuffer, y: bubbleBuffer, width: self.frame.width - (bubbleBuffer * 2), height: (self.frame.height / 2)))
bubble.isUserInteractionEnabled = false
bubble.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
bubble.layer.cornerRadius = 10
bubble.layer.zPosition = -1
self.addSubview(bubble)
}
}
The problem is, the width and the height of the UIView that gets added is not the correct size; both the width and height fall to short on larger buttons.
How do I add the UIView to the buttons so that the bubble view renders in the proper size?
Screenshot posted below:
Try adding constraints for the view inside button.
func addBubbleView() {
let bubble = UIView()
bubble.isUserInteractionEnabled = false
bubble.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.5)
bubble.layer.cornerRadius = 10
bubble.layer.zPosition = -1
self.addSubview(bubble)
bubble.translatesAutoresizingMaskIntoConstraints = false
let views = [
"bubble": bubble
]
var constraints = [NSLayoutConstraint]()
let vBtnConstraint = NSLayoutConstraint.constraints(withVisualFormat: "V:|-6-[bubble]-6-|", options: .init(rawValue: 0), metrics: nil, views: views)
let hBtnConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:|-6-[bubble]-6-|", options: .init(rawValue: 0), metrics: nil, views: views)
constraints += vBtnConstraint
constraints += hBtnConstraint
NSLayoutConstraint.activate(constraints)
}
You should set frame by the bounds of the superview and the correct autoresizingMask.
let bubble = UIView(frame: CGRect(x: bubbleBuffer, y: bubbleBuffer,
width: self.bounds.width - (bubbleBuffer * 2),
height: self.bounds.height - (2 * bubbleBuffer))
bubble.translatesAutoresizingMaskIntoConstraints = true
bubble.autoresizingMask = [ .flexibleWidth, .flexibleHeight ]
So bubble adapts its width when the frame of the superview changes.

How to apply rounded corners for button programmatically added to a StackView

I'm having an issue with adding top right and left rounded button, to a stackview. Buttons added via interface builder, and then rounded by the extension, will render, but not if the button is added via code. Possibly there is some additional attribute that IB applies.
Here is an extract of the code, which illustrates the problem.
import UIKit
class TestViewController: UIViewController {
let colorDictionary = ["Red":UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0),"Green":UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),"Blue":UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0),]
func colorButton(withColor color:UIColor, title:String) -> UIButton{
let newButton = UIButton(type: .system)
newButton.backgroundColor = color
newButton.setTitle(title, for: .normal)
newButton.setTitleColor(UIColor.white, for: .normal)
//Buttons fail to render if roundedButton() is applied
//**************************
//newButton.roundedButton()
//**************************
return newButton
}
override func viewDidLoad() {
super.viewDidLoad()
displayButtonsInStackView()
}
func displayButtonsInStackView(){
//generate an array
var buttonArray = [UIButton]()
for (myKey,myValue) in colorDictionary{
buttonArray += [colorButton(withColor: myValue, title: myKey)]
}
let stackView = UIStackView(arrangedSubviews: buttonArray)
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
//autolayout the stack view - pin 30 up 20 left 20 right 30 down
let viewsDictionary = ["stackView":stackView]
let stackView_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20-[stackView]-20-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-30-[stackView]-30-|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
}
extension UIButton {
func roundedButton(){
let maskPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.bottomRight , .topRight],
cornerRadii:CGSize(width:8.0, height:8.0))
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
}
}
The problem is that your buttons have no size. Change this:
// ...
newButton.setTitleColor(UIColor.white, for: .normal)
newButton.roundedButton()
// ...
to this:
// ...
newButton.setTitleColor(UIColor.white, for: .normal)
newButton.sizeToFit()
newButton.roundedButton()
// ...

addTarget and addGestureRecognizer not working, no crash/error

I a have an overlay with a table in and I'd like to add a Tap gesture recogniser to the background to dismiss the view and also addTarget to a button within the overlay which does the same thing.
The overlay displays fine as expected, however whenever I tap the black background or the cancel button, nothing happens. I've searched for an answer here but nothing found has worked. My code is as follows, followed by a screenshot of the overlay:
class importedFileView: NSObject {
let blackView = UIView()
let importedFileContainerView: UIView = {
let importedFileContainerView = UIView(frame: .zero)
importedFileContainerView.backgroundColor = .white
importedFileContainerView.layer.cornerRadius = 10
importedFileContainerView.layer.masksToBounds = true
return importedFileContainerView
}()
let headerLabel: UILabel = {
let headerLabel = UILabel()
headerLabel.translatesAutoresizingMaskIntoConstraints = false
headerLabel.font = UIFont(name: "HelveticaNeue-Thin" , size: 24)
headerLabel.text = "Attach file"
headerLabel.textColor = .darkGray
headerLabel.adjustsFontSizeToFitWidth = true
return headerLabel
}()
let fileTableView: UITableView = {
let fileTableView = UITableView()
return fileTableView
}()
let updateDetailsButton: UIButton = {
let updateDetailsButton = UIButton()
updateDetailsButton.translatesAutoresizingMaskIntoConstraints = false
updateDetailsButton.backgroundColor = UIColor(r:40, g:86, b:131)
updateDetailsButton.setTitleColor(UIColor.white, for: .normal)
updateDetailsButton.setTitle("Attach selected files", for: .normal)
updateDetailsButton.titleLabel!.font = UIFont(name: "HelveticaNeue-Light" , size: 18)
updateDetailsButton.layer.cornerRadius = 2
return updateDetailsButton
}()
let cancelButton: UIButton = {
let cancelButton = UIButton()
cancelButton.translatesAutoresizingMaskIntoConstraints = false
cancelButton.backgroundColor = UIColor.white
cancelButton.setTitleColor(UIColor(r:40, g:86, b:131), for: .normal)
cancelButton.setTitle("Cancel", for: .normal)
cancelButton.titleLabel!.font = UIFont(name: "HelveticaNeue-Light" , size: 18)
cancelButton.layer.cornerRadius = 2
return cancelButton
}()
let frameHeight: CGFloat = 450
func showFormView(){
if let window = UIApplication.shared.keyWindow {
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
window.addSubview(blackView)
window.addSubview(importedFileContainerView)
importedFileContainerView.addSubview(headerLabel)
importedFileContainerView.addSubview(fileTableView)
importedFileContainerView.addSubview(updateDetailsButton)
importedFileContainerView.addSubview(cancelButton)
cancelButton.addTarget(self, action: #selector(handleDismiss), for: .touchUpInside)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
layoutViews()
fileTableView.frame = CGRect(x: 30, y: window.frame.height, width: window.frame.width - 60, height: 230)
let frameY = (window.frame.height - frameHeight) / 2
importedFileContainerView.frame = CGRect(x: 20, y: window.frame.height, width: window.frame.width - 40, height: self.frameHeight)
blackView.frame = window.frame
blackView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.importedFileContainerView.frame = CGRect(x: 20, y: frameY, width: window.frame.width - 40, height: self.frameHeight)
}, completion: nil
)
}
}
func layoutViews(){
let views = ["v0" : headerLabel, "v1": fileTableView, "v2": updateDetailsButton, "v3": cancelButton]
let leftSpace = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v0]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace1 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v1]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace2 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v2]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let leftSpace3 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-20.0-[v3]-20.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
let topSpacing = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20.0-[v0(40)]-20.0-[v1(230)]-20.0-[v2(50)]-10.0-[v3(50)]-10.0-|", options: NSLayoutFormatOptions(), metrics: nil, views: views)
importedFileContainerView.addConstraints(topSpacing)
importedFileContainerView.addConstraints(leftSpace)
importedFileContainerView.addConstraints(leftSpace1)
importedFileContainerView.addConstraints(leftSpace2)
importedFileContainerView.addConstraints(leftSpace3)
}
func handleDismiss() {
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .curveEaseInOut,
animations: {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.importedFileContainerView.frame = CGRect(x: 20, y: window.frame.height, width: window.frame.width - 40, height: self.frameHeight)
}
},
completion: { [weak self] finished in
self?.blackView.removeFromSuperview()
self?.importedFileContainerView.removeFromSuperview()
})
}
override init() {
super.init()
}
}
self.blackView.isUserInteractionEnabled = true;
is all you need to add to the blackView (UIView).
Without that, the view doesn't have any interactions enabled and so the gesture recognizer's target/action isn't triggered.
Events are ignored.
https://developer.apple.com/reference/uikit/uiview/1622577-isuserinteractionenabled
You might also want to disable it during animations.
Are you keeping a strong reference to the instance of your importedFileView while your overlays are visible? As far as I tested, all actions are silently ignored when the target is lost.
For example, this does not work:
#IBAction func someAction(_ sender: Any) {
let ifv = importedFileView()
ifv.showFormView()
//`ifv` is released at the end of this method, then your overlays are shown...
}
This works:
let ifv = importedFileView() //keep the instance as a property of your ViewController.
#IBAction func someAction(_ sender: Any) {
ifv.showFormView()
}
Programmatically generated UIViews isUserInteractionEnabled is default to true. You have no need to explicitly set it to true.
By the way, you'd better not name your non-UIView class as ...View, and better make your type names start with capital letter, which makes your code more readable to experienced Swift programmers.

make button in tableview section header float/align right programmatically // Swift

I trying to align of float a UIButton to the right in the section header of a TableView universally.
So far I only managed to add constraints for one screen size...
Here is my code so far:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
var headerFrame:CGRect = tableView.frame
let titelArr: NSArray = ["1", "2", "3", "4", "5", "6"]
var title = UILabel(frame: CGRectMake(10, 10, 100, 30))
title.font = UIFont.boldSystemFontOfSize(20.0)
title.text = titelArr.objectAtIndex(section) as? String
title.textColor = UIColor.whiteColor()
var headBttn:UIButton = UIButton.buttonWithType(UIButtonType.ContactAdd) as UIButton
headBttn.frame = CGRectMake(320, 10, 30, 30)
headBttn.enabled = true
headBttn.titleLabel?.text = title.text
headBttn.tag = titelArr.indexOfObject(titelArr.objectAtIndex(section))
headBttn.addTarget(self, action: "addItem:", forControlEvents: UIControlEvents.TouchUpInside)
var headerView:UIView = UIView(frame: CGRectMake(0, 0, headerFrame.size.width, headerFrame.size.height))
headerView.backgroundColor = UIColor(red: 108/255, green: 185/255, blue: 0/255, alpha: 0.9)
headerView.addSubview(title)
headerView.addSubview(headBttn)
return headerView
}
How can I make the button float right? The rest of the constraints can stay as they are...
THX for your help!
//Seb
Thx to #rdelmar and some research here is the answer if anybody should be interessted ;-)
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
var headerFrame = tableView.frame
var headerView:UIView = UIView(frame: CGRectMake(0, 0, headerFrame.size.width, headerFrame.size.height))
headerView.backgroundColor = UIColor(red: 108/255, green: 185/255, blue: 0/255, alpha: 0.9)
var title = UILabel()
title.setTranslatesAutoresizingMaskIntoConstraints(false)
title.font = UIFont.boldSystemFontOfSize(20.0)
title.text = titelArr.objectAtIndex(section) as? String
title.textColor = UIColor.whiteColor()
headerView.addSubview(title)
var headBttn:UIButton = UIButton.buttonWithType(UIButtonType.System) as UIButton
headBttn.setTranslatesAutoresizingMaskIntoConstraints(false)
headBttn.enabled = true
headBttn.titleLabel?.text = title.text
headBttn.tag = titelArr.indexOfObject(titelArr.objectAtIndex(section))
headBttn.addTarget(self, action: "addItem:", forControlEvents: UIControlEvents.TouchUpInside)
headerView.addSubview(headBttn)
var viewsDict = Dictionary <String, UIView>()
viewsDict["title"] = title
viewsDict["headBttn"] = headBttn
headerView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-10-[title]-[headBttn]-15-|", options: nil, metrics: nil, views: viewsDict))
headerView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-[title]-|", options: nil, metrics: nil, views: viewsDict))
headerView.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-[headBttn]-|", options: nil, metrics: nil, views: viewsDict))
return headerView
}
Thnx for sharing the amazing answer #Seb. Since there have been some changes made in Swift, which affects your answer. I'll provide an example of how to do the same in Swift 3 and 4:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: tableView.frame.size.height))
headerView.backgroundColor = UIColor.lightGray
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = "myTitle"
headerView.addSubview(title)
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("myButton", for: .normal)
headerView.addSubview(button)
var headerViews = Dictionary<String, UIView>()
headerViews["title"] = title
headerViews["button"] = button
headerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[title]-[button]-15-|", options: [], metrics: nil, views: headerViews))
headerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[title]-|", options: [], metrics: nil, views: headerViews))
headerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[button]-|", options: [], metrics: nil, views: headerViews))
return headerView
}

Resources