Creating and Setting title of UIButton in FOR loop breaks loop - ios

In my app I'm creating a paged slider (UISlider + UIPageControl). The app reaches out to CoreData to get the array of Entities to create a "page" for.
I build the paginated scroller n the following loop:
for i in 0..<features.count {
var pageWidth = Int(view.frame.width)*i
var pageView = UIView(frame: CGRectMake(CGFloat(pageWidth), 0, scrollView.frame.width, scrollView.frame.height))
pageView.backgroundColor = UIColor.clearColor()
pageView.clipsToBounds = true
//Background image
var imageView = UIImageView(frame: CGRectMake(0, 0, scrollView.frame.width, scrollView.frame.height))
var image:UIImage!
imageView.contentMode = .ScaleAspectFill
pageView.addSubview(imageView)
if fileManager.fileExistsAtPath(features[i].coverImage){
//The background image
image = UIImage(contentsOfFile: self.features[i].coverImage)
imageView.image = image
}
else if !fileManager.fileExistsAtPath(features[i].coverImage) && !features[i].coverImage.isEmpty {
FileStoreClient.downloadFileIfNotExists(features[i].cover, completion: { (success, error) -> Void in
if error == nil {
image = UIImage(contentsOfFile: self.features[i].coverImage)
imageView.image = image
}
else {
println("There was an error during the download")
}
})
}
//The background cover
var coverView = UIView(frame: CGRectMake(0, 0, view.frame.width, 250))
coverView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
pageView.addSubview(coverView)
//The title
var titleLabel = UILabel(frame: CGRectMake(10, 40, view.frame.width-20, 25))
titleLabel.font = UIFont(name: "Haymaker", size: 24)
titleLabel.textColor = UIColor.whiteColor()
titleLabel.layer.shadowColor = UIColor.blackColor().CGColor
titleLabel.layer.shadowOffset = CGSizeMake(1, 1)
titleLabel.layer.shadowRadius = 1
titleLabel.layer.shadowOpacity = 1
titleLabel.textAlignment = .Center
titleLabel.text = features[i].title
pageView.addSubview(titleLabel)
//The text
var textLabel = UILabel(frame: CGRectMake(10, titleLabel.frame.origin.y+titleLabel.frame.height+5, view.frame.width-20, 60))
textLabel.numberOfLines = 0
textLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
textLabel.font = UIFont(name: "OpenSans", size: 13)
textLabel.textColor = UIColor.whiteColor()
textLabel.textAlignment = .Center
textLabel.text = features[i].summary
pageView.addSubview(textLabel)
//The button
var readMoreButton = UIButton(frame: CGRectMake(0, pageView.frame.height - 90, 200, 80))
readMoreButton.setAttributedTitle(NSAttributedString(string: Strings.readMoreButtonTitle, attributes: Fonts.featureButtonText), forState: UIControlState.Normal)
//readMoreButton.addTarget(self, action: "readArticle", forControlEvents: .TouchUpInside)
readMoreButton.layer.cornerRadius = 6
readMoreButton.layer.borderColor = UIColor.whiteColor().CGColor
readMoreButton.layer.borderWidth = 1
readMoreButton.center.x = pageView.center.x
pageView.addSubview(readMoreButton)
println(i)
scrollView.addSubview(pageView)
}
Everything works exactly as it should, except that the moment I try to assign a title to the new button, the loop turns into an infinite loop. If I print out the current index of the loop, it keeps cycling through endlessly. Example:
0
1
2
0
1
2
0
1
2
If I comment out the myButton.setTitle() method, the problem goes away and the indexes print out like the following:
0
1
2
I have tried setTitle and setAttributedTitle, both of which have the same effect. I have tried dispatching different queues and nothing seems to be working.
Again, everything works for the background image, the two labels, views, etc. Problem only exists when a button is brought into the mix. The issue exists even if I remove the FileStoreManager code.
Something about a button in a for loop isn't happy.
EDIT #1
I decided to try and create a new project just to test button creation in a for loop. This works perfectly fine.
var x = 20
var y = 20
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...10 {
var myview = UIView(frame: CGRectMake(CGFloat(x), CGFloat(y), 100, 50))
var button = UIButton(frame: CGRectMake(0, 0, 100, 50))
button.setTitle("Test", forState: .Normal)
button.setTitleColor(UIColor.blackColor(), forState: .Normal)
button.layer.borderColor = UIColor.blackColor().CGColor
button.layer.borderWidth = 2
myview.addSubview(button)
view.addSubview(myview)
x = x+20
y = y+20
println(i)
}
}

You can also try fast enumeration in case something is somehow getting messed up in your range.
for feature in features { ... }
or if you need the index:
for (index, features) in features.enumerate() { ... }
Not sure it will resolve the issue, but it might increase readability.

It turns out it was because I had the above code in the viewDidLayoutSubviews() function. Something about setting a buttons title triggers the method, it seems.
As I needed to use this method due to dynamic sizes, I simply added a "hasLoaded:Bool" to check if the function has already fired.
This resolved the issue.

Related

Adding image in the Navigation Bar

I was wondering the best approach to put an image into the navigation bar.
My initial thought was to create a cocoa touch class for UINavigationController and set it up that way, but I can seem to get it to working using the below code:
class NavBarImage: UINavigationController {
override func awakeFromNib() {
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.setupView()
}
func setupView()
{
let navController = navigationController!
let image = #imageLiteral(resourceName: "BarTabsNavLogoWhite")
let imageView = UIImageView(image: image)
let bannerWidth = navController.navigationBar.frame.size.width
let bannerHeight = navController.navigationBar.frame.size.height
let bannerX = bannerWidth / 2 - image.size.width / 2
let bannerY = bannerHeight / 2 - image.size.height / 2
imageView.frame = CGRect(x: bannerX, y: bannerY, width: bannerWidth,
height: bannerHeight)
imageView.contentMode = .scaleAspectFit
navigationItem.titleView = imageView
}
}
I keep getting an "unexpectedly found nil while unwrapping an optional value" on let navController = navigationController!.
However, this method has also been working for me too. I created a cocoa touch class for UINavigationBar and used this code below:
import UIKit
class NavBarImg: UINavigationBar {
override init(frame: CGRect) {
super.init(frame: frame)
initialise()
}
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)!
initialise()
}
func initialise(){
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 225, height: 40))
imageView.contentMode = .scaleAspectFit
let image = UIImage(named:"BarTabsNavLogoWhite")
imageView.image = image
imageView.frame.origin = CGPoint(x: self.superview?.center.x, y: self.superview?.center.y)
addSubview(imageView)
}
}
The only problem with this is that on different iPhones I cant figure out how to get the image to always be centered on any device using CGPoint.
Then for the last method I found and implemented is done by the code below:
#IBDesignable class test: UINavigationBar { #IBInspectable var imageTitle: UIImage? = nil {
didSet {
guard let imageTitle = imageTitle else {
topItem?.titleView = nil
return
}
let imageView = UIImageView(image: imageTitle)
imageView.frame = CGRect(x: 0, y: 0, width: 40, height: 30)
imageView.contentMode = .scaleAspectFit
topItem?.titleView = imageView
}
}
}
I really like this method because with the IBDesignable function you can see it in the storyboard. However the way I have my viewcontrollers set up with tableviews, after i go past the first view controller, the navigation bar image disappears in all other view controllers when I run the simulator.
Looking for advice to see which method is the best approach and how to possibly solve the problems I am having. Or if anyone has a different method that they have found that works, id love to see how it works!
you can simply add a image or customize the barbutton as follows:
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_right"), for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 53, height: 31)
button.imageEdgeInsets = UIEdgeInsetsMake(-1, 32, 1, -32)//move image to the right
let label = UILabel(frame: CGRect(x: 3, y: 5, width: 20, height: 20))
label.font = UIFont(name: "Arial-BoldMT", size: 16)
label.text = "title"
label.textAlignment = .center
label.textColor = .black
label.backgroundColor = .clear
button.addSubview(label)
let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton

Swift - Customize view to show over an empty table cells

I am using Swift 3, Xcode 8.2.
I've been able to create a label to cover the empty table view cells when there are none to display.
My code is below and it is located in the subclass of UITableViewController.
override func numberOfSections(in tableView: UITableView) -> Int {
// if there are scans to display...
if items.count > 0 {
tableView.backgroundView = nil
tableView.separatorStyle = .singleLine
return 1
}
else { // otherwise, return 0, remove cell lines, and display a Label
let rect = CGRect(x: 0,
y: 0,
width: tableView.bounds.size.width,
height: tableView.bounds.size.height)
let noScanLabel: UILabel = UILabel(frame: rect)
noScanLabel.text = "No Scans"
noScanLabel.textColor = UIColor.gray
noScanLabel.font = UIFont.boldSystemFont(ofSize: 24)
noScanLabel.textAlignment = NSTextAlignment.center
tableView.backgroundView = noScanLabel
tableView.separatorStyle = .none
return 0
}
}
Here is the result.
Looks fine. But how, do I make it such that I include another line of text with a downward arrow pointing at the raised center button. Something like "Click here to start a scan"?
I've tried adding new line characters to the noScanLabel.text field but that didn't work out. Any pointers in the right direction would be helpful.
The simple solution is to set numberOfLines to 0 on noScanLabel. This way, the new lines will show.
let noScanLabel: UILabel = UILabel(frame: rect)
noScanLabel.text = "No Scans"
noScanLabel.textColor = UIColor.gray
noScanLabel.font = UIFont.boldSystemFont(ofSize: 24)
noScanLabel.numberOfLines = 0
noScanLabel.textAlignment = NSTextAlignment.center
Note than in such cases, I would recommend, for better maintainability, to actually remove the TableView from the UIViewController (hence not inherit from UITableViewController) and replace it with an empty view when you detect no scans are available. This will make each state more independent of each other and make maintenance easier.
There are a few ways achieve your goal. There is a well known library called DZNEmptyDataSet for handling empty tableviews and collectionviews . https://github.com/dzenbot/DZNEmptyDataSet
The other way would be to create a uiview with your specified rect and then add two labels to that uiview. One would be your noScanLabel and the other would be a label or image containing your arrow. You can set the layout constraints as required so that the arrow appears pointing down.
This code seems to work well. Change constraints if needed
let rect = CGRect(x: 0, y: 0, width: tableview.bounds.size.width, height: tableview.bounds.size.height)
let noDataView = UIView(frame: rect)
let noScanLabel = UILabel()
noScanLabel.text = "No Scans"
noScanLabel.textColor = UIColor.gray
noScanLabel.font = UIFont.boldSystemFont(ofSize: 24)
noScanLabel.textAlignment = NSTextAlignment.center
let arrowLabel = UILabel()
arrowLabel.text = "Add Arrow Image to this label"
arrowLabel.textColor = UIColor.gray
arrowLabel.font = UIFont.boldSystemFont(ofSize: 24)
arrowLabel.textAlignment = NSTextAlignment.center
noScanLabel.widthAnchor.constraint(equalToConstant: 100)
arrowLabel.widthAnchor.constraint(equalToConstant: 50)
noDataView.addSubview(noScanLabel)
noDataView.addSubview(arrowLabel)
arrowLabel.translatesAutoresizingMaskIntoConstraints = false
noDataView.translatesAutoresizingMaskIntoConstraints = false
noScanLabel.translatesAutoresizingMaskIntoConstraints = false
self.tableview.addSubview(noDataView)
noDataView.isHidden = false
noDataView.centerXAnchor.constraint(equalTo: self.tableview.centerXAnchor).isActive = true
noDataView.centerYAnchor.constraint(equalTo: self.tableview.centerYAnchor).isActive = true
noScanLabel.centerXAnchor.constraint(equalTo: noDataView.centerXAnchor).isActive = true
noScanLabel.topAnchor.constraint(equalTo: noDataView.centerYAnchor).isActive = true
arrowLabel.centerXAnchor.constraint(equalTo: noDataView.centerXAnchor).isActive = true
arrowLabel.topAnchor.constraint(equalTo: noScanLabel.bottomAnchor).isActive = true
The other option is to set number of lines to zero as mentioned already
noScanLabel.numberLines = 0
You can take UIView and add your all UILabel and arrow Image on UIView and then assign that UIView to backgroundView of TableView.
Like this.
override func numberOfSections(in tableView: UITableView) -> Int {
// if there are scans to display...
if items.count > 0 {
tableView.backgroundView = nil
tableView.separatorStyle = .singleLine
return 1
}
else { // otherwise, return 0, remove cell lines, and display a Label
let rect = CGRect(x: 0,
y: 0,
width: tableView.bounds.size.width,
height: tableView.bounds.size.height)
let messageBaseView = UIView(frame: rect)
//Add your first label..
let noScanLabel: UILabel = UILabel()
noScanLabel.text = "No Scans"
noScanLabel.textColor = UIColor.gray
noScanLabel.font = UIFont.boldSystemFont(ofSize: 24)
noScanLabel.textAlignment = NSTextAlignment.center
messageBaseView.addSubView(noScanLabel)
//Add your second label.. and your arrow image here on messageBaseView
//Assign messageBaseView to backgroundView of tableView
tableView.backgroundView = messageBaseView
tableView.separatorStyle = .none
return 0
}
}

Dynamic Auto Layout

To everyone who reads it. I want to make chat application like Viber, WhatsApp, etc. And the main problem for me is dynamic design. I know how to work with constraints if user interface is stick but, if it is changing, I don’t.
Please, look at the animation from Viber:
Everything is moving and changing when typing and receiving or sending messages. How to do it using Autoalyout? Especially I am interesting how to do it with UITextView. To expand it when typing, and, what is more important, how to move table view above it. Maybe, change size of parent view which holds text view. I think all this stuff can be call Dynamic Auto Layout.
I will appreciate any help or advice!
I am using Swift and Xcode 7, all latest versions.
Here is a piece of code that builds a keypad using UIStackViews. The layout is managed by UIStackView.
func newPad(runModeVariable:Int) {
var xPos:CGFloat = 0
var yPos:CGFloat = 0
keyPadWindow = UIView(frame: CGRect(x: xPos, y: yPos, width: 320, height: 356))
var keyCount = 0
keyPadSV = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 356))
keyPadSV!.axis = .Vertical
keyPadSV!.spacing = 0.0
keyPadSV!.alignment = .Center
keyPadSV!.distribution = .EqualSpacing
for _ in 0 ..< 5 {
let keyPadSVB = UIStackView(frame:CGRect(x: 0, y: 0, width: 0, height: 0))
keyPadSVB.axis = .Horizontal
keyPadSVB.spacing = 0.0
keyPadSVB.alignment = .Center
keyPadSVB.distribution = .EqualSpacing
for _ in 0 ..< 4 {
let button = UIButton(type: UIButtonType.Custom) as UIButton
button.frame = CGRectMake(0, 0, 0, 0)
button.tag = keyCount
let blah = "x" + String(keyCount)
let blahIMAGE = UIImage(named: blah)
button.setImage(blahIMAGE, forState: UIControlState.Normal)
button.addTarget(self, action: #selector(ViewController.keyPadPress(_:)), forControlEvents: UIControlEvents.TouchUpInside)
keyPadSVB.addArrangedSubview(button)
keyCount += 1
}
keyPadSV.addArrangedSubview(keyPadSVB)
}
keyPadWindow.addSubview(keyPadSV)
self.view.addSubview(keyPadWindow)
}

Adding a button to a stackview via code

i'm trying to add a button back into a stackview and i'm not sure how to do it in the code, i only want the button to appear under a circumstance so that's why i'm doing it in code, here's what i have
i'm removing it with
func hideCompareButton() {
UIView.animateWithDuration(0.4, animations: {
self.compareButton.alpha = 0
}) { completed in
if completed == true {
self.compareButton.removeFromSuperview()
}
}
}
and i've got this that would normally work with other views, but i'm not sure how to add a button onto a stackview, at a prefered locations, there's 3 buttons when it's not added, 4 when it is, and id like it to be the 3rd button in from the left
func showCompareButton() {
// bottomStackView.addArrangedSubview(compareButton) ???
bottomStackView.addSubview(compareButton)
let bottomConstraint = compareButton.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
let leftConstraint = compareButton.leftAnchor.constraintEqualToAnchor(filterButton.rightAnchor)
let rightConstraint = compareButton.rightAnchor.constraintEqualToAnchor(shareButton.leftAnchor)
let heightConstraint = compareButton.heightAnchor.constraintEqualToConstant(44)
NSLayoutConstraint.activateConstraints([bottomConstraint, leftConstraint, rightConstraint, heightConstraint])
bottomStackView.layoutIfNeeded()
}
Why don't you build the stack view the way you want it with all the buttons and then simply hide the 3rd button. When you want it unhide it. The stack view will do all the rest. Here some stack view code to get you going. This builds a keypad, but its the same principle, it adds all the buttons and then makes it visible.
Here some stack view code to get you going.
keyPadWindow = UIView(frame: CGRect(x: 128, y: 224, width: 512, height: 356))
var keyCount = 0
keyPadSV = UIStackView(frame: CGRect(x: 0, y: 0, width: 512, height: 356))
print("pickerWindow.bounds \(pickerWindow.bounds)")
keyPadSV!.axis = .Vertical
keyPadSV!.spacing = 0.0
keyPadSV!.alignment = .Center
keyPadSV!.distribution = .EqualSpacing
for var k2s = 0; k2s < 5; k2s++ {
let keyPadSVB = UIStackView(frame:CGRect(x: 0, y: 0, width: 0, height: 0))
keyPadSVB.axis = .Horizontal
keyPadSVB.spacing = 0.0
keyPadSVB.alignment = .Center
keyPadSVB.distribution = .EqualSpacing
for var k4s = 0; k4s < 4; k4s++ {
let button = UIButton(type: UIButtonType.Custom) as UIButton
button.frame = CGRectMake(0, 0, 0, 0)
button.tag = keyCount
let blah = "x" + String(keyCount)
let blahIMAGE = UIImage(named: blah)
button.setImage(blahIMAGE, forState: UIControlState.Normal)
print("blah \(blah)")
button.addTarget(self, action: "keyPadPress:", forControlEvents: UIControlEvents.TouchUpInside)
keyPadSVB.addArrangedSubview(button)
keyCount++
}
keyPadSV.addArrangedSubview(keyPadSVB)
}
keyPadWindow.addSubview(keyPadSV)
self.view.addSubview(keyPadWindow)
}
Don't add any constraints! That is exactly the job of the stack view. Just make the button an arranged view of the stack view and stop.
If the problem is that the button is to be inserted in the midst of the existing arranged views, well, that is what insertArrangedSubview:atIndex: is for. See IOS stackview addArrangedSubview add at specific index

How to add multiple subviews on view without hiding the last added subview?

This is my code
var button = [UIButton?](count:3, repeatedValue: UIButton())
for i in 0...2{
button[i]!.tag = i
button[i]!.frame = CGRectMake(CGFloat(i) *(collectionViewLove?.frame.width)!/3,0,(collectionViewLove?.frame.width)!/3, 30)
button[i]!.setTitle(titleForButtonsOneSelected[i], forState: .Normal)
button[i]!.titleLabel?.adjustsFontSizeToFitWidth = true
button[i]!.layer.borderWidth = 1.0
button[i]!.layer.borderColor = UIColor.blueColor().CGColor
button[i]!.backgroundColor = UIColor.clearColor()
button[i]!.setTitleColor(UIColor.blackColor(), forState: .Normal)
view.addSubview(button[i]!)
}
The problem is only the last button added to the view is getting shown. How do I show all the button objects in the view?
EDIT:
The frame values for UIButton I am getting
(0.0, 0.0, 106.666666666667, 30.0)
(106.666666666667, 0.0, 106.666666666667, 30.0)
(213.333333333333, 0.0, 106.666666666667, 30.0)
let buttons = Array(0...2).map { number -> UIButton in
let button = UIButton(frame: CGRectMake(CGFloat(number) * collectionViewLove!.frame.width / 3, 0, collectionViewLove!.frame.width / 3, 30))
button.tag = number
button.setTitle(titleForButtonsOneSelected[number], forState: .Normal)
buttontitleLabel?.adjustsFontSizeToFitWidth = true
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.blueColor().CGColor
button.backgroundColor = UIColor.clearColor()
button.setTitleColor(UIColor.blackColor(), forState: .Normal)
view.addSubview(button)
return button
}
This way you have 3 different buttons in buttons. You can also customize the buttons right there as well.
Edit:
let buttons = Array(0...2).forEach {
let button = UIButton(frame: CGRectMake(CGFloat($0) * collectionViewLove!.frame.width / 3, 0, collectionViewLove!.frame.width / 3, 30))
button.tag = $0
button.setTitle(titleForButtonsOneSelected[$0], forState: .Normal)
buttontitleLabel?.adjustsFontSizeToFitWidth = true
button.layer.borderWidth = 1.0
button.layer.borderColor = UIColor.blueColor().CGColor
button.backgroundColor = UIColor.clearColor()
button.setTitleColor(UIColor.blackColor(), forState: .Normal)
view.addSubview(button)
}
It looks like button[i]!.frame = CGRectMake(CGFloat(i) *(collectionViewLove?.frame.width)!/3,0,(collectionViewLove?.frame.width)!/3, 30) is producing the same frame each time, so your buttons will be superimposed on each other. You need to have something dynamic in your call to have them be in different spots and visible.

Resources