Hit Test rotated view - ios

I need to write a function that will return true if given point is inside any of subviews hierarchy of view A. A can be transformed (rotated), and has clipToBounds disabled. It's subviews also can have clipToBounds disabled, and true must returned if any of subviews is tapped. I have something that almost works, but fails when view is rotated. Can someone suggest how to fix this?
extension UIView {
func hitTestIgnoringBounds(_ point: CGPoint) -> Bool {
guard !isHidden && alpha > 0 else { return false }
let subPoint = frame.origin - point
return frame.contains(point) || subviews.contains {
return $0.hitTestIgnoringBounds(subPoint)
}
}
}

I knew it should have been easy, but could not make it work. I made a stupid mistake - my code was adding sublayers instead of subviews. When I switched to subviews, everything worked fine:
func viewOrSubviewsContaintPoint(_ point: CGPoint) -> Bool {
let convertedPoint = convert(point, from: superview)
let containsPoint = bounds.contains(convertedPoint)
var subviewsContainsPoint: Bool {
return subviews.contains { $0.viewOrSubviewsContaintPoint(convertedPoint) }
}
return containsPoint || subviewsContainsPoint
}

Related

Height constraint animation 'jumping'

I've been creating my own UIControl subclass for use in my tweak iDunnoU. I've finished the UIControl, with the exception of the expand/collapse animation. The issue with this animation is that it 'jumps' down/up when expanding/collapsing, instead of fanning out smoothly like my original mockup (see below).
I have uploaded the code to a GitHub repository, found here. The code for adding the control to the superview can be found here, the code for setting up the height constraint can be found here, and the code for animating the height constraint can be found here.
UIView.animate() can be a little tricky -- you need to call .layoutIfNeeded() on the correct view.
Replace your isExpanded / didSet in iDUMenuButton class with this:
var isExpanded = false {
didSet {
if isExpanded != oldValue {
if isExpanded {
becomeFirstResponder()
let haptics = UIImpactFeedbackGenerator(style: .rigid)
haptics.impactOccurred()
}
guard let sv = self.superview else {
// shouldn't happen, but let's be thorough
fatalError("Self must have a superview!!!")
}
// not needed
//self.layoutIfNeeded()
UIView.animate(withDuration: 0.3) {
self.heightConstraint.isActive = !self.isExpanded
// call .layoutIfNeeded() on self's superview
//self.layoutIfNeeded()
sv.layoutIfNeeded()
self.layer.shadowOpacity = self.isExpanded ? 1 : 0
self.buttons.forEach { $0.setBadgeHidden(hidden: !self.isExpanded, animated: true) }
}
delegate?.menuButton(self, isExpandedDidUpdate: isExpanded)
}
}
}

Annotation not responding to hit even though overridden hittest is returning correct value

I have searched and tried all manner of solutions and the code below is what I've come up with so far. I've tried so many tips the code is untidy but I hope I have stripped out most of this.
I have a custom annotation with a banner that falls outside the bounds of the annotation itself. I want to respond to hits on the banner.
Using the code below, when I click on the banner I get this output:
AV returning hit view: <UILabel: 0x7fd70bd8f4e0; frame = (0 -10; 77 12); text = 'Coffee Republic'; layer = <_UILabelLayer: 0x60000008e920>> as <GJ3.AnnotationView: 0x7fd70f3035e0; frame = (112 349.5; 0 0); layer = <CALayer: 0x618000a37ac0>>
I have also set the code to return hit view!.superview! and the log above shows that would return the annotation.
When I click the annotation area itself, I get the callout I expect. Clicking on the banner produces no response. I did notice when I click on the actual annotation area, the logs only show 'nil' returns, I don't see why that works!
My specific question is, is the return I'm sending from hitTest correct, should it work? If it is, what could be preventing it?
class AnnotationView: MKAnnotationView {
var companyName: String!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if (hitView != nil) {
self.superview?.bringSubview(toFront: self)
}
if (hitView != nil) {
print ("AV returning hit view: \(hitView!) as \(hitView!.superview!)")
return hitView!
} else {
print ("AV returning hit view: nil")
return nil
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = rect.contains(point)
if(!isInside) {
for view in self.subviews as [UIView] {
print ("AV view: \(view)")
if !view.isHidden && view.alpha > 0 && view.isUserInteractionEnabled && view.point(inside: convert(point, to: view), with: event) {
print ("hit in \(view)")
isInside = true
return isInside
}
}
}
return isInside
}
}
I would advise against modifying the hitTest and instead increase the size of the annotation itself (split down the annotation into a container view and the banner into another view that slides in after but still within the main MKAnnotationView). Modifying the responder chain like that can lead to some undesirable behaviours in future OS releases.
I would suggest reading up on the responder chain to understand exactly whats going on if you still want to head that direction (a good recent post here: https://medium.com/bpxl-craft/event-delivery-on-ios-part-1-8e68b3a3f423#.og2cpaeb0).

Trying to loop through UIView and all subviews to get the first responder, not working

I am trying to find the current focused First Responder by looping through the main UIView and all of it's SubViews, and all of it's SubViews through recursion, but I am coming up with nil.
extension UIView {
func getCurrentFirstResponder() -> AnyObject? {
if self.isFirstResponder() {
return self
}
for subView: UIView in self.subviews as [UIView] {
if subView.isFirstResponder() {
return subView
}
else {
subView.getCurrentFirstResponder()
}
}
return nil
}
}
let focusedView = self.view.getCurrentFirstResponder() as? UIView
Does this look correct? Why am I getting a nil view when I use this?
You code doesn't return anything in case the recursive call to subView.getCurrentFirstResponder() actually finds a first responder.
Try this:
for subView: UIView in self.subviews as [UIView] {
if subView.isFirstResponder() {
return subView
}
else {
if let sub = subView.getCurrentFirstResponder() {
return sub;
}
}
}
return nil

how to hide uinavigationbar hairline with swift

I have found a good answer about this problem, look at here How to hide iOS7 UINavigationBar 1px bottom line
but i want to know how to implement it with swift, i've tried in this way
func findHairlineImageViewUnder(view:UIView!) {
if view is UIImageView && view.bounds.size.height <= 1.0 {
return view
}
var subview: UIView
for subview in view.subviews {
var imageView:UIImageView = self.findHairlineImageViewUnder(subview)
if imageView {
return imageView
}
}
}
i can't make it because the compiler told me
'UIView' is not convertible to ()
cannot convert the expression's type 'AnyObject' to type 'UIImageView'
Type 'UIImageView' does not conform to protocol 'BoooleanType'
i know why these errors came out but how can i fix it?
This extension should do it.
extension UINavigationController {
func hairLine(hide hide: Bool) {
//hides hairline at the bottom of the navigationbar
for subview in self.navigationBar.subviews {
if subview.isKindOfClass(UIImageView) {
for hairline in subview.subviews {
if hairline.isKindOfClass(UIImageView) && hairline.bounds.height <= 1.0 {
hairline.hidden = hide
}
}
}
}
}
}
Just call it like this:
navigationController?.hairLine(hide: true)
There are a few problems in your code.
You haven't specified a return type, so the compiler is trying to figure it out for you but is having trouble. You should specify that your return type is UIView?; it should be an optional because you may not be able to find the view and will need to return nil.
You don't need to declare subview before your for loop; the for loop will do it implicitly.
Declaring imageView to a UIImageView will not work without casting. However, you don't really need to make it a UIImageView in the first place and you can let swift deal with it.
By making the change in 1, you can now simplify the inside of your for loop using optional binding and just do if let foundView = self.findHairlineImageViewUnder(subview) { ... }
If you never find the view you're looking for, you never return anything from the function. That's going to cause a compile error in swift, so make sure to return nil at the very end.
Here is a working implementation with the above fixes:
func findHairlineImageViewUnder(view:UIView!) -> UIView? {
if view is UIImageView && view.bounds.size.height <= 1.0 {
return view
}
for subview in view.subviews as [UIView] {
if let foundView = self.findHairlineImageViewUnder(subview) {
return foundView
}
}
return nil
}

UIPickerView, detect "rolling wheel" start and stop?

I just discovered that if I do the following:
Click the button that animates a UIPickerView into my view
Quickly start the wheel rolling towards, then past, the last item
Dismiss the view with a button
Then it has not yet selected the last item yet.
I tried this by simply outputting to the console whenever the didSelectRow method was fired, and it fires when the wheel stabilizes on the last item.
Can I detect that the wheel is still rolling, so that I can delay checking it for a selected value until it stabilizes?
If it matters, I'm programming in MonoTouch, but I can read Objective-C code well enough to reimplement it, if you have a code example that is.
As animation keys don't work, I wrote this simple function that works for detecting if a UIPickerView is currently moving.
-(bool) anySubViewScrolling:(UIView*)view
{
if( [ view isKindOfClass:[ UIScrollView class ] ] )
{
UIScrollView* scroll_view = (UIScrollView*) view;
if( scroll_view.dragging || scroll_view.decelerating )
{
return true;
}
}
for( UIView *sub_view in [ view subviews ] )
{
if( [ self anySubViewScrolling:sub_view ] )
{
return true;
}
}
return false;
}
It ends up returning true five levels deep.
Swift 4 (updated) version with extension of #DrainBoy answers
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
}
Since animationKeys seems to not work anymore, I have another solution. If you check the subviews of UIPickerView, you'll see that there is a UIPickerTableView for each component.
This UIPickerTableView is indeed a subclass of UITableView and of course of UIScrollView. Therefore, you can check its contentOffset value to detect a difference.
Besides, its scrollViewDelegate is nil by default, so I assume you can safely set an object of yours to detect scrollViewWillBeginDragging, scrollViewDidEndDecelerating, etc.
By keeping a reference to each UIPickerTableView, you should be able to implement an efficient isWheelRolling method.
Expanded #iluvatar_GR answer
extension UIView {
func isScrolling () -> Bool {
if let scrollView = self as? UIScrollView {
if (scrollView.isDragging || scrollView.isDecelerating) {
return true
}
}
for subview in self.subviews {
if ( subview.isScrolling() ) {
return true
}
}
return false
}
func waitTillDoneScrolling (completion: #escaping () -> Void) {
var isMoving = true
DispatchQueue.global(qos: .background).async {
while isMoving == true {
isMoving = self.isScrolling()
}
DispatchQueue.main.async {
completion()}
}
}
}
Expanded #iluvatar_GR, #Robert_at_Nextgensystems answer
Used Gesture, UIScrollView isDragging or isDecelerating.
// Call it every time when Guesture action.
#objc func respondToSwipeGesture(gesture: UIGestureRecognizer) {
// Changes the button name to scrolling at the start of scrolling.
DispatchQueue.main.async {
self._button.setTitle("Scrolling...", for: .normal)
self._button.isEnabled = false
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "FF8FAE")
}
// Indication according to scrolling status
_datePicker.waitTillDoneScrolling(completion: {
print("completion")
DispatchQueue.main.async {
self._button.setTitle("Completion", for: .normal)
self._button.isEnabled = true
self._button.backgroundColor = Utils.hexStringToUIColor(hex: "7CB0FF")
}
})
}
[SWIFT4] Share Example Source link!
enter Sample Source link
Reference : How to recognize swipe in all 4 directions
I think you can just check if the UIPickerView is in the middle of animating and wait for it to stop. This was answered here link
You can use a SwipeGestureRecognizer on the picker.
I assume this is not a perfect solution at all.
- (void)viewDidLoad
{
[super viewDidLoad];
_pickerSwipeGestureRecognizer.delegate = self;
[_pickerSwipeGestureRecognizer setDirection:(UISwipeGestureRecognizerDirectionDown | UISwipeGestureRecognizerDirectionUp)];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
if([gestureRecognizer isEqual:_pickerSwipeGestureRecognizer]){
NSLog(#"start");
}
}
- (void)pickerView:(UIPickerView *)thePickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
NSLog(#"end");
}

Resources