Getting a UIView's visible rectangle - iphone

I have a UIScrollView that contains a custom UIView. Inside the custom UIView, I'd like to know the rectangle in which it's visible (i.e. not clipped).
The quick-n-dirty solution is to have the custom UIView assume that the parent is a UIScrollView and get the content size through it, but I'm looking for a better solution that doesn't involve make such assumptions.

This should do the trick
CGRect visibleRect = CGRectIntersection(self.frame, superview.bounds);
Use that in the UIView and it should get you the rectangle (if any) that represents the visible section of that view in it's superview (The UIScrollView). I'm assuming here that there is no view between them in the hierarchy, but if there is, fiddling the code should be trivial.
Hope I could help!

It would help if you would give more info on what is that you are trying to accomplish.
If you want to know the size of the super view you can do this:
CGRect superFrame = [self superview].frame;

Swift 3
extension UIView {
var visibleRect: CGRect? {
guard let superview = superview else { return nil }
return frame.intersection(superview.bounds)
}
}

Related

NSScrollView not scrolling

I have a form in a Mac app that needs to scroll. I have a scrollView embedded in a ViewController. I have the scrollView assigned with an identifier that links it to its own NSScrollView file. The constraints are set to the top, right, and left of the view controller, it also has the hight constraint set to the full height of the ViewController.
Here is my code:
import Cocoa
class ScrollView: NSScrollView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
NSRect documentView.NSMakeSize(0, 0, 1058.width, 1232.height)
}
override func scrollWheel(with event: NSEvent) {
switch event.phase {
case NSEvent.Phase.began:
Swift.print("Began")
// case NSEvent.Phase.changed:
// Swift.print("Changed")
case NSEvent.Phase.ended:
Swift.print("Ended")
default:
break
}
switch event.momentumPhase {
case NSEvent.Phase.began:
Swift.print("Momentum Began")
// case NSEvent.Phase.changed:
// Swift.print("Momentum Changed")
case NSEvent.Phase.ended:
Swift.print("Momentum Ended")
default:
break
}
super.scrollWheel(with: event)
}
I cant seem to get my app to scroll at all. I think I am not setting the frame correctly. What is the best way to do set the frame correctly? Am I coding the NSScrollView correctly?
I think you are making your life very hard because you are doing things that are not exactly recommended by Apple. First of all, you should not subclass NSScrollView. Rather you should read first Introduction to Scroll View Programming Guide for Cocoa by Apple to understand how you should create the correct hierarchy of views for an NSScrollView to work correctly.
A second recommendation is for you to check this nice article about how you should set up an NSScrollView in a playground, so that you can play with the code you want to implement.
Third, using Autolayout and NSScrollView has caused a lot of grief to a lot of people. You need to set up the AutoLayout just right, so that everything is going to work as expected. I recommend that you check this answer by Ken Thomases, which clearly explains how you need to set up auto layout constraints for an NSScrollView to work properly.
I just got over the "hump" with a NSScrollView inside a NSWindow. In order for scrolling to occur the view inside the NSScrollview needs to be larger than the content window. That's hard to set with dynamic constraints. Statically setting the inner view to a larger width/height than the window "works" but the static sizes usually are not what you want.
Here is my interface builder view hierarchy and constraints, not including the programmatically added boxes
In my app the user is adding "boxes" (custom draggable views) inside the mainView, which is inside a scrollview in a NSwindow.
Here's the functionality I wanted:
If I expanded the NSWindow, I wanted the mainView inside the scrollview to expand to fill the whole window. No scrolling needed in this case if all the boxes are visible.
If I shrank the NSWindow, I wanted the mainView inside the scrollview to shrink just enough to include all my mainView subviews ("boxes"), but not any further (i added a minBorder of 20). This results in scrolling if a box's position is further right/up than the nswindow's width/height.
I found the trick is to calculate the size of the mainView I want based on the max corner of each draggable boxview, or the height/width of the content frame of the nswindow, whichever is larger.
Below is my code, including some debugging prints.
Be careful of which subviews you use to calculate the max size. If you include a subview that's dynamically attached to the right/top of the window, then your window will never shrink. If you add +20 border to that, you might infinite loop. Not a problem in my case.
extension MapWindowController: NSWindowDelegate {
func windowDidEndLiveResize(_ notification: Notification) {
if let frame = window?.frame, let content = window?.contentRect(forFrameRect: frame) {
print("window did resize \(frame)")
var maxX: CGFloat = content.width
var maxY: CGFloat = content.height
for view in mainView?.subviews ?? [] {
let frameMaxX = view.frame.maxX + minBorder
let frameMaxY = view.frame.maxY + minBorder
if frameMaxX > maxX {
maxX = frameMaxX
}
if frameMaxY > maxY {
maxY = frameMaxY
}
}
print("view maxX \(maxX) maxY \(maxY)")
print("window width \(content.width) height \(content.height)")
mainView?.setFrameSize(NSSize(width: maxX, height: maxY))
}
}
}

unable to scroll the textview in Swift

I have a TextView as shown below
I am not able to scroll the text view , I have added UITextViewDelegate in UIViewController class as well as set isUserInteractionEnabled property in textViewDidBeginEditing
func textViewDidBeginEditing(_ textView: UITextView) {
textView.backgroundColor = UIColor.lightGray
textView.isEditable = false
textView.isUserInteractionEnabled = true
textView.isScrollEnabled = true
}
What did I need to do?
Also, the scrolling is enabled in attribute inspector
This issue has occurred because the actual UITextView's size was more than screen size as shown below
The real answer is that the UITextView needs its content frame height inferior to its content size height to be able to scroll.
In your case, the content frame height is equal to the content size, so it doesn't scroll.
You just have to set the leading left, trailing, top space and bottom space to the View, but first make sure the Text View is smaller than the actual View (parent).
It starts to get complicated when you have a UITextView on a UIView which is either a view on a UIScrollView or directly on a UIScrollview.
The best thing to do, is to give each of them their own actions when scrolling.
Firstly, make sure you have all of your delegates for EVERYTHING set.
Make sure your UITextViews are not user restricted for what they don't need to be.
What I have to do in one of my published apps is this👇
func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
{
if scrollView == textView1
{
// Do something if you actually want to, or just let textView1 scroll as intended
}
else if scrollView == textView2
{
// Do something if you actually want to, or just let textView2 scroll as intended
}
else if scrollView == zoomingScroll
{
// Do something if you need to or leave it
}
else
{
// Do something with all the other scrollable views if you need to
}
}

Get position of UIView in respect to its superview's superview

I have a UIView, in which I have arranged UIButtons. I want to find the positions of those UIButtons.
I am aware that buttons.frame will give me the positions, but it will give me positions only with respect to its immediate superview.
Is there is any way we can find the positions of those buttons, withe respect to UIButtons superview's superview?
For instance, suppose there is UIView named "firstView".
Then, I have another UIView, "secondView". This "SecondView" is a subview of "firstView".
Then I have UIButton as a subview on the "secondView".
->UIViewController.view
--->FirstView A
------->SecondView B
------------>Button
Now, is there any way we can find the position of that UIButton, with respect to "firstView"?
You can use this:
Objective-C
CGRect frame = [firstView convertRect:buttons.frame fromView:secondView];
Swift
let frame = firstView.convert(buttons.frame, from:secondView)
Documentation reference:
https://developer.apple.com/documentation/uikit/uiview/1622498-convert
Although not specific to the button in the hierarchy as asked I found this easier to visualize and understand:
From here: original source
ObjC:
CGPoint point = [subview1 convertPoint:subview2.frame.origin toView:viewController.view];
Swift:
let point = subview1.convert(subview2.frame.origin, to: viewControll.view)
Updated for Swift 3
if let frame = yourViewName.superview?.convert(yourViewName.frame, to: nil) {
print(frame)
}
UIView extension for converting subview's frame (inspired by #Rexb answer).
extension UIView {
// there can be other views between `subview` and `self`
func getConvertedFrame(fromSubview subview: UIView) -> CGRect? {
// check if `subview` is a subview of self
guard subview.isDescendant(of: self) else {
return nil
}
var frame = subview.frame
if subview.superview == nil {
return frame
}
var superview = subview.superview
while superview != self {
frame = superview!.convert(frame, to: superview!.superview)
if superview!.superview == nil {
break
} else {
superview = superview!.superview
}
}
return superview!.convert(frame, to: self)
}
}
// usage:
let frame = firstView.getConvertedFrame(fromSubview: buttonView)
Frame: (X,Y,width,height).
Hence width and height wont change even wrt the super-super view. You can easily get the X, Y as following.
X = button.frame.origin.x + [button superview].frame.origin.x;
Y = button.frame.origin.y + [button superview].frame.origin.y;
If there are multiple views stacked and you don't need (or want) to know any possible views between the views you are interested in, you could do this:
static func getConvertedPoint(_ targetView: UIView, baseView: UIView)->CGPoint{
var pnt = targetView.frame.origin
if nil == targetView.superview{
return pnt
}
var superView = targetView.superview
while superView != baseView{
pnt = superView!.convert(pnt, to: superView!.superview)
if nil == superView!.superview{
break
}else{
superView = superView!.superview
}
}
return superView!.convert(pnt, to: baseView)
}
where targetView would be the Button, baseView would be the ViewController.view.
What this function is trying to do is the following:
If targetView has no super view, it's current coordinate is returned.
If targetView's super view is not the baseView (i.e. there are other views between the button and viewcontroller.view), a converted coordinate is retrieved and passed to the next superview.
It continues to do the same through the stack of views moving towards the baseView.
Once it reaches the baseView, it does one last conversion and returns it.
Note: it doesn't handle a situation where targetView is positioned under the baseView.
You can easily get the super-super view as following.
secondView -> [button superview]
firstView -> [[button superview] superview]
Then, You can get the position of the button..
You can use this:
xPosition = [[button superview] superview].frame.origin.x;
yPosition = [[button superview] superview].frame.origin.y;
Please note that the following code works regardless of how deep (nested) is the view you need the location for in the view hierarchy.
Let's assume that you need the location of myView which is inside myViewsContainer and which is really deep in the view hierarchy, just follow the following sequence.
let myViewLocation = myViewsContainer.convert(myView.center, to: self.view)
myViewsContainer: the view container where the view you need the location for is located.
myView: the view you need the location for.
self.view : the main view

restrict a view inside the superview

I'm trying to move a UIView - "A" (Subview) inside another UIView - "B" (SuperView) using the touches moved method. I'm able to move the UIView outside the superview. I want to restrict the UIView inside the Superview bounds. Is there any way there can be a generic method to test if the subview is inside the visible rect of the superview ???
It sounds like you want to constrain the movement of the subview (viewA) to be always completely contained by the superview (viewB). CGRectContainsRect is the right answer, but it must be applied carefully, since a subview frame is specified in it's superview's coordinate system.
// inside touches moved, compute the newViewAFrame based on the movement
// but only assign it if it meets the containment constraint:
if (CGRectContainsRect(viewB.bounds, newViewAFrame)) {
viewA.frame = newViewAFrame;
}
Notice that we don't mention viewB.frame in the check. viewB's position in it's parent is not relevant to whether viewB contains viewA.
Use clipsToBounds method or CGRectContainsRect
youSuperView.clipsToBounds = YES;
I think it will be helpful to you
I had the same problem and this post helped me a lot, so I'll share my answer (that seems to fit exactly what you need) :
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if (SuperView.frame.contains(self.frame)) {
let oldCenter = self.center
self.center = touch.location(in: SuperView)
if(!SuperView.frame.contains(self.frame)) {
self.center = oldCenter
}
}
}
}
quite simple and you can use this in the methods touchesMoved and touchesEnded too, so it won't stop you when your UIView reach the limits (this is why oldCenter exists).
As mentioned by #dahn, if your view is INSIDE the SuperView, you must take care, because the coordinates of the first will be restrict to the frame of the second, so it may fail if the SuperView is not a full-screen view.
If it is not a full-screen view, the SuperView cannot be the dad of the SubView, because it will cause bugs. The solution is to keep the SuperView and the SubView both inside a third View (so their coordinate system will be the same and it will work fine).
Hope that helps someone someday :)
Swift 2.2:
let rect1 = CGRect(...)
let rect2 = CGRect(...)
There is a method to check this - CGRectContainsRect(rect1, rect2)

NSScrollView with unclipped content view?

Is there a way I can set my scrollview not to clip its contents? (Which is a NSTextView)
I have a subclass of NSScrollView and want its content not to be clipped to its bounds.
I have tried overriding:
- (BOOL) wantsDefaultClipping{
return NO;
}
in MyScrollView and in MytextView without any effect.
In the iOS I would simply would do: myuitextView.clipsToBounds=NO; how can I do this in Cocoa?
EDIT
This is an example of what I want to achieve but in the mac
The scrollview is white, the scroller will never go outside its bounds but the text does since I did myuitextView.clipsToBounds=NO
See picture here
EDIT2
I wouldn't mind clip my view like #Josh suggested. But the real behaviour I would like to have can be explained with this picture:
Do you see the word *****EDIT***** that has being cut in the very first line?
I want the text not to be cut this way, rather I want it to completely appear and I will put a semitransparent image so it looks like it fades off when it's outside the frame.
Q: Why don't I simply put a semitransparent NSImageView on it so it looks like what I want?
A: Because 1.Scroller will be faded as well. Even if I correctly place the semitransparent NSImageView so the scroller looks fine, the cursor/caret will be able to go underneath the semitransparent NSImageView again it does not look good.
I would like to be able to control the area is clipped by NSClipView. I think that would solve my problem. Is there any alternative I have? maybe I can control the caret position or scrolling position through NSTextView so caret will never go near the top/bottom frame limits? or any work-around?
Any advice is appreciated.
Now that it's 2016 and we're using vibrant titlebars with full size content views, I'll add my thoughts to how someone might accomplish this. Hopefully, this will help anyone who came here looking for help on this, as it helped me.
This answers the question in regards to scrolling under the titlebar, but you could easily modify this technique to scroll under other things using the insets and caret position.
To get a scroll view (with or without an NSTextView inside of it) to scroll behind a titlebar, you can use:
// For transparent title.
window.titlebarAppearsTransparent = true
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.appearance = NSAppearance(named: NSAppearanceNameVibrantLight)
This effectively overlays the titlebar of the NSWindow onto the window's contentView.
To constrain something to the top of the window without knowing the height of the titlebar:
// Make a constraint for SOMEVIEW to the top layout guide of the window:
let topEdgeConstraint = NSLayoutConstraint(
item: SOMEVIEW, attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem: window.contentLayoutGuide,
attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0)
// Turn the constraint on automatically:
topEdgeConstraint.active = true
This allows you to constrain the top of an element to the bottom of the titlebar (and or toolbar + any accessory views it may have). This was shown at WWDC in 2015: https://developer.apple.com/videos/play/wwdc2014/220/
To get the scrollview to scroll under the titlebar but show its scrollbars inside the unobscured part of the window, pin it to the top of the content view in IB or via code, which will cause it to be under the titlebar. Then, tell it to automatically update it's insets:
scrollView.automaticallyAdjustsContentInsets = true
Finally, you can subclass your window and handle the cursor/caret position. There is a presumed bug (or developer error on my part) that doesn't make the scrollview always scroll to the cursor/caret when it goes above or below the content insets of the scrollview.
To fix this, you must manually find the caret position and scroll to see it when the selection changes. Forgive my awful code, but it seems to get the job done. This code belongs in an NSWindow subclass, so self is referring to the window.
// MARK: NSTextViewDelegate
func textViewDidChangeSelection(notification: NSNotification) {
scrollIfCaretIsObscured()
textView.needsDisplay = true // Prevents a selection rendering glitch from sticking around
}
// MARK: My Scrolling Functions
func scrollIfCaretIsObscured() {
let rect = caretRectInWindow()
let y: CGFloat = caretYPositionInWindow() - rect.height
// Todo: Make this consider the text view's ruler height, if present:
let tbHeight: CGFloat
if textView.rulerVisible {
// Ruler is shown:
tbHeight = (try! titlebarHeight()) + textViewRulerHeight
} else {
// Ruler is hidden
tbHeight = try! titlebarHeight()
}
if y <= tbHeight {
scrollToCursor()
}
}
func caretYPositionInWindow() -> CGFloat {
let caretRectInWin: NSRect = caretRectInWindow()
let caretYPosInWin: CGFloat = self.contentView!.frame.height - caretRectInWin.origin.y
return caretYPosInWin
}
func caretRectInWindow() -> CGRect {
// My own version of something based off of an old, outdated
// answer on stack overflow.
// Credit: http://stackoverflow.com/questions/6948914/nspopover-below-caret-in-nstextview
let caretRect: NSRect = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWin: NSRect = self.convertRectFromScreen(caretRect)
return caretRectInWin
}
/// Scrolls to the current caret position inside the text view.
/// - Parameter textView: The specified text view to work with.
func scrollToCursor() {
let caretRectInScreenCoords = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWindowCoords = self.convertRectFromScreen(caretRectInScreenCoords)
let caretRectInTextView = textView.convertRect(caretRectInWindowCoords, fromView: nil)
textView.scrollRectToVisible(caretRectInTextView)
}
enum WindowErrors: ErrorType {
case CannotFindTitlebarHeight
}
/// Calculates the combined height of the titlebar and toolbar.
/// Don't try this at home.
func titlebarHeight() throws -> CGFloat {
// Try the official way first:
if self.titlebarAccessoryViewControllers.count > 0 {
let textViewInspectorBar = self.titlebarAccessoryViewControllers[0].view
if let titlebarAccessoryClipView = textViewInspectorBar.superview {
if let view = titlebarAccessoryClipView.superview {
if let titleBarView = view.superview {
let titleBarHeight: CGFloat = titleBarView.frame.height
return titleBarHeight
}
}
}
}
throw WindowErrors.CannotFindTitlebarHeight
}
Hope this helps!
I would simply try to observe the document view's frame and match the scroll view's frame when the document resizes.
This is a little hairy. AFAIK, NSViews can't draw outside their own frame. At any rate I've never seen it done, and I was somewhat surprised when I realized that UIView allows it by default. But what you probably want to do here is not manipulate clipping rectangles (doing any such thing inside NSScrollView will probably not do what you want or expect), but instead try to cover up the vertically-truncated text lines with either layers or views that are the same color as the background. Perhaps you could subclass NSClipView and override viewBoundsChanged: and/or viewFrameChanged: in order to notice when the text view is being shifted, and adjust your "shades" accordingly.
You might consider using a translucent layer to achieve this appearance, without actually drawing outside your view. I'm not certain of the rules on iOS, but on the Mac, a view drawing outside its bounds can cause interference with surrounding drawing.
However, you can set the clipping region to be whatever you like inside your scroll view subclass's drawRect: using -[NSBezierPath setClip:]:
- (void)drawRect:(NSRect)dirtyRect {
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRect:[[self documentView] frame]] setClip];
//...
[NSGraphicsContext restoreGraphicsState];
}
It might be possible (since you asked) to use this code in an NSClipView subclass, but there's not much info about that, and I think you may have a hard time making it interact properly with its scroll view. If it were me, I'd try subclassing NSScrollView first.