Swift - Attributed strings in buttons - ios

I have some UIbuttons in my app that have a number indicator before the text. Right now, I am just using string interpolation to display the number before the string as seen below.
fruitButton.setTitle("\(fruitCounter) Fruits", forState: UIControlState.Normal)
I need the number to stand out more, rather than just blending in with the title text. Something as simple as a circle surrounding it will do the trick, as seen in the design below:
.
I did some research on Attributed Strings in Swift. However, I am just seeing many examples on changing text properties. EG - changing the text color and size of the number indicator. I can't figure out how to add a circle behind.
I do not want this circle to be an image, simply for scalibility purposes. For instance, if that number ends up being 2 digits long, I need the circle to stretch to oval. My thought was using a small view behind the number, and then just applying a color / alpha / radius to achieve the look I need.
So to wrap this up: How can I add circles behind my number indicators using Attributed Strings in Swift?

You should create a UIView subclass that will contain these two elements. Create a corresponding .xib file that includes those entities or implement those entities in code. You can also implement touchesBegan so that this view can act like a button OR add a button instead of a text label and implement a protocol to fire every time the button is hit. I've started this for you with some semi-arbitrary numbers. You'll have to play with them to get them just right.
class UICoolButton: UIView {
var labelText: NSString?
var circledNumber: Int?
var circleSubview: UIView?
init(frame: CGRect, labelText: NSString, circledNumber: Int) {
super.init(frame: frame);
self.labelText = labelText;
self.circledNumber = circledNumber;
self.layer.cornerRadius = frame.height/3
self.clipsToBounds = true
addCircledNumber()
addTextLabel(labelText)
}
func setColors(numberColor:UIColor, backgroundColor: UIColor) {
self.backgroundColor = backgroundColor
self.circleSubview?.backgroundColor = numberColor
}
override init(frame: CGRect) {
super.init(frame: frame);
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
}
func addTextLabel(text: NSString) {
let origin = CGPoint(x:self.frame.width * 0.4, y:self.frame.height/10)
let size = CGSize(width: self.frame.width/2, height: self.frame.height * 0.8)
let rect = CGRect(origin: origin, size: size)
let label = UILabel(frame: rect)
let attributes: [String : AnyObject] = [NSFontAttributeName : UIFont(name: "Verdana", size: 16.0)!,
NSForegroundColorAttributeName : UIColor.whiteColor()]
label.attributedText = NSAttributedString(string: text, attributes: attributes)
self.addSubview(label)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
func addCircledNumber() {
let height = self.frame.height * 0.4;
let circleDimensions = CGSize(width: height , height:height)
let origin = CGPointMake(self.frame.width * 0.15, self.frame.height - self.frame.height/1.5)
let circleSubview = UIView(frame: CGRect(origin: origin, size: circleDimensions))
circleSubview.layer.cornerRadius = height/2;
circleSubview.backgroundColor = UIColor.darkGrayColor()
let labelHeight = height * 0.8;
let xPosition = circleSubview.bounds.origin.x + 3
let yPosition = circleSubview.bounds.origin.y + 2
let labelOrigin = CGPoint(x: xPosition, y: yPosition)
let labelRect = CGRect(origin: labelOrigin, size: CGSize(width: labelHeight, height: labelHeight))
let numberLabel = UILabel(frame: labelRect);
numberLabel.textAlignment = NSTextAlignment.Center
let numberAsString = NSString(format: "%i", circledNumber!) as String
let attributes: [String : AnyObject] = [NSFontAttributeName : UIFont(name: "Verdana", size: 16.0)!,
NSForegroundColorAttributeName : UIColor.whiteColor()]
numberLabel.attributedText = NSAttributedString(string: numberAsString, attributes: attributes)
circleSubview.addSubview(numberLabel);
self.circleSubview = circleSubview
self.addSubview(circleSubview)
}
Then in your View Controller, use the initilizer I wrote:
func addCoolButton() {
let rect = CGRect() //choose the frame for your button here
let button = UICoolButton(frame: rect, labelText: "Example", circledNumber: 10);
let backgroundColor = UIColor(red: 243/255.0 , green: 93/255.0, blue: 118/255.0, alpha: 1.0)
let numberColor = UIColor(red: 252/255.0, green: 118.0/255.0, blue: 135/255.0, alpha: 1.0)
button.setColors(numberColor, backgroundColor: backgroundColor)
self.view.addSubview(button)
}

I did sample some thing like your purpose with autolayout
with code here:
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *content;
#property (weak, nonatomic) IBOutlet UILabel *label;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.content.layer.cornerRadius = MIN(self.content.bounds.size.width, self.content.bounds.size.height)/2;
self.content.clipsToBounds = true;
}
#end
result with number is 1234:
result with number is 1:

Related

Is is possible to animate text color changing only in a part of text in 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

Background colour of UIVIew inside UIPickerView does not set properly

I am trying to implement a UIPickerView with custom design. I've taken a UIVIew inside a UIPickerView and a label inside my UIVIew. Color of my picker background is black and Now I am trying to apply white background color to UIVIew and label inside View. But the colour is not what exact I want. I want pure white colour but it's returning gray. Can anyone help me for this?
Here is my code and output:
if pickerView == pkr1 {
let myView = UIView(frame: CGRect(x: 5, y: 0, width: pickerView.bounds.width - 10, height: Utilities.isDeviceiPad() ? 80.0 : 60.0))
myView.backgroundColor = UIColor.white
myView.isOpaque = false
let myLabel = UILabel(frame: CGRect(x: 5, y: 0, width: myView.bounds.width - 10, height: myView.frame.height))
myLabel.text = arrTemp[row]
// myLabel.center.y = myView.center.y
myLabel.textAlignment = .center
myLabel.numberOfLines = 2
myLabel.backgroundColor = UIColor.white
myLabel.alpha = CGFloat(1)
myLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
myView.addSubview(myLabel)
return myView
}
Output:
The reason why the selected view is transparent is that UIPickerView subview, that holds all the content, uses CAGradientLayer as a mask to achieve the depth effect. For some reason, Apple decided to make the selected view transparent as well. We can't simply rid of that mask because we will lose that fancy effect. So we need to figure out more about that gradient.
Here is how this gradient looks. I've added the alpha channel values so you could see how this gradient behave.
As you can see, it has 6 colors and everything is fine except the two values in the middle. They are 0.8 instead of 1.0, that what makes you selected view transparent.
This is a very bad approach and I strongly do not recommend do that but if you need to make your selected item opaque no matter what, you can simply create a subclass of you UIPickerView and change the gradient like that.
class MyPickerView: UIPickerView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
self.changeGradientLayer()
}
func changeGradientLayer() {
guard let sublayers = self.subviews.first?.layer.sublayers else { return }
for case let sublayer as CAGradientLayer in sublayers {
sublayer.colors = [UIColor.black.withAlphaComponent(0).cgColor,
UIColor.black.withAlphaComponent(0.71).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(1.0).cgColor,
UIColor.black.withAlphaComponent(0.71).cgColor,
UIColor.black.withAlphaComponent(0).cgColor]
}
}
}
Hope it helps you.
Please add your view in picker view by below method and check it...hope it will work..
func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
let myView = UIView(frame: CGRect(x: 5, y: 0, width: pickerView.bounds.width - 10, height: Utilities.isDeviceiPad() ? 80.0 : 60.0))
myView.backgroundColor = UIColor.white
myView.isOpaque = false
let myLabel = UILabel(frame: CGRect(x: 5, y: 0, width: myView.bounds.width - 10, height: myView.frame.height))
myLabel.text = arrTemp[row]
// myLabel.center.y = myView.center.y
myLabel.textAlignment = .center
myLabel.numberOfLines = 2
myLabel.backgroundColor = UIColor.white
myLabel.alpha = CGFloat(1)
myLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
myView.addSubview(myLabel)
return myView
}

iOS swift: add kern space in the tab bar text

I'm not able to add kern space into the tab bar attributed text.
The UITabBar in question is a custom tabBar, you can find the code below.
I'm using the "attributed key" dictionary to add attributes to the items title, but I'm having an issue with the kern space.
class ProfileTabBar: UITabBar {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setStyle()
}
required override init(frame: CGRect) {
super.init(frame: frame)
self.setStyle()
}
func setStyle() {
self.tintColor = Style.shared.primary1
// Disable the default border
self.layer.borderWidth = 0.0
self.clipsToBounds = true
// Create a new bottom border
let bottomLine = CALayer()
let screenWidth = UIScreen.main.bounds.width
//let viewForFrame = self.superview ?? self
//let screenWidth = viewForFrame.bounds.width
bottomLine.frame = CGRect(x: 0.0, y: self.frame.height - 1, width: screenWidth, height: 2.0)
bottomLine.backgroundColor = UIColor(red: 235.0/255, green: 235.0/255, blue: 235.0/255, alpha: 1.0).cgColor
self.layer.addSublayer(bottomLine)
// Get the size of a single item
let markerSize = CGSize(width: screenWidth/CGFloat(self.items!.count), height: self.frame.height)
// Create the selection indicator
self.selectionIndicatorImage = UIImage().createSelectionIndicator(color: self.tintColor, size: markerSize , lineWidth: 3.0)
// Customizing the items
if let items = self.items {
for item in items {
item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -15)
let attributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font: UIFont(name: Style.shared.fontBold.fontName, size: 14) as Any,
NSAttributedStringKey.kern: NSNumber(value: 1.0)
]
item.setTitleTextAttributes(attributes, for: .normal)
}
}
}
All the attributes works except for the kern. What I'm doing wrong?
This question is old and there is an even older answer here. It appears that UITabBarItem appearance ignores NSAttributedString.Key.kern. That leaves us with a few options.
Subclass UITabBarItem this isn't easy because UITabBarItem inherits from UIBarItem which is an NSObject not a UIView.
Subclass UITabBar this can be done, but involves a decent amount of work for just some kern. You'll have to use UIButton instead of UITabBarItem so that the kern is applied.
You can add spacing using unicode characters in your title. This is really easy and can probably achieve the spacing you're looking for with just a few lines of code.
Unicode spacing:
U+0020 1/4 em
U+2004 1/3 em
U+2005 1/4 em
U+2006 1/6 em
U+2008 The width of a period “.”
U+2009 1/5 em (or sometimes 1/6 em)
You can use a unicode character in a String in Swift like this "\u{2006}". That means we can insert a small space between all the characters in our tabBarItem title. Like this:
extension String {
var withOneSixthEmSpacing: String {
let letters = Array(self)
return letters.map { String($0) + "\u{2006}" }.joined()
}
Using this for our tabBarItems:
self.navigationController.tabBarItem = UITabBarItem(
title: "Home".withOneSixthEmSpacing,
image: homeImage,
selectedImage: homeSelectedImage
)
Visually we end up with:
Instead of:
Another workaround is to subclass UITabBarController, and set the kerning in viewDidLayoutSubviews.
class FooTabBarController: UITabBarController {
private var tabBarButtons: [UIControl] {
tabBar.subviews.compactMap { $0 as? UIControl }
}
private var tabBarButtonLabels: [UILabel] {
tabBarButtons.compactMap { $0.subviews.first { $0 is UILabel } as? UILabel }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tabBarButtonLabels.forEach {
if let attributedText = $0.attributedText {
let mutable = NSMutableAttributedString(attributedString: attributedText)
mutable.addAttribute(.kern, value: 0.5, range: .init(location: 0, length: mutable.length))
$0.attributedText = mutable
$0.sizeToFit()
}
}
}
}
The caveats to this solution are:
It is somewhat fragile. It can break if Apple changes the view structure in the tab bar, ie if they stop using UIControl, or if they change the subview heirarchy.
It isn't all that efficient because the kerning has to be set every layout cycle.
I loved #DoesData's answer, it really helped me out a lot.
Here's a more "swifty" version of it I came up with if it helps anyone:
extension String {
var withAddedSpacing: String {
Array(self)
.compactMap { String($0) }
.joined(separator: "\u{2006}")
}
}

Create a variadic method in Swift

I want to make a variadic method to accept any number of textfields and a UIColor as inputs and my intention is to add a bottom line of that specific color to all thus textfields inside method.
How can I make such a method in Swift?
Probably something like this would do:
func applyColour(toTextfields textfields: UITextField..., colour: UIColor) {
for textfield in textfields {
// Apply the colour here.
}
}
Common.addBottonLine(color: UIColor.white, userNameTextField, passwordTextField)
Method definition
class func addBottonLine(color: UIColor, _ txtFields: UITextField... ) -> Void {
for textF in txtFields {
var bottomLine = CALayer()
bottomLine.frame = CGRect.init(origin: CGPoint.init(x: 0, y: textF.frame.height - 2) , size: CGSize.init(width: textF.frame.width, height: 2 ))
bottomLine.backgroundColor = color.cgColor
textF.borderStyle = .none
textF.layer.addSublayer(bottomLine)
}
}
Use this function to add bottom line
func addBottomLabel(_ color: UIColor) {
let lbl1 = UILabel()
lbl1.backgroundColor = color
addSubview(lbl1)
addVisualConstraints(["H:|[label]|", "V:[label(1)]|"], forSubviews: ["label": lbl1])
}
Use :
let textFields = [your textfield array]
for textFields in textFields
{
textFields.addBottomLabel(.yourcolor)
}

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:

Resources