I am working through the "Start Developing iOS Apps" tutorial provided by Apple, and having a problem in the Implement a Custom Control section. I have tried everything to get the button to print something to the console, but can't get it to work. I am on the section in the tutorial "Add Buttons to the View" about a fifth of the way down the page.
Everything else works fine.
I have set up the RatingControl.swift file as follows:
import UIKit
class RatingControl: UIView {
// MARK: Initialisation
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Create a square button
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
// Set the background colour of the button
button.backgroundColor = UIColor.darkGray
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(_:)), for: .touchDown)
// Add the button to the view
addSubview(button)
}
// MARK: Button Action
func ratingButtonTapped(_: UIButton) {
print("Button pressed")
}
}
A few things there are slightly different to the tutorial, but that is because Xcode says things have changed.
In my Main.storyboard file I have added a View using the RatingControl class. The button displays but nothing happens when I click it.
Any help would be appreciated. If any extra information is needed please let me know.
There is actually a hierarchy of three views in this story:
A stack view (not mentioned in the original question)
The RatingControl
The button
Well, the stack view, a complicated control whose job involves a lot of voodoo with the constraints of its arranged subviews, is reducing the size of the RatingControl to zero. The button is thus outside the RatingControl's bounds (easily proved by giving the RatingControl a background color — the background color doesn't appear). The RatingControl does not clip to its bounds, so the button is still visible — but, being outside its superview, it is not tappable.
As for why the stack view is reducing the size of the RatingControl to zero: it's because the RatingControl has no intrisicContentSize. The reason why we need this is complicated and has to do with how a stack view manipulates its arranged subviews and gives them constraints. But in essence, it makes a lot of its decisions based on an arranged view's intrinsicContentSize.
In the comments you said:
In size inspector it has intrinsic size set to placeholder, width 240, height 44. Or is there something I need to do to stop the stack view making it zero sized?
The key thing to understand here is that that setting in the size inspector is only a placeholder. It is merely to shut the storyboard up so that it won't complain. It doesn't actually give the view any intrinsic size at runtime. To do that, you must actually override the intrinsicContentSize property, in your code, as a computed variable returning an actual size.
Looks like you're stumbling through the "Start Developing iOS Apps (Swift)" tutorial before it's updated for iOS 10 and Swift 3. Amazing you've made it this far! Anyhow, #matt has a great technical explanation below but did not include the actual code to get your tutorial working:
Add the following to your class definition (outside the init function):
override var intrinsicContentSize: CGSize {
return CGSize(width: 240, height: 44)
}
then your code will work : -
class RatingView: UIView {
// MARK: Initialisation
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Create a square button
let button = UIButton(frame: CGRect(x: 200, y: 100, width: 44, height: 44))
// Set the background colour of the button
button.backgroundColor = UIColor.darkGray
button.addTarget(self, action: #selector(self.ratingButtonTapped(_:)), for: .touchDown)
// Add the button to the view
addSubview(button)
}
// MARK: Button Action
func ratingButtonTapped(_: UIButton) {
print("Button pressed")
}
}
Related
I´ve looked around but can´t find a solution...
I have a MPVolumeView in my app to control the unit´s system volume. I changed the MPVolumeView SliderThumbImage to an image of my own and what I´ve noticed is 2 bugs:
Bug 1 :
The thumb image sometimes gets offset horizontally to the right.
this usually happens when I run from Xcode and the phone´s volume is at maximum. see the image where the bug happened and I bring the volume down to a minimum.
if I bring the volume to minimum, close the app and reopen it, it will be put at the correct location. I think it may have something to do with how the units volume translates into a value for the slider and where the image gets positioned according to said value, but I´m unsure how to solve this. Bug 1
Bug 2: Apple´s own volume indicator layer sometimes shows up and sometimes does not.
I would rather it didn´t at all.
I use a view in the storyboard which I classed MPvolumeView and then in the viewdidload I use the following code to put the image
let faderknob : UIImage = UIImage.init(imageLiteralResourceName:"faderknob50.png")
func setVolumeThumbImage(_ image: UIImage?,for state: UIControl.State){
volumeView.setVolumeThumbImage(faderknob, for: .normal)
volumeView.showsRouteButton = false
}
setVolumeThumbImage(faderknob, for: .normal)
any help on how to fix theses 2 bugs would be great!
Thanks
Was also experiencing your first bug: how higher the volume, how greater the offset of the thumbimage to the right on intial appearance of the volumeview. When the app is relaunched from the background the offset is gone.
When overriding volumeSliderRect in a subclass, the problem is solved:
import MediaPlayer
class VolumeView: MPVolumeView {
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
return bounds
}
}
A side effect is that the thumb image now gets animated from the left to the initial position on first appearance. Actually looks not too bad in my opinion.
As you now have your own subclass, you can set your custom thumbimage during init:
import MediaPlayer
class VolumeView: MPVolumeView {
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
setValue(false, forKey: "showsRouteButton") // no deprecated warning
setVolumeThumbImage(UIImage(named: "volume.slider.thumb"), for: .normal)
}
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
return bounds
}
}
I have a strange issue with my NavigationBarItems. After first app start (when application was not running in background) the BarButton is misplaced (see Screenshot 1). The Button title should be "PDF".
Screenshot 1
However, when i press the home button and open up the app again (from background), the position is correct (Screenshot 2).
Screenshot 2
I can´t figure out what the problem is. I use a custom titleView for the navigation bar, which looks like that:
class TitleView : UIView {
var titleLabel:UILabel!
init(title:String) {
super.init(frame: CGRect(x: 0, y: 0, width: 200, height: 44))
titleLabel = UILabel()
titleLabel.textAlignment = .Center
titleLabel.font = UIFont.normalFont(15)
titleLabel.text = title.uppercaseString
titleLabel.textColor = UIColor.primaryColor()
self.addSubview(titleLabel)
titleLabel.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(self.snp_edges)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension UIViewController {
func setTitleView(title:String) {
self.navigationItem.titleView = TitleView(title: title)
self.view.backgroundColor = UIColor.whiteColor()
}
And i initialize the navigationTitle and item in viewDidLoad as follows:
self.setTitleView("Tanzkarte")
let sendDanceCardButton = UIBarButtonItem(title: "PDF", style: .Plain, target: self, action: #selector(DanceCardController.sendDanceCard))
self.navigationItem.rightBarButtonItem = sendDanceCardButton
I didn`t find any solution for that problem in the internet and hope someone of you has a solution for it.
EDIT: the custom title view is not the issue. Even when I don´t use any title for the navigation bar, the button is misplaced.
Your title view is initialized with an explicit width which probably exceeds the maximum width allowed by the navigation bar. Try to init it with zero size, and call sizeToFit() after initialization.
Problem solved. See end of post.
Sorry if this is a bit long but I'm hoping I've included as much info to get this solved.
Brief overview of problem: Enter value in a textField using my custom keypad. Tap done button(should trigger view.endEditing) and some textFields will cause the app to freeze, most the time Xcode won't even throw an error but instead just restart the app, but i did catch one once(pic below). It works as expected on some textFields.
So I have a view controller with a bunch of textFields for the user to fill out which then performs calculations.
I have made a custom Keypad which essentially is the decimal pad with a "Done" button. I did this by making an keyboard.xib file and a keyboard.swift file.
Heres a snapshot of the error, I've included a whole bunch of my code below incase I'm using a method that isn't the best.
This is how the keyboard.swift file looks:
import UIKit
// The view controller will adopt this protocol (delegate)
// and thus must contain the keyWasTapped method
protocol KeyboardDelegate: class {
func keyWasTapped(character: String)
func keyDone()
func backspace()
}
class keyboard: UIView {
// This variable will be set as the view controller so that
// the keyboard can send messages to the view controller.
weak var delegate: KeyboardDelegate?
// MARK:- keyboard initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
let xibFileName = "Keyboard" // xib extention not included
let view = NSBundle.mainBundle().loadNibNamed(xibFileName, owner: self, options: nil)[0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
// MARK:- Button actions from .xib file
#IBAction func keyTapped(sender: UIButton) {
// When a button is tapped, send that information to the
// delegate (ie, the view controller)
self.delegate?.keyWasTapped(sender.titleLabel!.text!) // could alternatively send a tag value
}
#IBAction func backspace(sender: UIButton) {
self.delegate?.backspace()
}
#IBAction func Done(sender: UIButton) {
self.delegate?.keyDone()
}
}
In the viewController I'm pretty sure I've included all the necessary things to access the keyboard seeing as it works for some textFields. Such as:
class myViewController: UITableViewController,UITextFieldDelegate, KeyboardDelegate
Then in viewDidLoad set each textField delegate:
self.textField1.delegate = self
self.textField2.delegate = self
self.textField3.delegate = self
// initialize custom keyboard
let keyboardView = keyboard(frame: CGRect(x: 0, y: 0, width: 0, height: numpad.height))
keyboardView.delegate = self // the view controller will be notified by the keyboard whenever a key is tapped
// replace system keyboard with custom keyboard
textField1.inputView = keyboardView
textField2.inputView = keyboardView
textField3.inputView = keyboardView
Then this function (which seems to me to be the problem):
func keyDone() {
view.endEditing(true)
//activeTextField.resignFirstResponder()
print("please dont freeze")
}
I have checked all the connections, they seem to be fine.
Let me know if I can add any more info to help work it out.
Many Thanks.
Solved!!!
I suppose ill just put it down to beating my head over it rather than taking a break from the screen! Still I'm confused why it wasn't given a more specific error.
The problem was that in some cases one of the functions was dividing by zero (this is undefined... not possible) but a good thing to take from this(thank you Olivier) is the Instruments Tools to help find where abouts the code was losing its mind. This tutorial helped me understand how to use instruments! So once I could see where it was going crazy I set up a bunch of print statements to watch the values as they went into the 'problem' calculation, where I found the denominator to be zero. Bit of rearranging the code around to avoid this and problem solved!
This error message is basically saying that there is a memory issue, try running the code with instruments (Allocations in particular) this might reveal is there is something amiss with your keyboard
Edit 2: for anyone finding this error message in future (actual solution in this case)
Double check any code code running after keyDone() to see if there are any infinite loops or situations that would cause the compiler to assume an infinite amount of memory is required. In this case a line of code was dividing by zero, causing a fatal memory error (unable to allocate the N/A value it generated)
I have a circular UIButton inside a UITableViewCell that I'm creating programmatically. It works fine, until I add a constraint to center it in it's superview.
This is what my button looks like before I add the constraint to it:
And this is what it looks like after I add the constraint:
Here's my code:
var thumbnailBtn = UIButton.buttonWithType(.Custom) as! UIButton
func setupViews() {
backgroundColor = .colorFromCode(0x3f3f3f)
thumbnailBtn.frame = CGRectMake(0, 0, 80, 80)
thumbnailBtn.layer.cornerRadius = 0.5 * thumbnailBtn.bounds.size.width
thumbnailBtn.backgroundColor = UIColor.greenColor()
thumbnailBtn.setTitle("Test", forState: UIControlState.Normal)
thumbnailBtn.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchUpInside)
contentView.addSubview(menuControlContainerView)
menuControlContainerView.addSubview(thumbnailBtn)
updateConstraints()
}
override func updateConstraints() {
if !didSetupConstraints {
UIView.autoSetPriority(1000) {
self.menuControlContainerView.autoSetContentCompressionResistancePriorityForAxis(.Vertical)
}
menuControlContainerView.autoPinEdgeToSuperviewEdge(.Top, withInset: 0)
menuControlContainerView.autoPinEdgeToSuperviewEdge(.Leading, withInset: 0)
menuControlContainerView.autoPinEdgeToSuperviewEdge(.Trailing, withInset: 0)
menuControlContainerView.autoPinEdgeToSuperviewEdge(.Bottom, withInset: 0)
thumbnailBtn.autoCenterInSuperview()
didSetupConstraints = true
}
super.updateConstraints()
}
I noticed if I add this line of code [ thumbnailBtn.autoSetDimensionsToSize(CGSizeMake(80, 80)) ] to updateConstraints() it works and looks like this:
But is this the cleanest way to do this since I'm setting the size twice?
Any help will greatly be appreciated. Thanks.
You set the size in the initial frame but that is inappropriate when you later start using constraints to specify the position (and the superview layout). Really you should just create a plain view (using the appropriate method from the 3rd party library you're using) and then specify the size and position using constraints (the ones you've already tried are appropriate).
The view is most likely resized when you centre it because the 3rd party library removes the auto-resizing mask conversion to constraints, this allows the constraint engine to basically do whatever it wants as any settings satisfy the equations.
I have a UIButton and a UIView. The View sits above the button, and is larger than the button in size. The view itself is what I want to have accept the touch events, and I'd like to forward them to the button, so that all the normal button visual changes on touch happen.
I can't seem to make this work-- my UIView implements touchesBegan/Moved/Ended/Cancelled, and turns around and calls the same methods on the button with the same arguments. But the button doesn't respond.
I have ensured that the touch methods are in fact being called on the UIView.
Is there something obvious I'm missing here, or a better way of getting the control messages across? Or is there a good reason why this shouldn't work?
Thanks!
[Note: I'm not looking for workarounds for the view+button design. Let's assume that that's a requirement. I'm interested in the notion of controls that are touch proxies for other controls. Thanks.]
Create a ContainerView that contains a button and override hitTest:withEvent: so that it returns the UIButton instance.
Are you sure your methods are being called? If you haven't set userInteractionEnabled=YES, then it won't work.
I've used such touch-forwarding before without problems, though, so I don't know why you're seeing the problems you're seeing.
I use the small class below when I need a larger touch target than the visual size of the button.
Usage:
let button = UIButton(...) // set a title, image, target/action etc on the button
let insets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
let buttonContainer = TouchForwardingButtonContainer(button: button, insets: insets) // this is what you add to your view
// a container view for a button that insets the button itself but forwards touches in the inset area to the button
// allows for a larger touch target than the visual size of the button itself
private class TouchForwardingButtonContainer: UIView {
private let button: UIButton
init(button: UIButton, insets: UIEdgeInsets) {
self.button = button
super.init(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
let constraints = [
button.topAnchor.constraint(equalTo: topAnchor, constant: insets.top),
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom),
button.leftAnchor.constraint(equalTo: leftAnchor, constant: insets.left),
button.rightAnchor.constraint(equalTo: rightAnchor, constant: -insets.right)
]
NSLayoutConstraint.activate(constraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return button
}
}