So I've been at this for a few days and no luck. I have a a custom UIView that's made from a xib file and I am adding it to my footerView. The view is displayed but it is not responding to the gesture recognizer that I added to a label in the ViewController class. In the VC I create an instance of the UIView and added it to my tableView.
What's odd to is that when using the accessibility inspector it does read the labels
Here is the containerView
class AdviceCardWidgetContainerCurvedView: UIView {
struct Constants {
static let nibName = "ClariContainerView"
static let containerHeight: CGFloat = 169.0
static let curvedPercent: CGFloat = 0.2
}
let nibName = "ClariContainerView"
var clariView: UIView?
var clariTitle: String?
var clariQuestion: String?
#IBOutlet weak var clariTitleLabel: UILabel!
#IBOutlet weak var clariQuestionLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func applyCurve(givenView view: UIView, curvedPercent: CGFloat) {
let shapeLayer = CAShapeLayer(layer: view.layer)
shapeLayer.path = self.pathForCurvedView(givenView: view, curvedPercent: curvedPercent).cgPath
shapeLayer.frame = view.bounds
view.layer.mask = shapeLayer
addSubview(view)
}
private func pathForCurvedView(givenView view: UIView, curvedPercent: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addLine(to: CGPoint(x: 0.0, y: view.bounds.size.height))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: view.bounds.size.height))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: 0.0))
path.addQuadCurve(to: CGPoint(x: 0, y: 0), controlPoint: CGPoint(x: UIScreen.main.bounds.width / 2, y: view.bounds.size.height * curvedPercent))
path.close()
return path
}
private func commonInit() {
guard let clariView = loadViewFromNib() else { return }
clariView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: containerHeight)
clariView.backgroundColor = .lightGray
applyCurve(givenView: clariView, curvedPercent: 0.1)
addSubview(clariView)
clariTitleLabel.text = "Ask TD Clari"
clariQuestionLabel.text = "What is my balance"
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: Constants.nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
And this is what's inside my view controller
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let curvedView = AdviceCardWidgetContainerCurvedView()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
curvedView.clariTitleLabel.accessibilityTraits = .button
curvedView.clariTitleLabel.isUserInteractionEnabled = true
curvedView.clariTitleLabel.backgroundColor = .red
curvedView.clariTitleLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(footerLableTapped)))
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 169))
tableView.tableFooterView?.addSubview(curvedView)
}
#objc func footerLableTapped() {
print("label tapped")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = "1"
return cell
}
}
Alright, so I solved this problem after a week of trying. In the end adding a custom xib file to a footerView causes some errors and conflicts with autoLayout. Everything looks like its supposed to be, but the frames are essentially non existent for your added subviews like buttons and label and that's why they can't be tapped. Make sure you have a reference to your tableView from storyboard or how ever you created it.
override func viewDidLoad() {
super.viewDidLoad()
let remainingBottomFooterSpace = remaingBottomFooterViewSpace(for: tableView)
let footerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: 169))
footerView.backgroundColor = .clear
applyCurve(givenView: footerView, curvedPercent: 0.04)
let remainingFooterView = UIView(frame: CGRect(x: 0, y: 169, width: tableView.frame.size.width, height: remainingBottomFooterSpace))
remainingFooterView.backgroundColor = .lightGray
let curve = UIView()
curve.translatesAutoresizingMaskIntoConstraints = false
curve.backgroundColor = .lightGray
let icon = UIImageView()
icon.translatesAutoresizingMaskIntoConstraints = false
icon.image = UIImage(named: "mytd_clari")
icon.contentMode = .scaleAspectFill
curved.addSubview(icon)
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.isUserInteractionEnabled = true
button.setTitle("Ask", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16.0, weight: .medium)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(footerLableTapped), for: .touchUpInside)
let questionLabel = UILabel()
questionLabel.translatesAutoresizingMaskIntoConstraints = false
questionLabel.text = "What is my balance"
questionLabel.textColor = .green
questionLabel.font = .systemFont(ofSize: 13, weight: .regular)
questionLabel.textAlignment = .center
curve.addSubview(clariQuestionLabel)
let buttonContainer = UIView(frame: CGRect(x: 0, y: 0, width: self.tableView.frame.size.width, height: 169))
curve.addSubview(buttonContainer)
buttonContainer.addSubview(button)
tableView.tableFooterView = footerView
tableView.tableFooterView?.addSubview(remainingFooterView)
tableView.tableFooterView?.addSubview(curve)
NSLayoutConstraint.activate([
footerView.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
footerView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor)
])
NSLayoutConstraint.activate([
curve.widthAnchor.constraint(equalToConstant: tableView.frame.size.width),
curve.heightAnchor.constraint(equalToConstant: 169)
])
NSLayoutConstraint.activate([
icon.topAnchor.constraint(equalTo: curve.topAnchor, constant: 39),
icon.widthAnchor.constraint(equalToConstant: 40),
icon.heightAnchor.constraint(equalToConstant: 40),
icon.centerXAnchor.constraint(equalTo: curve.centerXAnchor),
])
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: icon.bottomAnchor),
button.centerXAnchor.constraint(equalTo: icon.centerXAnchor)
])
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: button.bottomAnchor),
label.centerXAnchor.constraint(equalTo: icon.centerXAnchor),
])
}
If you want to add the curve here are the methods:
func applyCurve(givenView view: UIView, curvedPercent: CGFloat) {
let shapeLayer = CAShapeLayer(layer: view.layer)
shapeLayer.path = self.pathForCurvedView(curvedPercent: curvedPercent).cgPath
shapeLayer.frame = view.bounds
view.layer.mask = shapeLayer
}
func pathForCurvedView(curvedPercent: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addLine(to: CGPoint(x: 0.0, y: UIScreen.main.bounds.height))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: UIScreen.main.bounds.height))
path.addLine(to: CGPoint(x: UIScreen.main.bounds.width, y: 0.0))
path.addQuadCurve(to: CGPoint(x: 0, y: 0), controlPoint: CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * curvedPercent))
path.close()
return path
}
and if you want to add the remaining footerView space, here is the func
func remaingBottomFooterViewSpace(for tableView: UITableView) -> CGFloat {
//Get content height & calculate new footer height
let cells = tableView.visibleCells
var height: CGFloat = 0
for i in 0..<cells.count {
height += cells[i].frame.height
}
height = tableView.bounds.height - ceil(height)
//If theh footer new height is negative, we make it 0 since we don't need extra footer view anymore
height = height > 0 ? height : 0
return height
}
Related
I'm trying to change UIButton but it doesn't work. My code is like this.
#IBAction func addButtonTapped(_ sender: Any) {
print(defaultAddButton.center)
let buttonPos = CGPoint(x: defaultAddButton.frame.midX, y: defaultAddButton.frame.midY)
let width = self.view.bounds.width
let height: CGFloat = 100
let uiView = SubCollecionView(frame: CGRect(x: buttonPos.x - width / 2, y: buttonPos.y - height / 2, width: width, height: height))
self.view.addSubview(uiView)
self.defaultAddButton.center.y += 200
}
SubCollectionView is my custom view. Above code works without self.view.addSubview(uiView) line. I can't understand why it doesn't work. Thank you in advance!
do not fully understand your problem, but can suggest that you have a problem with animation, with this line of code:
self.defaultAddButton.center.y += 200
The above code doesn't work because you need to update the view to update its layout immediately, use this method to update view "layoutIfNeeded()"
Example of code:
class VC: UIViewController {
lazy var buttonAction: UIButton = {
let view = UIButton()
view.translatesAutoresizingMaskIntoConstraints = false
view.setTitle("Action", for: .normal)
view.setTitleColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1), for: .normal)
view.backgroundColor = .blue
view.addTarget(self, action: #selector(action(_:)), for: .touchUpInside)
return view
}()
#objc func action(_ sender: UIButton) {
print("action")
print(buttonAction.center)
let buttonPos = CGPoint(x: buttonAction.frame.midX, y: buttonAction.frame.midY)
let width = self.view.bounds.width
let height: CGFloat = 100
let uiView = MediaPlayer(frame: CGRect(x: buttonPos.x - width / 2, y: buttonPos.y - height / 2, width: width, height: height))
self.view.addSubview(uiView)
self.buttonAction.center.y += 200
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func setupButton() {
self.view.addSubview(buttonAction)
buttonAction.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
buttonAction.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -70).isActive = true
buttonAction.heightAnchor.constraint(equalToConstant: 50).isActive = true
buttonAction.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .gray
setupButton()
}
}
You can try setting changes/frame in view over
viewDidLayoutSubviews
You should call layoutIfNeeded() after this.
#IBAction func addButtonTapped(_ sender: Any) {
print(defaultAddButton.center)
let buttonPos = CGPoint(x: defaultAddButton.frame.midX, y: defaultAddButton.frame.midY)
let width = self.view.bounds.width
let height: CGFloat = 100
let uiView = SubCollecionView(frame: CGRect(x: buttonPos.x - width / 2, y: buttonPos.y - height / 2, width: width, height: height))
self.view.addSubview(uiView)
self.defaultAddButton.center.y += 200
self.view.layoutIfNeeded()
}
If this doesn't work. Please try to call UI modification code inside the main queue.
#IBAction func addButtonTapped(_ sender: Any) {
print(defaultAddButton.center)
DispatchQueue.main.async {
let buttonPos = CGPoint(x: defaultAddButton.frame.midX, y: defaultAddButton.frame.midY)
let width = self.view.bounds.width
let height: CGFloat = 100
let uiView = SubCollecionView(frame: CGRect(x: buttonPos.x - width / 2, y: buttonPos.y - height / 2, width: width, height: height))
self.view.addSubview(uiView)
self.defaultAddButton.center.y += 200
self.view.layoutIfNeeded()
}
}
I am trying to make a UIButton with rounded corners that has 2 colored shadows. Why is the red (and at this point also the blue "shadow" layer covering the button? How to get the shadows below the button canvas). I thought it was helping to insert sublayers instead of just adding them.
I have made a playground illustrating the issue
import UIKit
import PlaygroundSupport
This is the button I'm trying to implement
class PrimaryButton: UIButton {
required init(text: String = "Test 1", hasShadow: Bool = true) {
super.init(frame: .zero)
setTitle(text, for: .normal)
backgroundColor = UIColor.blue
layer.cornerRadius = 48 / 2
layer.masksToBounds = false
if hasShadow {
insertShadow()
}
}
fileprivate func insertShadow() {
let layer2 = CALayer(layer: layer), layer3 = CALayer(layer: layer)
layer2.applySketchShadow(color: UIColor.red, alpha: 0.5, x: 0, y: 15, blur: 35, spread: -10)
layer3.applySketchShadow(color: UIColor.blue, alpha: 0.5, x: 0, y: 10, blur: 21, spread: -9)
layer.insertSublayer(layer2, at: 0)
layer.insertSublayer(layer3, at: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { (sublayer) in
sublayer.shadowPath = UIBezierPath(rect: bounds).cgPath
}
}
}
This is an extension that helps adding the shadow from Sketch specification:
extension CALayer {
func applySketchShadow(
color: UIColor = .black,
alpha: Float = 0.5,
x: CGFloat = 0,
y: CGFloat = 2,
blur: CGFloat = 4,
spread: CGFloat = 0)
{
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2.0
if spread == 0 {
shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
masksToBounds = false
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let button = PrimaryButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 48)
view.addSubview(button)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
It seems legit to me. layer1 & layer2 are sublayers of the button layer.
You could add a third layer that will serve as a background. Here is an example based on your code:
class PrimaryButton: UIButton {
let layer1 = CALayer(), layer2 = CALayer(), layer3 = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
layer1.backgroundColor = UIColor.blue.cgColor
layer1.cornerRadius = 48 / 2
[layer1, layer2, layer3].forEach {
$0.masksToBounds = false
$0.frame = layer.bounds
layer.insertSublayer($0, at: 0)
}
layer2.applySketchShadow(color: UIColor.red, alpha: 0.5, x: 0, y: 15, blur: 35, spread: -10)
layer3.applySketchShadow(color: UIColor.blue, alpha: 0.5, x: 0, y: 10, blur: 21, spread: -9)
}
}
Note that I put most of the code inside layoutSubviews because most of your methods use the actual bounds of the button.
Change your insertions to:
layer.insertSublayer(layer2, at: 1)
layer.insertSublayer(layer3, at: 2)
That should do it.
Another way is to add double buttons without change your class.
let button = PrimaryButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 48)
button.backgroundColor = UIColor.clear
view.addSubview(button)
self.view = view
let button1 = PrimaryButton()
button1.frame = CGRect(x: 0, y: 0, width: 200, height: 48)
button.addSubview(button1)
button1.layer.sublayers?.forEach{$0.removeFromSuperlayer()}
How to draw about three circle in horizontally area with main and ring color in rectangle. I need to create custom button with this circles, something like this:
Is there any good way to do this?
We can design such kind of views with UIStackView in very ease manner.
Take a stackView, set its alignment to center, axis to horizontal and distribution to fill. Create a UILabel/UIButton/UIImageView or even UIView and add rounded radius and border to it. Finally, add those views to the main stackView.
Try this.
override func viewDidLoad() {
super.viewDidLoad()
//Setup stackView
let myStackView = UIStackView()
myStackView.axis = .horizontal
myStackView.alignment = .center
myStackView.distribution = .fillEqually
myStackView.spacing = 8
view.addSubview(myStackView)
//Setup circles
let circle_1 = circleLabel()
let circle_2 = circleLabel()
let circle_3 = circleLabel()
myStackView.addArrangedSubview(circle_1)
myStackView.addArrangedSubview(circle_2)
myStackView.addArrangedSubview(circle_3)
myStackView.translatesAutoresizingMaskIntoConstraints = false
myStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
myStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
}
func circleLabel() -> UILabel {
let label = UILabel()
label.backgroundColor = UIColor.red
label.layer.cornerRadius = 12.5
label.layer.masksToBounds = true
label.layer.borderColor = UIColor.orange.cgColor
label.layer.borderWidth = 3.0
label.widthAnchor.constraint(equalToConstant: 25.0).isActive = true
label.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
return label
}
To make a Single Circle like that, you need to make use of UIBezierPath and CAShapeLayer .
let outerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(50), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let outerCircleShapeLayer = CAShapeLayer()
outerCircleShapeLayer.path = outerCirclePath.cgPath
outerCircleShapeLayer.fillColor = UIColor.white.cgColor
outerCircleShapeLayer.lineWidth = 3.0
view.layer.addSublayer(outerCircleShapeLayer)
// Drawing the inner circle
let innerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(40), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let innerCircleShapeLayer = CAShapeLayer()
innerCircleShapeLayer.path = innerCirclePath.cgPath
innerCircleShapeLayer.fillColor = UIColor.blue.cgColor
view.layer.addSublayer(innerCircleShapeLayer)
I have attached an image below for the Playground version of it .
Just play around with arcCenter and radius values and you will get the desired output
My team helped me and here is solution to create this with dynamically changing state of circles (with different stroke and fill colors):
import UIKit
#IBDesignable
class CirclesButton: UIControl {
#IBInspectable
var firstCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var secondCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var thirdCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// get context
guard let context = UIGraphicsGetCurrentContext() else { return }
// make configurations
context.setLineWidth(1.0);
context.setStrokeColor(UIColor.white.cgColor)
context.setFillColor(red: 0.0, green: 0.58, blue: 1.0, alpha: 1.0)
// find view center
let dotSize:CGFloat = 11.0
let viewCenter = CGPoint(x: rect.midX, y: rect.midY)
// find personal dot rect
var dotRect = CGRect(x: viewCenter.x - dotSize / 2.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if secondCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find global notes rect
dotRect = CGRect(x: viewCenter.x - dotSize * 1.5 - 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if firstCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find music rect
dotRect = CGRect(x: viewCenter.x + dotSize / 2.0 + 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if thirdCircle {
context.setFillColor(red: 0.0, green: 1.0, blue: 0.04, alpha: 1.0)
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
}
}
It will looks like: CirclesButton
Сode:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let buttonSize: CGFloat = 80
let firstButton = CustomButton(position: CGPoint(x: 0, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(firstButton)
let secondButton = CustomButton(position: CGPoint(x: firstButton.frame.maxX, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(secondButton)
let thirdButton = CustomButton(position: CGPoint(x: secondButton.frame.maxX, y: 0), size: buttonSize, color: .green)
self.view.addSubview(thirdButton)
}
}
class CustomButton: UIButton {
init(position: CGPoint, size: CGFloat, color: UIColor) {
super.init(frame: CGRect(x: position.x, y: position.y, width: size, height: size))
self.backgroundColor = color
self.layer.cornerRadius = size / 2
self.clipsToBounds = true
self.layer.borderWidth = 4.0 // make it what ever you want
self.layer.borderColor = UIColor.white.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
You can handle button tapped like:
override func viewDidLoad() {
super.viewDidLoad()
firstButton.addTarget(self, action: #selector(handleFirstButton), for: .touchUpInside)
}
#objc func handleFirstButton(sender: UIButton) {
print("first button tapped")
}
Best and Universal Solution for **Button or Label creation (Fully Dynamic)**
var x = 10
var y = 5
var buttonHeight = 40
var buttonWidth = 40
for i in 0..<3 {
let roundButton = UIButton(frame: CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight))
roundButton.setTitle("Butt\(i)", for: .normal)
roundButton.layer.cornerRadius = roundButton.bounds.size.height/2
yourButtonBackView.addSubview(roundButton)
x = x + buttonWidth + 10
if x >= Int(yourButtonBackView.frame.width - 30) {
y = y + buttonHeight + 10
x = 10
}
}
Right now, I have a custom graph class which creates bar charts and displays them. However they only appear after I appear on the view controller twice. For eg. here I am on one of the view controllers that has the graph but it shows in an incomplete manner.
Then here I am at the same view controller but with the graph how it is meant to look like. As you can see, the bars have gotten longer and the dashed lines have appeared.
To achieve this, all I did was click on one of the tabs in the bottom and then clicked back on the tab for report and it changed the view. I really have no idea why this is happening and have been trying to tackle this for 2 days now. If someone can help me it would be great. Here is the code for a sample viewcontroller that shows the graph.
override func viewDidLoad() {
self.tabBarController?.tabBar.barTintColor = UIColor(red:0.18, green:0.21, blue:0.25, alpha:1.0)
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
self.tabBarController?.tabBar.barTintColor = UIColor(red:0.18, green:0.21, blue:0.25, alpha:1.0)
tupleArray.removeAll()
newTuple.removeAll()
filteredCategories.removeAll()
categories.removeAll()
self.categories = CoreDataHelper.retrieveCategories()
tableView.tableFooterView = UIView(frame: .zero)
tableView.tableFooterView?.isHidden = true
tableView.backgroundColor = UIColor.clear
tableView.layoutMargins = UIEdgeInsets.zero
tableView.separatorInset = UIEdgeInsets.zero
tableView.separatorStyle = UITableViewCellSeparatorStyle.singleLine
UIColor(red:0.40, green:0.43, blue:0.48, alpha:1.0)
currentYear1 = Calendar.current.date(byAdding: .year, value: -1, to: Date())!
currentYear = String(describing: Calendar.current.component(.year, from: currentYear1))
yearLabel.text = "\(currentYear)"
if ExpensesAdditions().yearHasExpense(year: currentYear){
noExpenseLabel.isHidden = true
}
else{
tableView.isHidden = true
barChartView.isHidden = true
noExpenseLabel.isHidden = false
totalSpentLabel.isHidden = true
}
totalSpent = ExpensesAdditions().retrieveYearlySum(year:currentYear)
totalSpentLabel.text = "Total Spent: " + ExpensesAdditions().convertToMoney(totalSpent)
for category in categories{
if ExpensesAdditions().categoryinYearHasExpense(year:currentYear,category:category.title!){
filteredCategories.append(category)
tupleArray.append((category.title!,ExpensesAdditions().retrieveCategoryExpenseForYear(category: category.title!, year: currentYear)))
}
else{}
}
newTuple = tupleArray.sorted(by: { $0.1 > $1.1 })
if ExpensesAdditions().yearHasExpense(year: currentYear){
let dataEntries = generateDataEntries()
barChartView.dataEntries = dataEntries
}
}
override func viewDidAppear(_ animated: Bool) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredCategories.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "lastYearAnalysisCell") as! LastYearAnalysisViewTableViewCell
cell.isUserInteractionEnabled = false
let row = indexPath.row
let percentage:Double = (newTuple[row].1/totalSpent)*100
let percentageDisplay:Int = Int(percentage.rounded())
cell.nameLabel.text = newTuple[row].0
cell.amountLabel.text = "(\(percentageDisplay)"+"%) "+String(describing: UserDefaults.standard.value(forKey: "chosenCurrencySymbol")!)+ExpensesAdditions().convertToMoney(newTuple[row].1)
return cell
}
func generateDataEntries() -> [BarEntry] {
let colors = [#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1), #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1), #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1), #colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1), #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1), #colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1)]
maximum = newTuple[0].1
var result: [BarEntry] = []
/// ---- Complicated Divising -------//////
var maximumString:String = ExpensesAdditions().convertToMoney(maximum)
var maximumInt:Int = Int(maximumString.getAcronyms())!
let endIndex = maximumString.index(maximumString.endIndex, offsetBy: -3)
let truncated = maximumString.substring(to: endIndex)
if maximumInt < 5{
divisor = Double((5*(pow(10, truncated.count - 1))) as NSNumber)
}
else if maximumInt > 5{
divisor = Double((pow(10, truncated.count)) as NSNumber)
}
for i in 0..<filteredCategories.count {
let value = ExpensesAdditions().convertToMoney(tupleArray[i].1)
var height:Double = Double(value)! / divisor
result.append(BarEntry(color: colors[i % colors.count], height: height, textValue: value, title: filteredCategories[i].title!))
}
return result
//// --- Complicated Divising End -------- /////
}
If you need anymore code, please let me know and thanks in advance!
Here is the code for the bar chart entry code:
import UIKit
class BasicBarChart: UIView {
/// the width of each bar
let barWidth: CGFloat = 40.0
/// space between each bar
let space: CGFloat = 20.0
/// space at the bottom of the bar to show the title
private let bottomSpace: CGFloat = 40.0
/// space at the top of each bar to show the value
private let topSpace: CGFloat = 40.0
/// contain all layers of the chart
private let mainLayer: CALayer = CALayer()
/// contain mainLayer to support scrolling
private let scrollView: UIScrollView = UIScrollView()
var dataEntries: [BarEntry]? = nil {
didSet {
mainLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
if let dataEntries = dataEntries {
scrollView.contentSize = CGSize(width: (barWidth + space)*CGFloat(dataEntries.count), height: self.frame.size.height)
mainLayer.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
drawHorizontalLines()
for i in 0..<dataEntries.count {
showEntry(index: i, entry: dataEntries[i])
}
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
convenience init() {
self.init(frame: CGRect.zero)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
scrollView.layer.addSublayer(mainLayer)
self.addSubview(scrollView)
}
override func layoutSubviews() {
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
}
private func showEntry(index: Int, entry: BarEntry) {
/// Starting x postion of the bar
let xPos: CGFloat = space + CGFloat(index) * (barWidth + space)
/// Starting y postion of the bar
let yPos: CGFloat = translateHeightValueToYPosition(value: Float(entry.height))
drawBar(xPos: xPos, yPos: yPos, color: entry.color)
/// Draw text above the bar
drawTextValue(xPos: xPos - space/2, yPos: yPos - 30, textValue: entry.textValue, color: entry.color)
/// Draw text below the bar
drawTitle(xPos: xPos - space/2, yPos: mainLayer.frame.height - bottomSpace + 10, title: entry.title, color: entry.color)
}
private func drawBar(xPos: CGFloat, yPos: CGFloat, color: UIColor) {
let barLayer = CALayer()
barLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth, height: mainLayer.frame.height - bottomSpace - yPos)
barLayer.backgroundColor = color.cgColor
mainLayer.addSublayer(barLayer)
}
private func drawHorizontalLines() {
self.layer.sublayers?.forEach({
if $0 is CAShapeLayer {
$0.removeFromSuperlayer()
}
})
let horizontalLineInfos = [["value": Float(0.0), "dashed": true], ["value": Float(0.5), "dashed": true], ["value": Float(1.0), "dashed": true]]
for lineInfo in horizontalLineInfos {
let xPos = CGFloat(0.0)
let yPos = translateHeightValueToYPosition(value: (lineInfo["value"] as! Float))
let path = UIBezierPath()
path.move(to: CGPoint(x: xPos, y: yPos))
path.addLine(to: CGPoint(x: scrollView.frame.size.width, y: yPos))
let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.lineWidth = 0.5
if lineInfo["dashed"] as! Bool {
lineLayer.lineDashPattern = [4, 4]
}
lineLayer.strokeColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1).cgColor
self.layer.insertSublayer(lineLayer, at: 0)
}
}
private func drawTextValue(xPos: CGFloat, yPos: CGFloat, textValue: String, color: UIColor) {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth+space, height: 22)
textLayer.foregroundColor = color.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 14
textLayer.string = textValue
mainLayer.addSublayer(textLayer)
}
private func drawTitle(xPos: CGFloat, yPos: CGFloat, title: String, color: UIColor) {
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth + space, height: 22)
textLayer.foregroundColor = color.cgColor
textLayer.backgroundColor = UIColor.clear.cgColor
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
textLayer.fontSize = 14
textLayer.string = title
mainLayer.addSublayer(textLayer)
}
private func translateHeightValueToYPosition(value: Float) -> CGFloat {
let height: CGFloat = CGFloat(value) * (mainLayer.frame.height - bottomSpace - topSpace)
return mainLayer.frame.height - bottomSpace - height
}
}
The problem is most likely that you set up your bar chart in viewWillAppear, at which point the final size of the view has not been set yet. To fix this you must either delay setting the data until in viewDidAppear, or you need to trigger a redraw of the bars and dashed lines when the frame size has changed i.e. from layoutSubviews.
One point of advice I want to share with you is that you should try to separate your code so that sizing and positioning of all the view's sub-items are done in layoutSubviews, and the actual adding and removal of them are done elsewhere.
Update:
In your class BasicBarChart you should try to separate adding the chart elements, and do the actual sizing of the elements. This is because sizing is an event that often occurs more than once, especially when using auto-layout.
If your BasicBarChart view is set up with constraints, it will usually receive at least two different sizes during setup, once with the sizes in your storyboard file, and once with the actual device derived sizes. Since you don't want to add everything more than once, you should do the adding elsewhere. A good place could be during initialization or when setting data.
The sizing of a view's sub-elements is best done in its layoutSubviews() function. You have already started! Just move over the sizing of your other elements here too, and everything should work much better.
class BasicBarChart: UIView {
override func layoutSubviews() {
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
// Size and position bars, labels and dashed lines here
}
}
I am currently messing around with IBDesignable Views, and I am curious if anyone has been able to solve this. I would like to have views added through the interface builder be automatically arranged using a custom layout algorithm within my subview. The view works great when I run the app, but in the interface builder, the views do not rearrange in real time.
I have tried debugging my UIView class, but it seems at all times when the interface builder is initializing the element, it thinks it has zero subviews. It seems the interface builder does not give you a chance to arrange these views after the fact. However, I'm wondering if maybe there is just something I'm missing. Is it possible to rearrange subviews added from the interface builder within an IBDesignable class, and have the views show up rearranged in the interface builder?
Try using the provided method for a custom view and IBDesignable if you are not already. You also might need to refresh your views in Xcode or have it automatically refresh views. Below is the function you may be missing. This is never called in a live app. It is only called in Xcode IB.
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
In this instance setUpView is laying out my subviews.
Here is an example that I made. https://github.com/agibson73/ICONButton
import UIKit
#IBDesignable class AGIconButton: UIControl {
private var iconImageView : UIImageView!
private var iconLabel : UILabel!
private var mainSpacer : UIView!
private var highlightView:UIView!
private var widthContraint : NSLayoutConstraint!
var padding : CGFloat = 5
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpView()
addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)
}
//only called at design time
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)
}
#IBInspectable var iconImage: UIImage = UIImage() {
didSet {
iconImageView.image = iconImage
}
}
#IBInspectable var imageSize: CGFloat = 40 {
didSet {
setUpView()
}
}
#IBInspectable var imagePadding: CGFloat = 10 {
didSet {
setUpView()
}
}
#IBInspectable var iconText: String = "Icon Button Time" {
didSet {
setUpView()
}
}
#IBInspectable var iconTextSize: CGFloat = 15 {
didSet {
setUpView()
}
}
#IBInspectable var iconTextColor: UIColor = UIColor.black {
didSet {
setUpView()
}
}
#IBInspectable var alignment: Int = 1 {
didSet {
setUpView()
}
}
override var intrinsicContentSize: CGSize {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: iconTextSize)
label.text = iconText
label.sizeToFit()
return CGSize(width: imageSize + label.frame.width + imagePadding + (padding * 2), height: CGFloat(max(label.frame.height, imageSize) + padding * 2))
}
#IBInspectable var highLightColor: UIColor = UIColor.lightGray {
didSet {
setUpView()
}
}
#IBInspectable var shouldBounce: Bool = true
#IBInspectable var borderColor: UIColor = UIColor.clear {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
}
}
private func setUpView(){
if iconImageView == nil{
iconImageView = UIImageView(image: iconImage)
iconImageView.contentMode = .scaleAspectFit
iconImageView.isUserInteractionEnabled = false
self.addSubview(iconImageView)
}
if mainSpacer == nil{
mainSpacer = UIView(frame: CGRect(x: 0, y: 0, width: imagePadding, height: self.bounds.height))
mainSpacer.isUserInteractionEnabled = false
self.addSubview(mainSpacer)
}
if iconLabel == nil{
iconLabel = UILabel()
iconLabel.isUserInteractionEnabled = false
self.addSubview(iconLabel)
}
if highlightView == nil{
highlightView = UIView(frame: self.bounds)
highlightView.autoresizingMask = [.flexibleWidth,.flexibleHeight]
highlightView.alpha = 0
highlightView.isUserInteractionEnabled = false
self.addSubview(highlightView)
self.bringSubview(toFront: highlightView)
}
highlightView.backgroundColor = highLightColor
iconLabel.font = UIFont.systemFont(ofSize: iconTextSize)
iconLabel.text = iconText
iconLabel.textColor = iconTextColor
iconLabel.sizeToFit()
var usedWidth : CGFloat = self.intrinsicContentSize.width
if bounds.width < usedWidth{
usedWidth = bounds.width
}
let maxImageHeight = min(self.bounds.height - padding, imageSize)
//resize iconlabel if we have to
if maxImageHeight + imagePadding + iconLabel.bounds.width + padding * 2 > usedWidth{
iconLabel.frame = CGRect(x: 0, y: 0, width: self.bounds.width - iconImageView.bounds.width - imagePadding - padding * 2, height: iconLabel.bounds.height)
iconLabel.fitFontForSize(minFontSize: 1, maxFontSize: iconTextSize, accuracy: 1.0)
}
let maxWidth = (self.bounds.width - iconLabel.bounds.width - maxImageHeight - imagePadding) / 2
switch alignment {
case 0:
//intrinsic left
iconImageView.frame = CGRect(x:padding, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
mainSpacer.frame = CGRect(x: maxImageHeight + padding, y: 0, width: imagePadding, height: self.bounds.height)
iconLabel.frame = CGRect(x: maxImageHeight + imagePadding + padding, y: 0, width: iconLabel.frame.width, height: bounds.height)
break
case 1:
//intrinsic center
iconImageView.frame = CGRect(x: maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
mainSpacer.frame = CGRect(x: maxWidth + maxImageHeight, y: 0, width: imagePadding, height: self.bounds.height)
iconLabel.frame = CGRect(x: maxWidth + maxImageHeight + imagePadding, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
break
case 2:
//intrinsic icon right text aligned right
iconLabel.frame = CGRect(x: maxWidth, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
iconLabel.textAlignment = .right
mainSpacer.frame = CGRect(x: iconLabel.frame.width + maxWidth, y: 0, width: imagePadding, height: self.bounds.height)
iconImageView.frame = CGRect(x: iconLabel.frame.width + imagePadding + maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
break
case 3:
//intrinsic center invert icon
iconLabel.frame = CGRect(x:maxWidth, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
mainSpacer.frame = CGRect(x: maxWidth + iconLabel.bounds.width, y: 0, width: imagePadding, height: self.bounds.height)
iconImageView.frame = CGRect(x: maxWidth + iconLabel.bounds.width + imagePadding, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
break
default:
//intrinsic center
iconImageView.frame = CGRect(x: maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
mainSpacer.frame = CGRect(x: maxWidth + maxImageHeight, y: 0, width: imagePadding, height: self.bounds.height)
iconLabel.frame = CGRect(x: maxWidth + maxImageHeight + imagePadding, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
}
}
//layout subviews
override func layoutSubviews() {
super.layoutSubviews()
setUpView()
}
//MARK: Touch Events
//TODO: run on timer to simulate a real press
func userDidTouchDown(){
if shouldBounce == true{
animateBouncyDown()
}else{
self.animateHighlightTo(alpha: 0.3)
}
}
func userDidTouchUp(){
if shouldBounce == true{
animateBouncyUp()
}else{
self.animateHighlightTo(alpha: 0)
}
}
func userDidTouchUpOutside(){
if shouldBounce == true{
animateBouncyUp()
}else{
self.animateHighlightTo(alpha: 0)
}
}
func animateHighlightTo(alpha:CGFloat){
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.highlightView.alpha = alpha
})
}
func animateBouncyDown(){
self.transform = CGAffineTransform.identity
UIView.animate(withDuration: 0.15, animations: { [weak self] in
self?.transform = CGAffineTransform(scaleX: 0.85, y: 0.85)
})
}
func animateBouncyUp(){
UIView.animate(withDuration: 0.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: .curveEaseInOut, animations: {[weak self] in
if self != nil{
self?.transform = CGAffineTransform.identity
}
}, completion: nil)
}
}
extension UILabel {
func fitFontForSize( minFontSize : CGFloat = 1.0, maxFontSize : CGFloat = 300.0, accuracy : CGFloat = 1.0) {
var maxFontSize = maxFontSize
var minFontSize = minFontSize
assert(maxFontSize > minFontSize)
layoutIfNeeded() // Can be removed at your own discretion
let constrainedSize = bounds.size
while maxFontSize - minFontSize > accuracy {
let midFontSize : CGFloat = ((minFontSize + maxFontSize) / 2)
font = font.withSize(midFontSize)
sizeToFit()
let checkSize : CGSize = bounds.size
if checkSize.height < constrainedSize.height && checkSize.width < constrainedSize.width {
minFontSize = midFontSize
} else {
maxFontSize = midFontSize
}
}
font = font.withSize(minFontSize)
sizeToFit()
layoutIfNeeded()
}
}
There doesn't seem to be any way to do this. If you drop a custom control into another xib and add subviews to the custom control in Interface Builder, those subviews appear at the same level as the custom control in the view hierarchy when you load the xib. It looks like custom controls can not act as containers in other xibs.