Gradient not applying to full view on larger screen - ios

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:

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:

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

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.

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:

UITextView gradient layer apply not working

I want to apply gradient layer on top 10% and bottom 10% of UITextView. To do this, I place a dummy UIView called container view and make UITextView a subview of it. And then I add the following code:
if let containerView = textView.superview {
let gradient = CAGradientLayer(layer: containerView.layer)
gradient.frame = containerView.bounds
gradient.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
gradient.locations = [0.0, 0.1, 0.9, 1.0]
containerView.layer.mask = gradient
}
But the gradient is only applied to the top, not the bottom. Is there something wrong with the code?
Further, if I resize the container view anytime by modifying it's constraints, do I need to edit the mask layer every time?
Edit: Here is the output from #DonMag answer.
But what I want is something like in this image that text fades at the bottom.
EDIT2:
Here are screenshots after DonMag's revised answer.
#DongMag solution is very complicated. Instead, you just need a mask implemented like:
#IBDesignable
class MaskableLabel: UILabel {
var maskImageView = UIImageView()
#IBInspectable
var maskImage: UIImage? {
didSet {
maskImageView.image = maskImage
updateView()
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateView()
}
func updateView() {
if maskImageView.image != nil {
maskImageView.frame = bounds
mask = maskImageView
}
}
}
Then with a simple gradient mask like this, You can see it even right in the storyboard.
Note: You can use this method and replace UILabel with any other view you like to subclass.
Here is the example project on the GitHub
Edit - after clarification of desired effect...
My initial answer as to why you were only seeing the gradient on the top stands:
You're only seeing the gradient on the top because you gave it four locations but only two colors.
So, now that you provided an image of what you're trying to do...
Use this DoubleGradientMaskView as the "container" view for the text view:
class DoubleGradientMaskView: UIView {
let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0.0, 0.1, 0.9, 1.0]
layer.mask = gradientLayer
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
Example controller:
class GradientTextViewViewController: UIViewController {
let textView = UITextView()
let containerView = DoubleGradientMaskView()
let bkgImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
[bkgImageView, textView, containerView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
bkgImageView.contentMode = .scaleAspectFill
if let img = UIImage(named: "background") {
bkgImageView.image = img
} else {
bkgImageView.backgroundColor = .blue
}
view.addSubview(bkgImageView)
view.addSubview(containerView)
containerView.addSubview(textView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// add an image view so we can see the white text
bkgImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
bkgImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
bkgImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
bkgImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constraint text view inside container
textView.topAnchor.constraint(equalTo: containerView.topAnchor),
textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
// constrain container Top / Bottom 40, Leading / Trailing 40
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
textView.isScrollEnabled = true
textView.font = UIFont.systemFont(ofSize: 48.0, weight: .bold)
textView.textColor = .white
textView.backgroundColor = .clear
textView.text = String((1...20).flatMap { "This is row \($0)\n" })
}
}
Result:
or, with a blue background instead of an image:
You're only seeing the gradient on the top because you gave it four locations but only two colors.
Changing the colors to:
gradient.colors = [UIColor.clear.cgColor, UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor]
would probably give you the appearance you want... but you'd need additional code to handle size changing.
If you use this class as your "container" view, sizing will be automatic:
class DoubleGradientView: UIView {
var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor, UIColor.black.cgColor]
gradientLayer.locations = [0.0, 0.1, 0.9, 1.0]
}
}
Here is an example controller. It creates two "text views in containers."
The top one is scrollable, with a height of 100.
The bottom one is NOT scrollable, so it will size its height to the text as you type.
Both are constrained Leading / Trailing at 60-pts, so you'll also see the automatic gradient update when you rotate the device.
class GradientBehindTextViewViewController: UIViewController {
let textView1 = UITextView()
let containerView1 = DoubleGradientView()
let textView2 = UITextView()
let containerView2 = DoubleGradientView()
override func viewDidLoad() {
super.viewDidLoad()
[textView1, containerView1, textView2, containerView2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
containerView1.addSubview(textView1)
view.addSubview(containerView1)
containerView2.addSubview(textView2)
view.addSubview(containerView2)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constraint text view inside container
textView1.topAnchor.constraint(equalTo: containerView1.topAnchor),
textView1.leadingAnchor.constraint(equalTo: containerView1.leadingAnchor),
textView1.trailingAnchor.constraint(equalTo: containerView1.trailingAnchor),
textView1.bottomAnchor.constraint(equalTo: containerView1.bottomAnchor),
// constrain container Top + 40, Leading / Trailing 80
containerView1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
containerView1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 80.0),
containerView1.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -80.0),
// text view 1 will have scrolling enabled, so we'll set its height to 100
containerView1.heightAnchor.constraint(equalToConstant: 100.0),
// constraint text view inside container
textView2.topAnchor.constraint(equalTo: containerView2.topAnchor),
textView2.leadingAnchor.constraint(equalTo: containerView2.leadingAnchor),
textView2.trailingAnchor.constraint(equalTo: containerView2.trailingAnchor),
textView2.bottomAnchor.constraint(equalTo: containerView2.bottomAnchor),
// constrain container2 Top to container1 bottom + 40, Leading / Trailing 80
containerView2.topAnchor.constraint(equalTo: containerView1.bottomAnchor, constant: 40.0),
containerView2.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 80.0),
containerView2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -80.0),
// text view 2 will NOT scroll (it will size with the text) so no height / bottom
])
// text view 1 should scroll
textView1.isScrollEnabled = true
// text view 1 should NOT scroll we want the text view to size itelf as we type
textView2.isScrollEnabled = false
// let the gradient show through
textView1.backgroundColor = .clear
textView2.backgroundColor = .clear
textView1.text = "Initial text for text view 1."
textView2.text = "Initial text for text view 2."
}
}

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