Why is this swift button setup crashing? - swift

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.

Related

How to catch all events in Swift without breaking other event handlers

How can I catch any touch up event in my application view without affecting any other event in subview or subview of the subview?
Currently whenever I add UILongPressGestureRecognizer to my root view, all other addTarget functions break in subviews and their subviews.
func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith: UIGestureRecognizer) -> Bool {
return true
}
override func viewDidLoad() {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
button.setTitle("Click me", for: .normal)
button.addTarget(self, action: #selector(buttonClick), for: .touchDown)
self.view.addSubview(button)
initLongTapGesture() // This kills the button click handler.
}
func initLongTapGesture () {
let globalTap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
globalTap.delegate = self
globalTap.minimumPressDuration = 0
self.view.addGestureRecognizer(globalTap)
}
#objc func tapHandler(gesture: UITapGestureRecognizer) {
if ([.ended, .cancelled, .failed].contains(gesture.state)) {
print("Detect global touch end / up")
}
}
#objc func buttonClick() {
print("CLICK") // Does not work when initLongTapGesture() is enabled
}
The immediate solution to allow the long press gesture to work without preventing buttons from working is to add the following line:
globalTap.cancelsTouchesInView = false
in the initLongTapGesture function. With that in place you don't need the gesture delegate method (which didn't solve the issue anyway).
The big question is why are you setting a "long" press gesture to have a minimum press duration of 0?
If your goal is to monitor all events in the app then you should override the UIApplication method sendEvent. See the following for details on how to subclass UIApplication and override sendEvent:
Issues in overwriting the send event of UIApplication in Swift programming
You can also go through these search results:
https://stackoverflow.com/search?q=%5Bios%5D%5Bswift%5D+override+UIApplication+sendEvent
Another option for monitoring all touches in a given view controller or view is to override the various touches... methods from UIResponder such as touchesBegan, touchesEnded, etc. You can override these in a view controller class, for example, to track various touch events happening within that view controller.
Unrelated but it is standard to use the .touchUpInside event instead of the touchDown event when handing button events.

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.

How to create a raised center tab bar button

I've examined many GitHub and StackOverflow solutions to creating a raised center tab bar button and tried multiple times into creating one but I always end of right back to square one.
So below are the steps of what I've been doing, if someone can solve the problem it would be greatly appreciated.
Created a Tab Bar Controller to navigate to 5 different View Controllers
Created a Custom Class for the Tab Bar Controller called CustomTabBarController
Inside my CustomTabBarController File is
import UIKit
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupMiddleButton()
}
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height:64))
var menuButtomFrame = menuButton.frame
menuButtomFrame.origin.y = view.bounds.height - menuButtomFrame.height
menuButtomFrame.origin.x = view.bounds.width/2 - menuButtomFrame.size.width/2
menuButton.frame = menuButtomFrame
view.addSubview(menuButton)
menuButton.setImage(UIImage(named: "MidPhoto"), for: .normal)
menuButton.addTarget(self, action: #selector(menuButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
tabBarController?.tabBar.addSubview(menuButton)
}
#objc private func menuButtonAction(sender: UIButton) {
selectedIndex = 2
}
}
That's it. I only edited 1 file (CustomTabBarController) and it works but the image is appearing into unwanted View Controllers and I've done multiple things like: Hide Bottom Bar On Push, self.tabBarController?.tabbar.isHidden = true, and so on.
What can I do to fix this?

Swift UIButton not appearing on screen

I have a view in my tabbar controller where I would like to show a button. I create this button programmatically based of a condition, therefore I use the following code but nothing is appearing:
override func viewDidLoad() {
super.viewDidLoad()
if !Settings.getIsConnected() {
notConnected()
}
}
func notConnected() {
let connectBtn = UIButton(frame: CGRect(x: self.view.center.x, y: self.view.center.y, width: 200, height: 45))
connectBtn.setTitle("Connect", forState: .Normal)
connectBtn.addTarget(self, action:#selector(self.pressedConnect(_:)), forControlEvents: .TouchUpInside)
self.view.addSubview(connectBtn)
print("Button created")
}
func pressedConnect(sender: UIButton!) {
}
I am clueless on what I am doing wrong. Anyone got suggestions? Cause it does print out "Button created" so it definitely runs the code inside the noConnected() method.
Add a background color to your UIButton and add a tint color to the title. This will resolve the problem
Try moving the code to viewDidAppear and see if the button is showing up.
The frame is not correctly set when in viewDidLoad. Use the method viewDidLayoutSubviews for the earliest possible time where the frame is correctly setup for a ViewController.
With this code change, you will need some additional logic for when your button should be added as a subview though.
A programmatically created button may not show up because of more reasons, e.g:
the tint color is not set
the background color is not set
the button is not added to the view hierarchy
the button is hidden
In your case, you should change the tint color or the background color of your button.
E.g.:
Swift 4.2:
private lazy var connectButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = .green
button.setTitleColor(.black, for: .normal)
button.setTitle(NSLocalizedString("Connect", comment: ""), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(connectButton)
}
You can re-check the button properties in the storyboard that it is not hidden.

Simple tvOS UIButton is not working

I'm trying to implement a simple UIButton (tvOS & Swift), but the TouchDown event isn't firing. (tried also TouchUpInside).
In the simulator I can see visually that the button is pressed.
my ViewController.swift code is:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: UIButtonType.System)
button.frame = CGRectMake(100, 100, 400, 150)
button.backgroundColor = UIColor.greenColor()
button.setTitle("Test", forState: UIControlState.Normal)
button.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchDown)
self.view.addSubview(button)
self.view.userInteractionEnabled = true
}
func buttonAction(sender:UIButton!){
NSLog("Clicked")
}
}
What do I need to do in order to detect a button click?
You shouldn't be using ".TouchDown" or ".TouchUpInside" on tvOS, since that doesn't do what you might expect: you probably want to use "UIControlEvents.PrimaryActionTriggered" instead.
TouchUpInside is triggered by actual touches, which isn't really what happen. If you want the Select button press on the remote to trigger your button, you should use PrimaryActionTriggered.
UIControl.Event.primaryActionTriggered is required for tvOS addTarget method:
button.addTarget(self, action: #selector(ViewController.didTapAcctionButton(_:)), for: .primaryActionTriggered)