iOS UISLider with gradient layer - ios

I'm building an iOS application in which I have to implement a custom UISlider. The problem is that the built-in UISlider doesn't support gradient track. Another issue is my UI style guide shows that the current tracking value rectangle should be a gradient of two colors as shown in the image
How can I build a customized version of the UISlider? I have thought of either subclassing the existing one or by building a UIControl subclass.
I'm using xcode 9.4 and swift 4.2
Thanks in advance!

I ended up solving the problem by setting the gradient layer as an image for the slider minimum track image as following:
#IBDesignable
class GradientSlider: UISlider {
#IBInspectable var thickness: CGFloat = 20 {
didSet {
setup()
}
}
#IBInspectable var sliderThumbImage: UIImage? {
didSet {
setup()
}
}
func setup() {
let minTrackStartColor = Palette.SelectiveYellow
let minTrackEndColor = Palette.BitterLemon
let maxTrackColor = Palette.Firefly
do {
self.setMinimumTrackImage(try self.gradientImage(
size: self.trackRect(forBounds: self.bounds).size,
colorSet: [minTrackStartColor.cgColor, minTrackEndColor.cgColor]),
for: .normal)
self.setMaximumTrackImage(try self.gradientImage(
size: self.trackRect(forBounds: self.bounds).size,
colorSet: [maxTrackColor.cgColor, maxTrackColor.cgColor]),
for: .normal)
self.setThumbImage(sliderThumbImage, for: .normal)
} catch {
self.minimumTrackTintColor = minTrackStartColor
self.maximumTrackTintColor = maxTrackColor
}
}
func gradientImage(size: CGSize, colorSet: [CGColor]) throws -> UIImage? {
let tgl = CAGradientLayer()
tgl.frame = CGRect.init(x:0, y:0, width:size.width, height: size.height)
tgl.cornerRadius = tgl.frame.height / 2
tgl.masksToBounds = false
tgl.colors = colorSet
tgl.startPoint = CGPoint.init(x:0.0, y:0.5)
tgl.endPoint = CGPoint.init(x:1.0, y:0.5)
UIGraphicsBeginImageContextWithOptions(size, tgl.isOpaque, 0.0);
guard let context = UIGraphicsGetCurrentContext() else { return nil }
tgl.render(in: context)
let image =
UIGraphicsGetImageFromCurrentImageContext()?.resizableImage(withCapInsets:
UIEdgeInsets.init(top: 0, left: size.height, bottom: 0, right: size.height))
UIGraphicsEndImageContext()
return image!
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(
x: bounds.origin.x,
y: bounds.origin.y,
width: bounds.width,
height: thickness
)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}

Related

Creating a haptic-touch-like button in Swift/Obj-C

I've been playing around with an idea for a button which, when held, expands to reveal other buttons (like the FAB in Android). Without releasing, sliding one's finger down should highlight other buttons, similar to haptic touch's behaviour.
Here's a crude mockup I've created using Drama: https://youtu.be/Iam8Gjv3gqM.
There should also be an option to have a label horizontally next to each button, and the order of buttons should be as shown (with the selected button at the top instead of its usual position).
I already have a class for each button (seen below), but do not know how to achieve this layout/behaviour.
class iDUButton: UIButton {
init(image: UIImage? = nil) {
super.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
if let image = image {
setImage(image, for: .normal)
}
backgroundColor = .secondarySystemFill
layer.cornerRadius = 15
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
class iDUBadgeButton: iDUButton {
var badgeLabel = UILabel()
var badgeCount = 0
override init(image: UIImage? = nil) {
super.init(image: image)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
badgeLabel.textColor = .white
badgeLabel.backgroundColor = .systemRed
badgeLabel.textAlignment = .center
badgeLabel.font = .preferredFont(forTextStyle: .caption1)
badgeLabel.alpha = 0
updateBadge()
addSubview(badgeLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
badgeLabel.sizeToFit()
let height = max(18, badgeLabel.frame.height + 5.0)
let width = max(height, badgeLabel.frame.width + 10.0)
badgeLabel.frame = CGRect(x: frame.width - 5, y: -badgeLabel.frame.height / 2, width: width, height: height);
badgeLabel.layer.cornerRadius = badgeLabel.frame.height / 2;
badgeLabel.layer.masksToBounds = true;
}
func setBadgeCount(badgeCount: Int) {
self.badgeCount = badgeCount;
self.updateBadge()
}
func updateBadge() {
if badgeCount != 0 {
badgeLabel.text = "\(badgeCount)"
}
UIView.animate(withDuration: 0.25, animations: {
self.badgeLabel.alpha = self.badgeCount == 0 ? 0 : 1
})
layoutSubviews()
}
}
If anyone could help with achieving this layout, I'd be very grateful! Thanks in advance.
PS: I'm developing the UI in Swift, but will be converting it to Objective-C once complete. If you're interested, the use case is to adapt the button in the top-left of this to offer a selection of different tags.

Gradient from UIView after orientation transition

I have an extension for UIView to apply gradient:
extension UIView {
func applyGradient(colors: [CGColor]) {
self.backgroundColor = nil
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds // Here new gradientLayer should get actual UIView bounds
gradientLayer.cornerRadius = self.layer.cornerRadius
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.masksToBounds = true
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
In my UIView subclass I'm creating all my view and setting up constraints:
private let btnSignIn: UIButton = {
let btnSignIn = UIButton()
btnSignIn.setTitle("Sing In", for: .normal)
btnSignIn.titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
btnSignIn.layer.cornerRadius = 30
btnSignIn.clipsToBounds = true
btnSignIn.translatesAutoresizingMaskIntoConstraints = false
return btnSignIn
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSubViews()
}
func addSubViews() {
self.addSubview(imageView)
self.addSubview(btnSignIn)
self.addSubview(signUpstackView)
self.addSubview(textFieldsStackView)
setConstraints()
}
I've overridden layoutSubviews function which is called each time when view bounds are changed(Orientation transition included), where I'm calling applyGradient.
override func layoutSubviews() {
super.layoutSubviews()
btnSignIn.applyGradient(colors: [Colors.ViewTopGradient, Colors.ViewBottomGradient])
}
The problem is that after orientation transition gradient applied wrong for some reason...
See the screenshot please
What am I missing here?
If you look at your button, you’ll see two gradients. That’s because layoutSubviews is called at least twice, first when the view was first presented and again after the orientation change. So you’ve added at least two gradient layers.
You want to change this so you only insertSublayer once (e.g. while the view is being instantiated), and because layoutSubviews can be called multiple times, it should limit itself to just adjusting existing layers, not adding any.
You can also just use the layerClass class property to make the button’s main layer a gradient, and then you don’t have to manually adjust layer frames at all:
#IBDesignable
public class RoundedGradientButton: UIButton {
static public override var layerClass: AnyClass { CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
#IBInspectable var startColor: UIColor = .blue { didSet { updateColors() } }
#IBInspectable var endColor: UIColor = .red { didSet { updateColors() } }
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}
private extension RoundedGradientButton {
func configure() {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
updateColors()
titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
}
This technique eliminates the need to adjust the layer’s frame manually and results in better mid-animation renditions, too.

UIButton align image left and center text

Introduction:
I have a class, which inherits from UIButton. In this class I want to update properties, like titleEdgeInsets, imageEdgeInsets, contentHorizontalAlignment.
My first approach was to use layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
// update properties
}
The layoutSubviews creates an infinity loop, so that I've searched for an alternative method.
My Question:
Is it a common way, to use the willMove method for updating UIButton properties?
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
// update properties
}
If not, why?
My goal is to align the imageView of the button left (with padding) and center the text.
UPDATE:
I need the button frame.size and the bounds.width to calculate the position of the text and the image view
All the properties you mentioned above can be set in the init of the UIButton there is absolutely no need to set them in layoutSubviews or willMove(toWindow.
layoutSubviews will be called multiple times so setting these properties again n agin in here makes no sense. willMove(toWindow will be called when button is added to some view and button is loaded but you dont have to wait till then to set these properties. Because you already have a subclass of button, so I would suggest doing
class SomeButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
self.contentHorizontalAlignment = .center
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
By the way creating a subclass of UIButton is not recommended, so if you wanna simply assign these properties to your button you can rather have a extension to UIButton
extension UIButton {
func applyStyle() {
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
self.contentHorizontalAlignment = .center
}
}
EDIT:
Is this what you want??
No matter what the text is, text is always in centre and image is to its left with 10 pixel padding
EDIT 2:
As OP has confirmed that, he wants the button to be styled as showed in images above, posting the code to achieve the same
class SomeButton: UIButton {
var titleFont: UIFont! = nil
var textSize: CGFloat = 0
let imageWidth: CGFloat = 20
let buttonHeight: CGFloat = 30
override init(frame: CGRect) {
super.init(frame: frame)
self.titleFont = titleLabel!.font
self.setTitle("here", for: .normal)
self.setTitleColor(UIColor.red, for: .normal)
self.setImage(UIImage(named: "hand"), for: .normal)
self.backgroundColor = UIColor.green
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
if let string = self.title(for: .normal) {
textSize = string.widthOfString(usingFont: self.titleFont)
//30 because imageWidth + 10 padding
return CGRect(origin: CGPoint(x: 30, y: 0), size: CGSize(width: textSize + 30, height: buttonHeight))
}
return CGRect.zero
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: imageWidth, height: buttonHeight))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override var intrinsicContentSize: CGSize {
//60 because you need eauql padding on both side 30 + 30 = 60
return CGSize(width: textSize + 60, height: buttonHeight)
}
}
extension String {
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
}
Hope it helps

Animate setFillColor color change in custom UIView

I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()

How to marquee UITextField placeholder if text longer then the UITextField width?

Is it possible to marquee UITextFiled placeholder, if placeholder text is longer then the size of UITextField width, There are lib for UILabel MarqueeLabel but I am not sure how to marquee placeholder, please provide some suggestion, Or you can explain what is a placeholder is actually, it doesn't look like UILabel
I am using bellow code for CustomTextField with validation error message
import UIKit
#IBDesignable
class CustomTextField: UITextField {
var placeholdertext: String?
#IBInspectable
public var cornerRadius :CGFloat {
set { layer.cornerRadius = newValue }
get {
return layer.cornerRadius
}
}
// Provides left padding for images
override func leftViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.leftViewRect(forBounds: bounds)
textRect.origin.x += leftPadding
return textRect
}
override func rightViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.rightViewRect(forBounds: bounds)
textRect.origin.x -= rightPadding
return textRect
}
#IBInspectable var leftImage: UIImage? {
didSet {
updateView()
}
}
#IBInspectable var isUnderLine:Bool = false
{
didSet{
updateView()
}
}
#IBInspectable var rightImage: UIImage? {
didSet {
updateView()
}
}
func setError(error:String?){
if(error != nil)
{
self.attributedPlaceholder = NSAttributedString(string: error!, attributes: [NSForegroundColorAttributeName: UIColor.red])
}
}
#IBInspectable var leftPadding: CGFloat = 0
#IBInspectable var rightPadding: CGFloat = 0
#IBInspectable var textLeftPadding:CGFloat = 0
#IBInspectable var color: UIColor = UIColor.lightGray {
didSet {
updateView()
}
}
#IBInspectable var underlineColor:UIColor = UIColor.black
{
didSet{
self.updateView()
}
}
private var placeholderColorValue:UIColor = UIColor.lightGray
#IBInspectable public var placeholderColor:UIColor
{
set{
self.attributedPlaceholder = NSAttributedString(string:placeholder!, attributes: [NSForegroundColorAttributeName: newValue])
placeholderColorValue = newValue
}
get{
return placeholderColorValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
}
func setUpView() {
if(rightImage != nil)
{
self.leftViewMode = UITextFieldViewMode.always
let rightImageView:UIImageView = UIImageView(image: rightImage)
rightImageView.frame = CGRect(x: 0, y: 0, width: self.frame.size.height, height: self.frame.size.height)
self.rightView = rightImageView
}
else {
rightViewMode = UITextFieldViewMode.never
rightView = nil
}
}
func updateView() {
if let imageLeft = leftImage {
leftViewMode = UITextFieldViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.image = imageLeft
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
leftView = imageView
} else {
leftViewMode = UITextFieldViewMode.never
leftView = nil
}
if let imageRight = rightImage {
rightViewMode = UITextFieldViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.image = imageRight
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
rightView = imageView
} else {
rightViewMode = UITextFieldViewMode.never
rightView = nil
}
// Placeholder text color
attributedPlaceholder = NSAttributedString(string: placeholder != nil ? placeholder! : "", attributes:[NSForegroundColorAttributeName: color])
if(self.isUnderLine)
{
let underline:UIView = UIView()
underline.frame = CGRect(x: 0, y: self.frame.size.height-1, width: self.frame.size.width, height: 1)
underline.backgroundColor = underlineColor
self.addSubview(underline)
}
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(x: textLeftPadding, y: 0, width: bounds.width, height: bounds.height)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
}
and using validation as
if (txtField.text != "condtion" ){
txtField.setError(error:"Error message for text field");
valid = false;
txtField.becomeFirstResponder()
}

Resources