performSegueWithIdentifier not working for programmatically generated UIButtons (iOS/Swift) - swift

Xcode 6, Swift, iOS8
I have a view controller that is populated by dynamically generated UIButtons. The number of buttons is dependent on a data feed, and is not static. I count the number of objects in the feed and generate a button for each object. Each button is supposed to segue into a details view that displays the information for its corresponding object.
In the Interface Builder I have created a segue between the two View Controllers and named it. I have not added an IBAction to initiate the segue as I cannot tie it to a specific button.
Inside the View Controller Class I execute the following:
#IBOutlet weak var localScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
//Define the button dimensions
let buttonWidth:CGFloat = 200
let buttonHeight:CGFloat = 113
var xPos:CGFloat = 0
var scrollViewContent:CGFloat = 0
var thumbURL:String
//keeps track of the number of videos. The count is used to set a tag on the button to help identify it.
var vidCount:Int = 0
//loop through the array of recommended video objects
for index in recommended{
//Create a button for each object
var myButton = UIButton.buttonWithType(UIButtonType.System) as! UIButton
myButton.frame = CGRectMake(xPos, 0.0, buttonWidth, buttonHeight)
//For the button action I call the handleTap function detailed below
myButton.addTarget(self, action: "handleTap:", forControlEvents: .TouchUpInside)
myButton.tag = vidCount
//The image for the button is pulled from a CDN. This code sets the image in the button.
let curVal = index.thumbURL
if let url = NSURL(string: curVal) {
if let data = NSData(contentsOfURL: url){
myButton.setBackgroundImage(UIImage(data:data), forState: UIControlState.Normal)
}
}
//add the button to the scroll view
localScrollView.addSubview(myButton)
let spacer:CGFloat = 10
xPos+=buttonWidth + spacer
scrollViewContent += buttonWidth + spacer
localScrollView.contentSize = CGSize(width: scrollViewContent, height: buttonHeight)
vidCount += 1
}
}
//function to handle the tap action
func handleTap(sender:UIButton){
//Set the variable that will be passed to the next view controller
curRecVid = recommended[sender.tag]
//initiate the segue
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("toSingle", sender: self)
}
}
//prepare the data to be transferred to the next view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "toSingle") {
var targetViewController = segue.destinationViewController as! SingleViewController
targetViewController.targetVid = curRecVid
}
}
}
When I run the app in the simulator, it gets to the self.performSegueWithIdentifier("toSingle", sender: self) call and then terminates to the following uncaught exception: 'NSInvalidArgumentException', reason: '-[UILabel copyWithZone:]: unrecognized selector sent to instance 0x7fd468c8ddc0'
Any assistance in helping to track down the cause of this exception would be greatly appreciated.

It appears that the class file for the target View Controller had some sort of problem. After a comment from Epic Defeater, I wiped out its swift file, re-generated it and put in the exact same code (literally, copy/paste) and that did the trick.

Related

How subViewA change the param on subviewB?

I created two views in Main.storyboard with
class ViewController: UIViewController
{
var cardsOnTableCounts = 0
#IBOutlet weak var headStackView: HeadStackView!
#IBOutlet weak var gridCardView: GridView!
#objc func touchDeal3Card ()
{
gridCardView.cellCounts += 3
}
}
I created a button in class HeadStackView
class HeadStackView: UIView
{
var deal3Card: UIButton = UIButton.init()
override func draw(_ rect: CGRect) {
setDeal3Card()
}
private func setDeal3Card () {
let leftButtonFrame : CGRect = CGRect(x: bounds.maxX/15, y: bounds.maxY/6, width: bounds.maxX/3, height: bounds.maxY*0.7)
deal3Card = UIButton.init(frame: leftButtonFrame)
deal3Card.backgroundColor = UIColor.purple
deal3Card.setTitle("Deal 3 Cards", for: .normal)
deal3Card.addTarget(self, action: #selector(ViewController.touchDeal3Card), for: .touchUpInside)
addSubview(deal3Card)
}
In GridView.swift
class GridView: UIView
{
var cellCount: Int = 5 {didSet { setNeedsDisplay(); setNeedsLayout()}}
override func layoutSubviews() {
super.layoutSubviews()
drawGrid() // it draws cellCount of cells
}
}
My goal is when the UIButton deal3Card is pressed, gridCardView will redraw itself with more cards on gridCardView.
The code above passed compiler and show the button (deal3card). but when I click on button, it gets exception:
2018-10-23 19:52:54.283032+0200 gridTest[23636:5175258]
-[gridTest.HeadStackView touchDeal3Card]: unrecognized selector sent to instance 0x7fb97180c8f0
2018-10-23 19:52:54.287469+0200 gridTest[23636:5175258] ***
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[gridTest.HeadStackView
touchDeal3Card]: unrecognized selector sent to instance
0x7fb97180c8f0'
The proximate cause of the crash is this line:
deal3Card.addTarget(self,
action: #selector(ViewController.touchDeal3Card), for: .touchUpInside)
The target self is wrong. You want to send this message to the view controller, which implements touchDeal3Card. But in your code, self is the button, which doesn't.
The simplest solution, given the architecture you've constructed, is to replace self with nil. This will cause the touchDeal3Card to percolate up the responder chain and reach the view controller.
Having said that, I would suggest that the architecture itself is wrong. View controller should control views; views should not control themselves. The view controller, not the view, should be creating the button. This is well indicated by the fact that this code is totally wrong:
override func draw(_ rect: CGRect) {
setDeal3Card()
}
That is a total misuse of draw and is going to land you in terrible trouble. The only thing you should do in draw is (wait for it) draw. Adding subviews in draw is as wrong as anything could possibly be.

Xcode Swift Navigation Bar on different ViewControllers

I have got a label that appears in the NavigationBar when the View of the FirstViewController opens for the first time. The label shows a number that should be able to change. If I click a button on the FirstViewController the View of the SecondViewController shows up in which's NavigationBar the label of the FirstViewController is still visible. When I change the number that is written in the label by clicking on a button in the SecondViewController the number of the label only changes when I go back to the View of the FirstViewController. That is because I update the title in the ViewDidLoad String loop.
Now my question is:
I want the number of the label to be changed at the moment when I click the button in the SecondViewController, although the label was defined in the Code of the FirstViewController. The number of the label shows an amount of money.
This is the Code of the FirstViewController:
var moneyLabel: UILabel?
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationLabel()
let newMoney:String = String(format: "%f", money)
updateTitle(title: "\(newMoney)")
}
func updateTitle(title: String) {
if let myTitleView = self.moneyLabel {
myTitleView.text = title
}
}
func setupNavigationLabel() {
let navigationBar = self.navigationController?.navigationBar
let moneyFrame = CGRect(x: 300, y: 0, width:
(navigationBar?.frame.width)!/2, height: (navigationBar?.frame.height)!)
moneyLabel = UILabel(frame: moneyFrame)
moneyLabel?.text = "\(money)"
navigationBar?.addSubview(moneyLabel!)
}
It seems that your problem is with the second ViewController but you are not sharing any code!!! Use the below to achieve your objective.
class secondViewControler:UIViewController{
var money:Int
var moneyLabel:UILAbel?
override func viewDidLoad(){
self.money= //pass the amount, i dont know which way you use to store and retrieve the amount
moneyLabel.text="\(money)"
}
#IBAction updateMoney(){
//get the amount from the textfield say the output is X
money=X
moneyLabel.text="\(money)"
}
}

swift call a func from another viewcontroller

I would like to call a func from another viewcontroller.
here with the code in pubListViewController: It is working fine.
override func viewDidAppear(_ animated: Bool) {
navigationBarTitleImage(imageTitle: "IconTitle")
}
func navigationBarTitleImage(imageTitle: String) {
// 1
// let nav = self.navigationController?.navigationBar
// 2
// nav?.barStyle = UIBarStyle.black
// nav?.tintColor = UIColor.yellow
// 3
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
imageView.contentMode = .scaleAspectFit
// 4
let image = UIImage(named: imageTitle)
imageView.image = image
// 5
navigationItem.titleView = imageView
}
now I try to call it in another viewcontroller as below, but it shows nothing.
override func viewDidAppear(_ animated: Bool) {
pubListViewController().navigationBarTitleImage(imageTitle: "addTitle")
}
When you're using notation like that pubListViewController() you call free empty initializer of pubListViewController which creates new instance of class pubListViewController, but you do already have one in your screens flow I bet, so all changes made by function you're calling later are being applied to invisible instance of pubListViewController.
To solve this problem you need a reference actually displaying instance of pubListViewController from your another viewcontroller
In another viewcontroller you can create a property of type pubListViewController, then before showing another viewcontroller set its property to self, and use that property from wherever you want in another viewcontroller.
class PubListViewController: UIViewController {
func prepareForSegue(/**/){ // actually do that in the place where you showing your another viewcontroller, I don't know if you're using segues or not
destinationViewController.parentPubListViewController = self
}
}
class AnotherViewController: UIViewController {
// declare property (weak and optional to avoid crashes or memory leaks if you forget to set that property from parent view controller
weak var parentPubListViewController: PubListViewController?
// use it anywhere you need
parentPubListViewController?.navigationBarTitleImage(imageTitle: "addTitle")
}

pass custom parameter to uibutton #selector swift 3

I have a 2 classes where I am passing uistackviews from one class to other. I want the controls to be created in same stackview. Hence I am passing the view in all the render function parameters. I also want that view to be passed with action #selector of uibutton
class 1:
class ViewController: UIViewController {
func createbutton(parentview: UIStackView) {
let buttn = UIButton()
buttn.backgroundColor = .gray
buttn.setTitle("testttt", for: .normal)
buttn.frame.size.height = 30
buttn.frame.size.width = 40
buttn.addTarget(self, action: #selector(anotherbutton(parentview:)), for: .touchUpInside)
parentview.addArrangedSubview(buttn)
}
func anotherbutton(parentview: UIStackView) {
//another button here
}
func loadpage() {
print("loadpage")
}
}
Class 2:
class plugin : UIViewController {
let vw = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
let parentview = getparentnode()
vw.createbutton(parentview: parentview)
}
func getparentnode() -> UIStackView {
let parentnode = UIStackView()
parentnode.axis = UILayoutConstraintAxis.vertical
parentnode.distribution = UIStackViewDistribution.equalSpacing
parentnode.alignment = UIStackViewAlignment.center
parentnode.spacing = 16.0
parentnode.tag = 50
parentnode.translatesAutoresizingMaskIntoConstraints = false;
self.view.addSubview(parentnode)
//Constraints
parentnode.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
parentnode.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
return parentnode
}
}
but this throws an error unrecognized selector sent to instance 0x7b25e010'
How to pass the UIView in action selector parameter ? Thank you for any help
You can't. The only things that you can pass through a selector is:
Nothing
The object itself (in this case the button)
These scenarios would look like this:
button.addTarget(self, action: #selector(myFunc), ...) //no parameters
or
button.addTarget(self, action: #selector(myFunc(_:)) //passes itself (the button)
If you want to pass the value of a view to another ViewController I recommend using the prepareForSegue method. That is how you are supposed to pass data from ViewController to ViewController.
In terms of the rest of your code, I believe you are breaking the MVC design pattern by creating an instance of your class in another class (this line: let vw = ViewController()). First of all, this will create an entirely new instance if your ViewController, which isn't the same as the one running on your device. Second of all, this is bad practice. You should be allowing each viewController to manage itself and not have outwards interference from other viewControllers. Using prepareForSegue is an example of using the MVC design pattern effectively.
Hope this helped.

Load data in new view when button is clicked with swift

I am developing an app, where I create buttons programmatically. When I click a button, it will request data from a database and show it in another view.
I use button.tag to determine what data to request, and I can only get the tag once the button is clicked.
However when I click the button, it shows nothing the first time. I must click it again to see the data which I want.
override func viewDidLoad() {
super.viewDidLoad()
//parseJSON(tagId)
createButton()
// Do any additional setup after loading the view, typically from a nib.
}
func createButton(){
var j:CGFloat=60
for var i:Int = 0 ; i < myImages.count;i = i+1 {
let myButton = UIButton()
myButton.setImage(UIImage(named: "carasusto.jpg"), forState: UIControlState.Normal)
myButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
myButton.frame = CGRectMake(j, j+60, 50, 50)
myButton.tag = i //assign a tag to every button
myButton.addTarget(self, action: "segueToCreate:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(myButton)
j=j+60
print(myImages[i])
}
}
and
#IBAction func segueToCreate(sender: UIButton){
tagId = String(sender.tag)//tagId needs to fetch the information
parseJSON(tagId)
performSegueWithIdentifier("segueToView", sender:self)
}
func parseJSON(tagID:String){
Alamofire.request(.GET, "http://smarttags-001-site1.btempurl.com/SmartTagsRequests.aspx", parameters: ["AjaxFunc":"GetTagAttr","TagID":"\(tagID)"]).validate().responseJSON{ response in
switch response.result{
case .Success:
if let value = response.result.value {
let json = JSON(value)
print("JSON: \(json)")
self.TagName = json[0]["TagName"].stringValue
NSLog("\(self.TagName)")
self.ContentTitle = json[0]["ContentTitle"].stringValue
NSLog("\(self.ContentTitle)")
}
case .Failure(let error):
print(error)
}enter code here
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var ViewTest : ViewTwo = segue.destinationViewController as! ViewTwo
var TagNameLabel = UILabel()
TagNameLabel.frame = CGRectMake(74, 112, 182, 64)
ViewTest.view.addSubview(TagNameLabel)
TagNameLabel.text = TagName
var ContentTitleLabel = UILabel()
ContentTitleLabel.frame = CGRectMake(74, 180, 182, 64)
ViewTest.view.addSubview(ContentTitleLabel)
ContentTitleLabel.text = ContentTitle
}
}
To follow up to MirekE's answer, here are some other steps you may want to consider:
Consider using Auto Layout in place of hard-coded frames, so your UI would adapt to different size classes.
Consider alternate approaches for showing an interactive list (of images) instead of programmatically adding buttons. For example, you could use a prototype (table view or collection view) cell. Cells are selectable and can take the place of a button. Other benefits include:
A scrollable container, should you have more buttons than would fit on screen.
A single segue (or selection) is handled by the storyboard prototype cell, instead of needing to make each programmatic "button" perform a segue (or action). Since you'd know which cell was selected, you'd no longer need a tag to figure that out.
Consider passing parameters to your destination view controller, instead of trying to instantiate and create controls in prepareForSegue. The destination's view is not loaded at that point.
Consider allowing the UI to feel more responsive, such as by performing the segue and showing placeholder details which you can then update once the network request completes. Otherwise, the user may have to wait for the network response before the segue occurs (which might make them think nothing happened and needlessly tap again, leading to an additional network request).
In general, Apple (or someone else) has already provided some way to do what you want, ideally leading to less code that you have to write, debug, test, and maintain to accomplish what you want your app to do.
Most likely cause of the problem is your call to parseJson followed by prepareForSegue. parseJson is asynchronous and you don't get data back from your service before prepareForSegue is called. As a first step, move the prepareForSegue to the completion block of the parseJson.