How to scrollRowToVisible without animations - cocoa

On a Mac, Mail and Finder have a solid looking scroll on their table views when the up or down arrow is held. The row highlight sits flush with the top or bottom of the column and the rows step through with no animation.
8 years ago it seems that it was hard to not do this. Now I can't seem to stop scrollRowToVisible on an NSOutlineView animating.
I have tried wrapping the call with NSAnimationContext.beginGrouping() or CATransaction.begin() etc to set any animation duration to 0.0 but no luck.
Is there anyway to make this call snap - or should I be using something a little lower level?
EDIT
Here is my code. The duration has no effect here. There are always a few frames of scroll animation, and the endpoint of the animation is slightly irregular (i.e. the bottom edge of the scrolled to view is not always aligned with the bottom edge).
if selectedRows != outlineView.selectedRowIndexes {
outlineView.selectRowIndexes(selectedRows, byExtendingSelection: false)
// I would love this not to animate like in mail, but it cannot be stopped!!!
if selectedRows.one {
NSAnimationContext.beginGrouping()
NSAnimationContext.current.allowsImplicitAnimation = false
NSAnimationContext.current.duration = 0
outlineView.scrollRowToVisible(selectedRows.first!)
NSAnimationContext.endGrouping()
}
}
Using runAnimationGroup has the same result:
NSAnimationContext.runAnimationGroup( { current in
current.allowsImplicitAnimation = false
current.duration = 0
outlineView.scrollRowToVisible(selectedRows.first!)
}, completionHandler: nil)
I have variable height rows in my table but I don't see why this would make a difference. From the above code, the change in selection is always highlighted before any movement in the table, further indication that the scroll animation is not being removed.

I had this problem myself, and solved it by subclassing NSClipView and overriding func scroll(to newOrigin: NSPoint) like this:
override func scroll(to newOrigin: NSPoint) {
super.setBoundsOrigin(newOrigin)
}
This should disable smooth scroll entirely, which is the animation effect you are describing, for the scroll view that houses your subclassed clip view.

Related

I am able to draw on each ViewController to edit Original ImageView. Is there any way to fix this?

I am working on an animation app and in each other ViewController I can draw on the image that's being currently shown on the original ImageView. Is there any way to fix this?
This is what exactly is happening. I don't really know where in the code the problem exists.
https://drive.google.com/file/d/1M7qWKMugaqeDjGls3zvVitoRmwpOUJFY/view?usp=sharing
Expected to be able to draw only on the DrawingFrame ViewController. However, I can draw on every single ViewController in my app
The problem would appear to be that your gesture recognizer is still operational, even though you’ve presented another view on top of the current one.
This is a bit unusual. Usually when you present a view like that, the old one (and its gesture recognizers) are removed from the view hierarchy. I’m guessing that you’re just sliding this second view on top of the other. There are a few solutions:
One solution would be to make sure to define this new view such that (a) it accepts user interaction; and (b) write code so that it handles those gestures. That will avoid having the view behind it picking up those gestures.
Another solution is to disable your gesture recognizer recognizer when the menu view is presented, and re-enable it when the menu is dismissed.
The third solution is to change how you present that menu view, making sure you remove the current view from the view hierarchy when you do so. A standard show/present transition generally does this, though, so we may need to see how you’re presenting this menu view to comment further.
That having been said, a few unrelated observations:
you should use UIGraphicsBeginImageContextWithOptions instead of UIGraphicsBeginImageContext;
rather than
if pencil.eraser == true { ... }
you can
if pencil.eraser { ... }
I’d suggest giving the pencil a computed property:
var color: UIColor { return UIColor(red: red, green: green, blue: blue, alpha: opacity) }
Then you can just refer to pencil.color;
property names should start with lowercase letter; and
drawingFrame is a confusing name, IMHO, because it’s not a “frame”, but rather likely a UIImageView. I’d call it drawingImageView or something like that.
Yielding:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
guard let pencil = pencil else { return }
//begins current context (and defer the ending of the context)
UIGraphicsBeginImageContextWithOptions(drawingImageView.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
//where to draw
drawingImageView.image?.draw(in: drawingImageView.bounds)
//saves context
guard let context = UIGraphicsGetCurrentContext() else { return }
//drawing the line
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.setLineCap(.round)
if pencil.eraser {
//Eraser
context.setBlendMode(.clear)
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
} else {
//opacity, brush width, etc.
context.setBlendMode(.normal)
context.setLineWidth(pencil.pencilWidth)
context.setStrokeColor(pencil.color.cgColor)
}
context.strokePath()
//storing context back into the imageView
drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
Or, even better, retire UIGraphicsBeginImageContext altogether and use the modern UIGraphicsImageRenderer:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
guard let pencil = pencil else { return }
drawingImageView.image = UIGraphicsImageRenderer(size: drawingImageView.bounds.size).image { _ in
drawingImageView.image?.draw(in: drawingImageView.bounds)
let path = UIBezierPath()
path.move(to: fromPoint)
path.addLine(to: toPoint)
path.lineCapStyle = .round
if pencil.eraser {
path.lineWidth = 10
UIColor.white.setStroke()
} else {
path.lineWidth = pencil.pencilWidth
pencil.color.setStroke()
}
path.stroke()
}
}
For more information on UIGraphicsImageRenderer, see the “Drawing off-screen” section of WWDC 2018 Image and Graphics Best Practices.
As an aside, once you get this problem behind you, you might want to revisit this “stroke from point a to point b and re-snapshot” logic to capture an array of points and build a path from a whole series, and don’t re-snapshot ever point, but only after a whole bunch have been added. This snapshotting process is slow and you’re going to find that the UX stutters a bit more than it needs. I personally re-snapshot after 100 points or so (at which point the amount of time to restroke the whole path is slow enough that it’s not much faster than the snapshot process, so if I snapshot and restart the path from where I left off, it then speeds up again).
But you say:
Expected to be able to draw only on the DrawingFrame ViewController. However, I can draw on every single ViewController in my app.
The above should draw only the image of drawingImageView and the stroke from fromPoint to toPoint. Your problem about drawing on “every single ViewController” rests elsewhere. We’d really need to see how precisely you are presenting this menu scene.

Autolayout support for custom text view

This question is about supporting a variable-height, custom text view using constraints and the view's intrinsicContentSize for autolayout. Before you click that 'duplicate' button, hear me out.
I have a custom text view (from scratch, inherits from NSView). It supports many of the usual NSTextView features, the most relevant here being multiple lines and laying out its content based on width available. The app it's built for loads a couple of these text views into each row of a table view. The issue is that the height doesn't get set properly according to its intrinsicContentSize.
I created a sample project that simplifies the problem. It uses a "pseudo" text view with a fixed number and size of characters used to determine width/height required. In the sample, there is a table view of one column whose cell view has only one subview, a PseudoTextView. The text view is pinned to the edges of its cell view with a little padding. How can I get the system to recognize that the text view should abide by the constraints that define the width while allowing the text view to grow in height, wrapped tightly by the cell view? Here's the text view code:
class PseudoTextView: NSView {
#IBInspectable var characterCount: Int = 0
#IBInspectable var characterWidth: CGFloat = 5
#IBInspectable var characterHeight: CGFloat = 8
#IBInspectable var background: NSColor = .blue {
didSet {
layer?.backgroundColor = background.cgColor
}
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
wantsLayer = true
layer?.backgroundColor = background.cgColor
}
override var intrinsicContentSize: NSSize {
let requiredWidth = characterWidth * CGFloat(characterCount)
let lineCount = (requiredWidth / frame.width).rounded(.up)
let usedHeight = lineCount * characterHeight
let charactersPerLine = (frame.width / characterWidth).rounded(.down)
let usedWidth = charactersPerLine * characterWidth
return NSSize(width: usedWidth, height: usedHeight)
}
This version returns the appropriate size based on the frame of the view. This obviously doesn't work because it's accessed during the updateConstraints phase of layout when the frame hasn't been set. I've also tried using NSView.noIntrinsicMetric for the width, but this will drive the text view to zero width and the height never recovers. There are an enormous number of other attempts I've made, but I won't bore you with them all.
NSTextField does something different (assuming 'Editable' is off, 'Wrap' is on). It's intrinsicContentSize reports the full width of the text on a single line (even if it's much longer than the width available), but it is somehow resized to the correct width. Once resized, the intrinsicContentWidth then still reports the full single-line width, but the height is adjusted to account for multiple lines. There is some magic somewhere I haven't been able to divine.
I've read every line of related documentation. If there's a blog post on the topic, I've probably read it. If there's a question on SO on the topic, I've probably read it. If you wrote a book on the topic, I've probably bought it. All of these sources tease at the problem I'm having, but none of them answer the question of how to handle this particular situation. Desperate.
Update:
After reading an old blog post by Jonathon Mah (http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html) I created an example that uses his approach. Here's another project that mimics his technique and works correctly. This is the top portion of the app. It's a fixed container view that's adjusted with a slider. The patchwork are the pseudo characters of the custom view whose background is the pink color.
However, when inserted into a self-sizing table view, the custom view correctly matches the width of its cell, but the cell is not adjusted to respect the intrinsic height. If you change the custom view's bottom constraint to be optional (say, with a >= relation) the custom view does shrink to the correct height, but the cell view remains fixed. How do I convince the cell view to shrink its height to respect the intrinsicContentSize.height of its subview?
I have a solution for your problem, although it may not be optimal, since I do not have too much experience with macos specifics. So, first of all, let's define that the table view should use automatic row heights:
override func awakeFromNib() {
super.awakeFromNib()
tableView.usesAutomaticRowHeights = true
}
In your second sample the tableView outlet was not not connected to TableViewController, but it probably should be, so do not forget to connect it.
Now, in your WrappingCellView, you override layout(), but the value that you set for preferredMaxLayoutWidth is incorrect. I think it should be the width of the superview:
override func layout() {
// 16 is used for the sake of example
wrappingView.preferredMaxLayoutWidth = (superview?.frame.width ?? 16) - 16
super.layout()
}
Next part is the one I am not sure about:
func tableViewColumnDidResize(_ notification: Notification) {
tableView.reloadData()
}
There should be a better API to recalculate row heights. I hope you or someone else can suggest something :)
These three adjustments result in proper recalculation of the cell height:

Override the Autolayout center values of UIButton in a Subview # viewDidLayoutSubviews

I use Autolayout for a fairly complex Menu and really need it. All Buttons, UIViews etc. of my Menu are in a separate UIView called "menuSubview".
If the user presses a button the whole "menuSubview" shifts to another position to reveal other parts of the menu. Sometimes the buttons in the menuSubview move as well. I always save the "Menu State" (with Userdefaults in the get-set variable "lastMenu") and have a function to set the alphas and centers according to the saved "Menu State".
I tried calling the "openLastMenu" function in viewDidAppear, viewDidLayoutSubview - all the "viewDid" functions of the ViewController. The "menuSubview" center and alphas of the buttons always behave as expected... but the centers of the buttons simply won't - no matter what "viewDid" I call the function in.
(the code is a lot more complex - I boiled it down to debug and state my point)
override func viewDidAppear(animated: Bool) {
if lastMenu != nil {openLastMenu()}
}
func openLastMenu(){
menuSubview.center.x = view.center.x //works
menuSubview.center.y = view.center.y + 200 //works
button1.center.x = view.center.x - 50 //why you no behave???
button2.center.x = view.center.x + 50 //why you no behave???
button3.alpha = 0 //works
button4.alpha = 0 //works
}
For debugging I even made a button Subclass to fetch the "center" values with a "didSet" if they change. Seems like after taking the correct values they change once more to their Autolayout-Position.
...oh and ignoring the constraints with "translatesAutoresizingMaskIntoConstraints" on the buttons always fucks up the whole menu. I'm starting to get crazy here :)
If you position views using autolayout, any changes to the frame, like what you do here with the center property, will be ignored.
What you need to do is identify the constraints that are you need to change to move the views in the desired position. Example:
You want to move button1 50 points to the left of view.center. Assuming view is the superview of menuSubview, you would
1) deactivate the the constraint responsible for button1's horizontal placement. How you do this mainly depends on whether you created the constraints in code or Interface Builder. The latter will require you to create outlets for some of the constraints.
2) create a new constraint between button1's centerX anchor and view's centerX anchor with a constant of -50, like so (iOS 9 code)
button1.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor, constant: -50.0).active = true

Animate View Movement for OS X app using Swift

I have two scroll views, one of them with constraints that make it take up the full parent view, and the other right next to it, but hidden outside the bounds of the parent view. I want to animate both sliding left until the second scroll view takes up the full parent view, and the first scroll view is now out of bounds on the left. How do I do this for an OS X app using Swift?
Figured this one out with a little hunting and piecing things together.
Create an IBOutlet for the constraints that you want to change in the animation. In this case, use the leading constraint for each scroll view.
#IBOutlet weak var ScrollView1LeadingConstraint: NSLayoutConstraint!
#IBOutlet weak var ScrollView2LeadingConstraint: NSLayoutConstraint!
Then, use the following:
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = //length of the animation time in seconds
self. ScrollView1LeadingConstraint.animator().constant = //negative width of scroll view
self.ScrollView2LeadingConstraint.animator().constant = 0
}, completionHandler: { () -> Void in
//insert any completion code here
})
This will animate the first scroll view out of frame on the left, and move the second scroll view into its former place.

Xcode - viewDidLayoutSubviews

i have a view inside a viewController, i wanted to start the smaller view outside the viewController in the left, and animate it to the centre when i press a button. so i made it like this:
override func viewDidLayoutSubviews() {
smallView.center = CGPointMake(smallView.center.x - 400, smallView.center.y)
}
And it works perfectly!, the problem is i have a text view inside that smaller view, and every time i start editing it it jumps outside of the main viewController right where it was, and i have to press the button again to bring it inside.
How to fix this?
PS: i tried positioning it to the centre when i start editing the text view like this:
func textViewDidBeginEditing(textView: UITextView) {
smallView.center = CGPointMake(smallView.center.x + 400, smallView.center.y)
}
But it doesn't work. and the method is connected to the textView properly(delegate)
PS2: i also have imagePickerController inside my viewController.
OK, as you're using Auto Layout.
The first rule of Auto Layout (you will see this in any Auto Layout book) is that you absolutely cannot touch the frame or center of a view directly.
// don't do these
myView.frame = CGRectMake(0, 0, 100, 100);
// ever
myView.center = CGPointMake(50, 50);
You can get the frame and center but you can never set the frame or center.
In order to move stuff around using Auto Layout you need to update the constraints.
For instance if I set up a view called myView and want it to grow and shrink in height I would do something like...
Set the top constraint to the superview at 0.
Set the left constraint to the superview at 0.
Set the right constraint to the superview at 0.
Set the height constraint to 50 (for example) and save it in a property called heightConstraint.
Now to animate the change in height I do this...
self.heightConstraint.constant = 100;
[UIView animateWithDuration:1.0
animations:^ {
[self.view layoutIfNeeded];
}];
This will then animate the height from 50 (where it was when I set it) to 100.
This is the same for ANY animation of views using Auto Layout. If (for instance) I had saved the left constraint I could change that and it would increase and decrease the gap from the left edge of the superview to myView.
There are several really good tutorials about AutoLayout on the Ray Wenderlich site. I'd suggest you take a look at them.
Whatever you do, I'd strongly suggest not just disabling Auto Layout. If you don't know how to use Auto Layout then you will very quickly fall behind with iOS 8 and the new device sizes.

Resources