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}")
}
}
Related
Can anyone help me break Apple's approach to create the phone app keypad screen? I have tried creating it with UIStackView with buttons of 1:1 and many other approaches but all in vain. I want to use such a screen in my own project for learning
This is what I want and this is what I have so far, Xcode view of storyboard
Part of solution may involve custom (round, green) buttons. To create a button with a red background (as a caution against critical action), I subclassed NSButton:
class ColorButton: NSButton
And its draw(...) method.
That gave me control over appearance. All other behavior inherited from NSButton.
Additionally, the #IBDesignable and #IBInspectable tags exposed the custom control in InterfaceBuilder.
That was the easy part. Getting the appearance just right was a non-trivial effort (for me) as Cocoa buttons have some subtle behaviors (e.g. disabled, highlight). Responding to desktop color options (such as Dark Mode) is also a consideration.
Here is code for my ColorButton which, again, was to alter the background color not the shape.
#IBDesignable
class ColorButton: NSButton {
#IBInspectable var backgroundColor: NSColor = NSColor.controlBackgroundColor
{ didSet { needsDisplay = true } }
#IBInspectable var textColor: NSColor = NSColor.controlTextColor
override func draw(_ dirtyRect: NSRect)
{
// fill in background with regular or highlight color
var backgroundColor = (isHighlighted ? self.backgroundColor.highlight(withLevel: 0.75) : isEnabled ? self.backgroundColor : NSColor.gray)!
if isEnabled == false { backgroundColor = NSColor.lightGray }
let textColor = (isEnabled ? self.textColor : NSColor.disabledControlTextColor )
var rect: NSRect = NSRect(x: 7.0, y: 5.0, width: self.frame.width - 14.0, height: self.frame.height - 13.0) // self.frame// dirtyRect
var path = NSBezierPath(roundedRect: rect, xRadius: 4.0, yRadius: 4.0)
backgroundColor.setFill()
NSColor.darkGray.setStroke()
path.lineWidth = 0
path.fill()
path.stroke()
let font = NSFont(name: "Helvetica", size: (rect.height) * 0.66)
let text = title as NSString
let textFontAttributes = [convertFromNSAttributedStringKey(NSAttributedString.Key.font): font!, convertFromNSAttributedStringKey(NSAttributedString.Key.foregroundColor): textColor] as [String : Any]
let size = text.size(withAttributes: convertToOptionalNSAttributedStringKeyDictionary(textFontAttributes))
let location = NSPoint(x: ((self.frame.width - size.width) / 2.0), y: ((self.frame.height - size.height) / 2.0) - 3.0)
text.draw(at: location, withAttributes: convertToOptionalNSAttributedStringKeyDictionary(textFontAttributes))
}// draw
}
Above was developed under Swift 3. Swift 4 migration tool added:
// Helper function inserted by Swift 4.2 migrator.
fileprivate func convertFromNSAttributedStringKey(_ input: NSAttributedString.Key) -> String {
return input.rawValue
}
// Helper function inserted by Swift 4.2 migrator.
fileprivate func convertToOptionalNSAttributedStringKeyDictionary(_ input: [String: Any]?) -> [NSAttributedString.Key: Any]? {
guard let input = input else { return nil }
return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value)})
}
https://www.youtube.com/watch?v=vI7m5RTYNng
Well explained and has code as well.
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)
}
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:
I am new to Swift and having fits with the UIView class. I have a TableView (below) with a View object (left) and Label (right). The table itself works fine and the labels appear as expected.
Where I am having trouble is that I want the View object next to the label to contain various shapes and colors depending on the values in the array that support the table...
var tArray = [["Row 1","Row 2", "Row 3", "Row 4", "Row 5"],
["Circle","Circle","Square","Square","Diamond"],
["Blue","Red","Green","Red","Purple"]]
So next to "Row 1", I want to have a blue circle, etc. I have linked the View object to a custom class. But I need an approach to dynamically create the shapes and fill with appropriate colors.
In the TableViewController, I have the following, which is calling the Symbol class, and I am getting back a black circle (I hard-coded circle for now)...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.cellLabel.text = tArray[0][indexPath.row]
cell.cellSymbol = Symbol.init()
return cell
}
In my custom Symbol class:
import UIKit
class Symbol: UIView {
var inColor: String
var inShape: String
init (in_color: String, in_shape: String) {
self.inColor = in_color
self.inShape = in_shape
super.init(frame: CGRect(x: 0, y: 0, width: 70, height: 70))
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(ovalInRect: rect)
switch self.inColor {
case "Green" : UIColor.greenColor().setFill()
case "Blue" : UIColor.blueColor().setFill()
case "Yellow" : UIColor.yellowColor().setFill()
case "Cyan" : UIColor.cyanColor().setFill()
case "Red" : UIColor.redColor().setFill()
case "Brown" : UIColor.brownColor().setFill()
case "Orange" : UIColor.orangeColor().setFill()
case "Purple" : UIColor.purpleColor().setFill()
case "Grey" : UIColor.darkGrayColor().setFill()
default: UIColor.blackColor().setFill()
}
path.fill()
}
required init?(coder aDecoder: NSCoder) {
self.inColor = ""
self.inShape = ""
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
self.inColor = ""
self.inShape = ""
super.init(frame: frame)
}
}
I may be going about this all wrong and am open to other approaches entirely. In order to compile, I had to add the required init? and override init(frame: CGRect) entires. I also had to put in the initialization of the self.inColor and .inShape to compile, but since I'm not passing in the parameters to those, I have nothing to assign.
So what I get is a black circle every time. I hard-coded circle for now to keep it simple. The switch self.inColor is nil every time, so it is going down to the default case.
Any suggestions would be greatly appreciated!!!!
When you set the cellSymbol, you are creating a new instance of the Symbol class. You never modify any of the properties of cellSymbol, so it is always black.
Try:
cell.cellSymbol.inColor = self.tArray[2][indexPath.row]
The "always black" error in your code has been addressed by https://stackoverflow.com/a/35049741/218152. Below are suggested improvements.
#IBInspectable
Less code, more features
Replace your entire Symbol class with this:
#IBDesignable class Symbol: UIView {
var color = UIColor.blackColor()
#IBInspectable var inColor: String = "Black" {
didSet {
switch inColor {
case "Green" : color = UIColor.greenColor()
case "Blue" : color = UIColor.blueColor()
case "Yellow" : color = UIColor.yellowColor()
case "Cyan" : color = UIColor.cyanColor()
case "Red" : color = UIColor.redColor()
case "Brown" : color = UIColor.brownColor()
case "Orange" : color = UIColor.orangeColor()
case "Purple" : color = UIColor.purpleColor()
case "Grey" : color = UIColor.darkGrayColor()
default: color = UIColor.blackColor()
}
}
}
override func drawRect(rect: CGRect) {
color.setFill()
let path = UIBezierPath(ovalInRect: rect)
path.fill()
}
}
Use it just the same way you would use the previous one (cell.cellSymbol.inColor = ...). It will have the advantage of also being visually editable from Interface Builder. It also does not require special initialization (no init).
This implementation has the added advantage to accept a UIColor directly, as in cell.cellSymbol.color = ...
Further improvements include using the tintColor instead of creating your own instance, case-insensitive colors, enum instead of names.
Thanks to #SwiftArchitect for most of this. I added in the shapes portion. As #SwiftArchitect noted, there are other improvements to be made, but it works perfectly!
import UIKit
#IBDesignable class Symbol: UIView {
var color = UIColor.blackColor()
#IBInspectable var inShape: String = "Circle"
#IBInspectable var inColor: String = "Black" {
didSet {
switch inColor {
case "Green" : color = UIColor.greenColor()
case "Blue" : color = UIColor.blueColor()
case "Yellow" : color = UIColor.yellowColor()
case "Cyan" : color = UIColor.cyanColor()
case "Red" : color = UIColor.redColor()
case "Brown" : color = UIColor.brownColor()
case "Orange" : color = UIColor.orangeColor()
case "Purple" : color = UIColor.purpleColor()
case "Grey" : color = UIColor.darkGrayColor()
default: color = UIColor.blackColor()
}
}
}
override func drawRect(rect: CGRect) {
var path = UIBezierPath()
switch shape {
case "Circle": path = UIBezierPath(ovalInRect: rect)
case "Square" : path = UIBezierPath(roundedRect: rect, cornerRadius: 5.0)
case "Triangle" :
path.moveToPoint(CGPoint(x: frame.width / 2, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: frame.height))
path.addLineToPoint(CGPoint(x: frame.width, y: frame.height))
case "Diamond" :
path.moveToPoint(CGPoint(x: frame.width / 2, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: frame.height / 2))
path.addLineToPoint(CGPoint(x: frame.width / 2, y: frame.height))
path.addLineToPoint(CGPoint(x: frame.width, y: frame.height / 2))
default: print("unknown shape")
}
color.setFill()
path.fill()
}
}
Currently we are changing the image shown in the UITabBarItem like this:
override func viewDidLoad() {
super.viewDidLoad()
// register additional NIB files
tableView.registerNib(UINib(nibName: "WeekDayTableViewCell", bundle: nil), forCellReuseIdentifier: "WeekDayCell")
tabBarItem.title = "Week".localized
tabBarItem.image = UIImage.fontAwesomeIconWithName(FontAwesome.Calendar, textColor: UIColor.blueColor(), size: CGSizeMake(30, 30))
}
The problem with this is, that you have to click on every tab to load the corresponding images.
I thought I could change the images in the UITabBarViewController if I get the list of all UITabBarItems. But this list is always empty.
Which is the correct way to do this?
An alternative—perhaps a more Swift-y—way to do this is to set the icons from a Dictionary of tabs. We have an enum TabTitles for the names of the tabs, and tabIcons to look it up.
This way, there's not so much to remember to change if you add a tab or change the order.
private enum TabTitles: String, CustomStringConvertible {
case Stacks
case Settings
case Clusters
case Services
case Registries
private var description: String {
return self.rawValue
}
}
private var tabIcons = [
TabTitles.Stacks: FontAwesome.Clone,
TabTitles.Settings: FontAwesome.Gears,
TabTitles.Clusters: FontAwesome.Cubes,
TabTitles.Services: FontAwesome.Exchange,
TabTitles.Registries: FontAwesome.Institution
]
override func viewDidLoad() {
super.viewDidLoad()
if let tabBarItems = tabBar.items {
for item in tabBarItems {
if let title = item.title,
tab = TabTitles(rawValue: title),
glyph = tabIcons[tab] {
item.image = UIImage.fontAwesomeIconWithName(glyph, textColor: UIColor.blueColor(), size: CGSizeMake(30, 30))
}
}
}
}
I found out, if I add this code to the viewDidAppear in the UITabBarController it will work.
let tabBarItems = tabBar.items! as [UITabBarItem]
tabBarItems[0].title = "Week".localized
tabBarItems[0].image = UIImage.fontAwesomeIconWithName(FontAwesome.Calendar, textColor: UIColor.blueColor(), size: CGSizeMake(30, 30))
tabBarItems[1].title = "Settings".localized
tabBarItems[1].image = UIImage.fontAwesomeIconWithName(FontAwesome.Gears, textColor: UIColor.blueColor(), size: CGSizeMake(30, 30))
Swift 5
You can set tabBarItems icons from your first controller 'viewDidLoad' method.
func setupTabBar() {
tabBarController?.tabBar.items![0].title = "Week"
tabBarController?.tabBar.items![0].image = #imageLiteral(resourceName: "icons8-clinic")
tabBarController?.tabBar.items![1].title = "Profile"
tabBarController?.tabBar.items![1].image = #imageLiteral(resourceName: "icons8-phone_not_being_used_filled")
}
Use item.image = UIImage.fontAwesomeIcon(name: glyph, textColor: UIColor.black, size: CGSize(width: 30, height: 30)) for Swift 3.
Also remember to import FontAwesome_swift