NSViewController slow to register mouse events? - swift

I've been working on a small image uploading menubar app for OS X. I've created custom NSView subclass for the uploaded items.
Here's what it looks like by default:
Mouse events are handled by the view's NSViewController in the following way:
import Cocoa
class MenuItemController: NSViewController {
private var trackingArea: NSTrackingArea?
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = true
v.needsDisplay = true
}
}
override func mouseExited(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
v.shouldHighlight = false
v.needsDisplay = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
if (trackingArea == nil) {
trackingArea = NSTrackingArea(rect: self.view.bounds, options: [.ActiveAlways, .MouseEnteredAndExited], owner: self, userInfo: nil)
self.view.addTrackingArea(trackingArea!)
}
/* rest of the code... */
}
}
It works fine until I move my cursor fast over the items. It seems like the mouseExited() event is not called, and the view remains with a blue background (mouse is actually on the Quit button):
I also tried moving the mouse handling into the NSView, but with same results. I appreciate any input! Thanks!

In my opinion, Apple has had bugs in this area.
Assuming you update your tracking area according to apple docs,
adding this additional fix might fix your problem... it fixes it for me in many cases.
I verify in mouseMoved / mouseEntered routine that the mouse cursor is still within my views frame, and if not, call mouseExited: myself.
- (void) adjustTrackingArea
{
if ( trackingArea )
{
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
// determine the tracking options
NSTrackingAreaOptions trackingOptions = // NSTrackingEnabledDuringMouseDrag | // don't track during drag
NSTrackingMouseMoved |
NSTrackingMouseEnteredAndExited |
//NSTrackingActiveInActiveApp | NSTrackingActiveInKeyWindow | NSTrackingActiveWhenFirstResponder |
NSTrackingActiveAlways;
NSRect theRect = [self visibleRect];
trackingArea = [[NSTrackingArea alloc]
initWithRect: theRect
options: trackingOptions
owner: self
userInfo: nil];
[self addTrackingArea:trackingArea];
}
- (void)resetCursorRects
{
[self adjustTrackingArea];
}
- (void)mouseEntered:(NSEvent *)ev
{
[self setNeedsDisplay:YES];
// make sure current mouse cursor location remains under the mouse cursor
NSPoint cursorPt = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:NULL];
// apple bug!!!
//NSPoint cursorPt2 = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x != cursorPt2.x )
// NSLog( #"hello old cursorPt" );
NSRect r = [self frame];
if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < 0 )
{
[self mouseExited:ev];
//cursorPt.x = [self convertPointFromBase:[ev locationInWindow]];
//if ( cursorPt.x > NSMaxX( r ) || cursorPt.x < r.origin.x )
return;
}
... your custom stuff here ...
}
- (void)mouseExited:(NSEvent *)theEvent
{
if ( isTrackingCursor == NO )
return;
[[NSCursor arrowCursor] set];
isTrackingCursor = NO;
[self setNeedsDisplay:YES];
}
- (void)mouseMoved:(NSEvent *)theEvent
{
[self mouseEntered:theEvent];
}

I do not understand whether you are installing an NSTrackingArea for the whole window (in this case menu) or for each item. If you are doing the latter, don't do it, or you will spend an endless time to correct the problems you are seeing. The way I handled this buggy behaviour is to create an NSTrackingArea for the whole window and I figure myself where the mouse is and handle the highlighting of each item myself. I know this is not ideal, but this was the only way I was able to solve it after knocking myself in the head for three days.

You can track which was the old active menuitem and set manage it accordingly, something like:
override func mouseEntered(theEvent: NSEvent) {
if let v = self.view as? MenuItemView {
lastEntered.shouldHighLight = false
lastEntered.needsDisplay = true
lastEntered = v;
lastEntered.shouldHighLight = true
lastEntered.needsDisplay = true
}
}
Thus you will ensure that at most one will be active.

Related

How can I implement the ignore(_:for:) method to ignore a button press?

I wanted to implement a UITapGestureRecognizer that closes a FanMenu (a circular based menu around a button with other buttons) when the user press outside of the menu.
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.delegate = self
tapRecognizer.require(toFail: doubleTapRecognizer)
scrollView.addGestureRecognizer(tapRecognizer)
var planScaleDetermination : PlanScaleDetermination?
#objc func handleTap(sender: UITapGestureRecognizer) {
if(sender.state == .ended) {
let point = sender.location(in: viewForTilingView)
if(fanMenu.isOpen && outerFanMenu.isOpen) {
fanMenu.close()
outerFanMenu.close()
outerFanMenu.isHidden = true
fanMenu.isHidden = true
}
if(isPointInPlanDimensions(point: point)) {
if(!planControlHelper!.scaleView.isHidden && planScaleDetermination != nil) {
if let psd = planScaleDetermination {
psd.addPoint(point: MobiPlanPoint(X: point.x, Y: point.y))
}
}
else if(isModeActive && planControlHelper!.documentationMode == .area && !area!.isCorrectionModeActive) {
area!.addPoint(point: MobiPlanPoint(X: point.x, Y: point.y), drawOnly: false)
}
}
}
}
If the menu isOpen it closes it correctly, but it closes it also when I select a button inside the menu. I found the ignore(_:for:) method that tells the gesture recognizer to ignore a specific press of the given event.
How can I implement the method correctly so that my menu will not close when I select a button?
Implementation of the button looks like this:
var outerActionItems : [FanMenuButton] = []
func initOuterFanMenu() {
outerFanMenu.button = FanMenuButton(
id: "main",
image: "baseline_clear_white_18pt_1x",
color: Color(val: 0xFFFFFFFF)
)
outerActionItems = [
FanMenuButton(
id: "cancelButton",
image: "baseline_clear_white_18pt_1x",
color: Color(val: 0xF55B58)
),
FanMenuButton(
id: "confirmButton",
image: "baseline_done_white_18pt_1x",
color: Color(val: 0xF55B58)
)
]
outerFanMenu.items = outerActionItems
outerFanMenu.menuRadius = 110.0
outerFanMenu.radius = 15.0
outerFanMenu.duration = 0.35
outerFanMenu.delay = 0.05
outerFanMenu.interval = (0, 2.0 * .pi)
outerFanMenu.onItemDidClick = { button in
self.contextMenuButtonClicked(buttonId: button.id)
}
outerFanMenu.onItemWillClick = { button in
if(button.id != "rotateMarker") {
self.outerFanMenu.isHidden = !self.outerFanMenu.isHidden
}
}
}
You can check if tapped view is menu button:
#objc func handleTap(sender: UITapGestureRecognizer) {
if sender.view is FanMenuButton { return }
... your code ... }
Just add another view with the name of the background view and add a gesture recognizer on it. Background view should be behind your view. just an example in objective c, you can translate to swift.
_backgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(backgroundTapAction:)];
[_backgroundView addGestureRecognizer:tapGesture];
- (void)backgroundTapAction:(UITapGestureRecognizer *)sender {
//dismiss your view here.
}

gesture recognizer issue with tic tac toe in xcode

hello there i am trying to figure out what the issue is why i cant get a cross on the game board when a tap is made. the TTT class is the one below the first code and it is called in the main code. i will appreciate if some can help me on this
#IBOutlet var fields: [TTTImageView]!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func fieldTapped (recognizer:UITapGestureRecognizer) {
let tappedField = recognizer.view as! TTTImageView
tappedField.setPlayer(_player: "x.png")
}
func setupField () {
for index in 0 ... fields.count - 1 {
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(fieldTapped))
gestureRecognizer.numberOfTapsRequired = 1
fields[index].addGestureRecognizer(gestureRecognizer)
}
}
import UIKit
class TTTImageView: UIImageView {
var player:String?
var activated:Bool! = false
func setPlayer (_player:String) {
self.player = _player
if activated == false {
if _player == "x.png" {
self.image = UIImage (named: "x.png")
}else{
self.image = UIImage (named: "o.png")
}
activated = true
}
}
}
Make sure when you set fields[index].addGestureRecognizer(gestureRecognizer) to set isUserInteractionEnabled to true.
fields[index].isUserInteractionEnabled = true
Whenever you want to detect user activity (e.g. UITapGestureRecognizer on a UIImageView, you need to set this to true
EDIT:
I also wanted to mention something I noticed after looking at your code again: the method you are using for selectors has been deprecated. If you are using Swift 3 (which I highly recommend you do), you shouldn't call selectors like this: "functionName:" anymore. Now we use selectors like so: #selector(functionName(_:)). You should start updating your code to the most current syntax sooner rather than later.

Detect if user is moving finger left or right (Swift)

This is not Sprite Kit.
If I have a variable like the one below
var value = 0
How am I able to increase the value if the user drags right and decrease if they drag left?
Thanks!
Like Caleb commented, Ray's tutorial is great, but if you want the actual swift example, please check the next example:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
private var value: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.blackColor()
let recognizer = UIPanGestureRecognizer(target: self, action: Selector("handleDragging:"))
let inputView = UIView(frame: CGRectMake(0, 0, 100, 100))
inputView.backgroundColor = UIColor.whiteColor()
inputView.userInteractionEnabled = true
inputView.addGestureRecognizer(recognizer)
self.view.addSubview(inputView)
}
func handleDragging(recognizer: UIPanGestureRecognizer) {
if (recognizer.state == .Changed) {
let point = recognizer.velocityInView(recognizer.view?.superview)
if (point.x > 0) {
self.value++;
} else {
self.value--;
}
println(self.value)
}
}
}
You can use the velocityInView method of UIPanGestureRecognizer to determine which direction you're going. It returns a CGPoint, so you can pull out the x and y values as you wish. Positive is right/down, negative is left/up.

How to make a UIScrollView auto scroll when a UITextField becomes a first responder

I've seen posts around here that suggest that UIScrollViews should automatically scroll if a subview UITextField becomes the first responder; however, I can't figure out how to get this to work.
What I have is a UIViewController that has a UIScrollView and within the UIScrollView there are multiple textfields.
I know how to do this manually if necessary; however, from what I've been reading, it seems possible to have it autoscroll. Help please.
I hope this example will help you
You can scroll to any point by this code.
scrollView.contentOffset = CGPointMake(0,0);
So if you have textfield, it must have some x,y position on view, so you can use
CGPoint point = textfield.frame.origin ;
scrollView.contentOffset = point
This should do the trick,
But if you don't know when to call this code, so you should learn UITextFieldDelegate methods
Implement this method in your code
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// Place Scroll Code here
}
I hope you know how to use delegate methods.
I know this question has already been answered, but I thought I would share the code combination that I used from #Adeel and #Basil answer, as it seems to work perfectly for me on iOS 9.
-(void)textFieldDidBeginEditing:(UITextField *)textField {
// Scroll to the text field so that it is
// not hidden by the keyboard during editing.
[scroll setContentOffset:CGPointMake(0, (textField.superview.frame.origin.y + (textField.frame.origin.y))) animated:YES];
}
-(void)textFieldDidEndEditing:(UITextField *)textField {
// Remove any content offset from the scroll
// view otherwise the scroll view will look odd.
[scroll setContentOffset:CGPointMake(0, 0) animated:YES];
}
I also used the animated method, it makes for a much smoother transition.
Here is the Swift 4 update to #Supertecnoboff's answer. It worked great for me.
func textFieldDidBeginEditing(_ textField: UITextField) {
scroll.setContentOffset(CGPoint(x: 0, y: (textField.superview?.frame.origin.y)!), animated: true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
scroll.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
Make sure to extend UITextFieldDelegate and set the textfields' delegate to self.
There is nothing you have to do manually. It is the default behavior. There are two possibilities as to why you are not seeing the behavior
The most likely reason is that the keyboard is covering your UITextField. See below for solution
The other possibility is that you have another UIScrollView somewhere in the view hierarchy between the UITextField and the UIScrollView that you want to auto scroll. This is less likely but can still cause problems.
For #1, you want to implement something similar to Apple's recommendations for Moving Content That Is Located Under the Keyboard. Note that the code provided by Apple does not account for rotation. For improvements on their code, check out this blog post's implementation of the keyboardDidShow method that properly translates the keyboard's frame using the window.
- (void)textFieldDidBeginEditing:(UITextField *)textField {
CGRect rect = [textField bounds];
rect = [textField convertRect:rect toView:self.scrollView];
rect.origin.x = 0 ;
rect.origin.y -= 60 ;
rect.size.height = 400;
[self.scrollView scrollRectToVisible:rect animated:YES];
}
You can use this function for autoScroll of UITextField
on UITextFieldDelegate
- (void)textFieldDidBeginEditing:(UITextField *)textField {
[self autoScrolTextField:textField onScrollView:self.scrollView];
}
- (void) autoScrolTextField: (UITextField *) textField onScrollView: (UIScrollView *) scrollView {
float slidePoint = 0.0f;
float keyBoard_Y_Origin = self.view.bounds.size.height - 216.0f;
float textFieldButtomPoint = textField.superview.frame.origin.y + (textField.frame.origin.y + textField.frame.size.height);
if (keyBoard_Y_Origin < textFieldButtomPoint - scrollView.contentOffset.y) {
slidePoint = textFieldButtomPoint - keyBoard_Y_Origin + 10.0f;
CGPoint point = CGPointMake(0.0f, slidePoint);
scrollView.contentOffset = point;
}
EDIT:
Im now using IQKeyboardManager
Kudos to the developer of this, you need to try this.
Solution
extension UIScrollView {
func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
guard let firstResponderSubview = findFirstResponderSubview() else { return }
scrollVertically(toFirstResponder: firstResponderSubview,
keyboardFrameHight: keyboardFrameHight, animated: true)
}
private func scrollVertically(toFirstResponder view: UIView,
keyboardFrameHight: CGFloat, animated: Bool) {
let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
let maxY = contentSize.height - scrollViewVisibleRectHeight
if contentOffset.y >= maxY { return }
var point = view.convert(view.bounds.origin, to: self)
point.x = 0
point.y -= scrollViewVisibleRectHeight/2
if point.y > maxY {
point.y = maxY
} else if point.y < 0 {
point.y = 0
}
setContentOffset(point, animated: true)
}
}
extension UIView {
func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
}
Full Sample
Do not forget to paste the Solution code here
import UIKit
class ViewController: UIViewController {
private weak var scrollView: UIScrollView!
private lazy var keyboard = KeyboardNotifications(notifications: [.willHide, .willShow], delegate: self)
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = UIScrollView()
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
scrollView.isScrollEnabled = true
scrollView.indicatorStyle = .default
scrollView.backgroundColor = .yellow
scrollView.keyboardDismissMode = .interactive
self.scrollView = scrollView
addTextField(y: 20)
addTextField(y: 300)
addTextField(y: 600)
addTextField(y: 950)
}
private func addTextField(y: CGFloat) {
let textField = UITextField()
textField.borderStyle = .line
scrollView.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
textField.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: y).isActive = true
textField.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 44).isActive = true
textField.widthAnchor.constraint(equalToConstant: 120).isActive = true
textField.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboard.isEnabled = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
keyboard.isEnabled = false
}
}
extension ViewController: KeyboardNotificationsDelegate {
func keyboardWillShow(notification: NSNotification) {
guard let userInfo = notification.userInfo as? [String: Any],
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
scrollView.contentInset.bottom = keyboardFrame.height
scrollView.scrollVerticallyToFirstResponderSubview(keyboardFrameHight: keyboardFrame.height)
}
func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = 0
}
}
/// Solution
extension UIScrollView {
func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
guard let firstResponderSubview = findFirstResponderSubview() else { return }
scrollVertically(toFirstResponder: firstResponderSubview,
keyboardFrameHight: keyboardFrameHight, animated: true)
}
private func scrollVertically(toFirstResponder view: UIView,
keyboardFrameHight: CGFloat, animated: Bool) {
let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
let maxY = contentSize.height - scrollViewVisibleRectHeight
if contentOffset.y >= maxY { return }
var point = view.convert(view.bounds.origin, to: self)
point.x = 0
point.y -= scrollViewVisibleRectHeight/2
if point.y > maxY {
point.y = maxY
} else if point.y < 0 {
point.y = 0
}
setContentOffset(point, animated: true)
}
}
extension UIView {
func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
}
// https://stackoverflow.com/a/42600092/4488252
import Foundation
protocol KeyboardNotificationsDelegate: class {
func keyboardWillShow(notification: NSNotification)
func keyboardWillHide(notification: NSNotification)
func keyboardDidShow(notification: NSNotification)
func keyboardDidHide(notification: NSNotification)
}
extension KeyboardNotificationsDelegate {
func keyboardWillShow(notification: NSNotification) {}
func keyboardWillHide(notification: NSNotification) {}
func keyboardDidShow(notification: NSNotification) {}
func keyboardDidHide(notification: NSNotification) {}
}
class KeyboardNotifications {
fileprivate var _isEnabled: Bool
fileprivate var notifications: [NotificationType]
fileprivate weak var delegate: KeyboardNotificationsDelegate?
fileprivate(set) lazy var isKeyboardShown: Bool = false
init(notifications: [NotificationType], delegate: KeyboardNotificationsDelegate) {
_isEnabled = false
self.notifications = notifications
self.delegate = delegate
}
deinit { if isEnabled { isEnabled = false } }
}
// MARK: - enums
extension KeyboardNotifications {
enum NotificationType {
case willShow, willHide, didShow, didHide
var selector: Selector {
switch self {
case .willShow: return #selector(keyboardWillShow(notification:))
case .willHide: return #selector(keyboardWillHide(notification:))
case .didShow: return #selector(keyboardDidShow(notification:))
case .didHide: return #selector(keyboardDidHide(notification:))
}
}
var notificationName: NSNotification.Name {
switch self {
case .willShow: return UIResponder.keyboardWillShowNotification
case .willHide: return UIResponder.keyboardWillHideNotification
case .didShow: return UIResponder.keyboardDidShowNotification
case .didHide: return UIResponder.keyboardDidHideNotification
}
}
}
}
// MARK: - isEnabled
extension KeyboardNotifications {
private func addObserver(type: NotificationType) {
NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil)
}
var isEnabled: Bool {
set {
if newValue {
for notificaton in notifications { addObserver(type: notificaton) }
} else {
NotificationCenter.default.removeObserver(self)
}
_isEnabled = newValue
}
get { _isEnabled }
}
}
// MARK: - Notification functions
extension KeyboardNotifications {
#objc func keyboardWillShow(notification: NSNotification) {
delegate?.keyboardWillShow(notification: notification)
isKeyboardShown = true
}
#objc func keyboardWillHide(notification: NSNotification) {
delegate?.keyboardWillHide(notification: notification)
isKeyboardShown = false
}
#objc func keyboardDidShow(notification: NSNotification) {
isKeyboardShown = true
delegate?.keyboardDidShow(notification: notification)
}
#objc func keyboardDidHide(notification: NSNotification) {
isKeyboardShown = false
delegate?.keyboardDidHide(notification: notification)
}
}
If you have multiple textfields say Textfield1, Textfield2, Textfield3 and you want to scroll the scrollview along the y-axis when textfield2 becomes first responder:
if([Textfield2 isFirstResponder])
{
scrollView.contentOffset = CGPointMake(0,yourY);
}
As Michael McGuire mentioned in his point #2 above, the system's default behavior misbehaves when the scroll view contains another scroll view between the text field and the scroll view. I've found that the misbehavior also occurs when there's a scroll view merely next to the text field (both embedded in the scroll view that needs to be adjusted to bring the text field into view when the text field wants to start editing. This is on iOS 12.1.
But my solution is different from the above. In my top-level scroll view, which is sub-classed so I can add properties and override methods, I override scrollRectToVisible:animated:. It simply calls its [super scrollRectToVisible:animated:] unless there's a property set that tells it to adjust the rect passed in, which is the frame of the text field. When the property is non-nil, it is a reference to the UITextField in question, and the rect is adjusted so that the scroll view goes further than the system thought it would. So I put this in the UIScrollView's sub-classed header file:
#property (nullable) UITextField *textFieldToBringIntoView;
(with appropriate #synthesize textFieldToBringIntoView; in the implementation. Then I added this override method to the implementation:
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)how
{
if (textFieldToBringIntoView) {
// Do whatever mucking with `rect`'s origin needed to make it visible
// based on context or its spatial relationship with the other
// view that the system is getting confused by.
textFieldToBringIntoView = nil; // Go back to normal
}
[super scrollRectToVisible:rect animated:how];
}
In the delegate method for the UITextField for when it's about to begin editing, just set textFieldToBringIntoView to the textField in question:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
// Ensure it scrolls into view so that keyboard doesn't obscure it
// The system is about to call |scrollRectIntoView:| for the scrolling
// superview, but the system doesn't get things right in certain cases.
UIScrollView *parent = (UIScrollView *)textField.superview;
// (or figure out the parent UIScrollView some other way)
// Tell the override to do something special just once
// based on this text field's position in its parent's scroll view.
parent.textFieldToBringIntoView = textField;
// The override function will set this back to nil
return(YES);
}
It seems to work. And if Apple fixes their bug, it seems like it might still work (fingers crossed).
Building off of Vasily Bodnarchuk's answer I created a gist with a simple protocol that you can implement and it'll do it all for you.
All you need to do is call registerAsTextDisplacer()
I created a BaseViewController in my project and made that implement it
https://gist.github.com/CameronPorter95/cb68767f5f8052fdc70293c167e9430e
Other solutions I saw, let you set the offset to the origin of the textField but this makes the scroller view go beyond it bounds.
I did this adjustment to the offset instead to not go beyond the bottom nor the top offsets.
Set the keyboardHeightConstraint to the bottom of the page.
When the keyboard shows, update its constraint's constant to negative the keyboard height.
Then scroll to the responderField as we will show below.
#IBOutlet var keyboardHeightConstraint: NSLayoutConstraint?
var responderField: String?
#objc func keyboardNotification(notification: NSNotification) {
guard let keyboardValue = notification.userInfo [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardHeight = keyboardValue.cgRectValue.height
keyboardHeightConstraint?.constant = -keyboardHeight
scroll(field: responderField!)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
responderField = textField
}
Now we want to make sure we do not scroll greater than the bottom offset nor less than the top offset.
At the same time, we want to calculate the offset of the field's maxY value.
To do that, we subtract the scrollView.bounds.size.height from the maxY value.
let targetOffset = field.frame.maxY - scrollView.bounds.size.height
I found it nicer to scroll an extra distance of the keyboard height, but you could neglect that if you want to scroll right below the field.
let targetOffset = keyboardHeight + field.frame.maxY - scrollView.bounds.size.height
Remember to add the scrollView.contentInset.bottom if you have the tab bar visible.
func scroll(field: UITextField) {
guard let keyboardConstraintsConstant = keyboardHeightConstraint?.constant else { return }
let keyboardHeight = -keyboardConstraintsConstant
view.layoutIfNeeded()
let bottomOffset = scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom
let topOffset = -scrollView.safeAreaInsets.top
let targetOffset = keyboardHeight + field.frame.maxY + scrollView.contentInset.bottom - scrollView.bounds.size.height
let adjustedOffset = targetOffset > bottomOffset ? bottomOffset : (targetOffset < topOffset ? topOffset : targetOffset)
scrollView.setContentOffset(CGPoint(x: 0, y: adjustedOffset), animated: true)
}
If you have scrollView and tableView with invalidating intrinsicContentSize as the subview, you can disable tableView scrolling in storyboard or set tableView.isScrollEnabled to false in code.

Determine if UIView is visible to the user?

is it possible to determine whether my UIView is visible to the user or not?
My View is added as subview several times into a Tab Bar Controller.
Each instance of this view has a NSTimer that updates the view.
However I don't want to update a view which is not visible to the user.
Is this possible?
Thanks
For anyone else that ends up here:
To determine if a UIView is onscreen somewhere, rather than checking superview != nil, it is better to check if window != nil. In the former case, it is possible that the view has a superview but that the superview is not on screen:
if (view.window != nil) {
// do stuff
}
Of course you should also check if it is hidden or if it has an alpha > 0.
Regarding not wanting your NSTimer running while the view is not visible, you should hide these views manually if possible and have the timer stop when the view is hidden. However, I'm not at all sure of what you're doing.
You can check if:
it is hidden, by checking view.hidden
it is in the view hierarchy, by checking view.superview != nil
you can check the bounds of a view to see if it is on screen
The only other thing I can think of is if your view is buried behind others and can't be seen for that reason. You may have to go through all the views that come after to see if they obscure your view.
This will determine if a view's frame is within the bounds of all of its superviews (up to the root view). One practical use case is determining if a child view is (at least partially) visible within a scrollview.
Swift 5.x:
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convert(view.bounds, from: view)
if viewFrame.intersects(inView.bounds) {
return isVisible(view: view, inView: inView.superview)
}
return false
}
return isVisible(view: view, inView: view.superview)
}
Older swift versions
func isVisible(view: UIView) -> Bool {
func isVisible(view: UIView, inView: UIView?) -> Bool {
guard let inView = inView else { return true }
let viewFrame = inView.convertRect(view.bounds, fromView: view)
if CGRectIntersectsRect(viewFrame, inView.bounds) {
return isVisible(view, inView: inView.superview)
}
return false
}
return isVisible(view, inView: view.superview)
}
Potential improvements:
Respect alpha and hidden.
Respect clipsToBounds, as a view may exceed the bounds of its superview if false.
The solution that worked for me was to first check if the view has a window, then to iterate over superviews and check if:
the view is not hidden.
the view is within its superviews bounds.
Seems to work well so far.
Swift 3.0
public func isVisible(view: UIView) -> Bool {
if view.window == nil {
return false
}
var currentView: UIView = view
while let superview = currentView.superview {
if (superview.bounds).intersects(currentView.frame) == false {
return false;
}
if currentView.isHidden {
return false
}
currentView = superview
}
return true
}
I benchmarked both #Audrey M. and #John Gibb their solutions.
And #Audrey M. his way performed better (times 10).
So I used that one to make it observable.
I made a RxSwift Observable, to get notified when the UIView became visible.
This could be useful if you want to trigger a banner 'view' event
import Foundation
import UIKit
import RxSwift
extension UIView {
var isVisibleToUser: Bool {
if isHidden || alpha == 0 || superview == nil {
return false
}
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return false
}
let viewFrame = convert(bounds, to: rootViewController.view)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootViewController.view.safeAreaInsets.top
bottomSafeArea = rootViewController.view.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootViewController.view.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootViewController.view.bounds.height - bottomSafeArea
}
}
extension Reactive where Base: UIView {
var isVisibleToUser: Observable<Bool> {
// Every second this will check `isVisibleToUser`
return Observable<Int>.interval(.milliseconds(1000),
scheduler: MainScheduler.instance)
.map { [base] _ in
return base.isVisibleToUser
}.distinctUntilChanged()
}
}
Use it as like this:
import RxSwift
import UIKit
import Foundation
private let disposeBag = DisposeBag()
private func _checkBannerVisibility() {
bannerView.rx.isVisibleToUser
.filter { $0 }
.take(1) // Only trigger it once
.subscribe(onNext: { [weak self] _ in
// ... Do something
}).disposed(by: disposeBag)
}
Tested solution.
func isVisible(_ view: UIView) -> Bool {
if view.isHidden || view.superview == nil {
return false
}
if let rootViewController = UIApplication.shared.keyWindow?.rootViewController,
let rootView = rootViewController.view {
let viewFrame = view.convert(view.bounds, to: rootView)
let topSafeArea: CGFloat
let bottomSafeArea: CGFloat
if #available(iOS 11.0, *) {
topSafeArea = rootView.safeAreaInsets.top
bottomSafeArea = rootView.safeAreaInsets.bottom
} else {
topSafeArea = rootViewController.topLayoutGuide.length
bottomSafeArea = rootViewController.bottomLayoutGuide.length
}
return viewFrame.minX >= 0 &&
viewFrame.maxX <= rootView.bounds.width &&
viewFrame.minY >= topSafeArea &&
viewFrame.maxY <= rootView.bounds.height - bottomSafeArea
}
return false
}
I you truly want to know if a view is visible to the user you would have to take into account the following:
Is the view's window not nil and equal to the top most window
Is the view, and all of its superviews alpha >= 0.01 (threshold value also used by UIKit to determine whether it should handle touches) and not hidden
Is the z-index (stacking value) of the view higher than other views in the same hierarchy.
Even if the z-index is lower, it can be visible if other views on top have a transparent background color, alpha 0 or are hidden.
Especially the transparent background color of views in front may pose a problem to check programmatically. The only way to be truly sure is to make a programmatic snapshot of the view to check and diff it within its frame with the snapshot of the entire screen. This won't work however for views that are not distinctive enough (e.g. fully white).
For inspiration see the method isViewVisible in the iOS Calabash-server project
The simplest Swift 5 solution I could come up with that worked in my situation (I was looking for a button embedded in my tableViewFooter).
John Gibbs solution also worked but in my cause I did not need all the recursion.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
In viewWillAppear set a value "isVisible" to true, in viewWillDisappear set it to false. Best way to know for a UITabBarController subviews, also works for navigation controllers.
Another useful method is didMoveToWindow()
Example: When you push view controller, views of your previous view controller will call this method. Checking self.window != nil inside of didMoveToWindow() helps to know whether your view is appearing or disappearing from the screen.
This can help you figure out if your UIView is the top-most view. Can be helpful:
let visibleBool = view.superview?.subviews.last?.isEqual(view)
//have to check first whether it's nil (bc it's an optional)
//as well as the true/false
if let visibleBool = visibleBool where visibleBool { value
//can be seen on top
} else {
//maybe can be seen but not the topmost view
}
try this:
func isDisplayedInScreen() -> Bool
{
if (self == nil) {
return false
}
let screenRect = UIScreen.main.bounds
//
let rect = self.convert(self.frame, from: nil)
if (rect.isEmpty || rect.isNull) {
return false
}
// 若view 隐藏
if (self.isHidden) {
return false
}
//
if (self.superview == nil) {
return false
}
//
if (rect.size.equalTo(CGSize.zero)) {
return false
}
//
let intersectionRect = rect.intersection(screenRect)
if (intersectionRect.isEmpty || intersectionRect.isNull) {
return false
}
return true
}
In case you are using hidden property of view then :
view.hidden (objective C) or view.isHidden(swift) is read/write property. So you can easily read or write
For swift 3.0
if(view.isHidden){
print("Hidden")
}else{
print("visible")
}