Present AirPlay route picker from a custom UIButton - ios

I have a stylized UIButton subclass that I want to present the standard AirPlay route selection interface on tap. I know AVRoutePickerView is available but it does not appear to offer any customizations except to configure its tint and active tint color on iOS.
Is there a way to present the route picker using your own button?
It appears this app was able to do just that:

Since the AVRoutePickerView is just a view you could add it to a custom button or add custom views on top of it. This is a very rough example
let frame = CGRect(x: 50, y: 50, width: 100, height: 50)
let routePickerView = AVRoutePickerView(frame: frame)
routePickerView.backgroundColor = UIColor.clear
view.addSubview(routePickerView)
let label = UILabel(frame: routePickerView.bounds)
label.backgroundColor = .black
label.textColor = .white
routePickerView.addSubview(label)
label.text = "Rough"
I spoke hastily with my examples of how you could make it work, button tap pass through is touchy as well as trying to use a gesture recognizer. However I came up with three alternative approaches that with some effort can be used. They all rely on having a view added to the picker as a subview as above. That view should have .isUserInteractionEnabled = true. I tested with a UILabel and just with a plain UIView, it probably needs to be something that doesn't have touch processing so avoid UIControls. Once you have this view in place you can use touchesBegan touchesCancelled and touchesEnded to do any visual adjustments or animation then pass along the touches. As always when fiddling with custom provided controls this may not always work but this is currently working for me and doesn't use private api just workarounds and a bit of luck.
Using a custom UILabel as the subview
class CustomLabel: UILabel {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .init(scaleX: 0.75, y: 0.75)
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .identity
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .identity
super.touchesCancelled(touches, with: event)
}
}
Using a subclass of AVRoutePickerView
class CustomRoutePicker: AVRoutePickerView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .init(scaleX: 0.75, y: 0.75)
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .identity
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
transform = .identity
super.touchesCancelled(touches, with: event)
}
}
Using UIViewController with a reference to a pickerView
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
pickerView?.transform = .init(scaleX: 0.75, y: 0.75)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
pickerView?.transform = .identity
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
pickerView?.transform = .identity
}

Related

How to handle touches by an UIView on top of a UIScrollView and the UIScrollView both at the same time

I have the following hierarchy, the root view of the controller. Added two subviews, a standard UIView and a UIScrollView. The UIView is on top.
What I want to do is to receive touches on that UIView, meaning touchesBegan, touchesMoved... like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Began")
super.touchesBegan(touches, with: event)
next?.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Moved")
super.touchesMoved(touches, with: event)
next?.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Ended")
super.touchesEnded(touches, with: event)
next?.touchesEnded(touches, with: event)
}
But at the same time I want the scroll to work. The UIScrollView that is below, I want that to keep scrolling as usual.
This seems impossible to do. If my view handles touches, then I can't forward them to the UIScrollView. But I want both things: see the raw touches on the top view while the scrollview to work as usual.
How can I accomplish this?
The opposite would also work. Meaning, a scrollview on top that scrolls as usual but I also receive the raw touches (touchesBegan, touchesMoved...) on the view that is underneath.
Following this answer worked for me, though with some slight changes. Do note that for this solution, the UIScrollView is on top of the UIView. Firstly, you need to create a subclass of UIScrollView and override the touch methods.
protocol CustomScrollViewDelegate {
func scrollViewTouchBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func scrollViewTouchMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func scrollViewTouchEnded(_ touches: Set<UITouch>, with event: UIEvent?)
}
class CustomScrollView: UIScrollView {
var customDelegate: CustomScrollViewDelegate?
override func awakeFromNib() {
super.awakeFromNib()
for gesture in self.gestureRecognizers ?? [] {
gesture.cancelsTouchesInView = false
gesture.delaysTouchesBegan = false
gesture.delaysTouchesEnded = false
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchBegan(touches, with: event)
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchMoved(touches, with: event)
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
customDelegate?.scrollViewTouchEnded(touches, with: event)
super.touchesEnded(touches, with: event)
}
}
Take note of the code in awakeFromNib(). UIScrollView has its own set of gestures. So for each gesture, delaysTouchesBegan and delaysTouchesEnded needs to be false to prevent delays for the touch events.
Finally, just assign the delegate to your ViewController and implement the methods like so.
class ViewController: UIViewController {
#IBOutlet weak var scrollView: CustomScrollView!
#IBOutlet weak var touchView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.customDelegate = self
}
}
extension ViewController: CustomScrollViewDelegate {
func scrollViewTouchBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Began")
}
}
}
func scrollViewTouchMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Moved")
}
}
}
func scrollViewTouchEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if touch location is within the bounds of the UIView
if let touch = touches.first {
let position = touch.location(in: view)
if touchView.bounds.contains(position) {
print("Ended")
}
}
}
}

touchesMoved touches.first!.view remains the same, if I move a finger out of view

I have a problem with touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) function. More specifically - touches.first!.view property.
I need to know the tag of view that user is pointing at. But when I drag finger out of view bounds, touches.first!.view value does not change.
This is what I am doing:
I have this code to print out selected view's tag and background color:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("\(touches.first!.view!.backgroundColor), View tag: " + String(touches.first!.view!.tag))
}
But I expect to get printed out is UIExtendedGrayColorSpace 1 1, View tag: 0, but instead of this I get <UIDynamicSystemColor: 0x600000820b40; name = systemGroupedBackgroundColor>, View tag: 5. This means, that even if I drag my cursor from one view, I still get the first clicked view from touches.first!.view.
How can I get the current selected view?
Answer based on Rob's comment above
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
if ( !self.grayUIView.frame.contains(touch.location(in: self.grayUIView)) ){
return
}else {
//still in current view - process with your logic
}
}
}

Button changing after image moves over it?

I am making a "slide to confirm" button, the set up is a custom UIButton + a regular UIImageView, the image is passed to the button and the button listen to touches and move the image accordingly.
Everything is working great except for one detail after the button is released everything should go back to their initial state, yet the button stay changed as if its alpha was halved
i commented the alpha because the button was going to hide but i disabled that once i run into this problem, the problem happens regardless of setting the alpha or not.
This is the code for the custom button
class SlideButton: UIButton {
var arrowImage: UIImageView?
var initialLocation: CGPoint?
var initialArrowPosition: CGPoint?
func setArrow(arrow:UIImageView){
arrowImage = arrow
initialArrowPosition = arrow.frame.origin
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first!
let location = touch.location(in: self.superview)
initialLocation = location
UIView.animate(withDuration: 0.2) {
//self.alpha = 0.0
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
UIView.animate(withDuration: 0.3) {
self.arrowImage!.frame.origin = self.initialArrowPosition!
//self.alpha = 1.0
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let currentLocation = touch.location(in: self.superview)
arrowImage!.frame.origin.x = initialArrowPosition!.x - initialLocation!.x + (currentLocation.x)
}
}
You are remembering to call super for one of your touches methods:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
But for the other touches methods you've forgotten to do that, so you've broken the core functionality of the button.
you might have to also add an override to touchesCancelled because you might not always hit the touchesEnded endpoint.

Getting the number of fingers touching a UIView

I want to change the color of a UIView based on the number of touches currently on it. I have managed to create this with a single touch:
class colorChangerViewClass: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
setColor(color: .blue)
print("touchesBegan")
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setColor(color: .green)
print("touchesEnded")
}
func setColor(color: UIColor) {
self.backgroundColor = color
}
}
But I am struggling with implementing multitouch. I feel like I'm not understanding something here.
Does UIView have a property to check how many fingers are currently touching it?
I could check that each time a new touchesBegan or touchesEnded occurs.
First enable multi touch on your subclass
self.isMultipleTouchEnabled = true
Then inside your touch event functions you can get all touches with event from your UIView class.
let touchCount = event?.touches(for: self)?.count
Make sure the UIView has isMultipleTouchEnabled = true, the default is false.
Then change the touchesBegan method
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
setColor(color: .blue)
let touchCount = event?.touches(for: self)?.count
}

Can a subclass of UIButton trigger segues?

Is it possible for a subclass of a UIButton to trigger a segue in storyboard?
The segue stop working as soon as I change the custom class of a button in storyboard.
I have UIPageViewController embedded in a ViewController inside a NavigationController. One of the ViewControllers in the PageController have a subclassed Button that should perform a segue in the NavigationController.
The Button subclass is as follows:
Every thing else is wired up in storyboard only
class MiniButton:UIButton{
var originalBackgroundColor:UIColor?
var originalTextColor:UIColor?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
originalBackgroundColor = self.backgroundColor
originalTextColor = self.titleColor(for: .normal)
self.backgroundColor = MiniColor.miniYellow
self.setTitleColor(MiniColor.white, for: .normal)
}
func backToOriginalColors(){
self.backgroundColor = originalBackgroundColor
self.setTitleColor(originalTextColor, for: .normal)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
backToOriginalColors()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
backToOriginalColors()
}
}
Calling super on your subclass' methods is 'super' important =(
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event) // <--------
originalBackgroundColor = self.backgroundColor
originalTextColor = self.titleColor(for: .normal)
self.backgroundColor = MiniColor.miniYellow
self.setTitleColor(MiniColor.white, for: .normal)
}

Resources