Is there a way to specify which edges can be used when resizing a sheet? - swift

I wanted a sheet to be attached to the bottom edge of its parent window, so that I could still see the top part of the parent's content. I show it in the usual way in the parent's view controller.
#IBAction func clickShowSheet(_ sender: NSButton) {
mySheet = MySheetController(windowNibName: "MySheet")
view.window!.beginSheet(mySheet.window!,
completionHandler: {response in
print(response == NSModalResponseOK ? "OK" : "Cancel")
self.mySheet = nil
})
}
To attach it to the bottom edge I implement the parent window's delegate method:
func window(_ window: NSWindow, willPositionSheet sheet: NSWindow, using rect: NSRect) -> NSRect {
var rect = rect
rect.origin.y = sheet.frame.size.height + 15
return rect
}
The effect is satisfactory, tho' I'd like the animation to work the other way.
I also want the sheet to be resizable, so have left the checkbox in the nib on. Resizing by dragging a side is great: the sheet stays centred. The top edge can't be dragged, which would make sense if the sheet was at the top of its window, but the bottom edge can be dragged, leaving the sheet somewhere in the middle. I can make it spring back by implementing windowDidEndLiveResize. It looks silly. So what I need to know is, is there a way to enable/disable dragging of a particular edge?

Pass this as a prop if you are using spring bottom sheet.
blocking={false}

Related

UITextView Moves to Left on Keyboard Show

I have an app with a build target of IOS 14 that is causing a problem regarding automatic positioning of the view on keyboard show.
I have a UITextView that is draggable and can be positioned partially outside of the main view that it sits within. If the field is large enough then it will extend beyond the parent view and safe area also. The parent view has clipsToBounds set as true so the overflow of the text view is not visible.
The problem is when the text field is positioned so that its right hand side is outside of the safe area and the keyboard is presented, the screen automatically scrolls left to include the far right edge of the text view, even though it is not visible due to clipsToBounds being set on its parent. I need to disable the behaviour that is causing this to happen but can't find anything that covers this for UIKit.
See below for a visual example. Can anybody please help?
Image 1
Image 2
Edit:
The structure of the screen is:
View Controller:
.....UICollectionView:
..........UICollectionViewCell:
...............UIView:
....................Elements (UITextView in this case)
func calculateCarouselOffset(formHeight: CGFloat) -> CGAffineTransform {
let carouselOffset: CGAffineTransform!
let currentElementMaxY = returnCurrentElementMaxY()
let elementMaxYTransformRemoved = currentElementMaxY + -self.scalingCarousel.transform.ty
let newFormOriginY = safeAreaFrame.height - formHeight
let topOfFormMargin: CGFloat = 20
if (newFormOriginY - topOfFormMargin) < elementMaxYTransformRemoved {
// Form will overlap element - move carousel view to compensate
let oldToNewLocDist = (newFormOriginY - topOfFormMargin) - currentElementMaxY
let moveScreenBy = self.scalingCarousel.transform.ty + oldToNewLocDist
carouselOffset = CGAffineTransform(translationX: 0, y: moveScreenBy)
} else {
// Form will not overlap element - reset carousel view
carouselOffset = self.formDeactivate
}
return carouselOffset
}
And it is called as below:
func textViewDidChage() {
let backgorundTransform = calculateCarouselOffset(formHeight: currentElementFormHeight)
let modifyBackground = UIViewPropertyAnimator(duration: 0.2, curve: .linear, animations: {
self.scalingCarousel.transform = backgorundTransform
})
modifyBackground.startAnimation()
}
It looks like this is (possibly new?) built-in behaviour for text fields. I reproduced this both with a collection view controller and a view controller holding a collection view. The text field moves itself to visible like this:
I found this by adding a symbolic breakpoint on contentOffset and then making a field editable - there are a lot of calls before you get to this point because it's also adjusting things for the keyboard coming up.
Unfortunately in your case, I think the scroll view is moving the text field's visible bounds into the visible area, which means you're scrolling horizontally since the text field is off screen.
You can't override scrollTextFieldToVisibleIfNecessary as it is private API. There are probably some hacks you can do by overriding becomeFirstResponder but they seem quite likely to either not work, or break other things.

How do you animate a UICollectionView using auto layout anchors?

I'm trying to animate a collectionview by using layout constraints however I cannot get it to work. I have a collectionview that takes up the entire screen and on a button tap I want to essentially move the collectionview up to make room for another view to come in from the bottom - see image below
The incoming UIView animates just fine (the view coming up from the bottom) - The reason I want to move the collectionview is that the incoming UIView obscures the collection view so am just trying to move the collectionview up at the same time as the new view so that all of the content in the collectionview can be displayed without being hidden by the new view - I use a reference view to get the right layout constraints for the final position for the collectionview Image to show what I am trying to achieve Am I going about it the right way?
Nothing happens with the code example below and I am not sure where to go from here - the same approach is used for animating the incoming view and works just fine but doesn't seem to work for the collectionview...
Any help would be kindly appreciated
var colViewBottomToReferenceTop: NSLayoutConstraint?
var colViewBottomToViewBottom: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
colViewBottomToReferenceTop = musicCollectionView.bottomAnchor.constraint(equalTo: referenceView.topAnchor)
colViewBottomToViewBottom = musicCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.active([colViewBottomToViewBottom!])
NSLayoutConstraint.deactivate([colViewBottomToReferenceTop!])
}
func playerShow() {
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
#IBAction func btnTapped(_ sender: UIButton) {
playerShow()
}
You want to deactivate the other before animation
NSLayoutConstraint.deactivate([colViewBottomToViewBottom!])
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
Look to this Demo

Dragging NSSplitView divider does not resize views

I'm working with Cocoa and I create my views in code (no IB) and I'm hitting an issue with NSSplitView.
I have a NSSplitView that I configure in the following way in my view controller, in Swift:
override func viewDidLoad() {
super.viewDidLoad()
let splitView = NSSplitView()
splitView.isVertical = true
splitView.addArrangedSubview(self.createLeftPanel())
splitView.addArrangedSubview(self.createRightPanel())
splitView.adjustSubviews()
self.view.addSubview(splitView)
...
}
The resulting view shows the two subviews and the divider for the NSSplitView, and one view is wider than the other. When I drag the diver to change the width, as soon as I release the mouse, the divider goes back to its original position, as if pulled back by a "spring".
I can't resize the two subviews; the right one always keeps a fixed size. However, nowhere in the code I fix the width of that subview, or any of its content, to a constant.
What I would like to achieve instead is that the right view size is not fixed, and that if I drag the divider at halfway through, the subviews will resize accordingly and end up with the same width.
This is a screen recording of the problem:
Edit: here is how I set the constraints. I'm using Carthography, because otherwise setting constraints in code is extremely verbose beyond the most simple cases.
private func createLeftPanel() -> NSView {
let view = NSView()
let table = self.createTable()
view.addSubview(table)
constrain(view, table) { view, table in // Cartography magic.
table.edges == view.edges // this just constraints table.trailing to
// view.trailing, table.top to view.top, etc.
}
return view
}
private func createRightPanel() -> NSView {
let view = NSView()
let label = NSTextField(labelWithString: "Name of item")
view.addSubview(label)
constrain(view, label) { view, label in
label.edges == view.edges
}
return view
}

Modal Panel with rounded corners

I'm wondering if there is a way to display a modal window with rounded corners instead of the default sharp corners. The image shows what corners I'm referring to.
I tried with changing the contentView.layer?.cornerRadius but it didn't work. What can I do to get the result I need?
After working a lot on it, I finally found a solution:
Frist create a new borderless window in your interface builder. Place a custom box in that view and make sure it leaves a bit of space from the top border of the window:
Then add an outlet of that window object in your app delegate:
#IBOutlet weak var saveWindow: NSWindow!
So copy that extension for loading and dismissing a panel as a modal sheet:
extension NSWindow {
public func loadPanel(named: NSWindow) {
named.isOpaque = false
named.backgroundColor = NSColor.clear
named.hasShadow = false
self.beginSheet(named, completionHandler: nil)
}
public func closePanel(named: NSWindow) {
self.endSheet(named)
}
}
You just need to call this two functions if you want to open a panel.
window.loadPanel(named: saveWindow)
And when you're done:
window.closePanel(named: saveWindow)
This is the result:

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.