NSAttributedString Shadow and Stroke on iOS? - ios

When I use stroke and shadow, I get some sort of double-stroke. How can I fix this?
Playground Code:
import UIKit
var shadow = NSShadow()
shadow.shadowColor = UIColor.black
shadow.shadowOffset = CGSize(width: 0, height: 3)
class CustomLabel: UILabel {
override func drawText(in rect: CGRect) {
let attributes: [String: Any] = [NSStrokeWidthAttributeName: -2.0,
NSStrokeColorAttributeName: UIColor.black,
NSForegroundColorAttributeName: UIColor.white,
NSShadowAttributeName: shadow,
NSFontAttributeName: UIFont(name: "AvenirNext-Bold", size: 50)]
self.attributedText = NSAttributedString(string: self.text ?? "", attributes: attributes)
super.drawText(in: rect)
}
}
let label = CustomLabel(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
label.backgroundColor = UIColor.orange
label.text = "Hello"
Result:

I figured it out. If I apply a shadow to the label's CALayer, and disable the background-color, it works as expected:
import UIKit
class CustomLabel: UILabel {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 1.0
self.layer.shadowRadius = 0.0
}
override func drawText(in rect: CGRect) {
let attributes: [String: Any] = [NSStrokeWidthAttributeName: -2.0,
NSStrokeColorAttributeName: UIColor.black,
NSForegroundColorAttributeName: UIColor.white,
NSFontAttributeName: UIFont(name: "AvenirNext-Bold", size: 50)]
self.attributedText = NSAttributedString(string: self.text ?? "", attributes: attributes)
super.drawText(in: rect)
}
}
let label = CustomLabel(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
label.text = "Hello"

Related

Subclassing UITextfield in Swift

I am currently initializing quite a few properties in my view controller, but it just feels very, very messy. I have been creating UITextField like:
lazy var passwordTextField: UITextField = {
let textField = UITextField()
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 30))
textField.leftView = paddingView
textField.leftViewMode = .always
textField.placeholder = "Password"
textField.isSecureTextEntry = true
textField.autocorrectionType = .no
textField.autocapitalizationType = UITextAutocapitalizationType.none
textField.attributedPlaceholder = NSAttributedString(string: "Password", attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray])
textField.tintColor = UIColor.white
textField.borderStyle = .none
textField.layer.borderColor = UIColor.white.cgColor
textField.layer.borderWidth = 0.7
textField.delegate = self
textField.textColor = UIColor.white
return textField
}()
I have about 5 of these per UIViewController. I would like to create a UITextField class that is basically a factory that pumps out custom UITextFields. How might I accomplish this?
Subclassing any item is quite easy. Just do something like
class MyTextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
textFieldSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
textFieldSetup()
}
private func textFieldSetup() {
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 30))
leftView = paddingView
leftViewMode = .always
...
...
//Add the common properties here that you want replicated for every instance
...
...
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 0.7
textColor = UIColor.white
}
}
Then, you can create an instance of MyTextField instead of UITextField
var passwordTextField: MyTextField!
You can subclass UITextField like,
class CustomTextField: UITextField {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.customInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.customInit()
}
func customInit(){
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 30))
self.leftView = paddingView
self.leftViewMode = .always
self.placeholder = "Password"
self.isSecureTextEntry = true
self.autocorrectionType = .no
self.autocapitalizationType = .none
self.attributedPlaceholder = NSAttributedString(string: "Password", attributes: [NSAttributedString.Key.foregroundColor: UIColor.lightGray])
self.tintColor = .white
self.borderStyle = .none
self.layer.borderColor = UIColor.white.cgColor
self.layer.borderWidth = 0.7
self.textColor = .white
}
}
To use the custom label or text field,
class ViewController: UIViewController {
#IBOutlet weak var myCustomTextField: CustomTextField!
}
You can create extension for textfield like,
extension UITextField {
func setLeftPaddingPoints(_ amount:CGFloat){
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: self.frame.size.height))
self.leftView = paddingView
self.leftViewMode = .always
}
func setRightPaddingPoints(_ amount:CGFloat) {
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: amount, height: self.frame.size.height))
self.rightView = paddingView
self.rightViewMode = .always
}
}

Border width and placeholder in custom UITextField

I want to make this TextField
So I write this class
class UnderLineTextField:UITextField{
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let border = CALayer()
let width = CGFloat(2.0)
border.borderColor = UIColor.darkgray().cgColor
border.frame = CGRect(x: 0, y: frame.size.height - width, width: frame.size.width, height: frame.size.height)
textColor = UIColor.gold()
backgroundColor = UIColor.clear
border.borderWidth = width
layer.addSublayer(border)
layer.masksToBounds = true
attributedPlaceholder = NSAttributedString(string: "",
attributes: [NSAttributedStringKey.foregroundColor: UIColor.darkgray()])}
}
But there are two problems
The underline is not equal to TextField, if I add more width width: frame.size.width + 500, the problem can be fixed, but why? what is the correct value I should pass to width:?
attributedPlaceholder will replace the placeholder I set in xib, how can I change placeholder without adding empty string?
The underline is not equal to TextField, if I add more width width: frame.size.width + 500, the problem can be fixed, but why? what is the correct value I should pass to width:?
You should put your draw underline code to draw(_ rect: CGRect) instead of init. Because of you using autolayout, so the width, that you got in init is difference with the width that you can see when run app.
attributedPlaceholder will replace the placeholder I set in xib, how can I change placeholder without adding empty string?
You can redraw the attributedPlaceholder base on your placeholder in draw(_ rect: CGRect), too
attributedPlaceholder = NSAttributedString(
string: self.placeholder,
attributes: [NSAttributedStringKey.foregroundColor: UIColor.darkgray()]
)
It seems your code is ok.
use below code for setting placeholder:
class UnderLineTextField:UITextField{
#IBInspectable var borderColor: UIColor = UIColor.white {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable var placeHolderText: String = "default place holder" {
didSet {
attributedPlaceholder = NSAttributedString(string: placeHolderText,
attributes: [NSAttributedStringKey.foregroundColor: UIColor.darkGray])}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let border = CALayer()
let width = CGFloat(2.0)
border.borderColor = UIColor.darkGray.cgColor
border.frame = CGRect(x: 0, y: frame.size.height - width, width: frame.size.width, height: frame.size.height)
textColor = UIColor.lightGray
backgroundColor = UIColor.clear
border.borderWidth = width
layer.addSublayer(border)
layer.masksToBounds = true
attributedPlaceholder = NSAttributedString(string: placeHolderText,
attributes: [NSAttributedStringKey.foregroundColor: UIColor.darkGray])}
}
and set your placeholder in storyboard:
The result will look like below:
For UITextField underline use this one...
let border = CALayer()
let width = CGFloat(1.0)
border.borderColor = UIColor.gray.cgColor
border.frame = CGRect(x: 0, y: textField.frame.size.height - width, width: textField.frame.size.width, height: textField.frame.size.height)
border.borderWidth = width
textField.layer.addSublayer(border)
textField.layer.masksToBounds = true
Hey already lot of answers :), let's see if this detailed answer can help you achieve more:
I prefer using extensions over inheritance if possible.
extension UITextField {
func setTextColor(_ color: UIColor, font: UIFont) {
self.textColor = color
self.font = font
}
func setBottomBorder(with color: UIColor, width: CGFloat) {
self.borderStyle = .none
self.layer.backgroundColor = UIColor.white.cgColor // bg color of your choice
self.layer.masksToBounds = false
self.layer.shadowColor = color.cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: width)
self.layer.shadowOpacity = 1.0
self.layer.shadowRadius = 0.0
}
func setPlaceHolderAttributes(placeHolderText : String, colour : UIColor , font : UIFont){
self.attributedPlaceholder = NSAttributedString(string:placeHolderText, attributes:[NSAttributedStringKey.foregroundColor: colour , NSAttributedStringKey.font : font])
}
}
//MARK: To give text insets
class MyTextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect.init(x:10, y:2, width:bounds.width-20, height:bounds.height - 4)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return textRect(forBounds: bounds)
}
}
And I prefer to configure UI from Code, it will fasten up your coding speed and achieve doing project wise change from one place, so you can do it from code like:
let font: UIFont = UIFont.systemFont(ofSize: 14.0)
let placeHolderTextColor: UIColor = UIColor.brown
let textColor: UIColor = UIColor.darkText
nameTextField.setBottomBorder(with: UIColor.darkGray, width: 1.0)
passwordTextField.setBottomBorder(with: UIColor.darkGray, width: 1.0)
nameTextField.setPlaceHolderAttributes(placeHolderText: "Name", colour: placeHolderTextColor, font: font)
nameTextField.setTextColor(textColor, font: font)
passwordTextField.setPlaceHolderAttributes(placeHolderText: "Password", colour: placeHolderTextColor, font: font)
passwordTextField.setTextColor(textColor, font: font)
Hope this helps :)

UITextField Standard Style programmatically

When I create a UITextField using storyboard, it looks like the image below. However, when I create a UITextField programmatically it has absolutely no style. I know that I can apply custom styles to the text field, but is there an easy way to get this standard style when creating the text field programmatically?
Something like this:
let t = UITextField()
t.frame = CGRect(x: 10, y: 20, width: self.view.frame.width - 20, height: 40)
t.layer.cornerRadius = 5
t.layer.borderColor = UIColor.lightGray.cgColor
t.layer.borderWidth = 1
t.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: t.frame.height))
t.leftViewMode = .always
t.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: t.frame.height))
t.rightViewMode = .always
t.clearButtonMode = .whileEditing
self.view.addSubview(t)
EDIT:
Add this class below somewhere such that it is easily / globally accessible.
class StyledTextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 5
self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.borderWidth = 1
self.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: t.frame.height))
self.leftViewMode = .always
self.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: t.frame.height))
self.rightViewMode = .always
self.clearButtonMode = .whileEditing
}
}
Then you can call this UITextField from where ever you want as follows
let t = StyledTextField()
t.frame = CGRect(x: 10, y: 20, width: self.view.frame.width - 20, height: 40)
self.view.addSubview(t)
EDIT 2:
Use UIEdgeInsets to get padding on all four sides.
class StyledTextField: UITextField {
let insetConstant = UIEdgeInsets(top: 4, left: 10, bottom: 4, right: 10)
override func textRect(forBounds bounds: CGRect) -> CGRect {
return UIEdgeInsetsInsetRect(bounds, insetConstant)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return UIEdgeInsetsInsetRect(bounds, insetConstant)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return UIEdgeInsetsInsetRect(bounds, insetConstant)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 5
self.layer.borderColor = UIColor(white: 2/3, alpha: 0.5).cgColor
self.layer.borderWidth = 1
self.clearButtonMode = .whileEditing
self.keyboardType = UIKeyboardType.decimalPad
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can add borderStyle as .roundedRect and use White background. Something like this:
class MyCustmUITextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
self.borderStyle = .roundedRect
self.backgroundColor = .white
}
}
You can create a subclass from UITextField to serve as your custom text field.
Following this, you need only to instantiate your custom class and then you're good to go.
Something like this:
class MyCustmUITextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 5
self.layer.borderColor = UIColor.black
self.font = UIFont.systemFont(ofSize: 20)
self.adjustsFontSizeToFitWidth = true
// then add whatever styles you wish
}
}
From there all you need is to create a new instance of MyCustomUITextField

How to update CustomView width with different devices

I have a custom Button with 2 Labels and Image as shown in below class
import UIKit
#IBDesignable
class CustomButton : UIButton {
let secondLine : UILabel = UILabel()
override func layoutSubviews() {
super.layoutSubviews()
// self.layer.cornerRadius = 10
self.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.layer.shadowOpacity = 1.0
self.layer.masksToBounds = false
}
#IBInspectable var rightLebelText : String?{
didSet {
updateView()
}
}
func updateView(){
if let mytext = rightLebelText {
let firstLine = UILabel(frame: CGRect(x: self.bounds.size.width - 210, y: 0, width: 200, height: 40))
firstLine.text = mytext
firstLine.textAlignment = .right
firstLine.font = UIFont.systemFont(ofSize: 20)
self.addSubview(firstLine)
var imageView : UIImageView
imageView = UIImageView(frame:CGRect(x: 5, y: 10, width: 20, height: 20))
imageView.image = UIImage(named:"arrow.png")
self.addSubview(imageView)
}
}
public func setSecondlabel(title : String){
secondLine.removeFromSuperview()
secondLine.frame = CGRect(x: 50, y: 0, width: 200, height: 40)
secondLine.text = title
secondLine.font = UIFont.boldSystemFont(ofSize: 20)
secondLine.removeFromSuperview()
self.addSubview(secondLine)
}
}
My issue is my view size is not updating on different devices when using
self.bounds.size.width
for the firstLine label as shown in below image its position should be on the custom button right edge
You need to override the layoutSubviews function have the frame of each element examine and update based on updated bounds of the custom view or assign proper layout constraints on each element while adding it.
If you are overriding the UIButton which has already a label and image property, you can use that one as well or create a custom class inherited from UIControl and create required three property as needed. I am adding an example of the custom class with image, title, and detail as shown in the problem.
class CustomButton : UIControl {
let imageView : UIImageView
let titleLabel : UILabel
let detailLabel : UILabel
fileprivate func setup() {
self.detailLabel.textAlignment = .right
self.detailLabel.font = UIFont.systemFont(ofSize: 20)
self.detailLabel.textColor = UIColor.black
self.titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
self.titleLabel.textColor = UIColor.black
self.backgroundColor = UIColor.white
self.addSubview(self.imageView)
self.addSubview(self.titleLabel)
self.addSubview(self.detailLabel)
}
override init(frame: CGRect) {
self.imageView = UIImageView(frame: .zero)
self.titleLabel = UILabel(frame: .zero)
self.detailLabel = UILabel(frame: .zero)
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
self.imageView = UIImageView(frame: .zero)
self.titleLabel = UILabel(frame: .zero)
self.detailLabel = UILabel(frame: .zero)
super.init(coder: aDecoder)
self.setup()
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView.frame = CGRect(x: 5.0, y: self.bounds.midY - 10.0, width: 20.0, height: 20.0)
//You can make this width dynamic if you want to calculate width of text using self.detailLabel.text
var width : CGFloat = 200.0
self.titleLabel.frame = CGRect(x: self.imageView.frame.maxX + 5.0, y: self.bounds.minY, width: 200.0, height: self.bounds.height)
//Give the remaining space to the second label
width = self.bounds.width - (self.titleLabel.frame.maxX + 15.0)
self.detailLabel.frame = CGRect(x: self.titleLabel.frame.maxX + 5.0, y: self.bounds.minY, width: width, height: self.bounds.height)
}
}

single border for UITextField within a UIcollectionViewCell (swift 3 xcode)

I'm trying to apply a single bottom border to a textField that sits within one of my collectionViewCells. Here is the code:
class AddressCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func basicTextField(placeHolderString: String) -> UITextField {
let textField = UITextField()
textField.font = UIFont.boldSystemFont(ofSize: 12)
textField.attributedPlaceholder = NSAttributedString(string: placeHolderString, attributes:[NSForegroundColorAttributeName: UIColor.lightGray, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 12)])
textField.backgroundColor = UIColor.white
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
backgroundColor = UIColor.white
layer.addBorder(edge: UIRectEdge.bottom, color: .black, thickness: 0.5)
let streetTextfield = basicTextField(placeHolderString: "street")
addSubview(streetTextfield)
}
}
I am using an extension that enables me to apply a single border, which has worked great so far:
extension CALayer {
func addBorder(edge: UIRectEdge, color: UIColor, thickness: CGFloat) {
let border = CALayer()
switch edge {
case UIRectEdge.top:
border.frame = CGRect.init(x: 0, y: 0, width: frame.width, height: thickness)
break
case UIRectEdge.bottom:
border.frame = CGRect.init(x: 0, y: frame.height - thickness, width: frame.width, height: thickness)
break
case UIRectEdge.left:
border.frame = CGRect.init(x: 0, y: 0, width: thickness, height: frame.height)
break
case UIRectEdge.right:
border.frame = CGRect.init(x: frame.width - thickness, y: 0, width: thickness, height: frame.height)
break
default:
break
}
border.backgroundColor = color.cgColor;
self.addSublayer(border)
}
}
When i simply add a borderWidth to the textfield like this:
textField.layer.borderWidth = 0.5
I get a border and it renders fine. However, when i apply the extension to add a bottom border, like this:
textField.layer.addBorder(edge: UIRectEdge.bottom, color: .black, thickness: 0.5)
the border doesn't apply for some reason.
I'm not sure about the extension, but I have a working solution for a TextField with a bottom border.
basically create a class BottomBorderedTextField subclassing UITextField and insert the following code:
class BottomBorderedTextField: UITextField {
override func draw(_ rect: CGRect) {
super.draw(rect)
//this is the color of the bottom border. Change to whatever you what
let color: UIColor = UIColor.rgb(red: 230, green: 230, blue: 230)
let bottomBorder = CALayer()
bottomBorder.frame = CGRect(x: 0, y: bounds.size.height - 1, width: bounds.size.width, height: 2)
bottomBorder.backgroundColor = color.cgColor
self.layer.addSublayer(bottomBorder)
}
}
Then on your basicTextField function set the return type to the BottomBorderedTextField class you just made. so your new code will look like:
func basicTextField(placeHolderString: String) -> BottomBorderedTextField {
let textField = BottomBorderedTextField()
textField.font = UIFont.boldSystemFont(ofSize: 12)
textField.attributedPlaceholder = NSAttributedString(string: placeHolderString, attributes:[NSForegroundColorAttributeName: UIColor.lightGray, NSFontAttributeName: UIFont.boldSystemFont(ofSize: 12)])
textField.backgroundColor = UIColor.white
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}
I wrote a UIView extension that accomplishes the same thing:
extension UIView {
func addBottomBorder(width: CGFloat, color: UIColor, alpha: CGFloat) {
let border = CALayer()
let width = width
border.borderColor = color.withAlphaComponent(alpha).cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: self.frame.size.height)
border.borderWidth = width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
}
Usage:
usernameField.addBottomBorder(width: 2.0, color: UIColor.white, alpha: 0.5)
Also, seems within setupViews() you're calling:
layer.addBorder... //this is being called on the collection view cells layer
As opposed to:
streetTextfield.layer.addBorder...

Resources