I create this image for illustration of my issue.
I have a button that on touch hold changes background to pink. Then a view in-between the two buttons to display a view. Then another button on top to add an inner shadow.
I cannot get both targets to work at the same time.
Is this even achievable?
I tried using HitTest without success, as it pass the click to the button below but then then first button loses its target.
I am considering maybe using UIViews to achieve the same effect as I am using Layers inside the button's target to achieve the effect that I am after.
I just need two views on top of each other that can receive touches simultaneously. I would prefer a UIButton solution as the hold touch is almost instantaneous whilst a LongPress gesture takes some time to take effect.
I have been on this problem for quite a while now so I decided to ask some help.
You can (probably) accomplish this easier by using a UIView
and handling touches rather than using a UIButton
...
First, let's create a basic "change background on touch" view:
class HighlightView: UIView {
// very light gray
var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
// light pink
var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = normalColor
}
func doHighlight(_ b: Bool) {
backgroundColor = b ? hightlightColor : normalColor
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = hightlightColor
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let pt = t.location(in: self)
// if touch is inside my bounds
// use highlight color
// else, touch moved outside so
// use normal color
if bounds.contains(pt) {
backgroundColor = hightlightColor
} else {
backgroundColor = normalColor
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = normalColor
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = normalColor
}
}
with an example view controller:
class TouchHoldVC: UIViewController {
let holdView = HighlightView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
holdView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holdView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
holdView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
holdView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
holdView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
holdView.heightAnchor.constraint(equalToConstant: 200.0),
])
}
}
On init
we set the background color to "normal" ...
touchesBegan
- we set the background color to "highlighted"looks like this:
Next we can create an "inner shadow view" class:
class InnerShadowView: UIView {
// "inner" shadow
private let innerShadowLayer = CAShapeLayer()
private let innerShadowMaskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
// add sublayer
self.layer.addSublayer(innerShadowLayer)
// fillColor doesn't matter - just needs to be opaque
innerShadowLayer.fillColor = UIColor.white.cgColor
innerShadowLayer.fillRule = .evenOdd
}
override func layoutSubviews() {
super.layoutSubviews()
// for the "inner" shadow,
// rectangle path needs to be larger than
// bounds + shadow offset + shadow raidus
// so the shadow doesn't "bleed" from all sides
let path = UIBezierPath(rect: bounds.insetBy(dx: -40, dy: -40))
// create a path for the "hole" in the layer
let holePath = UIBezierPath(rect: bounds)
// this "cuts a hole" in the path
path.append(holePath)
path.usesEvenOddFillRule = true
innerShadowLayer.path = path.cgPath
// mask the layer, so we only "see through the hole"
innerShadowMaskLayer.path = holePath.cgPath
innerShadowLayer.mask = innerShadowMaskLayer
// adjust properties as desired
innerShadowLayer.shadowOffset = .zero
innerShadowLayer.shadowColor = UIColor.black.cgColor
innerShadowLayer.shadowRadius = 5
// setting .shadowOpacity to a very small value (such as 0.025)
// results in very light shadow
// set .shadowOpacity to 1.0 to clearly see
// what the shadow is doing
innerShadowLayer.shadowOpacity = 0.75
}
}
Modify HighlightView
by adding InnerShadowView
as a subview:
class HighlightView: UIView {
var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
let shadView = InnerShadowView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
shadView.translatesAutoresizingMaskIntoConstraints = false
addSubview(shadView)
let g = self
NSLayoutConstraint.activate([
shadView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
shadView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
shadView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
shadView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
backgroundColor = normalColor
shadView.isHidden = true
}
func doHighlight(_ b: Bool) {
backgroundColor = b ? hightlightColor : normalColor
shadView.isHidden = !b
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(true)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let pt = t.location(in: self)
if bounds.contains(pt) {
doHighlight(true)
} else {
doHighlight(false)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(false)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(false)
}
}
using that same example controller, now:
touchesBegan
- we set the background color to "highlighted" and we show the shadow viewLooks like this:
Next, a simple "content view" with an image and a label:
class SomeContentView: UIView {
let label = UILabel()
let imgView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
imgView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imgView)
let pad: CGFloat = 8.0
let g = self
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: g.topAnchor, constant: pad),
label.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 12.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -pad),
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: pad),
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: pad),
//imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
imgView.widthAnchor.constraint(equalToConstant: 80.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0),
imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -pad),
])
label.numberOfLines = 0
label.textAlignment = .center
label.text = "This an image view and a label, in a view we will call \"SomeContentView\""
if let img = UIImage(named: "test300x300") {
imgView.image = img
}
}
}
by itself, it looks like this:
we'll add a function to our HighlightView
-- func addContent(_ cView: UIView)
-- so we can add that as a subview:
class HighlightView: UIView {
var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
let shadView = InnerShadowView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
shadView.translatesAutoresizingMaskIntoConstraints = false
addSubview(shadView)
let g = self
NSLayoutConstraint.activate([
shadView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
shadView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
shadView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
shadView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
backgroundColor = normalColor
shadView.isHidden = true
}
func doHighlight(_ b: Bool) {
backgroundColor = b ? hightlightColor : normalColor
shadView.isHidden = !b
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(true)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let pt = t.location(in: self)
if bounds.contains(pt) {
doHighlight(true)
} else {
doHighlight(false)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(false)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
doHighlight(false)
}
func addContent(_ cView: UIView) {
cView.translatesAutoresizingMaskIntoConstraints = false
insertSubview(cView, at: 0)
let g = self
NSLayoutConstraint.activate([
cView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
cView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
cView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
}
and a couple changes to our sample controller -- we'll add an instance of SomeContentView
and let its constraints set the height:
class TouchHoldVC: UIViewController {
let holdView = HighlightView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
holdView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holdView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
holdView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
holdView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
holdView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// the content view will set the height of the "touch hold" Highlight view
//holdView.heightAnchor.constraint(equalToConstant: 200.0),
])
holdView.addContent(SomeContentView())
}
}
and we get this: