Change color of UISwitch in "off" state - iphone

I've learned that we can change the UISwitch button appearance in its "on" state,
but is it also possible to change the color of the UISwitch in the "off" state?

My solution with #swift2:
let onColor = _your_on_state_color
let offColor = _your_off_state_color
let mSwitch = UISwitch(frame: CGRect.zero)
mSwitch.on = true
/*For on state*/
mSwitch.onTintColor = onColor
/*For off state*/
mSwitch.tintColor = offColor
mSwitch.layer.cornerRadius = mSwitch.frame.height / 2.0
mSwitch.backgroundColor = offColor
mSwitch.clipsToBounds = true
Result:

Try using this
yourSwitch.backgroundColor = [UIColor whiteColor];
youSwitch.layer.cornerRadius = 16.0;
All thanks to #Barry Wyckoff.

You can use the tintColor property on the switch.
switch.tintColor = [UIColor redColor]; // the "off" color
switch.onTintColor = [UIColor greenColor]; // the "on" color
Note this requires iOS 5+

Swift IBDesignable
import UIKit
#IBDesignable
class UISwitchCustom: UISwitch {
#IBInspectable var OffTint: UIColor? {
didSet {
self.tintColor = OffTint
self.layer.cornerRadius = 16
self.backgroundColor = OffTint
}
}
}
set class in Identity inspector
change color from Attributes inspector
Output

Here's a pretty good trick: you can just reach right into the UISwitch's subview that draws its "off" background, and change its background color. This works a lot better in iOS 13 than it does in iOS 12:
if #available(iOS 13.0, *) {
self.sw.subviews.first?.subviews.first?.backgroundColor = .green
} else if #available(iOS 12.0, *) {
self.sw.subviews.first?.subviews.first?.subviews.first?.backgroundColor = .green
}

Working 100% IOS 13.0 and Swift 5.0 switch both state color set same #ios13 #swift #swift5
#IBOutlet weak var switchProfile: UISwitch!{
didSet{
switchProfile.onTintColor = .red
switchProfile.tintColor = .red
switchProfile.subviews[0].subviews[0].backgroundColor = .red
}
}

The Best way to manage background color & size of UISwitch
For now it's Swift 2.3 code
import Foundation
import UIKit
#IBDesignable
class UICustomSwitch : UISwitch {
#IBInspectable var OnColor : UIColor! = UIColor.blueColor()
#IBInspectable var OffColor : UIColor! = UIColor.grayColor()
#IBInspectable var Scale : CGFloat! = 1.0
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCustomUserInterface()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setUpCustomUserInterface()
}
func setUpCustomUserInterface() {
//clip the background color
self.layer.cornerRadius = 16
self.layer.masksToBounds = true
//Scale down to make it smaller in look
self.transform = CGAffineTransformMakeScale(self.Scale, self.Scale);
//add target to get user interation to update user-interface accordingly
self.addTarget(self, action: #selector(UICustomSwitch.updateUI), forControlEvents: UIControlEvents.ValueChanged)
//set onTintColor : is necessary to make it colored
self.onTintColor = self.OnColor
//setup to initial state
self.updateUI()
}
//to track programatic update
override func setOn(on: Bool, animated: Bool) {
super.setOn(on, animated: true)
updateUI()
}
//Update user-interface according to on/off state
func updateUI() {
if self.on == true {
self.backgroundColor = self.OnColor
}
else {
self.backgroundColor = self.OffColor
}
}
}

Swift 5:
import UIKit
extension UISwitch {
func set(offTint color: UIColor ) {
let minSide = min(bounds.size.height, bounds.size.width)
layer.cornerRadius = minSide / 2
backgroundColor = color
tintColor = color
}
}

Should you need other switches around your app, it might be also a good idea implementing #LongPham's code inside a custom class.
As others have pointed out, for the "off" state you'll need to change the background colour as well, since the default is transparent.
class MySwitch: UISwitch {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Setting "on" state colour
self.onTintColor = UIColor.green
// Setting "off" state colour
self.tintColor = UIColor.red
self.layer.cornerRadius = self.frame.height / 2
self.backgroundColor = UIColor.red
}
}

Swift 4 easiest and fastest way to get it in 3 steps:
// background color is the color of the background of the switch
switchControl.backgroundColor = UIColor.white.withAlphaComponent(0.9)
// tint color is the color of the border when the switch is off, use
// clear if you want it the same as the background, or different otherwise
switchControl.tintColor = UIColor.clear
// and make sure that the background color will stay in border of the switch
switchControl.layer.cornerRadius = switchControl.bounds.height / 2
If you manually change the size of the switch (e.g., by using autolayout), you will have to update the switch.layer.cornerRadius too, e.g., by overriding layoutSubviews and after calling super updating the corner radius:
override func layoutSubviews() {
super.layoutSubviews()
switchControl.layer.cornerRadius = switchControl.bounds.height / 2
}

In Swift 4+:
off state:
switch.tintColor = UIColor.blue
on state:
switch.onTintColor = UIColor.red

The UISwitch offTintColor is transparent, so whatever is behind the switch shows through. Therefore, instead of masking the background color, it suffices to draw a switch-shaped image behind the switch (this implementation assumes that the switch is positioned by autolayout):
func putColor(_ color: UIColor, behindSwitch sw: UISwitch) {
guard sw.superview != nil else {return}
let onswitch = UISwitch()
onswitch.isOn = true
let r = UIGraphicsImageRenderer(bounds:sw.bounds)
let im = r.image { ctx in
onswitch.layer.render(in: ctx.cgContext)
}.withRenderingMode(.alwaysTemplate)
let iv = UIImageView(image:im)
iv.tintColor = color
sw.superview!.insertSubview(iv, belowSubview: sw)
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: sw.topAnchor),
iv.bottomAnchor.constraint(equalTo: sw.bottomAnchor),
iv.leadingAnchor.constraint(equalTo: sw.leadingAnchor),
iv.trailingAnchor.constraint(equalTo: sw.trailingAnchor),
])
}
[But see now my other answer.]

2020 As of Xcode 11.3.1 & Swift 5
Here's the simplest way I've found of doing setting the UISwitch off-state colour with one line of code. Writing this here since this page is what came up first when I was looking and the other answers didn't help.
This is if I wanted to set the off state to be red, and can be added to the viewDidLoad() function:
yourSwitchName.subviews[0].subviews[0].backgroundColor = UIColor.red
Note - what this is actually doing is setting the background colour of the switch. This may influence the colour of the switch in the on-state too (though for me this wasn't a problem since I wanted the on and off state to be the same colour).
A solution for this:
Simply tie in the colours with an 'if else' statement inside your IBAction. If the switch is off, colour the background red. If the switch is on, leave the background clear so your chosen 'on' colour will display properly.
This goes inside the switch IBAction.
if yourSwitch.isOn == false {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
} else {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.clear
}
I found some behaviour where, upon the app resuming from background, the switch background would return to clear. To remedy this problem I simply added in the following code to set the colour every time the app comes to the foreground:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground(_:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
#objc func applicationWillEnterForeground(_ notification: NSNotification) {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
}
Seems simpler than the other answers. Hope that helps!

More safe way in Swift 3 without magical 16pt values:
class ColoredBackgroundSwitch: UISwitch {
var offTintColor: UIColor {
get {
return backgroundColor ?? UIColor.clear
}
set {
backgroundColor = newValue
}
}
override func layoutSubviews() {
super.layoutSubviews()
let minSide = min(frame.size.height, frame.size.width)
layer.cornerRadius = ceil(minSide / 2)
}
}

objective c category to use on any UISwitch in project using code or storyboard:
#import <UIKit/UIKit.h>
#interface UISwitch (SAHelper)
#property (nonatomic) IBInspectable UIColor *offTint;
#end
implementation
#import "UISwitch+SAHelper.h"
#implementation UISwitch (SAHelper)
#dynamic offTint;
- (void)setOffTint:(UIColor *)offTint {
self.tintColor = offTint; //comment this line to hide border in off state
self.layer.cornerRadius = 16;
self.backgroundColor = offTint;
}
#end

XCode 11, Swift 5
I don't prefer using subViews, cause you never know when apple gonna change the hierarchy.
so I use mask view instead.
it works with iOS 12, iOS 13
private lazy var settingSwitch: UISwitch = {
let swt: UISwitch = UISwitch()
// set border color when isOn is false
swt.tintColor = .cloudyBlueTwo
// set border color when isOn is true
swt.onTintColor = .greenishTeal
// set background color when isOn is false
swt.backgroundColor = .cloudyBlueTwo
// create a mask view to clip background over the size you expected.
let maskView = UIView(frame: swt.frame)
maskView.backgroundColor = .red
maskView.layer.cornerRadius = swt.frame.height / 2
maskView.clipsToBounds = true
swt.mask = maskView
// set the scale to your expectation, here is around height: 34, width: 21.
let scale: CGFloat = 2 / 3
swt.transform = CGAffineTransform(scaleX: scale, y: scale)
swt.addTarget(self, action: #selector(switchOnChange(_:)), for: .valueChanged)
return swt
}()
#objc
func switchOnChange(_ sender: UISwitch) {
if sender.isOn {
// set background color when isOn is true
sender.backgroundColor = .greenishTeal
} else {
// set background color when isOn is false
sender.backgroundColor = .cloudyBlueTwo
}
}

I tested on IOS 14, set background as off color and onTintColor as On and works:
uiSwitch.onTintColor = UIColor.blue
uiSwitch.backgroundColor = UIColor.red

XCode 11, Swift 4.2
Starting with Matt's solution I added it to a custom, IBDesignable control. There is a timing issue in that didMoveToSuperview() is called before the offTintColor is set that needed to be handled.
#IBDesignable public class UISwitchCustom: UISwitch {
var switchMask: UIImageView?
private var observers = [NSKeyValueObservation]()
#IBInspectable dynamic var offTintColor : UIColor! = UIColor.gray {
didSet {
switchMask?.tintColor = offTintColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeObservers()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeObservers()
}
private func initializeObservers() {
observers.append(observe(\.isHidden, options: [.initial]) {(model, change) in
self.switchMask?.isHidden = self.isHidden
})
}
override public func didMoveToSuperview() {
addOffColorMask(offTintColor)
super.didMoveToSuperview()
}
private func addOffColorMask(_ color: UIColor) {
guard self.superview != nil else {return}
let onswitch = UISwitch()
onswitch.isOn = true
let r = UIGraphicsImageRenderer(bounds:self.bounds)
let im = r.image { ctx in
onswitch.layer.render(in: ctx.cgContext)
}.withRenderingMode(.alwaysTemplate)
let iv = UIImageView(image:im)
iv.tintColor = color
self.superview!.insertSubview(iv, belowSubview: self)
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: self.topAnchor),
iv.bottomAnchor.constraint(equalTo: self.bottomAnchor),
iv.leadingAnchor.constraint(equalTo: self.leadingAnchor),
iv.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
switchMask = iv
switchMask?.isHidden = self.isHidden
}
}

all I finally used transform and layer.cornerRadius too.
But I have added translation to it to be center.
private func setSwitchSize() {
let iosSwitchSize = switchBlockAction.bounds.size
let requiredSwitchSize = ...
let transform = CGAffineTransform(a: requiredSwitchSize.width / iosSwitchSize.width, b: 0,
c: 0, d: requiredSwitchSize.height / iosSwitchSize.height,
tx: (requiredSwitchSize.width - iosSwitchSize.width) / 2.0,
ty: (requiredSwitchSize.height - iosSwitchSize.height) / 2.0)
switchBlockAction.layer.cornerRadius = iosSwitchSize.height / 2.0
switchBlockAction.transform = transform
}
And I did use backgroundColor and tintColor in designer.
Hope it helps.

Related

StoryboardUI how to make the label follow the slider

I'm doing a storyboardUI app. One part of the UI design is kind of like this:
I want the label position follows the slider position all the time, like below:
How can I do it?
Assign this class to UISlider.
In this class created one label and change the position according to the slider thumb.
class ThumbTextSlider: UISlider {
private var thumbTextLabel: UILabel = UILabel()
private var thumbFrame: CGRect {
return thumbRect(forBounds: bounds, trackRect: trackRect(forBounds: bounds), value: value)
}
private lazy var thumbView: UIView = {
let thumb = UIView()
return thumb
}()
override func layoutSubviews() {
super.layoutSubviews()
thumbTextLabel.frame = CGRect(x: thumbFrame.origin.x, y: thumbFrame.maxY - 5, width: thumbFrame.size.width, height: 30)
self.setValue()
}
private func setValue() {
thumbTextLabel.text = String(format: "%0.2f", self.value)
}
override func awakeFromNib() {
super.awakeFromNib()
addSubview(thumbTextLabel)
thumbTextLabel.textAlignment = .center
thumbTextLabel.textColor = .blue
thumbTextLabel.layer.zPosition = layer.zPosition + 1
thumbTextLabel.adjustsFontSizeToFitWidth = true
}
}

Swift 3 - Custom button (image and text) only draws correctly if there's no outlet?

EDIT: Although I accepted an answer that helped me get the buttons drawn correctly at runtime, I still have other issues. The root of which, I suspect, is the issue of why giving my custom button an outlet interferes with how it is drawn. I still need to know why this is. (see my answer below)
I have my own button class that extends UIButton (see below) and has several IBInspectable properties like border width/color, corner radius, and even start/end colors for a gradient background. I also use such properties for setting the image and titles' insets programmatically so I can account for various screen sizes.
Previously I had an issue where if I changed the "View as" device in the storyboard, let's say from iPhone SE to iPhone 7, and then refreshed the views and ran on a physical iPhone SE it would deploy with the insets of an iPhone 7 instead of calculating the insets with the physical device's own screen size. However, I nearly resolved this problem by overriding draw in my custom button class.
My problem now is that the overridden draw method is only called (or seems to be effective only when) the button has no outlet to the ViewController class it's placed in.
For example:
*The img is a placeholder; the insets suit the real imgs appropriately.
That bottom-right button has the gradient and corners drawn properly, where the other 3 do not. I have confirmed that adding an outlet to this button makes it behave like the rest, and removing an outlet from any of the other 3 causes it to be drawn correctly.
For reference, the outlets are just
#IBOutlet weak var button1: UIButton!
#IBOutlet weak var button2: UIButton!
#IBOutlet weak var button3: UIButton!
immediately under the class MyViewController: UIViewController { declaration.
Also, I didn't forget to set the Custom Class in the interface builder for each button.
Here is the button class:
import UIKit
#IBDesignable
class ActionButton: UIButton {
override init(frame: CGRect){
super.init(frame: frame)
setupView()
}
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)!
setupView()
}
#IBInspectable var cornerRadius: CGFloat = 0{
didSet{
setupView()
}
}
#IBInspectable var startColor: UIColor = UIColor.white{
didSet{
setupView()
}
}
#IBInspectable var endColor: UIColor = UIColor.black{
didSet{
setupView()
}
}
#IBInspectable var borderWidth: CGFloat = 0{
didSet{
self.layer.borderWidth = borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear{
didSet{
self.layer.borderColor = borderColor.cgColor
}
}
private func setupView(){
let colors:Array = [startColor.cgColor, endColor.cgColor]
gradientLayer.colors = colors
gradientLayer.cornerRadius = cornerRadius
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.setNeedsDisplay()
}
var gradientLayer: CAGradientLayer{
return layer as! CAGradientLayer
}
override func draw(_ rect: CGRect) {
//Draw image
setupView()
let btnW = self.frame.size.width
let btnH = self.frame.size.height
//Compute and set image insets
let topBtnInset = btnH * (-5/81)
let bottomBtnInset = -4 * topBtnInset
let leftBtnInset = btnW / 4 //btnW * (35/141)
let rightBtnInset = leftBtnInset
self.imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)
//Compute and set title insets
let topTitleInset = btnH * (47/81)
let bottomTitleInset = CGFloat(0)
let leftTitleInset = CGFloat(-256) //Image width
let rightTitleInset = CGFloat(0)
self.titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)
//Draw text
//Default font size
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 15)
if(btnH > 81){
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
}else if(btnH > 97){
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
}
}
override class var layerClass: AnyClass{
return CAGradientLayer.self
}
}
Please ignore the wonky way I handled insets, font sizes, etc..
Does anyone know why specifying an outlet for the button makes it not draw the layer correctly?
A couple of thoughts:
The draw(_:) method is for stroking paths, drawing images, etc. It is for rendering the view at a given moment of time. You should not be updating layers, UIKit subviews, etc.
In your snippet, your draw(_:) is calling setupView, which then calls setNeedsDisplay (which theoretically could trigger draw(_:) to be called again). I'd suggest reserving draw(_:) for only those things that need to be manually drawn. (As it is now, you probably could completely eliminate this method as there's nothing here that is drawing anything.)
For any manual configuration of subviews (their frames, etc.), that should be moved to layoutSubviews, which is called whenever the OS determines that the subviews might need to have their frame values adjusted.
If you have any troubles with designable views, it's worth confirming that you have these in a separate target. It minimizes what needs to be recompiled when rendering in IB. It also prevents issues in the main target affecting the ability to render these designables in IB. (When Apple introduced designable views, they were very specific that one should put them in a separate target.)
I've also had intermittent problems with designable views in Xcode, but these problems are often resolved by quitting Xcode, emptying the derived data folder, and restarting.
I must confess that I do not know why adding an outlet to a designable view would affect how the view is rendered. I know that when I did the above, though, I was unable to reproduce your problem.
Anyway, I tweaked your ActionButton like so, and it appears to work for me:
#import UIKit
#IBDesignable
public class ActionButton: UIButton {
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupView()
}
public required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setupView()
}
#IBInspectable public var cornerRadius: CGFloat = 0 { didSet { setupView() } }
#IBInspectable public var startColor: UIColor = .white { didSet { setupView() } }
#IBInspectable public var endColor: UIColor = .black { didSet { setupView() } }
#IBInspectable public var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } }
#IBInspectable public var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
private func setupView() {
let colors = [startColor.cgColor, endColor.cgColor]
gradientLayer.colors = colors
gradientLayer.cornerRadius = cornerRadius
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
private var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
public override func layoutSubviews() {
super.layoutSubviews()
let btnW = frame.width
let btnH = frame.height
//Compute and set image insets
let topBtnInset = btnH * (-5/81)
let bottomBtnInset = -4 * topBtnInset
let leftBtnInset = btnW / 4 //btnW * (35/141)
let rightBtnInset = leftBtnInset
imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)
//Compute and set title insets
let topTitleInset = btnH * (47/81)
let bottomTitleInset = CGFloat(0)
let leftTitleInset = CGFloat(-256) //THIS MUST BE EQUAL TO -IMAGE WIDTH
let rightTitleInset = CGFloat(0)
titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)
//Adjust text font
if btnH > 81 {
titleLabel?.font = .boldSystemFont(ofSize: 17)
} else if btnH > 97 {
titleLabel?.font = .boldSystemFont(ofSize: 20)
} else {
//Default font size
titleLabel?.font = .boldSystemFont(ofSize: 15)
}
}
public override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
I've accepted Rob's answer because it is far more likely to be of use to anyone else, but I've found what specifically was my issue, on the off-chance someone has a similar problem.
Apparently in my code for the View Controller on which those buttons were placed I had a function I had forgot about that enables/disables those custom buttons, as well as set their background color (to a solid color). This is why those buttons looked wrong. The 4th button was never subject to mutation by this function. It was not the fact the other 3 had an outlet, but that when I removed the outlet I commented-out the call to that function.
Nevertheless, other problems were addressed in this question!
-I had a really sloppy implementation of a UIButton that overrode and called UI functions improperly, that caused problems like freezing on rotation. These were also addressed in Rob's (the accepted) answer.
-"Refresh All Views" in the Interface Builder was hanging on "Signing product", which was caused by calling self.titleLabel?.font. According to some answers on the Apple developer forums, doing this in layoutSubViews() is bad!

Creating a pie chart in swift

I'm trying to create a pie chart in swift, and would like to create the code from scratch rather than use a 3rd party extension.
I like the idea of it being #IBDesignable, so I started with this:
import Foundation
import UIKit
#IBDesignable class PieChart: UIView {
var data: Dictionary<String,Int>?
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override fun drawRect(rect: CGRect) {
// draw the chart in here
}
}
What I'm not sure about, is how best to get the data into the chart. Should I have something like this:
#IBOutlet weak var pieChart: PieChart!
override func viewDidLoad() {
pieChart.data = pieData
pieChart.setNeedsDisplay()
}
Or is there a better way? Presumably, there is no way to include the data in the init function?
Thanks in advance!
You could create a convenience init that includes the data, but that would only be useful if you are creating the view from code. If your view is added in the Storyboard, you will want a way to set the data after the view has been created.
It is good to look at the standard UI elements (like UIButton) for design clues. You can change properties on a UIButton and it updates without you having to call myButton.setNeedsDisplay(), so you should design your pie chart to work in the same manner.
It is fine to have a property of your view that holds the data. The view should take responsibility for redrawing itself, so define didSet for your data property and call setNeedsDisplay() there.
var data: Dictionary<String,Int>? {
didSet {
// Data changed. Redraw the view.
self.setNeedsDisplay()
}
}
Then you can simply set the data, and the pie chart will redraw:
pieChart.data = pieData
You can extend this to other properties on your pie chart. For instance, you might want to change the background color. You'd define didSet for that property as well and call setNeedsDisplay.
Note that setNeedsDisplay just sets a flag and the view will be drawn later. Multiple calls to setNeedsDisplay won't cause your view to redraw multiple times, so you can do something like:
pieChart.data = pieData
pieChart.backgroundColor = .redColor()
pieChart.draw3D = true // draw the pie chart in 3D
and the pieChart would redraw just once.
No, you cannot set the data in the init method if you have added this to a scene in a storyboard (because init(coder:) will be called).
So, yes, you could just populate the data for the pie chart in viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
pieChart.dataPoints = ...
}
But, because this PieChart is IBDesignable, that means that you probably wanted to see a rendition of the pie chart in IB. So you can implement prepareForInterfaceBuilder in the PieChart class, supplying some sample data:
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
dataPoints = ...
}
That way you can now enjoy the designable view (e.g., see a preview; other inspectable properties can be manifested) in Interface Builder. The preview is our sample data, not the data that will be shown at runtime, but it may be enough to appreciate the overall design:
And, as vacawama said, you'd want to move the setNeedsDisplay into the didSet observer for the property.
public class PieChart: UIView {
public var dataPoints: [DataPoint]? { // use whatever type that makes sense for your app, though I'd suggest an array (which is ordered) rather than a dictionary (which isn't)
didSet { setNeedsDisplay() }
}
#IBInspectable public var lineWidth: CGFloat = 2 {
didSet { setNeedsDisplay() }
}
...
}
Just in case anybody looks at this question again, I wanted to post my finished code so that it can be useful to others. Here it is:
import Foundation
import UIKit
#IBDesignable class PieChart: UIView {
var dataPoints: Dictionary<String,Double> = ["Alpha":1,"Beta":2,"Charlie":3,"Delta":4,"Echo":2.5,"Foxtrot":1.4] {
didSet { setNeedsDisplay() }
}
#IBInspectable var lineWidth: CGFloat = 1.0 {
didSet { setNeedsDisplay()
}
}
#IBInspectable var lineColor: UIColor = uicolor_normal {
didSet { setNeedsDisplay() }
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)!
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
// set font for labels
let fieldColor: UIColor = UIColor.darkGrayColor()
let fieldFont = uifont_piechartkey
var fieldAttributes: NSDictionary = [
NSForegroundColorAttributeName: fieldColor,
NSFontAttributeName: fieldFont!
]
// get the graphics context and prepare an inset box for the pie
let ctx = UIGraphicsGetCurrentContext()
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let keyHeight = CGFloat( ceil( Double(dataPoints.count) / 3.0) * 24 ) + 16
let side : CGFloat = min(box0.width, box0.height-keyHeight)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side-keyHeight)/2,side,side)
let radius : CGFloat = min(box.width, box.height)/2.0
// converts percentages to radians for drawing the segment
func percent_to_rad(p: Double) -> CGFloat {
let rad = CGFloat(p * 0.02 * M_PI)
return rad
}
// draws a segment
func draw_arc(start: CGFloat, end: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,box.midX,box.midY,radius-lineWidth/2,start,end,0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
// draws a key item
func draw_key(keyName: String, keyValue: Double, color: CGColor, keyX: CGFloat, keyY: CGFloat) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, keyX, keyY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx,keyX,keyY,8,0,CGFloat(2 * M_PI),0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
keyName.drawInRect(CGRectMake(keyX + 12,keyY-8,self.bounds.width/3,16),withAttributes: fieldAttributes as? [String : AnyObject])
}
let total = Double(dataPoints.values.reduce(0, combine: +)) // the total of all values
// convert dictionary to sorted touples
let dataPointsArray = dictionary_to_sorted_array(dataPoints)
// now sort the dictionary into an Array
var start = -CGFloat(M_PI_2) // start at 0 degrees, not 90
var end: CGFloat
var i = 0
// draw all segments
for dataPoint in dataPointsArray {
end = percent_to_rad(Double( (dataPoint.value)/total) * 100 )+start
draw_arc(start,end:end,color: uicolors_chart[i%uicolors_chart.count].CGColor)
start = end
i++
}
// the key
var keyX = self.bounds.minX + 8
var keyY = self.bounds.height - keyHeight + 32
i = 0
for dataPoint in dataPointsArray {
draw_key(dataPoint.key, keyValue: dataPoint.value, color: uicolors_chart[i%uicolors_chart.count].CGColor, keyX: keyX, keyY: keyY)
if((i+1)%3 == 0) {
keyX = self.bounds.minX + 8
keyY += 24
} else {
keyX += self.bounds.width / 3
}
i++
}
}
}
This will create a pie chart, that looks something like this:
[
The other bits of code you'll need are the colours array:
let uicolor_chart_1 = UIColor.init(red: 0.0/255, green:153.0/255, blue:255.0/255, alpha:1.0) //16b
let uicolor_chart_2 = UIColor.init(red: 0.0/255, green:200.0/255, blue:120.0/255, alpha:1.0)
let uicolor_chart_3 = UIColor.init(red: 140.0/255, green:220.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_4 = UIColor.init(red: 255.0/255, green:240.0/255, blue:0.0/255, alpha:1.0)
let uicolor_chart_5 = UIColor.init(red: 255.0/255, green:180.0/255, blue:60.0/255, alpha:1.0)
let uicolor_chart_6 = UIColor.init(red: 235.0/255, green:60.0/255, blue:150.0/255, alpha:1.0)
let uicolors_chart : [UIColor] = [uicolor_chart_1,uicolor_chart_2,uicolor_chart_3,uicolor_chart_4,uicolor_chart_5,uicolor_chart_6]
And the code to convert the dictionary to an array:
func dictionary_to_sorted_array(dict:Dictionary<String,Double>) ->Array<(key:String,value:Double)> {
var tuples: Array<(key:String,value:Double)> = Array()
let sortedKeys = (dict as NSDictionary).keysSortedByValueUsingSelector("compare:")
for key in sortedKeys {
tuples.append((key:key as! String,value:dict[key as! String]!))
}
return tuples
}

How to set UISegmentedControl Tint Color for individual segment

Starting to learn Swift and am attempting to convert this ObjectiveC code:
[[mySegmentedControl.subviews objectAtIndex:0] setTintColor:[UIColor blueColor]]
This correctly sets the tint color of the first segment.
This is the closest I've come to getting a Swift version of the same code:
mySegmentedControl?.subviews[0].tintColor = UIColor.blueColor()
The error I get is '#Ivalue $T9' is not identical to 'UIColor!!'
I don't understand what this error means. When I look at the .tintColor method it list UIColor!? and I haven't found what the !? together means in Swift yet.
This will solve your problem:
var subViewOfSegment: UIView = mySegmentedControl.subviews[0] as UIView
subViewOfSegment.tintColor = UIColor.blueColor()
You can also
(mySegmentedControl.subviews[0] as UIView).tintColor = UIColor .blueColor()
The easiest way I have found is:
segmentControl.setTitleTextAttributes([NSForegroundColorAttributeName: UIColor.redColor()], forState: UIControlState.Selected)
This code works fine with latest version of Swift as on August 2019 (Swift 3.0)
Here this code I have implemented is the extension for the Segment control and can be used for all the segment controls in the application, where the set of code has to defined in application class.
Extension method can be used directly in the application, also you can add all the settings to same method or different methods in extension class, as shown below.
extension UISegmentedControl {
func setSegmentStyle() {
setBackgroundImage(imageWithColor(color: backgroundColor!), for: .normal, barMetrics: .default)
setBackgroundImage(imageWithColor(color: tintColor!), for: .selected, barMetrics: .default)
setDividerImage(imageWithColor(color: UIColor.clear), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
let segAttributes: NSDictionary = [
NSForegroundColorAttributeName: UIColor.gray,
NSFontAttributeName: UIFont(name: "System-System", size: 14)!
]
setTitleTextAttributes(segAttributes as [NSObject : AnyObject], for: UIControlState.selected)
}
// create a 1x1 image with this color
private func imageWithColor(color: UIColor) -> UIImage {
let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor);
context!.fill(rect);
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image!
}
}
Everywhere for the segments the below code can be used
self.mySegment.setSegmentStyle()
For changing the tintColor of the selected segment
Use the value changed event for the UISegmentControl to sort the segments in order by their origin x value, then loop and compare the selectedSegmentIndex property. Here is an example with assuming a segmented control of 4 segments:
#IBAction func indexChanged(sender: UISegmentedControl) {
let sortedViews = sender.subviews.sort( { $0.frame.origin.x < $1.frame.origin.x } )
for (index, view) in sortedViews.enumerate() {
if index == sender.selectedSegmentIndex {
view.tintColor = UIColor.blueColor()
} else {
view.tintColor = UIColor.lightGrayColor()
}
}
}
Then in viewDidLoad set the tintColor for the initially selected segment, in this case it is the first:
let sortedViews = segmentedControlOutletVariable.subviews.sort( { $0.frame.origin.x < $1.frame.origin.x } )
sortedViews[0].tintColor = UIColor.blueColor()
For Swift 5.1 i found to work:
//To set Text Colour when Segment Selected
segmentOutlet.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for: UIControl.State.selected)
//To Set Background Colour when Segment Selected,
//The number in the [] is the segment that gets value change
let subViewOfSegment: UIView = segmentOutlet.subviews[1] as UIView
subViewOfSegment.backgroundColor = UIColor.blue
I place these within the Switch Statement used to capture the Action of the button when pressed.
After analyzing and trying lots of answers to this and other similar questions I realized that with a 3rd-party custom segmented control it will be much easier and safer to do customizations than trying to hack Apple's UISegmentedControl.
Here's an example of customization with XMSegmentedControl (Swift 3).
Some in code:
mySegmentControl.delegate = self
mySegmentControl.font = UIFont.systemFont(ofSize: 12)
And some in the Interface Builder (can be done in code too if you want):
The result is:
In my case it looks very much like system one, but still there are minor differences I had to do exactly as designer wanted it to be.
Note that XMSegmentedControl doesn't allow to have different background colors for different segments, but you can easily add this if needed, since it is a simple .swift file, which is very easy to understand and modify.
sender.subviews.sort doesn't work in Swift 4 and remove border is referenced on How to remove border from segmented control
extension UISegmentedControl {
// create a 1x1 image with this color
private func imageWithColor(color: UIColor) -> UIImage {
let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor);
context!.fill(rect);
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image!
}
func removeBackgroundColors() {
self.setBackgroundImage(imageWithColor(color: .clear), for: .normal, barMetrics: .default)
self.setBackgroundImage(imageWithColor(color: .clear), for: .selected, barMetrics: .default)
self.setDividerImage(imageWithColor(color: UIColor.clear), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
}
struct viewPosition {
let originX: CGFloat
let originIndex: Int
}
func updateTintColor(selected: UIColor, normal: UIColor) {
let views = self.subviews
var positions = [viewPosition]()
for (i, view) in views.enumerated() {
let position = viewPosition(originX: view.frame.origin.x, originIndex: i)
positions.append(position)
}
positions.sort(by: { $0.originX < $1.originX })
for (i, position) in positions.enumerated() {
let view = self.subviews[position.originIndex]
if i == self.selectedSegmentIndex {
view.tintColor = selected
} else {
view.tintColor = normal
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
mySegment.removeBackgroundColors()
mySegment.backgroundColor = .clear
mySegment.updateTintColor(selected: myNavigationColor, normal: text1Color)
}
The solution is only for two segments but it could be easily extended to use for as much as you need.
Firstly I will suggest to create an enum:
enum SegmentedSections: Int {
case first,
case second
}
Than create a funciton, and call this function in viewDidLoad, and also each time call it when .valueChanged happens in segmentedControl:
func setProperSegmentedControlColoring(_ segment: UISegmentedControl, type: SegmentedSections) {
setSeparatorImages(for: segment, with: type)
let subviews = segment.subviews
let sortedViews = subviews.sorted(by: { $0.frame.origin.x < $1.frame.origin.x })
for (index, view) in sortedViews.enumerated() {
switch type {
case .first:
if index == segment.selectedSegmentIndex {
view.tintColor = .red
} else {
view.tintColor = .blue
}
case .second:
if index == segment.selectedSegmentIndex {
view.tintColor = .blue
} else {
view.tintColor = .red
}
}
}
}
Also you will need to change divider image accordingly:
func setSeparatorImages(for segment: UISegmentedControl, with type: EarnType) {
switch type {
case .first:
let image = UIImage(color: .red)
segment.setDividerImage(image, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default)
case .second:
let image = UIImage(color: .blue)
segment.setDividerImage(image, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default)
}
}
Also, you will need to have an extension for UIImage. You can find it here.
From iOS 13 onwards, the property selectedSegmentTintColor can be used to set tint color of segment control!
So simply do:
segmentControl.selectedSegmentTintColor = .red
If you are supporting below iOS 13,
if #available(iOS 13.0, *) {
segmentControl.selectedSegmentTintColor = .red
} else {
// Fallback on earlier versions
// Solution posted by David can be used here
}
class MyUISegmentedControl: UISegmentedControl {
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)!
for subViewOfSegment: UIView in subviews {
subViewOfSegment.tintColor = UIColor.red
}
}
}

Any way to determine if adjustsFontSizeToFitWidth has triggered?

Is there anyway to determine if adjustsFontSizeToFitWidth has been triggered? My app has a UILabel that takes up the entire view. By using the UIPinchGestureRecognizer you can pinch in and out to change the font size. Works great. However, when the font reaches the max size for the UILabel, it displays bizarre behaviour. The text in the UILabel moves down the UILabel. Without adding bannerLabel.adjustsFontSizeToFitWidth = true the text gets clipped. I would like to know when the font is at the maximum size for the UILabel, and i will stop trying to increase the font size.
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var bannerLabel: UILabel!
var perviousScale:CGFloat = 0
var fontSize:CGFloat = 0
var originalFontSize: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let pinchGesture = UIPinchGestureRecognizer(target: self, action: Selector("pinch:"))
view.addGestureRecognizer(pinchGesture)
perviousScale = pinchGesture.scale
bannerLabel.adjustsFontSizeToFitWidth = true
originalFontSize = bannerLabel.font.pointSize
fontSize = originalFontSize
}
func pinch(sender:UIPinchGestureRecognizer) {
println("font size \(bannerLabel.font.pointSize)")
if perviousScale >= sender.scale //Zoom In
{
decreaseFontSize()
}
else if perviousScale < sender.scale //Zoom Out
{
increaseFontSize()
}
}
func threeFingers(sender:UIPanGestureRecognizer) {
println("threeFingers")
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
bannerLabel.font = UIFont(name: bannerLabel.font.fontName, size: fontSize)
}
func increaseFontSize(){
bannerLabel.font = UIFont(name: bannerLabel.font.fontName, size: fontSize)
fontSize = fontSize + 2.5
}
func decreaseFontSize(){
bannerLabel.font = UIFont(name: bannerLabel.font.fontName, size: fontSize)
if fontSize >= originalFontSize {
fontSize = fontSize - 2.5
}
}
}
Yup, you can add an observer:
1) Add the dynamic modifier to the property you would like to observe:
dynamic UILabel bannerLabel = UILabel()
2) Create a global context variable
private var myContext = 0
3) Add an observer for the key-path
bannerLabel.addObserver(self, forKeyPath: "adjustsFontSizeToFitWidth", options:.New, context:&myContext)
4) Override observeValueForKeyPath and remove observer in deinit
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject: AnyObject], context: UnsafeMutablePointer<Void>) {
if context == &myContext {
println("Font size adjusted to fit width: \(change[NSKeyValueChangeNewKey])")
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
deinit {
bannerLabel .removeObserver(self, forKeyPath: "adjustsFontSizeToFitWidth", context: &myContext)
}