I've created UIBarButtonItem with customView set to BadgeButton (a UIButton that has badge label added).
I am setting accessibilityIdentifier = "properIDHere" and isAccessibilityElement = true for both: aBadgeButton and aBadgeButton.badgeLabel
When running UITests the badge button is visible and accessible by the ID, but the badgeLabel is not. Do you have ideas why?
final class BadgeButton: MyBaseButton {
let badgeLabel = UILabel()
override func addSubviews() {
addSubview(badgeLabel)
}
override func setupConstraints() {
badgeLabel.snp.makeConstraints { make in
make.width.height.equalTo(badgeSize)
make.centerX.equalToSuperview().offset(badgeXOffset)
make.centerY.equalToSuperview().offset(badgeYOffset)
}
}
The runTime lable need a frame or constraint.
final class BadgeButton: UIButton {
let badgeLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView() {
self.addSubview(badgeLabel)
badgeLabel.frame = CGRect(x: self.frame.width / 2, y: 0, width: 20, height: 20)
//or you setup runTime Constraint
}
}
On the BadgeButton parent/custom view, try disabling accessibility. Leave it enabled for the UILabel.
After this, back in your XCTestCase, print out the XCUIApplication().debugDescription to see if you're now able to see the UILabel's accessibility information in the app's element hierarchy.
See this post as a possible duplicate:
Xcode UI Tests can't find views that are added programatically
Related
I'm going through Apple's iOS development with Swift tutorial and have gotten myself stuck on the 5th stage:
import UIKit
class RatingControl: UIStackView {
// MARK: Initialisation
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupButtons()
}
// MARK: Button action
#objc func ratingButtonTapped(button: UIButton) {
NSLog("Button pressed")
}
// MARK: Private methods
private func setupButtons() {
NSLog("setupButtons() called")
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
}
}
setupButtons is called and I see the message in Xcode's debug console, and the button appears as a red box on the simulator:
However, when I click on the button, nothing happens - no message is printed to the debug console.
I honestly have no clue how I would even begin debugging this, since everything compiles/runs without errors, and I've already tried placing debug messages around what seems to be the issue.
Does anyone have any ideas on what's going on?
EDIT: Here is where the class is being used, and the output in the console:
I have tested your code in Playgrounds, and it works as expected. Therefore I expect that you are using the RatingControl incorrectly. One possible bug might be that one of its superviews has isUserInteractionEnabled set to false. Check this example:
// A UIKit based Playground for presenting user interface
import PlaygroundSupport
import UIKit
class RatingControl: UIStackView {
// MARK: Initialisation
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupButtons()
}
// MARK: Button action
#objc func ratingButtonTapped(button: UIButton) {
print("Button pressed")
}
// MARK: Private methods
private func setupButtons() {
print("setupButtons() called")
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
}
}
class A: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(RatingControl(frame: CGRect(x: 0, y: 0, width: 40, height: 40)))
// if you uncomment the following line, the button will stop working
// self.view.isUserInteractionEnabled = false
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = A()
If that's not the case, include the code in which you use the RatingControl, because that's where the bug lies.
It's working normally with me, I've put self.view.addSubview(button)
instead of addArrangedSubview(button) .
I created simple button - class Btn: UIControl and then add to parent View. Inside my Btn class I have override func touchesBegan.
Why touchesBegan not callend when I tapped to the button?
As I think, my Btn extends UIControl and Btn have to call touchesBegan.
My code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// add my button
let button = Btn()
view.addSubview(button)
}
}
class Btn: UIControl {
fileprivate let button: UIButton = {
let swapButton = UIButton()
let size = CGFloat(50)
swapButton.frame.size = CGSize(width: size, height: size)
swapButton.backgroundColor = UIColor.green
return swapButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(button)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(button)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// Why it not called?
print("test")
}
}
Play around with the following code for playgrounds based on your code, and you will understand what's going on. While your control wants to process the touchesBegan callback, it has an UIButton inside of it, which consumes the event itself. In the example below, the inner button is taking one quarter of the Btn size (red part), if you click on it, the "test" text won't get printed. The rest of the Btn space (green part) does not contain any other view that would consume the touchesBegan, thus clicking there will print "test" text.
I recommend you to take a look at the responder chain.
import PlaygroundSupport
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// add my button
let button = Btn(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.addSubview(button)
}
}
class Btn: UIControl {
fileprivate let button: UIButton = {
let swapButton = UIButton()
let size = CGFloat(50)
swapButton.frame.size = CGSize(width: size, height: size)
swapButton.backgroundColor = UIColor.green
return swapButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(button)
self.backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(button)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// Why it not called?
print("test")
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = ViewController()
I've been setting the backgroundColor of buttons by doing self.layer.backgroundColor = someColor.
However, this doesn't seem to work with a custom class? I have this general class that I use:
class DarkButton: BaseButton {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 5
self.setTitleColor(UIColor.black, for: .normal)
self.setTitleColor(UIColor.lightGray, for: .highlighted)
self.layer.backgroundColor = UIColor(red:0.77, green:0.77,
blue:0.77, alpha: 1.0).cgColor
self.clipsToBounds = false
}
}
the self.layer.backgroundColor works just fine. Now if I extend it, like so:
class SuperCoolButton: DarkButton {
required init() {
super.init()
self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
}
the backgroundColor stays that of the DarkButton backgroundColor, however setting the image does indeed work >_>
This is happening because layoutSubviews() implemented in the super class(DarkButton) gets called after setUp method of DarkButton. You need to override layoutSubviews in your class SuperCoolButton and call setUp there instead of at its init method.
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
EDIT:
I think you should move the code which you have written inside [layoutSubviews][1] of DarkButton class.
layoutSubviews method gets called multiple times and only the code related to the layout of the view should be there.
Subclasses can override this method as needed to perform more precise
layout of their subviews. You should override this method only if the
autoresizing and constraint-based behaviors of the subviews do not
offer the behavior you want.
The ideal place to change layer's corner radius and setTitleColor or background color is either at init of the custom view or at awakeFromNib:(only, if the view is always going to be designed in nib).
Because you want to change the backgroud color of button at some later time. You can simply call your changeBgColor method on SuperCoolButton. Earlier it was not working, because layoutSubviews must be setting the background color back to the default.
Do not call your setUp() method in init instead of this call it in layoutSubviews
class SuperCoolButton: DarkButton {
required init() {
super.init()
//self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
//self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
}
I want to create a Circle image view for my profile avatar. I have tried this:-
class CircleImageView: UIImageView {
override func draw(_ rect: CGRect) {
// Drawing code
layer.masksToBounds = true
layer.cornerRadius = min(rect.width/2 , rect.height/2)
clipsToBounds = true
}
}
But its not working.
An extension will be great to set corner or do round image:
extension UIImageView {
func setRadius(radius: CGFloat? = nil) {
self.layer.cornerRadius = radius ?? self.frame.width / 2;
self.layer.masksToBounds = true;
}
}
Use:
imgview.setRadius(radius: 10)
imgview.setRadius() //default frame.width/2
Draw function is for drawing not changing layer.
Use layoutSubviews
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = min(self.frame.width/2 , self.frame.height/2)
clipsToBounds = true
}
You are adding the code in the wrong place, drawRect: is not really the right method to do such a functionality for editing the layer, you can achive this by:
Editing the layer when init(frame:) the imageView (also, adding the same functionality in init(coder:) because it should work for both approaches: programmatically and via storyboard):
class CircleImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
setupCircleLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupCircleLayer()
}
private func setupCircleLayer() {
layer.masksToBounds = true
layer.cornerRadius = min(frame.width/2 , frame.height/2)
clipsToBounds = true
}
}
Or as #Mohammadalijf suggested in his answer by overriding layoutSubviews() method:
class CircleImageView: UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = min(frame.width/2 , frame.height/2)
clipsToBounds = true
backgroundColor = UIColor.black
}
}
It does the desired fucntionality that you are asking for, but note that:
Subclasses can override this method as needed to perform more precise
layout of their subviews. You should override this method only if the
autoresizing and constraint-based behaviors of the subviews do not
offer the behavior you want. You can use your implementation to set
the frame rectangles of your subviews directly.
i.e, it is related to updating the layout of the view, check the documentation for more information; That's why I prefer to do it in the init methods.
I am having a problem getting the UITapGestureRecognizer in my custom UIView.to work properly. I Have created a view: CategoryViewButton which adds a UITapGestureRecognizer in the init:
class CategoryViewButton: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.addGestureRecognizer(tapGesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func handleTap() {
print("Hello again")
}
}
This gesture recognizer works without issue when added directly in a View Controller. However, when I add a CategoryViewButton as a subview of another custom view, the gesture recognizer method does not get called. My subview:
class CategoryView: UIView, CategoryButtonDelegate {
var button : CategoryViewButton?
override init(frame: CGRect) {
super.init(frame: frame)
button = CategoryViewButton(frame: CGRect(x: 10, y: 0, width: 40, height: 25))
self.addSubview(button!)
self.bringSubview(toFront: button!)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When I create a CategoryView in a View Controller, the handleTap() function is not being called. What am I missing?
For anyone curious, the issue was that the subview with gesture recognizer was outside the frame of the superview. This means even though the view was being drawn, the gestures were not detected