I have buttons inside a ButtonView class, to add some background and a label. These ButtonViews are added to a UIStackView which is a view in the PlayOverlay Class. PlayOverlay serves as a parent class to different kinds of overlays, in this example I have only included the BeginOverlay.
BeginOverlay is presented by the PlaySecVC. The Buttons in the BeginOverlay can't be tapped for some reason. I have tried the UIDebugging in XCode to see if there are any views in front of them, and there aren't. They are the frontmost views. I do get one error When UIDebugging that tells me that ButtonView's width, height, and x and y are ambiguous. This is because i have no constraints on it, as shown below, since they are laid out the stack view. How can I make these buttons tappable?
ViewController:
import UIKit
fileprivate struct scvc {
static let overlayWidth: CGFloat = 330
static let overlayFromCenter: CGFloat = 25
static let hotspotSize: CGFloat = 30
static let detailHeight: CGFloat = 214
static let detailWidth: CGFloat = 500
static let arrowMargin: CGFloat = 9
static let arrowSize: CGFloat = 56
static let zoomRect: CGFloat = 200
static let overlayHeight: CGFloat = 267
}
enum playState {
case play
case shuffle
case favorites
}
protocol PlaySec: class {
}
class PlaySecVC: UIViewController, PlaySec {
// MARK: UIComponents
lazy var scrollView: UIScrollView = {
let _scrollView = UIScrollView(frame: .zero)
_scrollView.translatesAutoresizingMaskIntoConstraints = false
_scrollView.clipsToBounds = false
//_scrollView.isUserInteractionEnabled = true
return _scrollView
}()
lazy var imageView: UIImageView = {
let _imageView = UIImageView(frame: .zero)
_imageView.translatesAutoresizingMaskIntoConstraints = false
_imageView.contentMode = .scaleAspectFit
//_imageView.isUserInteractionEnabled = true
return _imageView
}()
lazy var beginOverlay: BeginOverlay = {
let _beginOverlay = BeginOverlay(frame: .zero)
_beginOverlay.translatesAutoresizingMaskIntoConstraints = false
return _beginOverlay
}()
lazy var detailView: UIView = {
let _detailView = UIView(frame: .zero)
_detailView.translatesAutoresizingMaskIntoConstraints = false
_detailView.isHidden = true
//_detailView.isUserInteractionEnabled = false
return _detailView
}()
lazy var leftArrow: UIButton = {
let _leftArrow = UIButton(frame: .zero)
_leftArrow.translatesAutoresizingMaskIntoConstraints = false
_leftArrow.isHidden = false
_leftArrow.setImage(#imageLiteral(resourceName: "Left-Arrow-Outline"), for: .normal)
return _leftArrow
}()
lazy var rightArrow: UIButton = {
let _rightArrow = UIButton(frame: .zero)
_rightArrow.translatesAutoresizingMaskIntoConstraints = false
_rightArrow.isHidden = false
_rightArrow.setImage(#imageLiteral(resourceName: "Right-Arrow-Outline"), for: .normal)
return _rightArrow
}()
var state: playState = .play
// MARK: Setup
private func setup() {
let viewController = self
}
private func setupConstraints() {
view.addSubview(scrollView)
scrollView.addSubview(imageView)
view.addSubview(detailView)
view.addSubview(beginOverlay)
view.addSubview(leftArrow)
view.addSubview(rightArrow)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
beginOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor),
beginOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -25),
beginOverlay.widthAnchor.constraint(equalToConstant: scvc.overlayWidth),
beginOverlay.heightAnchor.constraint(equalToConstant: scvc.overlayHeight),
detailView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
detailView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
detailView.heightAnchor.constraint(equalToConstant: scvc.detailHeight),
detailView.widthAnchor.constraint(equalToConstant: scvc.detailWidth),
leftArrow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: scvc.arrowMargin),
leftArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
leftArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * scvc.arrowMargin),
rightArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
rightArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
])
}
func favorite() {
}
func play() {
state = .play
}
func favoritesPlay() {
play()
state = .favorites
}
func shufflePlay() {
play()
state = .shuffle
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/*var touch: UITouch? = touches.first
if (touch?.view != detailView && !detailView.isHidden) {
detailView.isHidden = true
}*/
super.touchesBegan(touches, with: event)
}
}
Overlay:
fileprivate struct sizeConstants {
static let pillHeight: CGFloat = 38
static let pillCornerRadius: CGFloat = sizeConstants.pillHeight / 2
static let titleFontSize: CGFloat = 13
static let detailFontSize: CGFloat = 10
static let imageCenterToLeading: CGFloat = 3
static let circleDiameter: CGFloat = 66
static let circleRadius: CGFloat = sizeConstants.circleDiameter / 2
static let buttonTextHPadding: CGFloat = 4
static let buttonTextVPadding: CGFloat = 2
static let badgeSpacing: CGFloat = 5.5
static let titleBadgeSpacing: CGFloat = 19
static let badgeImageSize: CGFloat = 32
static let badgeTextFromCenter: CGFloat = 0
static let badgeTextToImage: CGFloat = 8
static let buttonBackgroundToText: CGFloat = 6
static let circleButtonSize: CGFloat = 48
static let rectButtonWidth: CGFloat = 36
static let rectButtonHeight: CGFloat = 39
static let badgesToButtons: CGFloat = 21.5
}
class ButtonView: UIView {
lazy var buttonBackgroundView: UIView = {
let _buttonBackgroundView = UIView(frame: .zero)
_buttonBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_buttonBackgroundView.backgroundColor = .black
_buttonBackgroundView.layer.cornerRadius = sizeConstants.circleRadius
return _buttonBackgroundView
}()
lazy var textBackgroundView: UIView = {
let _textBackgroundView = UIView(frame: .zero)
_textBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_textBackgroundView.backgroundColor = .black
_textBackgroundView.layer.cornerRadius = _textBackgroundView.frame.height / 2
return _textBackgroundView
}()
lazy var button: UIButton = {
let _button = UIButton(frame: .zero)
_button.translatesAutoresizingMaskIntoConstraints = false
return _button
}()
lazy var label: UILabel = {
let _label = UILabel(frame: .zero)
_label.translatesAutoresizingMaskIntoConstraints = false
_label.font = .systemFont(ofSize: 15)
_label.textColor = .white
return _label
}()
var isRect: Bool = false
convenience init(rect: Bool) {
self.init(frame: .zero)
self.isRect = rect
setupViews()
}
override func updateConstraints() {
NSLayoutConstraint.activate([
buttonBackgroundView.topAnchor.constraint(equalTo: topAnchor),
buttonBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonBackgroundView.widthAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
buttonBackgroundView.heightAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
button.centerXAnchor.constraint(equalTo: buttonBackgroundView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: buttonBackgroundView.centerYAnchor),
textBackgroundView.topAnchor.constraint(equalTo: buttonBackgroundView.bottomAnchor, constant: sizeConstants.buttonBackgroundToText),
textBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
textBackgroundView.heightAnchor.constraint(equalTo: label.heightAnchor, constant: sizeConstants.buttonTextVPadding),
textBackgroundView.widthAnchor.constraint(equalTo: label.widthAnchor, constant: sizeConstants.buttonTextHPadding),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: textBackgroundView.centerYAnchor),
])
if (isRect) {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.rectButtonWidth),
button.heightAnchor.constraint(equalToConstant: sizeConstants.rectButtonHeight),
])
} else {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
button.heightAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
])
}
super.updateConstraints()
}
private func setupViews() {
addSubview(buttonBackgroundView)
addSubview(textBackgroundView)
addSubview(label)
addSubview(button)
label.sizeToFit()
setNeedsUpdateConstraints()
}
func setButtonProps(image: UIImage, text: String, target: Any, selector: Selector) {
self.button.addTarget(target, action: selector, for: .touchUpInside)
self.button.setImage(image, for: .normal)
self.label.text = text
}
#objc private func tapped() {
print("tapped")
}
}
class PlayOverlay: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
}
lazy var badgeStackView: UIStackView = {
let _badgeStackView = UIStackView(frame: .zero)
_badgeStackView.translatesAutoresizingMaskIntoConstraints = false
_badgeStackView.axis = .vertical
_badgeStackView.spacing = sizeConstants.badgeSpacing
_badgeStackView.distribution = .equalSpacing
return _badgeStackView
}()
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .equalSpacing
return _buttonStackView
}()
var vc: PlaySecVC!
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
NSLayoutConstraint.activate([
badgeStackView.topAnchor.constraint(equalTo: topAnchor, constant: sizeConstants.titleBadgeSpacing),
badgeStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
badgeStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.topAnchor.constraint(equalTo: badgeStackView.bottomAnchor, constant: sizeConstants.badgesToButtons),
buttonStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
super.updateConstraints()
}
}
class BeginOverlay: PlayOverlay {
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(badgeStackView)
addSubview(buttonStackView)
let shuffleButton = ButtonView(rect: false)
shuffleButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "SHUFFLE", target: self, selector: #selector(shuffle))
let favoritesButton = ButtonView(rect: false)
favoritesButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "FAVORITES", target: self, selector: #selector(favorites))
let playButton = ButtonView(rect: false)
playButton.setButtonProps(image: UIImage()/* replaced with empty image for demo */, text: "PLAY", target: self, selector: #selector(play))
buttonStackView.addArrangedSubview(shuffleButton)
buttonStackView.addArrangedSubview(favoritesButton)
buttonStackView.addArrangedSubview(playButton)
}
#objc private func shuffle() {
vc.shufflePlay()
}
#objc private func favorites() {
vc.favoritesPlay()
}
#objc private func play() {
vc.play()
}
}
I did some research and I figured out that since there are 2 UIStackView inside BeginOverlay, there is position ambiguity for the second one that contains the 3 UIButton. The image below may help.
Here is a place of fix. Tested with Xcode 11.4 / iOS 13.4
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .fillEqually // << here !!!
return _buttonStackView
}()
Here is complete tested module (for comparison, just in case I changed anything else). Just created single view iOS project from template and assign controller class in Storyboard.
fileprivate struct scvc {
static let overlayWidth: CGFloat = 330
static let overlayFromCenter: CGFloat = 25
static let hotspotSize: CGFloat = 30
static let detailHeight: CGFloat = 214
static let detailWidth: CGFloat = 500
static let arrowMargin: CGFloat = 9
static let arrowSize: CGFloat = 56
static let zoomRect: CGFloat = 200
static let overlayHeight: CGFloat = 267
}
enum playState {
case play
case shuffle
case favorites
}
protocol PlaySec: class {
}
class PlaySecVC: UIViewController, PlaySec {
// MARK: UIComponents
lazy var scrollView: UIScrollView = {
let _scrollView = UIScrollView(frame: .zero)
_scrollView.translatesAutoresizingMaskIntoConstraints = false
_scrollView.clipsToBounds = false
//_scrollView.isUserInteractionEnabled = true
return _scrollView
}()
lazy var imageView: UIImageView = {
let _imageView = UIImageView(frame: .zero)
_imageView.translatesAutoresizingMaskIntoConstraints = false
_imageView.contentMode = .scaleAspectFit
//_imageView.isUserInteractionEnabled = true
return _imageView
}()
lazy var beginOverlay: BeginOverlay = {
let _beginOverlay = BeginOverlay(frame: .zero)
_beginOverlay.translatesAutoresizingMaskIntoConstraints = false
return _beginOverlay
}()
lazy var detailView: UIView = {
let _detailView = UIView(frame: .zero)
_detailView.translatesAutoresizingMaskIntoConstraints = false
_detailView.isHidden = true
//_detailView.isUserInteractionEnabled = false
return _detailView
}()
lazy var leftArrow: UIButton = {
let _leftArrow = UIButton(frame: .zero)
_leftArrow.translatesAutoresizingMaskIntoConstraints = false
_leftArrow.isHidden = false
_leftArrow.setImage(UIImage(systemName: "arrow.left")!, for: .normal)
return _leftArrow
}()
lazy var rightArrow: UIButton = {
let _rightArrow = UIButton(frame: .zero)
_rightArrow.translatesAutoresizingMaskIntoConstraints = false
_rightArrow.isHidden = false
_rightArrow.setImage(UIImage(systemName: "arrow.right")!, for: .normal)
return _rightArrow
}()
var state: playState = .play
// MARK: Setup
private func setup() {
// let viewController = self
self.beginOverlay.vc = self
}
private func setupConstraints() {
view.addSubview(scrollView)
scrollView.addSubview(imageView)
view.addSubview(detailView)
view.addSubview(beginOverlay)
view.addSubview(leftArrow)
view.addSubview(rightArrow)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
beginOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor),
beginOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -25),
beginOverlay.widthAnchor.constraint(equalToConstant: scvc.overlayWidth),
beginOverlay.heightAnchor.constraint(equalToConstant: scvc.overlayHeight),
detailView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
detailView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
detailView.heightAnchor.constraint(equalToConstant: scvc.detailHeight),
detailView.widthAnchor.constraint(equalToConstant: scvc.detailWidth),
leftArrow.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: scvc.arrowMargin),
leftArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
leftArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
leftArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -1 * scvc.arrowMargin),
rightArrow.centerYAnchor.constraint(equalTo: view.centerYAnchor),
rightArrow.widthAnchor.constraint(equalToConstant: scvc.arrowSize),
rightArrow.heightAnchor.constraint(equalToConstant: scvc.arrowSize),
])
}
func favorite() {
}
func play() {
state = .play
}
func favoritesPlay() {
play()
state = .favorites
}
func shufflePlay() {
play()
state = .shuffle
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
setupConstraints()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
/*var touch: UITouch? = touches.first
if (touch?.view != detailView && !detailView.isHidden) {
detailView.isHidden = true
}*/
super.touchesBegan(touches, with: event)
}
}
fileprivate struct sizeConstants {
static let pillHeight: CGFloat = 38
static let pillCornerRadius: CGFloat = sizeConstants.pillHeight / 2
static let titleFontSize: CGFloat = 13
static let detailFontSize: CGFloat = 10
static let imageCenterToLeading: CGFloat = 3
static let circleDiameter: CGFloat = 66
static let circleRadius: CGFloat = sizeConstants.circleDiameter / 2
static let buttonTextHPadding: CGFloat = 4
static let buttonTextVPadding: CGFloat = 2
static let badgeSpacing: CGFloat = 5.5
static let titleBadgeSpacing: CGFloat = 19
static let badgeImageSize: CGFloat = 32
static let badgeTextFromCenter: CGFloat = 0
static let badgeTextToImage: CGFloat = 8
static let buttonBackgroundToText: CGFloat = 6
static let circleButtonSize: CGFloat = 48
static let rectButtonWidth: CGFloat = 36
static let rectButtonHeight: CGFloat = 39
static let badgesToButtons: CGFloat = 21.5
}
class ButtonView: UIView {
lazy var buttonBackgroundView: UIView = {
let _buttonBackgroundView = UIView(frame: .zero)
_buttonBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_buttonBackgroundView.backgroundColor = .black
_buttonBackgroundView.layer.cornerRadius = sizeConstants.circleRadius
return _buttonBackgroundView
}()
lazy var textBackgroundView: UIView = {
let _textBackgroundView = UIView(frame: .zero)
_textBackgroundView.translatesAutoresizingMaskIntoConstraints = false
_textBackgroundView.backgroundColor = .black
_textBackgroundView.layer.cornerRadius = _textBackgroundView.frame.height / 2
return _textBackgroundView
}()
lazy var button: UIButton = {
let _button = UIButton(frame: .zero)
_button.translatesAutoresizingMaskIntoConstraints = false
return _button
}()
lazy var label: UILabel = {
let _label = UILabel(frame: .zero)
_label.translatesAutoresizingMaskIntoConstraints = false
_label.font = .systemFont(ofSize: 15)
_label.textColor = .white
return _label
}()
var isRect: Bool = false
convenience init(rect: Bool) {
self.init(frame: .zero)
self.isRect = rect
setupViews()
}
override func updateConstraints() {
NSLayoutConstraint.activate([
buttonBackgroundView.topAnchor.constraint(equalTo: topAnchor),
buttonBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonBackgroundView.widthAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
buttonBackgroundView.heightAnchor.constraint(equalToConstant: sizeConstants.circleDiameter),
button.centerXAnchor.constraint(equalTo: buttonBackgroundView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: buttonBackgroundView.centerYAnchor),
textBackgroundView.topAnchor.constraint(equalTo: buttonBackgroundView.bottomAnchor, constant: sizeConstants.buttonBackgroundToText),
textBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
textBackgroundView.heightAnchor.constraint(equalTo: label.heightAnchor, constant: sizeConstants.buttonTextVPadding),
textBackgroundView.widthAnchor.constraint(equalTo: label.widthAnchor, constant: sizeConstants.buttonTextHPadding),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: textBackgroundView.centerYAnchor),
])
if (isRect) {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.rectButtonWidth),
button.heightAnchor.constraint(equalToConstant: sizeConstants.rectButtonHeight),
])
} else {
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
button.heightAnchor.constraint(equalToConstant: sizeConstants.circleButtonSize),
])
}
super.updateConstraints()
}
private func setupViews() {
addSubview(buttonBackgroundView)
addSubview(textBackgroundView)
addSubview(label)
addSubview(button)
label.sizeToFit()
setNeedsUpdateConstraints()
}
func setButtonProps(image: UIImage, text: String, target: Any, selector: Selector) {
self.button.addTarget(target, action: selector, for: .touchUpInside)
self.button.setImage(image, for: .normal)
self.label.text = text
}
#objc private func tapped() {
print("tapped")
}
}
class PlayOverlay: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
}
lazy var badgeStackView: UIStackView = {
let _badgeStackView = UIStackView(frame: .zero)
_badgeStackView.translatesAutoresizingMaskIntoConstraints = false
_badgeStackView.axis = .vertical
_badgeStackView.spacing = sizeConstants.badgeSpacing
_badgeStackView.distribution = .equalSpacing
return _badgeStackView
}()
lazy var buttonStackView: UIStackView = {
let _buttonStackView = UIStackView(frame: .zero)
_buttonStackView.translatesAutoresizingMaskIntoConstraints = false
_buttonStackView.axis = .horizontal
_buttonStackView.distribution = .fillEqually
return _buttonStackView
}()
var vc: PlaySecVC!
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
NSLayoutConstraint.activate([
badgeStackView.topAnchor.constraint(equalTo: topAnchor, constant: sizeConstants.titleBadgeSpacing),
badgeStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
badgeStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.topAnchor.constraint(equalTo: badgeStackView.bottomAnchor, constant: sizeConstants.badgesToButtons),
buttonStackView.widthAnchor.constraint(equalTo: widthAnchor),
buttonStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
super.updateConstraints()
}
}
class BeginOverlay: PlayOverlay {
override init(frame: CGRect) {
super.init(frame: .zero)
self.setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(badgeStackView)
addSubview(buttonStackView)
let shuffleButton = ButtonView(rect: false)
shuffleButton.setButtonProps(image: UIImage(systemName: "shuffle")!/* replaced with empty image for demo */, text: "SHUFFLE", target: self, selector: #selector(shuffle))
let favoritesButton = ButtonView(rect: false)
favoritesButton.setButtonProps(image: UIImage(systemName: "bookmark")!/* replaced with empty image for demo */, text: "FAVORITES", target: self, selector: #selector(favorites))
let playButton = ButtonView(rect: false)
playButton.setButtonProps(image: UIImage(systemName: "play")!/* replaced with empty image for demo */, text: "PLAY", target: self, selector: #selector(play))
buttonStackView.addArrangedSubview(shuffleButton)
buttonStackView.addArrangedSubview(favoritesButton)
buttonStackView.addArrangedSubview(playButton)
}
#objc private func shuffle() {
vc.shufflePlay()
}
#objc private func favorites() {
vc.favoritesPlay()
}
#objc private func play() {
vc.play()
}
}
Note: as mentioned it is better to review all constrains and fix run-time ambiguities.
I have a custom inputAccessoryView and am trying to toggle hiding/showing it. I don't want to utilize .isHidden or .removeFromSuperView(), rather, use the bottom slide in/out, which seems to be native as I present other viewControllers onto the hierarchy and this animation executes.
I've tried to resignFirstResponder with no luck and there doesn't seem to be any existing commentary around this. Any thoughts would be appreciated as I am admittedly stumped.
class CustomInputAccessoryView: UIView {
let customTextView: CustomInputTextView = {
let tv = CustomInputTextView()
tv.isScrollEnabled = false
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
autoresizingMask = .flexibleHeight
addSubview(customTextView)
customTextView.topAnchor.constraint(equalTo: topAnchor, constant: 12).isActive = true
customTextView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: 8).isActive = true
customTextView.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
customTextView.rightAnchor.constraint(equalTo: rightAnchor, constant: 10).isActive = true
customTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
class CustomInputTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
override var canResignFirstResponder: Bool {
return true
}
override var canBecomeFirstResponder: Bool {
return true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init coder has not been implemented")
}
}
//in viewcontroller
lazy var inputContainerView: CustomInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 60)
let customInputAccessoryView = CustomInputAccessoryView(frame: frame)
return customInputAccessoryView
}()
//onLoad
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
I don't know if this is really what you're going for, but give it a try.
Tapping anywhere in the view will show/hide the input accessory view:
class TestInputViewController: UIViewController {
//in viewcontroller
lazy var inputContainerView: CustomInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 60)
let customInputAccessoryView = CustomInputAccessoryView(frame: frame)
return customInputAccessoryView
}()
override var canBecomeFirstResponder: Bool {
return true
}
//onLoad
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
override func viewDidLoad() {
super.viewDidLoad()
// so we can see the frame
inputContainerView.backgroundColor = .blue
// tap anywhere in view to show / hide input accessory view
let g = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
view.addGestureRecognizer(g)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
#objc func didTap(sender: UITapGestureRecognizer) {
if self.isFirstResponder {
resignFirstResponder()
} else {
becomeFirstResponder()
}
}
}
class CustomInputAccessoryView: UIView {
let customTextView: CustomInputTextView = {
let tv = CustomInputTextView()
tv.isScrollEnabled = false
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
autoresizingMask = .flexibleHeight
addSubview(customTextView)
customTextView.translatesAutoresizingMaskIntoConstraints = false
customTextView.topAnchor.constraint(equalTo: topAnchor, constant: 12).isActive = true
customTextView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true
customTextView.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
customTextView.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
customTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
class CustomInputTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
override var canResignFirstResponder: Bool {
return true
}
override var canBecomeFirstResponder: Bool {
return true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init coder has not been implemented")
}
}
Not related to showing / hiding, but a few of your constraints were wrong, causing the text view to be misplaced.
Who can help me? I create chat controller via UITableViewController, UINavigationController and I use InputAccessoryView. If I swipe screen to left (dismiss this controller) and return swipe to right (cancel dismiss) - table set adjustContentInset bottom to zero and InputAcessoryView close bottom content tableView. This problem created in ViewWillAppear event. My code:
UITableViewController:
// MARK: - Controller data
lazy var inputContainerView = ChatAccessoryView(frame: .zero, buttonSelector: #selector(sendMessage(sender:)), controller: self)
public var ticketID: Int = 0
private var lastMessageID: Int = 0
private var tableSections: [String] = []
private var tableRows: [[SupportTicketMessage]] = []
private var sendingMessage: Bool = false
private var URLTaskGetMessages: URLSessionDataTask?
private var URLTaskSendMessage: URLSessionDataTask?
// MARK: - Controller overrides
override func loadView() {
super.loadView()
tableView.register(ChatHeaderView.self, forHeaderFooterViewReuseIdentifier: "chatHeader")
tableView.register(ChatFromMessageTableViewCell.self, forCellReuseIdentifier: "chatFromMessage")
tableView.register(ChatToMessageTableViewCell.self, forCellReuseIdentifier: "chatToMessage")
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
if let messages = supportClass.getTicketMessages(ticketID) {
tableSections = messages.sections
tableRows = messages.rows
lastMessageID = messages.lastMessage
scrollTableToBottom(false)
} else {
tableView.setLoaderBackground("Загрузка сообщений...")
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadMessages()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
URLTaskGetMessages?.cancel()
URLTaskSendMessage?.cancel()
}
override var inputAccessoryView: UIView? {
return inputContainerView
}
override var canBecomeFirstResponder: Bool {
return true
}
ChatAccessoryView:
class ChatAccessoryView: UIView {
// MARK: - Get params to init
let buttonSelector: Selector
let controller: UIViewController
// MARK: - Data
private let textView = ChatTextView()
private let sendButton = LoadingButton(frame: .zero, text: "Отпр.")
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
// MARK: - Init
required init(frame: CGRect, buttonSelector: Selector, controller: UIViewController) {
self.buttonSelector = buttonSelector
self.controller = controller
super.init(frame: frame)
configureContents()
blurEffectConfigure()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Overrides
override var intrinsicContentSize: CGSize {
return .zero
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
blurEffectConfigure()
}
override func didMoveToWindow() {
super.didMoveToWindow()
if let window = window {
textView.bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0).isActive = true
}
}
// MARK: - Private methods
private func configureContents() {
backgroundColor = .clear
autoresizingMask = .flexibleHeight
textView.placeholder = "Напишите сообщение..."
textView.maxHeight = 160
textView.font = .systemFont(ofSize: 17)
textView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
textView.layer.cornerRadius = 8
textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 4)
sendButton.titleLabel?.font = .boldSystemFont(ofSize: 17)
sendButton.setTitleColor(controller.view.tintColor, for: .normal)
sendButton.addTarget(controller, action: buttonSelector, for: .touchUpInside)
blurView.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
sendButton.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(blurView)
self.addSubview(textView)
self.addSubview(sendButton)
blurView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
blurView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
blurView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
blurView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
textView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20).isActive = true
textView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
textView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8 ).isActive = true
textView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -20).isActive = true
sendButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20).isActive = true
sendButton.widthAnchor.constraint(equalToConstant: 48).isActive = true
sendButton.centerYAnchor.constraint(equalTo: textView.centerYAnchor).isActive = true
}
private func blurEffectConfigure() {
if #available(iOS 13.0, *) {
if traitCollection.userInterfaceStyle == .light {
blurView.effect = UIBlurEffect(style: .extraLight)
} else {
blurView.effect = UIBlurEffect(style: .dark)
}
}
}
// MARK: - Public methods
public func successSend() {
textView.text = ""
sendButton.loadingMode(false)
}
}
ChatTextView:
class ChatTextView: UITextView {
// MARK: - Data
var maxHeight: CGFloat = 0.0
public let placeholderTextView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.backgroundColor = .clear
textView.isScrollEnabled = false
textView.isUserInteractionEnabled = false
textView.textColor = UIColor.black.withAlphaComponent(0.33)
return textView
}()
var placeholder: String? {
get {
return placeholderTextView.text
}
set {
placeholderTextView.text = newValue
}
}
// MARK: - Init
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
backgroundColor = UIColor.black.withAlphaComponent(0.06)
isScrollEnabled = false
autoresizingMask = [.flexibleWidth, .flexibleHeight]
NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: UITextView.textDidChangeNotification, object: self)
placeholderTextView.font = font
addSubview(placeholderTextView)
NSLayoutConstraint.activate([
placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
colorThemeConfigure()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Overrides
override var text: String! {
didSet {
invalidateIntrinsicContentSize()
placeholderTextView.isHidden = !text.isEmpty
}
}
override var font: UIFont? {
didSet {
placeholderTextView.font = font
invalidateIntrinsicContentSize()
}
}
override var contentInset: UIEdgeInsets {
didSet {
placeholderTextView.contentInset = contentInset
}
}
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
if size.height == UIView.noIntrinsicMetric {
layoutManager.glyphRange(for: textContainer)
size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
}
if maxHeight > 0.0 && size.height > maxHeight {
size.height = maxHeight
if !isScrollEnabled {
isScrollEnabled = true
}
} else if isScrollEnabled {
isScrollEnabled = false
}
return size
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
colorThemeConfigure()
}
// MARK: - Private methods
private func colorThemeConfigure() {
if #available(iOS 13.0, *) {
if traitCollection.userInterfaceStyle == .light {
backgroundColor = UIColor.black.withAlphaComponent(0.06)
placeholderTextView.textColor = UIColor.black.withAlphaComponent(0.33)
} else {
backgroundColor = UIColor.white.withAlphaComponent(0.08)
placeholderTextView.textColor = UIColor.white.withAlphaComponent(0.33)
}
}
}
// MARK: - Obj C methods
#objc private func textDidChange(_ note: Notification) {
invalidateIntrinsicContentSize()
placeholderTextView.isHidden = !text.isEmpty
}
}
Screenshots error:
TableView loaded (all right)
Swipe screen to left and cancel this action (swipe right)
When I returned to controller I see this Bug (last message go to bottom)
Thanks!
You could try something like this:
UITableViewController:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateTableViewInsets()
}
private func updateTableViewInsets() {
let offSet = inputContainerView.frame.height
tableView?.contentInset.bottom = offSet
tableView?.scrollIndicatorInsets.bottom = offSet
}
Thank you all for your attention and trying to help me! I solved the problem as follows:
tableView.contentInsetAdjustmentBehavior = .never
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 53, right: 0)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
I disable automatic tableView.contentInsetAdjustmentBehavior and use tableView.conentInset. On open/close keyboard I change tableView.contentInset.
This works well for me, and pretty simple solution:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
reloadInputViews()
prepareTableViewForViewPresented()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
prepareTableViewForViewDismissed()
}
private func prepareTableViewForViewPresented() {
self.tableView.contentInset.bottom = 0
}
private func prepareTableViewForViewDismissed() {
self.tableView.contentInset.bottom = view.safeAreaInsets.bottom + (inputAccessoryView?.frame.height ?? 0)
}
Known issues:
- when keyboard is shown, tableview still goes behind keyboard
I create a custom view in xib file. I add 3 views(which inherited from custom) in viewController. Initially they have white color, but when i click on first view it should be changed other color and if i click on second view, the first view should be back in white color.
I need help to change first view color back to white when second view is selected.
My code for customView here
class SubscriptionView: UIView {
#IBOutlet weak var title: UILabel!
#IBOutlet weak var subTitle: UILabel!
#IBOutlet weak var checkMark: UIImageView!
var isSelect: Bool = false
let nibName = "SubscriptionView"
var contentView: UIView?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override func awakeFromNib() {
super.awakeFromNib()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapAction)))
}
func commonInit() {
guard let view = loadViewFromNib() else {
return
}
view.frame = self.bounds
view.layer.masksToBounds = true
view.layer.cornerRadius = 14
view.layerBorderColor = AppColor.amaranth
view.layerBorderWidth = 0.5
self.addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
public func selectedView(_ isSelect: Bool) {
self.isSelect = isSelect
title.textColor = isSelect ? UIColor.white : AppColor.amaranth
subTitle.textColor = isSelect ? UIColor.white : AppColor.amaranth
checkMark.alpha = isSelect ? 1.0 : 0.0
contentView!.backgroundColor = isSelect ? AppColor.amaranth : UIColor.white
}
#objc private func tapAction() {
///????? selectedView
}
}
Here is a very simple example using the Delegate / Protocol pattern.
Define a Protocol:
// Protocol / Delegate pattern
protocol SubscriptionViewDelegate {
func gotTap(from sender: SubscriptionView)
}
Have your view controller conform to that protocol. In your custom SubscriptionView class, when the user taps you will tell the delegate that a tap was received, and the controller will loop through the SubscriptionView objects setting the "selected" state to true or false, based on the tapped view.
class SubscriptionsViewController: UIViewController, SubscriptionViewDelegate {
let theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20
return v
}()
// array to track the "subscription" views
var arrayOfSubscriptionViews: [SubscriptionView] = [SubscriptionView]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add a stack view to hold the "subscription" views
view.addSubview(theStackView)
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
theStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
theStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])
// instantiate 3 "subscription" views
for _ in 1...3 {
// instantiate view
let v = SubscriptionView()
// set self as its delegate
v.delegate = self
// add it to our stack view
theStackView.addArrangedSubview(v)
// append it to our tracking array
arrayOfSubscriptionViews.append(v)
}
}
func gotTap(from sender: SubscriptionView) {
// just for dev / debugging
print("got tap from", sender)
// loop through the subscription views,
// setting the sender selected to TRUE
// the others to FALSE
arrayOfSubscriptionViews.forEach {
$0.selectedView($0 == sender)
}
}
}
// Protocol / Delegate pattern
protocol SubscriptionViewDelegate {
func gotTap(from sender: SubscriptionView)
}
class SubscriptionView: UIView {
#IBOutlet weak var title: UILabel!
#IBOutlet weak var subTitle: UILabel!
#IBOutlet weak var checkMark: UIImageView!
var isSelect: Bool = false
var delegate: SubscriptionViewDelegate?
let nibName = "SubscriptionView"
var contentView: UIView?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override func awakeFromNib() {
super.awakeFromNib()
}
func commonInit() {
guard let view = loadViewFromNib() else {
return
}
view.frame = self.bounds
view.layer.masksToBounds = true
view.layer.cornerRadius = 14
view.layer.borderColor = UIColor.red.cgColor // AppColor.amaranth
view.layer.borderWidth = 0.5
self.addSubview(view)
contentView = view
selectedView(false)
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapAction)))
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
public func selectedView(_ isSelect: Bool) {
self.isSelect = isSelect
title.textColor = isSelect ? UIColor.white : .red // AppColor.amaranth
subTitle.textColor = isSelect ? UIColor.white : .red // AppColor.amaranth
checkMark.alpha = isSelect ? 1.0 : 0.0
// contentView!.backgroundColor = isSelect ? AppColor.amaranth : UIColor.white
contentView!.backgroundColor = isSelect ? UIColor.red : UIColor.white
}
#objc private func tapAction() {
// for dev / debugging
print("sending tap from", self)
// tell the delegate self got tapped
delegate?.gotTap(from: self)
}
}
I only made minor changes to your SubscriptionView class, so you should be able to use it as-is with your existing .xib