Swift autolayout: update view's dimension - swift

I'm using PureLayout
I'm doing all the layout by code, by the way.
I have a UIScrollView that functions like Facebook's news feed. My scroll view has some subviews. Let's call 'em "cards".
I've set my constraints inside updateConstraints as such:
for (i, view) in enumerate(self.viewsForAutoLayout) {
/** Set view dimension **/
view.autoSetDimension(ALDimension.Width, toSize: view.frame.width)
view.autoSetDimension(ALDimension.Height, toSize: view.frame.height)
/** Make 'em stack **/
if i == 0 {
view.autoPinEdgeToSuperviewEdge(
ALEdge.Top,
withInset: offset
)
} else {
view.autoPinEdge(
ALEdge.Top,
toEdge: ALEdge.Bottom,
ofView: self.viewsForAutoLayout[i - 1],
withOffset: offset
)
}
}
self.setNeedsLayout()
And in layoutSubviews, this is where I resize the cards. Their respective height is being updated after an image is loaded from the internet (via NSURLConnection.sendAsynchronousRequest, fyi, but not related to this topic.) After that, I update my scrollView's height.
for (i, view) in enumerate(self.viewsForAutoLayout) {
view.realignContent()
}
totalHeight += view.frame.size.height + offset
let size = CGSizeMake(AppConstants.ScreenSize.SCREEN_WIDTH, totalHeight)
self.scrollView.resizeContent(size) // extension
The problem is that they're not being aligned properly. It's like calling setNeedsLayout() (which eventually calls layoutSubviews() -- correct me if I've mistaken) that does not align the content at all. I've also made sure that setNeedsUpdateConstraints() is called as well.
I've tried calling this in layoutSubviews...
view.autoSetDimension(ALDimension.Height, toSize: view.frame.height)
...but I get some kind of warning:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this: (1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand,
refer to the documentation for the UIView property
translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x1700918a0 V:[XXX.ContentBox:0x15fd63a80(928.986)]>",
"<NSLayoutConstraint:0x170098bf0 V:[XXX.ContentBox:0x15fd63a80(1275.99)]>"
)
(ContentBox is my UIView for the card)
The cards look messed up. What should I do to align my cards properly?
I'd gladly answer side comments regarding the code if it helps understand the question. :) Suggestions on the side aren't bad either.

Going by the warning you're seeing, it looks like you need to set view.translatesAutoresizingMaskIntoConstraints = false when you instantiate your card view.

Related

NSGridView custom view intrinsic size

I'm building a simple NSGridView, and want to have a custom NSView as each element of the grid. Eventually, each NSView will be a xib based label (NSTextField) centered in the NSView.
The problem I am having is with the intrinsic size of the NSView. I want to define the size of the NSView and have auto layout work based on that. I added this code to the custom view (labelView):
override var intrinsicContentSize: NSSize {
return NSSize(width:100, height:100);
};
And it is indeed called; but apparently ignored. As a test, I have on the same row some other labels, and the height for the row is always set to the largest of the row text heights (including the label in the custom view); but the length is set to the longest of column text fields, ignoring the label in the custom view. And anyway, I want to arbitrarily make the NSView a certain height and length, as I tried (but failed) to do with the intrinsicContentSize.
NSStackview seems to do the right thing; but NSGridView does not.
I can force the width of a particular column with
grid.column(at:0).width = 400;
but want I really want to do is define the size of the NSView, and let autolayout use that as a building block.
This strikes me as a conceptual error on my part, so if someone could explain these NSGridView-autolayout-NSView subtleties, I think many might benefit.
I was having the exact same issue, tried to use custom NSView's inside a NSGridView and couldn't get them to draw correctly. What finally worked for me was setting the following:
let gridSize = 5
let cellSize: CGFloat = 50
gridView.xPlacement = .fill // this was key part of the solution
gridView.yPlacement = .fill
for i in 0 ..< gridSize {
gridView.row(at: i).height = cellSize
gridView.column(at: i).width = cellSize
}
Note that I'm setting the size of each cell with the row height and column width of the NSGridView, and not using the NSView size, but this is the only way I got it working.

Swift SnapKit dynamic height issue

i have used snapKit on DispatchQueue.main.async to set constraints programmatically. But i noticed that main.async sometimes causes freeze UI and i change method to use it. Now i have problem to use dynamic height, old method cause error when i try to make dynamic height depends of content...
OLD METHOD (innerView - is subview of view)
DispatchQueue.main.async {
view.snp.makeConstraints({ (make) in
make.top.equalToSuperview().inset(45)
make.left.right.equalToSuperview().inset(12)
make.bottom.equalTo(self.innerView.snp.bottom).offset(12)
})
}
//After i add it like superView.addSubView(view)
//superView contains view, and view(dynamic height) contains innerView
NEW METHOD
superView.addSubview(view)
view.snp.makeConstraints({ (make) in
make.top.equalToSuperview().inset(45)
make.left.right.equalToSuperview().inset(12)
make.bottom.equalTo(self.innerView.snp.bottom).offset(12)
})
You have to add both before setting the constraints
superView.addSubview(view)
superView.addSubview(innerView) // or view.addSubview(innerView) if it's nested UI
view.snp.makeConstraints({ (make) in
make.top.equalToSuperview().inset(45)
make.left.right.equalToSuperview().inset(12)
make.bottom.equalTo(self.innerView.snp.bottom).offset(12)
})
I found issue, just in case someone will need it... add make.bottom.equalTo(self.innerView.snp.bottom).offset(12), after you add all superview constraints and it will work!

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))
}
}
}

Why does UITableViewCell.layoutSubviews() have frame.width as 600 (universal storyboard width) and not 320 (specific iPhone device)?

I have a project with a Universal storyboard (screen size: 600 x 600) containing a class that overrides UITableViewCell:
class MyTableCell: UITableViewCell
{
...
}
Rather than utilising a prototype cell, the UITableView that displays this cell registers MyTableCell as a class:
myTableView.registerClass( MyTableCell.self, forCellReuseIdentifier: "myCell" )
MyTableCell then overrides layoutSubviews:
override func layoutSubviews ()
{
super.layoutSubviews()
let width = CGRectGetWidth( frame )
}
Great! However, the width that is returned is the universal 600, and not the device-specific 320 that I need.
What am I doing wrong please? Thank you.
layoutSubviews() will get called at different times in the cell's lifecycle, and can have different values as it gets moved around. When you first get it off of the storyboard it will have the width you see in the storyboard. After it gets inserted in the table view it will have the same width as the table view.
Normally this isn't a problem. It will get called with the wrong size once and then again with the correct size. But it may seem confusing in the debugger.
If you never see it change to the 320 you expect, that probably means that your table view isn't configured properly for auto layout in the storyboard, and is actually drawing at 600 pixels wide at runtime.
Put a breakpoint on that method, and try entering this into the debug console to see what the table view size is. This should return lots of information about the table view, including its size.
po superview

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.