StoryboardUI how to make the label follow the slider - swift

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
}
}

Related

How do I Create my own Progressbar in swift? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 years ago.
Improve this question
I to create my own custom progressbar ie:NSProgressIndicator using swift how do I do that?
You can just make your own progress indicator, e.g.:
#IBDesignable
open class ColorfulProgressIndicator: NSView {
#IBInspectable open var doubleValue: Double = 50 { didSet { needsLayout = true } }
#IBInspectable open var minValue: Double = 0 { didSet { needsLayout = true } }
#IBInspectable open var maxValue: Double = 100 { didSet { needsLayout = true } }
#IBInspectable open var backgroundColor: NSColor = .lightGray { didSet { layer?.backgroundColor = backgroundColor.cgColor } }
#IBInspectable open var progressColor: NSColor = .blue { didSet { progressShapeLayer.fillColor = progressColor.cgColor } }
#IBInspectable open var borderColor: NSColor = .clear { didSet { layer?.borderColor = borderColor.cgColor } }
#IBInspectable open var borderWidth: CGFloat = 0 { didSet { layer?.borderWidth = borderWidth } }
#IBInspectable open var cornerRadius: CGFloat = 0 { didSet { layer?.cornerRadius = cornerRadius } }
private lazy var progressShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = progressColor.cgColor
return shapeLayer
}()
public override init(frame: NSRect = .zero) {
super.init(frame: frame)
configure()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// needed because IB doesn't don't honor `wantsLayer`
open override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
layer = CALayer()
configure()
}
open override func layout() {
super.layout()
updateProgress()
}
open func animate(to doubleValue: Double? = nil, minValue: Double? = nil, maxValue: Double? = nil, duration: TimeInterval = 0.25) {
let currentPath = progressShapeLayer.presentation()?.path ?? progressShapeLayer.path
// stop prior animation, if any
progressShapeLayer.removeAnimation(forKey: "updatePath")
// update progress properties
if let doubleValue = doubleValue { self.doubleValue = doubleValue }
if let minValue = minValue { self.minValue = minValue }
if let maxValue = maxValue { self.maxValue = maxValue }
// create new animation
let animation = CABasicAnimation(keyPath: "path")
animation.duration = duration
animation.fromValue = currentPath
animation.toValue = progressPath
progressShapeLayer.add(animation, forKey: "updatePath")
}
}
private extension ColorfulProgressIndicator {
func configure() {
wantsLayer = true
layer?.cornerRadius = cornerRadius
layer?.backgroundColor = backgroundColor.cgColor
layer?.borderWidth = borderWidth
layer?.borderColor = borderColor.cgColor
layer?.addSublayer(progressShapeLayer)
}
func updateProgress() {
progressShapeLayer.path = progressPath
}
var progressPath: CGPath? {
guard minValue != maxValue else { return nil }
let percent = max(0, min(1, CGFloat((doubleValue - minValue) / (maxValue - minValue))))
let rect = NSRect(origin: bounds.origin, size: CGSize(width: bounds.width * percent, height: bounds.height))
return CGPath(rect: rect, transform: nil)
}
}
You can either just set its doubleValue, minValue and maxValue, or if you want to animate the change, just:
progressIndicator.animate(to: 75)
For example, below I set the progressColor and borderColor to .red, set the borderWidth to 1, set the cornerRadius to 10. I then started animating to 75, and then, before it’s even done, triggered another animation to 100 (to illustrate that animations can pick up from wherever it left off):
There are tons of ways of implementing this (so get too lost in the details of the implementation, above), but it illustrates that creating our own progress indicators is pretty easy.
You can subclass it just like any other view. But for all you're doing that is likely unnecessary.
class CustomIndicator: NSProgressIndicator {
// ...
}
As far as setting the height goes, you can do this by initializing the view with a custom frame.
let indicator = NSProgressIndicator(frame: NSRect(x: 0, y: 0, width: 100, height: 20))
There is a property called controlTint that you can set on NSProgressIndicator, but this only allows you to set the color to the predefined ones under NSControlTint. For truly custom colors, I'd recommend this route using Quartz filters via the Interface Builder.

How to add UIGestureRecongnizer programmatically on custom views that are created via code?

I want to add a UITapGestureRecognizer to my view named SetView. My setviews are created programmatically on another custom view called GridView.
This is what I have tried so far but I am not seeing any action while tapping my subvies.
import UIKit
#IBDesignable
class GridView: UIView {
private(set) lazy var deckOfCards = createDeck()
lazy var grid = Grid(layout: Grid.Layout.fixedCellSize(CGSize(width: 128.0, height: 110.0)), frame: CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY), size: CGSize(width: bounds.width, height: bounds.height)))
lazy var listOfSetCard = createSetCards()
private func createDeck() -> [SetCard] {
var deck = [SetCard]()
for shape in SetCard.Shape.allShape {
for color in SetCard.Color.allColor {
for content in SetCard.Content.allContent {
for number in SetCard.Number.allNumbers {
deck.append(SetCard(shape: shape, color: color, content: content, rank: number))
}
}
}
}
deck.shuffle()
return deck
}
private func createSetCards() -> [SetView] {
var cards = [SetView]()
for _ in 0..<cardsOnScreen {
let card = SetView()
let contentsToBeDrawn = deckOfCards.removeFirst()
card.combinationOnCard.shape = contentsToBeDrawn.shape
card.combinationOnCard.color = contentsToBeDrawn.color
card.combinationOnCard.content = contentsToBeDrawn.content
card.combinationOnCard.rank = contentsToBeDrawn.rank
/* print(contentsToBeDrawn.color) */
addSubview(card)
cards.append(card)
}
return cards
}
override func layoutSubviews() {
super.layoutSubviews()
for index in listOfSetCard.indices {
let card = listOfSetCard[index]
if let rect = grid[index] {
card.frame = rect.insetBy(dx: 2.5, dy: 2.5)
card.frame.origin = rect.origin
print(card.frame.origin)
}
}
}
Here is the function didTap(sender: UITapGestureRecognizer) that I wrote on SetView:
#objc func didTap(sender: UITapGestureRecognizer) {
switch sender.state {
case .changed,.ended:
let rect = UIBezierPath(rect: bounds)
fillBoundingRect(inRect: rect, color: UIColor.gray)
default:
break
}
And ViewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...12 {
let card = game.drawModelCard()
game.deck.append(card)
}
}
lazy var game = SetGame()
weak var setView : SetView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target:
setView, action: #selector(SetView.didTap(sender:)))
setView.isUserInteractionEnabled = true
setView.addGestureRecognizer(tapGestureRecognizer)
}
}
}
My subviews(SetViews) should change the background color once tapped.

UILabel preferredMaxLayout breaking self-sizing cells

I found some really annoying problem with UILabel not working with AutoLayout.
I found multiple threads about this, but none of solutions worked for me.
class AudiosHeaderCell: CollectionViewCell<AudiosHeaderItemViewModel> {
var label: UILabelPreferedWidth? {
didSet {
self.label?.textAlignment = .center
self.label?.numberOfLines = 0
self.label?.lineBreakMode = .byWordWrapping
self.label?.font = Font.Standard.size14
self.label?.textColor = UIColor(netHex: 0x185B97)
}
}
let labelLeftRightMargin = CGFloat(16)
override func setupViews() {
self.backgroundColor = UIColor.white
self.label = UILabelPreferedWidth()
self.contentView.addSubview(self.label!)
}
override func setupConstraints() {
self.label?.snp.makeConstraints { (make) in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 8, left: labelLeftRightMargin, bottom: 8, right: labelLeftRightMargin))
}
}
override func bindViewModel(viewModel: AudiosHeaderItemViewModel) {
self.label?.text = viewModel.text
}
}
class UILabelPreferedWidth : UILabel {
override var bounds: CGRect {
didSet {
print("SET BOUNDS", bounds)
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints()
}
}
}
override func updateConstraints() {
print("updateConstraints", preferredMaxLayoutWidth, bounds)
if(preferredMaxLayoutWidth != bounds.size.width) {
preferredMaxLayoutWidth = bounds.size.width
}
super.updateConstraints()
}
}
I use a method to calculate the size of the cell like this:
func sizeForCellWithViewModel(_ viewModel: IReusableViewModel, fittingSize: CGSize) -> CGSize {
let cell = self.classRegistry.instances[viewModel.reuseIdentifier]!
(cell as! ICollectionViewCell).setViewModel(viewModel)
cell.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.translatesAutoresizingMaskIntoConstraints = false
cell.frame = CGRect(x: 0, y: 0, width: fittingSize.width, height: fittingSize.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
print("SIZE FOR ", cell, "FITTING ", fittingSize, "IS", cell.systemLayoutSizeFitting(fittingSize))
return cell.systemLayoutSizeFitting(fittingSize)
}
It works for multiple cells that has some images and other content, but it fails on such a simple problem like scaling to content of UILabel.
Problem I have is that systemLayoutSizeFitting.width returns size that is larger than fittingSize.width parameter I pass.
I've been debugging this long time and I found out that preferredMaxLayoutWidth is not updating properly, as bounds for this UILabel are going beyond cell frame - despite the constraints I use there.
Does anyone have a good solution for that ?
The only one I found is to use this on CollectionViewCell:
override var frame: CGRect {
didSet {
self.label?.preferredMaxLayoutWidth = self.frame.size.width - 32
}
}
But I hate it because it forces me to synchronise that with constraints and it will be required to all other use-cases in my application to remember to copy that.
What I'm looking for is AutoLayout, Constraint only solution.
Ok problem solved by adding width constraint to the Cell's contentView:
func sizeForCellWithViewModel(_ viewModel: IReusableViewModel, fittingSize: CGSize) -> CGSize {
let cell = self.classRegistry.instances[viewModel.reuseIdentifier]!
(cell as! ICollectionViewCell).setViewModel(viewModel)
cell.contentView.frame = CGRect(x: 0, y: 0, width: fittingSize.width, height: fittingSize.height)
cell.contentView.snp.removeConstraints()
if fittingSize.width != 0 {
cell.contentView.snp.makeConstraints { (make) in
make.width.lessThanOrEqualTo(fittingSize.width)
}
}
if fittingSize.height != 0 {
cell.contentView.snp.makeConstraints({ (make) in
make.height.lessThanOrEqualTo(fittingSize.height)
})
}
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
return cell.contentView.systemLayoutSizeFitting(fittingSize)
}
Seems that this somehow makes UILabel works and preferredWidth not going crazy.

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
}

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)
}