How to achieve a thick stroked border around text UILabel - swift

This is what I am trying to achieve:
Heres a few things I tried:
NSAttributedString
I have tried using NSAttributedString with the below attributes, but there seems to be a bug with text paths in iOS 14+
private var attributes: [NSAttributedString.Key: Any] {
[
.strokeColor : strokeColor,
.strokeWidth : -8,
.foregroundColor : foregroundColor,
.font: UIFont.systemFont(ofSize: 17, weight: .black)
]
}
I'd be perfectly fine with this result from NSAttributedString if it didn't have that weird pathing issue with some of the letters.
Draw Text Override
I have also tried to override drawText, but as far as I can tell, I cant find any way of changing the stroke thickness and having it blend together with the next character:
override func drawText(in rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineJoin(.round)
context?.setTextDrawingMode(.stroke)
self.textColor = strokeColor
super.drawText(in: rect)
context?.setTextDrawingMode(.fill)
self.textColor = foregroundColor
super.drawText(in: rect)
layer.shadowColor = UIColor.black.cgColor
layer.shadowRadius = 2
layer.shadowOpacity = 0.08
layer.shadowOffset = .init(width: 0, height: 2)
}

Use this designable class to render labels with the stroke on the storyboard. Most of the fonts I tried look bad (with CGLineJoin.miter), I found the "PingFang TC" font most closely resembles the desired output. Though CGLineJoin.round lineJoin looks fine on most of the font.
#IBDesignable
class StrokeLabel: UILabel {
#IBInspectable var strokeSize: CGFloat = 0
#IBInspectable var strokeColor: UIColor = .clear
override func drawText(in rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let textColor = self.textColor
context?.setLineWidth(self.strokeSize)
context?.setLineJoin(CGLineJoin.miter)
context?.setTextDrawingMode(CGTextDrawingMode.stroke)
self.textColor = self.strokeColor
super.drawText(in: rect)
context?.setTextDrawingMode(.fill)
self.textColor = textColor
super.drawText(in: rect)
}
}
Output: (Check used values in the attribute inspector for reference)

Related

Is there a way to set border to round rectangle that is added to CGMutablePath in Swift?

I am trying to create an overlay view, that has a "cutout" part that comes from a frame that is passed to the view, that size and position of that passed frame will change upon creation of the view. And in that "cutout" part I am expecting to see the content that is under that overlay view. Tried to set border to a rounded rectangle that is added to a CGMutablePath, but no luck.
The expected result is something like this:
The code I currently have in my UIView class, without tried solutions as I can't seem to get them to work properly. This current code displays the expected result, but without the red border:
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setFill()
UIRectFill(rect)
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
// frame that will change position on the screen
if let frame = changingFrame {
path.addRoundedRect(in: frame, cornerWidth: 16, cornerHeight: 16)
}
path.addRect(bounds)
shapeLayer.path = path
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.layer.mask = shapeLayer
}
I have tried solutions from here, here, but no luck as CAShapeLayer for border just overlays existing one.
What can I do differently to achieve the expected result? Thanks!
try this ⭐️
If all you want to do is create a rounded rectangle, then you can simply use.
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
let path = UIBezierPath(roundedRect: rectangle, cornerRadius: 20)
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [1.0]
view.layer.addSublayer(border)
One approach is to use two sublayers... a "cutout" layer and a "border" layer.
Use the same path for the cutout and the border shape, setting the line width and stroke color for the "outline".
Here's an example -- including making it #IBDesignable with a few #IBInspectable properties:
#IBDesignable
class BorderedCutoutView: UIView {
#IBInspectable
var bkgColor: UIColor = .systemBlue {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdColor: UIColor = .white {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdWidth: CGFloat = 1 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var radius: CGFloat = 20 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var horizInset: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var vertInset: CGFloat = 60.0 {
didSet {
setNeedsLayout()
}
}
private let cutoutLayer = CAShapeLayer()
private let borderLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
backgroundColor = .clear
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(cutoutLayer)
layer.addSublayer(borderLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
let cp = UIBezierPath(roundedRect: bounds.insetBy(dx: horizInset, dy: vertInset), cornerRadius: radius)
path.append(cp)
path.usesEvenOddFillRule = true
cutoutLayer.path = path.cgPath
cutoutLayer.fillRule = .evenOdd
cutoutLayer.fillColor = bkgColor.cgColor
borderLayer.path = cp.cgPath
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.lineWidth = brdWidth
borderLayer.strokeColor = brdColor.cgColor
}
}
This example uses horizontal and vertical "inset" values to center the cutout in the view.
Result:

Swift shadow does not spread correctly

I'm been trying to create a shadow for my UIView. I looked around and found an extension for the CALayer class from this post. https://stackoverflow.com/a/48489506/9188318
So far its been working well for me until I try to put in a non 0 number for the spread.
With the spread being 0. This is the result
And here is the result using a spread of 1
And it gets even worse with a spread of 5
The problem that I'm having is that it doesn't have rounded corners and I have no idea how to fix this. Heres my code for the UIView that uses this view. The extension that is used to make the shadow is in the post above.
UIView code
class FileCalculateOperatorSelectionButton : UIView {
private var isActivated : Bool = false
//Background colors
private var unSelectedBackgroundColor : UIColor = UIColor(red: 178/255, green: 90/255, blue: 253/255, alpha: 1.0)
private var selectedBackgroundColor : UIColor = UIColor.white
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInit()
}
private func commonInit() {
self.layer.cornerRadius = self.frame.height/2
self.backgroundColor = self.unSelectedBackgroundColor
let shadowColor = UIColor(red: 0xC8, green: 0xC6, blue: 0xC6)
//Change the spread argument here
self.layer.applySketchShadow(color: .black, alpha: 0.5, x: 0, y: 0, blur: 5, spread: 0)
}
}
Try setting the layer's masksToBounds property to true.
self.layer.masksToBounds = true
Change shadowPath in applySketchShadow function in CALayer extension like below;
// shadowPath = UIBezierPath(rect: rect).cgPath
shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
when spread: 1

Is possible make a UITextField like this?

Situation: I want to use a custom UITextField class for my textField in a xcode project.
I want to the textField look like this:
I had no problems in making the edges rounded, and change the color of my placeholder, but I have no idea how to keep the bottom edges flat and draw a black border only on the bottom.
This is my code:
import UIKit
class GrayTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = .grayf1f1f1
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
layer.cornerRadius = 10
clipsToBounds = true
}
override var placeholder: String? {
didSet {
let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .thin)]
attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: attributes)
}
}
}
And my current result:
In your answer, still there is some issue in bottom left and right corner.
To achieve exact result, change your UITextField Border Style to No Border.
Padding for Text:
class GrayTextField: UITextField {
let padding = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 5)
..... Your Exact Code .....
override open func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: padding)
}
}
OutPut
Well, after some researching, i do it.
Here if my final code:
import UIKit
class GrayTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
backgroundColor = .grayf1f1f1
clipsToBounds = true
let maskPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 10.0, height: 10.0))
let shape = CAShapeLayer()
shape.path = maskPath.cgPath
layer.mask = shape
addBottomBorder(with: .darkGray, andWidth: 1)
}
override var placeholder: String? {
didSet {
let attributes = [ NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .thin)]
attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: attributes)
}
}
func addBottomBorder(with color: UIColor?, andWidth borderWidth: CGFloat) {
let border = UIView()
border.backgroundColor = color
border.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
border.frame = CGRect(x: 0, y: frame.size.height - borderWidth, width: frame.size.width, height: borderWidth)
addSubview(border)
}
}
And this is the result:

How to set shadow outline (solid) around UILabel text in Swift?

I want to set shadow outline (solid) around UILabel text.
I tried below code: -
class BottomToolBarLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowColor = UIColor.red.cgColor
layer.shadowOffset = CGSize(width: 5.0, height: 5.0)
layer.shadowOpacity = 1.0
layer.shadowRadius = 0.0
}
}
But this is not working as I required. Please suggest. Thanks in advance.
I required output like: -
Try using this code :-
class BottomToolBarLabel : UILabel{
override func drawText(in rect: CGRect) {
let c = UIGraphicsGetCurrentContext();
c!.setLineWidth(10);
c!.setLineJoin(.round);
c!.setTextDrawingMode(.stroke);
self.textColor = UIColor.white
super.drawText(in: rect)
c!.setTextDrawingMode(.fill);
self.textColor = UIColor.black
self.shadowOffset = CGSize.init(width: 0, height: 0)
super.drawText(in: rect)
}
}

Set gradient on UIView partially, half color is gradient and half is single color

Trying to show the progress bar made it custom, took an UIView set it frame to percent of progress. Say 20% of frame width and made gradient but remaining 80% should be white color and text on it defining percentage.
Problems facing is not able to display text set UILabel instead of UIView but text not displaying. Please guide.
Below is what i have tried.
let view: UILabel = UILabel(frame: CGRectMake(0.0, self.scrollMainView.frame.size.height-50, self.view.frame.size.width/5, 50))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = view.bounds
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
let color0 = UIColor(red:71.0/255, green:198.0/255, blue:134.0/255, alpha:1.0).CGColor
let color1 = UIColor(red:25.0/255, green:190.0/255, blue: 205.0/255, alpha:1.0).CGColor
// let color2 = UIColor(red:0.0/255, green:0.0/255, blue: 0.0/255, alpha:1.0).CGColor
gradient.colors = [color1, color0]
view.layer.insertSublayer(gradient, atIndex: 0)
self.scrollMainView.addSubview(view)
view.text = "20%"
view.textColor = UIColor.blackColor()
view.layer.shadowColor = UIColor.blackColor().CGColor
view.layer.shadowOpacity = 1
view.layer.shadowOffset = CGSizeZero
view.layer.shadowRadius = 2
I cant understand your problem from your question exactly but I will answer based on question title only. To get half color gradient and half single color you have to use three colors gradient and set their locations accordingly :
gradient.colors = [color1,color1, color0]
gradient.locations = [0.0, 0.5, 1.0]
This way you will draw a gradient from color1 to color1 (which in fact is single color) and fill 50% of frame's area with it and a gradient from color1 to color0 that will fill other half of the frame.
I answer to why you can't see the text.
Forget for one second layers and think about subviews. What should happen if you add a subview to a UILabel? It will be on top of the label's content, of course.
So, the same applies to layers. The UILabel draws its text on its main layer, and any sublayer you add to the main layer is on top of it.
My suggestion is to use a UIView with a CAGradientLayer sublayer and a UILabel subview.
Or even better, subclass a UIView in order to use a CAGradientLayer as backing layer (through class func layerClass() -> AnyClass method) and just add UILabel as subview.
Here is an example:
class CustomView : UIView {
lazy var label : UILabel = {
let l = UILabel(frame: self.bounds)
l.text = "20%"
l.textColor = UIColor.blackColor()
l.backgroundColor = UIColor.clearColor()
return l
}()
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
let gradient: CAGradientLayer = self.layer as! CAGradientLayer
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
let color0 = UIColor(red:71.0/255, green:198.0/255, blue:134.0/255, alpha:1.0).CGColor
let color1 = UIColor(red:25.0/255, green:190.0/255, blue: 205.0/255, alpha:1.0).CGColor
gradient.colors = [color1, color0]
label.frame = bounds
label.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(label)
}
}