How to fade out end of last line in multiline label? - ios

Note, that it must work with different number of lines in UILabel - 1,2,3 etc.
I've already found solution for 1 line label, where you mask UILabel's layer with CAGradientLayer, but it doesn't work for multiline labels, as it masks the whole layer and fades out all lines.
I tried to make another CALayer with position calculated to be in the position of last line with desired width and used CAGradientLayer as mask and add this layer as sublayer of UILabel, it worked for static objects, but i use this UILabel in UITableViewCell and when it's tapped - it changes color to gray and i can see my layer, because it uses background color of UILabel when view layout its subviews, and also something wrong with x position calculation:
extension UILabel {
func fadeOutLastLineEnd() { //Call in layoutSubviews
guard bounds.width > 0 else { return }
lineBreakMode = .byCharWrapping
let tmpLayer = CALayer()
let gradientWidth: CGFloat = 32
let numberOfLines = CGFloat(numberOfLines)
tmpLayer.backgroundColor = UIColor.white.cgColor
tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
y: layer.frame.height / numberOfLines,
width: gradientWidth,
height: layer.frame.height / numberOfLines)
let tmpGrLayer = CAGradientLayer()
tmpGrLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
tmpGrLayer.endPoint = CGPoint(x: 0, y: 0)
tmpGrLayer.frame = tmpLayer.bounds
tmpLayer.mask = tmpGrLayer
layer.addSublayer(tmpLayer)
}
}
So, i need :
which can be multiline
end of last line needs to be faded out (gradient?)
works in UITableViewCell, when the whole object changes color

There are various ways to do this -- here's one approach.
We can mask a view by setting the layer.mask. The opaque areas of the mask will show-through, and the transparent areas will not.
So, what we need is a custom layer subclass that will look like this:
This is an example that I'll call InvertedGradientLayer:
class InvertedGradientLayer: CALayer {
public var lineHeight: CGFloat = 0
public var gradWidth: CGFloat = 0
override func draw(in inContext: CGContext) {
// fill all but the bottom "line height" with opaque color
inContext.setFillColor(UIColor.gray.cgColor)
var r = self.bounds
r.size.height -= lineHeight
inContext.fill(r)
// can be any color, we're going from Opaque to Clear
let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
// start the gradient "grad width" from right edge
let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
// end the gradient at the right edge, but
// probably want to leave the farthest-right 1 or 2 points
// completely transparent
let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)
// gradient rect starts at the bottom of the opaque rect
r.origin.y = r.size.height - 1
// gradient rect height can extend below the bounds, becuase it will be clipped
r.size.height = bounds.height
inContext.addRect(r)
inContext.clip()
inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)
}
}
Next, we'll make a UILabel subclass that implements that InvertedGradientLayer as a layer mask:
class CornerFadeLabel: UILabel {
let ivgLayer = InvertedGradientLayer()
override func layoutSubviews() {
super.layoutSubviews()
guard let f = self.font, let t = self.text else { return }
// we only want to fade-out the last line if
// it would be clipped
let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
if boundingBox.height <= bounds.height {
layer.mask = nil
return
}
layer.mask = ivgLayer
ivgLayer.lineHeight = f.lineHeight
ivgLayer.gradWidth = 60.0
ivgLayer.frame = bounds
ivgLayer.setNeedsDisplay()
}
}
and here is a sample view controller showing it in use:
class FadeVC: UIViewController {
let wordWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byWordWrapping
return v
}()
let charWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byCharWrapping
return v
}()
let normalLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 1
return v
}()
let numLinesLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
var numLines: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
wordWrapFadeLabel.text = sampleText
charWrapFadeLabel.text = sampleText
normalLabel.text = sampleText
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let btnUP: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
return v
}()
let btnDown: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
return v
}()
bStack.addArrangedSubview(btnUP)
bStack.addArrangedSubview(numLinesLabel)
bStack.addArrangedSubview(btnDown)
let v1 = UILabel()
v1.text = "Word-wrapping"
v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v2 = UILabel()
v2.text = "Character-wrapping"
v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v3 = UILabel()
v3.text = "Normal Label (Truncate Tail)"
v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
stack.addArrangedSubview(bStack)
stack.addArrangedSubview(v1)
stack.addArrangedSubview(wordWrapFadeLabel)
stack.addArrangedSubview(v2)
stack.addArrangedSubview(charWrapFadeLabel)
stack.addArrangedSubview(v3)
stack.addArrangedSubview(normalLabel)
stack.setCustomSpacing(20, after: bStack)
stack.setCustomSpacing(20, after: wordWrapFadeLabel)
stack.setCustomSpacing(20, after: charWrapFadeLabel)
view.addSubview(stack)
// dashed border views so we can see the lable frames
let wordBorderView = DashedView()
let charBorderView = DashedView()
let normalBorderView = DashedView()
wordBorderView.translatesAutoresizingMaskIntoConstraints = false
charBorderView.translatesAutoresizingMaskIntoConstraints = false
normalBorderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wordBorderView)
view.addSubview(charBorderView)
view.addSubview(normalBorderView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
])
// set initial number of lines to 1
btnUpTapped()
}
#objc func btnUpTapped() {
numLines += 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
#objc func btnDownTapped() {
if numLines == 1 { return }
numLines -= 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
}
When running, it looks like this:
The red dashed borders are there just so we can see the frames of the labels. Tapping the up/down arrows will increment/decrement the max number of lines to show in each label.

You should create a CATextLayer with the same text properties as your UILabel.
Fill it with the end of your text you wish to fade.
Then calculate the position of this text segment in your UILabel.
Finally overlay the two.
Here are some aspect explained.

Related

How to achieve "application guide tutorial screen" with the focus on a particular view swift

I want to achieve the following focused UI. It is basically the guideline tutorial view for my users.
The background is my main view controller with a bunch of real functional views and I put a view with 50% transparency on black on top of it to achieve the focused UI but it's far away from what I am trying to achieve. Any suggestion would be highly appreciated.
Code:
func createOverlay(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
radius: CGFloat) -> UIView {
// Step 1
let overlayView = UIView(frame: frame)
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
// Step 2
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// For Swift 4.2
maskLayer.fillRule = .evenOdd
// Step 4
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
Current output:
We can do this by using an "inverted" shadow-path on the overlay view's layer. That will give us a "feathered-edge" oval.
Here is an example view class:
class FocusView : UIView {
// this will be the frame of the "see-through" oval
public var ovalRect: CGRect = .zero {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
// create an oval inside ovalRect
let clearPath = UIBezierPath(ovalIn: ovalRect)
// create a rectangle path larger than entire view
// so we don't see "feathering" on outer edges
let opaquePath = UIBezierPath(rect: bounds.insetBy(dx: -80.0, dy: -80.0)).reversing()
// append the paths so we get a "see-through" oval
clearPath.append(opaquePath)
self.layer.shadowPath = clearPath.cgPath
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize.zero
// adjust the opacity as desired
self.layer.shadowOpacity = 0.5
// adjust shadow radius as desired (controls the "feathered" edge)
self.layer.shadowRadius = 8
}
}
and an example controller. We'll add 6 colored rectangles to use as a "background" and a UILabel to "focus" on:
class ViewController: UIViewController {
let focusView = FocusView()
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
// let's add six different color rectangles as the "background"
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemBrown, .systemYellow, .systemCyan,
]
let vStack = UIStackView()
vStack.axis = .vertical
vStack.distribution = .fillEqually
var i: Int = 0
for _ in 0..<3 {
let hStack = UIStackView()
hStack.distribution = .fillEqually
for _ in 0..<2 {
let v = UIView()
v.backgroundColor = colors[i % colors.count]
hStack.addArrangedSubview(v)
i += 1
}
vStack.addArrangedSubview(hStack)
}
vStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vStack)
label.font = .systemFont(ofSize: 30.0, weight: .bold)
label.textColor = .white
label.textAlignment = .center
label.text = "Example"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
focusView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(focusView)
// start with it hidden and transparent
focusView.isHidden = true
focusView.alpha = 0.0
NSLayoutConstraint.activate([
vStack.topAnchor.constraint(equalTo: view.topAnchor),
vStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
vStack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 80.0),
focusView.topAnchor.constraint(equalTo: view.topAnchor),
focusView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
focusView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
focusView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// we may want to make sure the focusView is on top of all other views
view.bringSubviewToFront(focusView)
// set the focusView's "see-through" oval rect
// it can be set with a hard-coded rect, or
// for this example, we'll use the label frame
// expanded by 80-points horizontally, 60-points vertically
focusView.ovalRect = label.frame.insetBy(dx: -40.0, dy: -30.0)
if focusView.isHidden {
focusView.isHidden = false
UIView.animate(withDuration: 0.3, animations: {
self.focusView.alpha = 1.0
})
} else {
UIView.animate(withDuration: 0.3, animations: {
self.focusView.alpha = 0.0
}, completion: { _ in
self.focusView.isHidden = true
})
}
}
}
Each tap anywhere will either fade-in or fade-out the focus view:

iOS TextView is saved blurry when scaled

I tried save textview as image with not device scale. I implemented a method to save an image by adding an arbitrary textview according to the UI value. Because when I tried save image using drawHierarchy method in up scale, image was blurry.
Condition when textview is saved blurry
not device scale (up scale)
1-1. isScrollEnabled = false and height of textview is more than 128.
1-2. isScrollEnabled = true (always text is blurry)
here is my code
func drawQuoteImage() {
var campusSize = view.frame.size
var scale = UIScreen.main.scale + 2
// 1. Create View
let quoteView = UIView(frame: CGRect(x: 0, y: 0, width: campusSize.width, height: campusSize.height))
let textview = UITextView()
textview.attributedText = NSAttributedString(string: quoteLabel.text, attributes: textAttributes as [NSAttributedString.Key : Any])
textview.frame = transfromFrame(originalFrame: quoteLabel.frame, campusSize: campusSize)
quoteView.addSubview(textview)
// 2. Render image
UIGraphicsBeginImageContextWithOptions(quoteView.frame.size, false, scale)
let context = UIGraphicsGetCurrentContext()!
context.setRenderingIntent(.relativeColorimetric)
context.interpolationQuality = .high
quoteView.drawHierarchy(in: quoteView.frame, afterScreenUpdates: true)
quoteView.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
quoteImage = image
}
private func transfromFrame(originalFrame: CGRect, campusSize: CGSize) -> CGRect
{
if UIDevice.current.screenType == .iPhones_X_XS {
return CGRect(x: round(originalFrame.origin.x), y: round(originalFrame.origin.y), width: round(originalFrame.width), height: round(originalFrame.height))
}
else {
var frame = CGRect()
let ratioBasedOnWidth = campusSize.width / editView.frame.width
let ratioBasedOnHeight = campusSize.height / editView.frame.height
frame.size.width = round(originalFrame.width * ratioBasedOnWidth)
frame.size.height = round(originalFrame.height * ratioBasedOnHeight)
frame.origin.x = round(originalFrame.origin.x * ratioBasedOnWidth)
frame.origin.y = round(originalFrame.origin.y * ratioBasedOnHeight)
return frame
}
}
Wired Point
when height of textview is more than 128, textview is save blurry. I found related value when I put textview default height is 128.
Height is 128 or less (when isScrollEnabled is false), textview is saved always clear. But when height is more than 128, it looks blurry.
Height 128
Height 129
I'd like to know how to clearly draw image with textview at #5x scale. (textview height is bigger than 128)
Here's a quick example using a UIView extension from this accepted answer: https://stackoverflow.com/a/51944513/6257435
We'll create a UITextView with a size of 240 x 129. Then add 4 buttons to capture the text view at 1x, 2x, 5x and 10x scale.
It looks like this when running:
and the result...
At 1x scale - 240 x 129 pixels:
At 2x scale - 480 x 258 pixels:
At 5x scale - 1200 x 645 pixels (just showing a portion):
At 10x scale - 2400 x 1290 pixels (just showing a portion):
The extension:
extension UIView {
func scale(by scale: CGFloat) {
self.contentScaleFactor = scale
for subview in self.subviews {
subview.scale(by: scale)
}
}
func getImage(scale: CGFloat? = nil) -> UIImage {
let newScale = scale ?? UIScreen.main.scale
self.scale(by: newScale)
let format = UIGraphicsImageRendererFormat()
format.scale = newScale
let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
let image = renderer.image { rendererContext in
self.layer.render(in: rendererContext.cgContext)
}
return image
}
}
Sample controller code:
class TextViewCapVC: UIViewController {
let textView = UITextView()
let resultLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// add a stack view with buttons
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12
[1, 2, 5, 10].forEach { i in
let btn = UIButton()
btn.setTitle("Create Image at \(i)x scale", for: [])
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.backgroundColor = .systemBlue
btn.tag = i
btn.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(btn)
}
[textView, stack, resultLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// text view 280x240, 20-points from top, centered horizontally
textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
textView.widthAnchor.constraint(equalToConstant: 240.0),
textView.heightAnchor.constraint(equalToConstant: 129.0),
// stack view, 20-points from text view, same width, centered horizontally
stack.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20.0),
stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
stack.widthAnchor.constraint(equalTo: textView.widthAnchor),
// result label, 20-points from stack view
// 20-points from leading/trailing
resultLabel.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let string = "Test"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.blue,
.font: UIFont.italicSystemFont(ofSize: 104.0),
]
let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
textView.attributedText = attributedString
resultLabel.font = .systemFont(ofSize: 14, weight: .light)
resultLabel.numberOfLines = 0
resultLabel.text = "Results:"
// so we can see the view frames
textView.backgroundColor = .yellow
resultLabel.backgroundColor = .cyan
}
#objc func gotTap(_ sender: Any?) {
guard let btn = sender as? UIButton else { return }
let scaleFactor = CGFloat(btn.tag)
let img = textView.getImage(scale: scaleFactor)
var s: String = "Results:\n\n"
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fName: String = "\(btn.tag)xScale-\(img.size.width * img.scale)x\(img.size.height * img.scale).png"
let url = documents.appendingPathComponent(fName)
if let data = img.pngData() {
do {
try data.write(to: url)
} catch {
s += "Unable to Write Image Data to Disk"
resultLabel.text = s
return
}
} else {
s += "Could not get png data"
resultLabel.text = s
return
}
s += "Logical Size: \(img.size)\n\n"
s += "Scale: \(img.scale)\n\n"
s += "Pixel Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n\n"
s += "File \"\(fName)\"\n\nsaved to Documents folder\n"
resultLabel.text = s
// print the path to documents in debug console
// so we can copy/paste into Finder to get to the files
print(documents.path)
}
}

Gradient not applying to full view on larger screen

I implement a code to apply diagonal gradient to different views. it worked fine for all small views and all views on small screens but on iPhone 13 Pro Max simulator it doesn't apply to full view and leaves some part to the right side. I only noticed even in some other views which are wider this gradient only applies about 80% in width.
Here's the screenshot how its showing in one view:
It does not apply to right of the view and the right corner radius is invisible.
Here's the code for Gradient:
func applyGradientDiagonal(isVertical: Bool, colorArray: [UIColor]) {
layer.sublayers?.filter({ $0 is CAGradientLayer }).forEach({ $0.removeFromSuperlayer() })
let gradientLayer = CAGradientLayer()
gradientLayer.colors = colorArray.map({ $0.cgColor })
if isVertical {
//top to bottom
gradientLayer.locations = [0.0, 1.0]
} else {
//left to right
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
}
backgroundColor = .clear
gradientLayer.frame = bounds
layer.insertSublayer(gradientLayer, at: 0)
}
And I'm calling this in viewDidLoad() like this:
self.viewTAGradient.applyGradientDiagonal(isVertical: false, colorArray: [UIColor.init(named: "gradient1")!, UIColor.init(named: "gradient2")!, UIColor.init(named: "gradient3")!])
I looked for some solutions and even tried to call it like this but this also didn't work:
override func viewDidLayoutSubviews() {
self.viewTAGradient.applyGradientDiagonal(isVertical: false, colorArray: [UIColor.init(named: "gradient1")!, UIColor.init(named: "gradient2")!, UIColor.init(named: "gradient3")!])
}
Edit: New Update:
the issue is with all the gradients on iPhone 13 pro max. I copied some codes from internet for gradients and created new view controller with just one view of fixed height stuck to top and all shifted to left side on 13 pro max. works fine on other devices though. here's the screenshots:
Can anyone help me why on one particular device its behaving like that?
There's no reason for the gradient to behave differently on an iPhone 13 Pro Max, so my guess (without seeing your complete layout) would be related to constraints or when you're actually calling applyGradientDiagonal().
You may find it much easier to get reliable results by subclassing UIView and putting the gradient code inside layoutSubview().
Here's a simple example:
class MyGradientView: UIView {
var colorArray: [UIColor] = [] {
didSet {
setNeedsLayout()
}
}
var vertical: Bool = true {
didSet {
setNeedsLayout()
}
}
static override var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
guard let layer = layer as? CAGradientLayer else { return }
layer.colors = colorArray.map({ $0.cgColor })
if vertical {
// top-down gradient
layer.startPoint = CGPoint(x: 0.5, y: 0.0)
layer.endPoint = CGPoint(x: 0.5, y: 1.0)
} else {
// diagonal gradient
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
}
}
}
and an example view controller to demonstrate:
class GradientTestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
stack.distribution = .fillEqually
let colorPairs: [[UIColor]] = [
[.blue, .cyan],
[.red, .yellow],
[.blue, .yellow, .red],
]
// 3 pairs of gradient views
colorPairs.forEach { colors in
let hstack = UIStackView()
hstack.spacing = 8
hstack.distribution = .fillEqually
var v = MyGradientView()
v.colorArray = colors
v.vertical = true
hstack.addArrangedSubview(v)
v = MyGradientView()
v.colorArray = colors
v.vertical = false
hstack.addArrangedSubview(v)
stack.addArrangedSubview(hstack)
}
// a pair of plain, solid color views for comparison
let hstack = UIStackView()
hstack.spacing = 8
hstack.distribution = .fillEqually
var v = UIView()
v.backgroundColor = .systemRed
hstack.addArrangedSubview(v)
v = UIView()
v.backgroundColor = .systemBlue
hstack.addArrangedSubview(v)
stack.addArrangedSubview(hstack)
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// outline the stack view
// so we can see its frame
let outlineView = UIView()
outlineView.backgroundColor = .clear
outlineView.layer.borderColor = UIColor.black.cgColor
outlineView.layer.borderWidth = 3
outlineView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outlineView)
NSLayoutConstraint.activate([
outlineView.topAnchor.constraint(equalTo: stack.topAnchor, constant: -2.0),
outlineView.leadingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -2.0),
outlineView.trailingAnchor.constraint(equalTo: stack.trailingAnchor, constant: 2.0),
outlineView.bottomAnchor.constraint(equalTo: stack.bottomAnchor, constant: 2.0),
])
}
}
The result of running that code:

Fix two colours on UISlider swift

Slider with two different colors
How can we make a slider with two fixed colors? The colors won't change even if the slider is moving. Also, the slider thumb should be able to side over any of those two colors. I should be able to define the length of the first section.
func createSlider(slider:UISlider) {
let frame = CGRect(x: 0.0, y: 0.0, width: slider.bounds.width, height: 4)
let tgl = CAGradientLayer()
tgl.frame = frame
tgl.colors = [UIColor.gray.cgColor,UIColor.red.cgColor]
tgl.endPoint = CGPoint(x: 0.4, y: 1.0)
tgl.startPoint = CGPoint(x: 0.0, y: 1.0)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, false, 0.0)
tgl.render(in: UIGraphicsGetCurrentContext()!)
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
slider.setMaximumTrackImage(backgroundImage?.resizableImage(withCapInsets:.zero), for: .normal)
slider.setMinimumTrackImage(backgroundImage?.resizableImage(withCapInsets:.zero), for: .normal)
}
I have already tried this. But this dosent give me exactly what I wanted to achieve.
Here's one approach...
set both min and max track images to "empty" images
add left and right subviews to act as "fake" track images
add a different color sublayer on each side subview
when you slide the thumb, update the frames of the layers
Here's some example code... you may want to do some tweaks:
class XebSlider: UISlider {
private var colors: [[UIColor]] = [
[.black, .black],
[.black, .black],
]
// left and right views will become the "fake" track images
private let leftView = UIView()
private let rightView = UIView()
// each view needs a layer to "change color"
private let leftShape = CALayer()
private let rightShape = CALayer()
// constraints that will be updated
private var lvWidth: NSLayoutConstraint!
private var lvLeading: NSLayoutConstraint!
private var rvTrailing: NSLayoutConstraint!
// how wide the two "sides" should be
private var leftPercent: CGFloat = 0
private var rightPercent: CGFloat = 0
// track our current width, so we don't repeat constraint updates
// unless our width has changed
private var currentWidth: CGFloat = 0
init(withLeftSidePercent leftWidth: CGFloat, leftColors: [UIColor], rightColors: [UIColor]) {
super.init(frame: .zero)
commonInit()
leftView.backgroundColor = leftColors[1]
rightView.backgroundColor = rightColors[1]
leftShape.backgroundColor = leftColors[0].cgColor
rightShape.backgroundColor = rightColors[0].cgColor
leftPercent = leftWidth
rightPercent = 1.0 - leftPercent
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// set both track images to "empty" images
setMinimumTrackImage(UIImage(), for: [])
setMaximumTrackImage(UIImage(), for: [])
// add left and right subviews
[leftView, rightView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.layer.cornerRadius = 4.0
v.layer.masksToBounds = true
v.isUserInteractionEnabled = false
insertSubview(v, at: 0)
}
// add sublayers
leftView.layer.addSublayer(leftShape)
rightView.layer.addSublayer(rightShape)
// create constraints whose .constant values will be modified in layoutSubviews()
lvLeading = leftView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
rvTrailing = rightView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
lvWidth = leftView.widthAnchor.constraint(equalToConstant: 0.0)
// avoids auto-layout complaints when the frame changes (such as on device rotation)
lvWidth.priority = UILayoutPriority(rawValue: 999)
// set constraints for subviews
NSLayoutConstraint.activate([
lvLeading,
rvTrailing,
lvWidth,
leftView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0.0),
leftView.heightAnchor.constraint(equalToConstant: 8.0),
rightView.centerYAnchor.constraint(equalTo: leftView.centerYAnchor),
rightView.heightAnchor.constraint(equalTo: leftView.heightAnchor),
rightView.leadingAnchor.constraint(equalTo: leftView.trailingAnchor, constant: 1.0),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// we only want to do this if the bounds width has changed
if bounds.width != currentWidth {
let trackRect = self.trackRect(forBounds: bounds)
lvLeading.constant = trackRect.origin.x
rvTrailing.constant = -(bounds.width - (trackRect.origin.x + trackRect.width))
lvWidth.constant = trackRect.width * leftPercent
}
// get percentage of thumb position
// based on min and max values
let pctValue = (self.value - self.minimumValue) / (self.maximumValue - self.minimumValue)
// calculate percentage of each side that needs to be "covered"
// by the different color layer
let leftVal = max(0.0, min(CGFloat(pctValue) / leftPercent, 1.0))
let rightVal = max(0.0, min((CGFloat(pctValue) - leftPercent) / rightPercent, 1.0))
var rLeft = leftView.bounds
var rRight = rightView.bounds
rLeft.size.width = leftView.bounds.width * leftVal
rRight.size.width = rightView.bounds.width * rightVal
// disable default layer animations
CATransaction.begin()
CATransaction.setDisableActions(true)
leftShape.frame = rLeft
rightShape.frame = rRight
CATransaction.commit()
}
}
and a controller example showing its usage:
class ViewController: UIViewController {
var slider: XebSlider!
override func viewDidLoad() {
super.viewDidLoad()
let leftSideColors: [UIColor] = [
#colorLiteral(red: 0.4796532989, green: 0.4797258377, blue: 0.4796373844, alpha: 1),
#colorLiteral(red: 0.8382737041, green: 0.8332912326, blue: 0.8421040773, alpha: 1),
]
let rightSideColors: [UIColor] = [
#colorLiteral(red: 0.9009097219, green: 0.3499996662, blue: 0.4638580084, alpha: 1),
#colorLiteral(red: 0.9591985345, green: 0.8522816896, blue: 0.8730568886, alpha: 1),
]
let leftSideWidthPercent: CGFloat = 0.5
slider = XebSlider(withLeftSidePercent: leftSideWidthPercent, leftColors: leftSideColors, rightColors: rightSideColors)
view.addSubview(slider)
slider.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain slider 40-pts from Top / Leading / Trailing
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
slider.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
])
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
// to get teh custom slider to update properly
self.slider.setNeedsLayout()
}, completion: {
_ in
})
}
}
Result:

How to add dots (ellipses) to LabelView as in the table of contents of a book

I have stackView which contains few labelViews in each of which two words are written. And I want them to be separated by an ellipsis across the entire width of labelView. As a result: one word close to the left, another - to the right, and dots between them. Note: the label can take up several lines if the words length is long.
EDIT
Here filling my stackView
for ingredient in ingredients {
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.yellow // just for my needs
textLabel.widthAnchor.constraint(equalToConstant: ingredientsStackView.frame.width).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
textLabel.text = ingredient.getName() + " " + String(ingredient.getAmount()) + " " + ingredient.getMeasure()
textLabel.textAlignment = .left
textLabel.numberOfLines = 0
textLabel.lineBreakMode = .byWordWrapping
ingredientsStackView.addArrangedSubview(textLabel)
}
ingredientsStackView.translatesAutoresizingMaskIntoConstraints = false
and that looks like this
But I want something like this
You can see dots between ingredientName and ingredientAmount.
I had an idea to implement this through CGFloat conversion here, but this question was closed.
One technique is to use size() or boundingRect(with:options:context:) to calculate the size, repeating that for more and more series of dots, until you reach the desired width.
But that ignores a subtle (but IMHO, important) aspect, namely that the dots from all the rows should all line up perfectly. If they don’t line up, it can be surprisingly distracting.
So, I’d be inclined to define a view that does that, performing a modulus calculation against some common ancestor view coordinate system. And, I’d personally just render the dots as UIBezierPath.
For example:
class EllipsesView: UIView {
let spacing: CGFloat = 3
let radius: CGFloat = 1.5
var color: UIColor {
UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark: return .white
default: return .black
}
}
}
let shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.strokeColor = UIColor.clear.cgColor
return layer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.fillColor = color.cgColor
let point = convert(bounds.origin, to: window)
let diff = radius * 3 + spacing
let offset = diff - point.x.truncatingRemainder(dividingBy: diff)
let rect = CGRect(x: bounds.minX + offset, y: bounds.maxY - radius * 2, width: bounds.width - offset, height: radius * 2)
let path = UIBezierPath()
var center = CGPoint(x: rect.minX + radius, y: rect.midY)
while center.x + radius < rect.maxX {
path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
center.x += diff
}
shapeLayer.path = path.cgPath
}
}
private extension EllipsesView {
func configure() {
layer.addSublayer(shapeLayer)
}
}
Then you can add your two labels, lining up the bottom of the ellipses view with the bottom baseline of the labels:
let stringPairs = [("foo", "$1.37"), ("foobar", "$0.42"), ("foobarbaz", "$10.00"), ("foobarbazqux", "$100.00")]
for stringPair in stringPairs {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
let leftLabel = UILabel()
leftLabel.translatesAutoresizingMaskIntoConstraints = false
leftLabel.text = stringPair.0
leftLabel.setContentHuggingPriority(.required, for: .horizontal)
container.addSubview(leftLabel)
let ellipsesView = EllipsesView()
ellipsesView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(ellipsesView)
let rightLabel = UILabel()
rightLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.font = UIFont.monospacedDigitSystemFont(ofSize: rightLabel.font.pointSize, weight: .regular)
rightLabel.text = stringPair.1
rightLabel.setContentHuggingPriority(.required, for: .horizontal)
container.addSubview(rightLabel)
NSLayoutConstraint.activate([
// horizontal constraints
leftLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
ellipsesView.leadingAnchor.constraint(equalTo: leftLabel.trailingAnchor, constant: 3),
rightLabel.leadingAnchor.constraint(equalTo: ellipsesView.trailingAnchor, constant: 3),
rightLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
// align last baseline of three subviews
leftLabel.lastBaselineAnchor.constraint(equalTo: ellipsesView.bottomAnchor),
leftLabel.lastBaselineAnchor.constraint(equalTo: rightLabel.lastBaselineAnchor),
// vertical constraints to container
leftLabel.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
rightLabel.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
ellipsesView.topAnchor.constraint(equalTo: container.topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
verticalStackView.addArrangedSubview(container)
}
That yields the ellipses, but they all line up perfectly, too:
Thanks for #koen. He gave me a link to the another question that also helped me. But I see that he deleted that comment maybe because Rob's answer. And I couldn't save it.
The idea was to add horizontal stacks of 3 views to the root vertical stack.
let stackView = UIStackView()
ingredientsStackView.addArrangedSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: ingredientsStackView.leadingAnchor,
constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: ingredientsStackView.trailingAnchor,
constant: 0.0).isActive = true
Next create and set constraints for these views
let nameLabel = UILabel()
let ellipsesLabel = UILabel()
let amountAndMeasureLabel = UILabel()
for label in [nameLabel, ellipsisLabel, amountAndMeasureLabel] {
label.font = UIFont(name: "Montserrat-Medium", size: 14)
stackView.addArrangedSubview(label)
}
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
nameLabel.trailingAnchor.constraint(equalTo: ellipsisLabel.leadingAnchor, constant: 0.0),
nameLabel.widthAnchor.constraint(equalToConstant: nameLabel.intrinsicContentSize.width),
amountAndMeasureLabel.leadingAnchor.constraint(equalTo: ellipsisLabel.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.widthAnchor.constraint(equalToConstant: amountAndMeasureLabel.intrinsicContentSize.width)
])
After that DON'T FORGET to call stackView.layoutIfNeeded()
And then fill the middle view with dots until it is completely filled.
while ellipsisLabel.intrinsicContentSize.width <= ellipsisLabel.frame.width {
ellipsisLabel.text?.append(".")
}
Whole code
private func setIngredients(ingredients: [Ingredient], ingredientsStackView: UIStackView) {
for ingredient in ingredients {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fill
ingredientsStackView.addArrangedSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: ingredientsStackView.leadingAnchor,
constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: ingredientsStackView.trailingAnchor,
constant: 0.0).isActive = true
let nameLabel = UILabel()
nameLabel.text = ingredient.name
nameLabel.lineBreakMode = .byWordWrapping
nameLabel.contentMode = .left
nameLabel.numberOfLines = 0
let ellipsisLabel = UILabel()
ellipsisLabel.text = ""
let amountAndMeasureLabel = UILabel()
amountAndMeasureLabel.text = String(ingredient.amount) + " " + ingredient.measure
amountAndMeasureLabel.contentMode = .left
for label in [nameLabel, ellipsisLabel, amountAndMeasureLabel] {
label.font = UIFont(name: "Montserrat-Medium", size: 14)
stackView.addArrangedSubview(label)
}
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
nameLabel.trailingAnchor.constraint(equalTo: ellipsisLabel.leadingAnchor, constant: 0.0),
nameLabel.widthAnchor.constraint(
equalToConstant: nameLabel.intrinsicContentSize.width),
amountAndMeasureLabel.leadingAnchor.constraint(equalTo: ellipsisLabel.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.widthAnchor.constraint(
equalToConstant: amountAndMeasureLabel.intrinsicContentSize.width)
])
stackView.layoutIfNeeded()
while ellipsisLabel.intrinsicContentSize.width <= ellipsisLabel.frame.width {
ellipsisLabel.text?.append(".")
}
}
ingredientsStackView.translatesAutoresizingMaskIntoConstraints = false
}
BUT
I did this before I saw #Rob answer. Thanks! Probably it's more difficult implementation compared to mine and you can choose which is better for you. I think his solution is more rational and cleaner so as an answer I will choose his post.

Resources