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.
Related
I'm working on a crossword puzzle app for swift and I am having trouble getting a UIButton from a subview in the UIButton that is a textfield.
The text field takes up the space where the UIButton title should be, but when clicking on the textfield it doesn't click the UIButton.
The UIButton itself as of now, highlights all the UIButtons of the some column and row.
There a few things I've tried such as graving the superclass of the subview
var myButton: CustomButton = textfield.superclass as? CustomButton
and I also tried using
var myObject : CustomButton? {
return view.compactMap({$0 as? CustomButton }).first
}
In the CustomButton.swift
class CustomButton: UIButton {
var number: Int = 0
var letter: Character?
var textField: UITextField!
var cornerLabel: UILabel!
// this init will intialize the button through programatically
override init(frame: CGRect) {
super.init(frame: frame)
textField = UITextField(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
textField.translatesAutoresizingMaskIntoConstraints = false
textField.center = .zero
textField.textAlignment = .center
textField.text = ""
I have a ButtonStore.swift that stores all the CustomButtons in the array so I can manage them and retrieve certain ones.
And the MainController.swift has all the reference CustomButton. I am using a UITextFieldDelegate from the MainController
class MainController : UIViewController, UITextFieldDelegate{
var buttonStore : ButtonStore!
#IBOutlet var A1 : CustomButton!
#IBAction func buttonIsPress(sender: CustomButton){
let button : CustomButton = sender
let identifier : String = sender.accessibilityIdentifier ?? ""
// Clear all backgroundbox back to white unless the background is black
buttonStore.removeHighlights()
// Highlight the button pressed and its column and row
buttonStore.highlightRowColumn(identifier: identifier)
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool
{
print("something happened")
// TRIED RECEIVING THE CUSTOMBUTTON HERE
return true
}
The expected result is to have had the CustomButton trigger when textfield is pressed or grabbing the CustomButton of the textfield pressed so I use the CustomButton as reference.
You can get the superview by
if let button = textField.superview as? CustomButton {
// Do what you want to here
}
You can grab any superview or any parent view controller generically in a type safe way by walking up the responder chain (because both UIViewController and UIView inherit from UIResponder and implement its next method):
extension UIResponder {
func firstParent<T: UIResponder>(ofType type: T.Type ) -> T? {
return next as? T ?? next.flatMap { $0.firstParent(ofType: type) }
}
}
Use like:
if let button = textField.firstParent(ofType: CustomButton.self) {
//Do button stuff here
}
This method has the advantage that you can find the next parent of a particular type even if it isn't the immediate parent (ie you may have several views between the textfield and the CustomButton and this still works, while calling superview does not).
My View hierarchy looks like this:
ElevethViewController of type UIViewController
Container View
ManagedTableEleventhViewController of type UITableViewController embedded in Container View
ManagedTableEleventhViewController contains 4 static cells containing 1 textField each and one empty static cell.
class ManagedTableEleventhViewController: UITableViewController,UITextFieldDelegate {
var hasText:Bool!
#IBOutlet weak var fullName: UITextField!
#IBOutlet weak var flatNumber: UITextField!
#IBOutlet weak var streetAddress: UITextField!
#IBOutlet weak var phoneNumber: UITextField!
//checkValue takes ELViewController parameter so that segue can be
//performed when button is touched in EleventhViewController
func checkValue(ELViewController:EleventhViewController) {
//loop through the textfields and check if they have text
for case let textField as UITextField in viewController.view.subviews {
//print is not executed meaning loop is not performed
print("some text")
if textField.text == "" {
self.hasText = false
textField.layer.borderColor = UIColor.red.cgColor
} else {
print("true value in for loop")
self.hasText = true
performSegue(withIdentifier: "elevethToTwelveth", sender: ELViewController)
}
}//end of for loop
}
class EleventhViewController: UIViewController {
var nextButtonOutlet:UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//create button programmatically
var button = UIButton(type: UIButtonType.custom) as UIButton
button = UIButton(frame: CGRect(x: 0, y: 637, width: 375, height: 50))
button.titleLabel?.textColor = UIColor.white
button.backgroundColor = UIColor(colorLiteralRed: 117/255, green: 232/255, blue: 0, alpha: 1)
button.setTitle("Next", for: .normal)
button.addTarget(self, action: #selector(EleventhViewController.nextButton), for: .touchUpInside)
self.view.addSubview(button)
self.nextButtonOutlet = button
}
func nextButton(sender: UIButton) {
//create instance of tableView
let managedTable = ManagedTableEleventhViewController()
managedTable.checkValue(viewController: self)
} //end of EleventhViewController class
Well first I can give you an answer that might satisfy you and fix your loop but I would recommend not doing it that way to alter your textfields. I would recommend doing it in cellForRow even though they may be static cells. Depending on your view setup in the cells it would look like this if the textfield is added directly to the cells and not to another view.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Testing")
for cell in tableView.visibleCells{
for sub in cell.contentView.subviews{
if sub is UITextField{
print("Textfield")
}
}
}
}
Just to follow up, if this is for validation you should'nt be only checking "" case, because you allow " ", " " etc. Use isEmpty, it should work better, if you only want to check existence of text.
Also you dont have to extract fields from subviews as you already have properties, i'm not sure if you have any other reason for this logic though.
Edit. Ooops, i just noticed your checking for textfields in a controller which does not have any visible fields, so normally your check never passes.
I think you should'nt even validate textfields for one class in another class, unless its a class handling textfield validation in general.
In EleventhViewController you have no textfields, so nothing to validate.
I have a UIButton inside my cell together with an image and a text label. I manage to change the image and label programatically, but the UIButton does not seem to respond to anything except isHidden.
This is my code, the button that is not changing is followButton:
import UIKit
class ProfileTableCell: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var profileImage: UIImageView!
#IBOutlet weak var followButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
self.profileImage.layer.borderWidth = 0.0;
self.profileImage.layer.cornerRadius = self.profileImage.frame.size.width/2;
self.profileImage.clipsToBounds = true
self.profileImage.image = UIImage(named: "belt")
self.name.text = "Bar Refaeli"
self.followButton.layer.borderColor = UIColor.black.cgColor
self.followButton.layer.borderWidth = 3.0;
self.followButton.layer.cornerRadius = self.frame.size.width/4
self.followButton.backgroundColor = UIColor.black
}
func setCell(image: UIImage, name: String){
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
The profileImage and name outlets change the appearance fine, like mentioned above.
I also tried to remove the button and bring it back in, clean xcode project, remove the outlet reference and connecting it again. Pretty frustrated by now.
I also tried to change the background color of the button through the storyboard, just for testing, and it does not change it! what does change is the titleLabel and the text color.
awakeFromNib()- Prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file.
Given that, move your code to a view initiating method like viewDidLoad or viewDidAppear(_:)
Child objects that are attributes like textLabels act differently than child view objects.
Eventually I actually solved this by tossing the table view to the garbage and implementing the same needs using a collection view. there was no problem there..
I'm trying to programmatically add a button to a view along with its action method. The key thing is that the action method should be in the same file with the button so I can drop the file into other apps. When the button is tapped I want the action method to be executed but it crashes instead. It just gives EXC_BAD_ACCESS with no log output.
Here's a simplified test app with only two classes: ViewController and BlueButton:
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let button = BlueButton(mainView: view)
button.installButton()
}
}
class BlueButton: NSObject {
var mainView: UIView
init(mainView: UIView) {
self.mainView = mainView
}
func installButton() {
let button = UIButton(frame: CGRect(x: 25, y: 100, width: 150, height: 50))
button.setTitle("Tap Me", forState: UIControlState.Normal)
button.setTitleColor(UIColor.blueColor(), forState: UIControlState.Normal)
button.addTarget(self, action: "someAction", forControlEvents: .TouchUpInside)
mainView.addSubview(button)
}
func someAction() {
println("this is someAction")
}
}
Back trace shows nothing helpful. It ends with objc_msgSend. I tried many different ways of rearranging the code but nothing I tried worked. I added an 'All Exceptions' breakpoint but it doesn't get hit. I put a breakpoint on the someAction method just to be sure it isn't called -- it isn't. Can somebody tell me what's going on?
Your BlueButton instance is deallocated when viewDidAppear returns because there is no longer a strong reference to it.
When the button is tapped, it tries to reference the instance, but it's been deallocated, which causes the crash.
You could resolve the issue a number of ways. The simplest would be to create a property in the ViewController class and store the BlueButton in it as long as the button is visible.
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.