Using Swift, how do I animate the .setPosition() method of an NSSplitView without visually stretching its contents? - swift

I would like to animate the appearance of a NSSplitViewItem using .setPosition() using Swift, Cocoa and storyboards. My app allows a student to enter a natural deduction proof. When it is not correct, an 'advice view' appears on the right. When it is correct, this advice view will disappear.
The code I'm using is the below, where the first function makes the 'advice' appear, and the second makes it disappear:
func showAdviceView() {
// Our window
let windowSize = view.window?.frame.size.width
// A CGFloat proportion currently held as a constant
let adviceViewProportion = BKPrefConstants.adviceWindowSize
// Position is window size minus the proportion, since
// origin is top left
let newPosition = windowSize! - (windowSize! * adviceViewProportion)
NSAnimationContext.runAnimationGroup { context in
context.allowsImplicitAnimation = true
context.duration = 0.75
splitView.animator().setPosition(newPosition, ofDividerAt: 1)
}
}
func hideAdviceView() {
let windowSize = view.window?.frame.size.width
let newPosition = windowSize!
NSAnimationContext.runAnimationGroup{ context in
context.allowsImplicitAnimation = true
context.duration = 0.75
splitView.animator().setPosition(newPosition, ofDividerAt: 1)
}
}
My problem is that the animation action itself is causing the text in the views to stretch, as you can see in this example: Current behaviour
What I really want is the text itself to maintain all proportions and slide gracefully in the same manner that we see when the user themselves moves the separator: Ideal behaviour (but to be achieved programmatically, not manually)
Thus far in my troubleshooting process, I've tried to animate this outside of NSAnimationContext; played with concurrent drawing and autoresizing of subviews in XCode; and looked generally into Cocoa's animation system (though much of what I've read doesn't seem to have direct application here, but I might well be misunderstanding it). I suspect what's going on is that the .animator() proxy object allows only alpha changes and stretches---redrawing so that text alignment is honoured during the animation might be too non-standard. My feeling is that I need to 'trick' the app into treating the animation as though it's being performed by the user, but I'm not sure how to go about that.
Any tips greatly appreciated...
Cheers

Related

NSAnimationContext issue: no animation with isHidden property

I have the following view structure:
In the Date Stack View, I want to hide/unhide the Advanced Stack Viewin an animated way (like the height was reduced to zero).
I searched a lot for a solution and I came up with this:
dateStackView.wantsLayer = true
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
context.allowsImplicitAnimation = true
advancedDateStackView.animator().isHidden = true
self.view.layoutSubtreeIfNeeded()
}, completionHandler: nil)
But it does not work --> no animation
However, if I replace:
advancedDateStackView.animator().isHidden = true
by
advancedDateStackView.animator().alphaValue = 0,
I can see a fadeout animation for the advancedDateStackView. Which is not the desired behaviour, but proves the animation can work.
I also tried self.advancedDateStackView.animator().frame.size.height = 0
but no animation either.
Any help would be appreciated, I struggle to find a solution.
Thanks!!
EDIT to answer the comment below saying my question is a duplicate: Here my code works and allows me to perform a transition with alphaValue. However, what I need, and what does not work, is to perform a transition on isHidden.

MTKView refresh issue

I am compositing an array of UIImages via an MTKView, and I am seeing refresh issues that only manifest themselves during the composite phase, but which go away as soon as I interact with the app. In other words, the composites are working as expected, but their appearance on-screen looks glitchy until I force a refresh by zooming in/translating, etc.
I posted two videos that show the problem in action: Glitch1, Glitch2
The composite approach I've chosen is that I convert each UIImage into an MTLTexture which I submit to a render buffer set to ".load" which renders a poly with this texture on it, and I repeat the process for each image in the UIImage array.
The composites work, but the screen feedback, as you can see from the videos is very glitchy.
Any ideas as to what might be happening? Any suggestions would be appreciated
Some pertinent code:
for strokeDataCurrent in strokeDataArray {
let strokeImage = UIImage(data: strokeDataCurrent.image)
let strokeBbox = strokeDataCurrent.bbox
let strokeType = strokeDataCurrent.strokeType
self.brushStrokeMetal.drawStrokeImage(paintingViewMetal: self.canvasMetalViewPainting, strokeImage: strokeImage!, strokeBbox: strokeBbox, strokeType: strokeType)
} // end of for strokeDataCurrent in strokeDataArray
...
func drawStrokeUIImage (strokeUIImage: UIImage, strokeBbox: CGRect, strokeType: brushTypeMode) {
// set up proper compositing mode fragmentFunction
self.updateRenderPipeline(stampCompStyle: drawStampCompMode)
let stampTexture = UIImageToMTLTexture(strokeUIImage: strokeUIImage)
let stampColor = UIColor.white
let stampCorners = self.stampSetVerticesFromBbox(bbox: strokeBbox)
self.stampAppendToVertexBuffer(stampUse: stampUseMode.strokeBezier, stampCorners: stampCorners, stampColor: stampColor)
self.renderStampSingle(stampTexture: stampTexture)
} // end of func drawStrokeUIImage (strokeUIImage: UIImage, strokeBbox: CGRect)
func renderStampSingle(stampTexture: MTLTexture) {
// this routine is designed to update metalDrawableTextureComposite one stroke at a time, taking into account
// whatever compMode the stroke requires. Note that we copy the contents of metalDrawableTextureComposite to
// self.currentDrawable!.texture because the goal will be to eventually display a resulting composite
let renderPassDescriptorSingleStamp: MTLRenderPassDescriptor? = self.currentRenderPassDescriptor
renderPassDescriptorSingleStamp?.colorAttachments[0].loadAction = .load
renderPassDescriptorSingleStamp?.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0)
renderPassDescriptorSingleStamp?.colorAttachments[0].texture = metalDrawableTextureComposite
// Create a new command buffer for each tessellation pass
let commandBuffer: MTLCommandBuffer? = commandQueue.makeCommandBuffer()
let renderCommandEncoder: MTLRenderCommandEncoder? = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptorSingleStamp!)
renderCommandEncoder?.label = "Render Command Encoder"
renderCommandEncoder?.setTriangleFillMode(.fill)
defineCommandEncoder(
renderCommandEncoder: renderCommandEncoder,
vertexArrayStamps: vertexArrayStrokeStamps,
metalTexture: stampTexture) // foreground sub-curve chunk
renderCommandEncoder?.endEncoding() // finalize renderEncoder set up
//begin presentsWithTransaction approach (needed to better synchronize with Core Image scheduling
copyTexture(buffer: commandBuffer!, from: metalDrawableTextureComposite, to: self.currentDrawable!.texture)
commandBuffer?.commit() // commit and send task to gpu
commandBuffer?.waitUntilScheduled()
self.currentDrawable!.present()
// end presentsWithTransaction approach
self.initializeStampArray(stampUse: stampUseMode.strokeBezier) // clears out the stamp array in preparation of next draw call
} // end of func renderStampSingle(stampTexture: MTLTexture)
First of all, the domain Metal is very deep, and it's use within the MTKView construct is sparsely documented, especially for any applications that fall outside the more traditional gaming paradigm. This is where I have found myself in the limited experience I have accumulated with Metal with the help from folks like #warrenm, #ken-thomases, and #modj, whose contributions have been so valuable to me, and to the Swift/Metal community at large. So a deep thank you to all of you.
Secondly, to anyone troubleshooting metal, please take note of the following: If you are getting the message:
[CAMetalLayerDrawable present] should not be called after already presenting this drawable. Get a nextDrawable instead
please don't ignore it. It mays seem harmless enough, especially if it only gets reported once. But beware that this is a sign that a part of your implementation is flawed, and must be addressed before you can troubleshoot any other Metal-related aspect of your app. At least this was the case for me. As you can see from the video posts, the symptoms of having this problem were pretty severe and caused unpredictable behavior that I was having a difficult time pinpointing the source of. The thing that was especially difficult for me to see was that I only got this message ONCE early on in the app cycle, but that single instance was enough to throw everything else graphically out of whack in ways that I thought were attributable to CoreImage and/or other totally unrelated design choices I had made.
So, how did I get rid of this warning? Well, in my case, I assumed that having the settings:
self.enableSetNeedsDisplay = true // needed so we can call setNeedsDisplay() to force a display update as soon as metal deems possible
self.isPaused = true // needed so the draw() loop does not get called once/fps
self.presentsWithTransaction = true // for better synchronization with CoreImage (such as simultaneously turning on a layer while also clearing MTKView)
meant that I could pretty much call currentDrawable!.present() or commandBuffer.presentDrawable(view.currentDrawable) directly whenever I wanted to refresh the screen. Well, this is not the case AT ALL. It turns out these calls should only be made within the draw() loop and only accessed via a setNeedsDisplay() call. Once I made this change, I was well on my way to solving my refresh riddle.
Furthermore, I found that the MTKView setting self.isPaused = true (so that I could make setNeedsDisplay() calls directly) still resulted in some unexpected behavior. So, instead, I settled for:
self.enableSetNeedsDisplay = false // needed so we can call setNeedsDisplay() to force a display update as soon as metal deems possible
self.isPaused = false // draw() loop gets called once/fps
self.presentsWithTransaction = true // for better synchronization with CoreImage
as well as modifying my draw() loop to drive what kind of update to carry out once I set a metalDrawableDriver flag AND call setNeedsDisplay():
override func draw(_ rect: CGRect) {
autoreleasepool(invoking: { () -> () in
switch metalDrawableDriver {
case stampRenderMode.canvasRenderNoVisualUpdates:
return
case stampRenderMode.canvasRenderClearAll:
renderClearCanvas()
case stampRenderMode.canvasRenderPreComputedComposite:
renderPreComputedComposite()
case stampRenderMode.canvasRenderStampArraySubCurve:
renderSubCurveArray()
} // end of switch metalDrawableDriver
}) // end of autoreleasepool
} // end of draw()
This may seem round-about, but it was the only mechanism I found to get consistent user-driven display updates.
It is my hope that this post describes an error-free and viable solution that Metal developers may find useful in the future.

Added subview is positioned slightly off - how can I get it back into place?

UPDATE: Solved! While the contentMode for pianoNoteDisplayed and piano_background were indeed the same, apparently this wasn't true for the added subviews. I simply added the line subview.contentMode = superview.contentMode to the function vdmzz suggested, and now everything looks right on all 4 screen sizes.
There are two image views: one called "piano_background" holds a background image (a piano keyboard) and the other will be used to display highlighted notes. The second is constrained to the first:
(the width constraint is probably unnecessary, because the leading and trailing constraints are already set, right?)
To display multiple highlighted keys, I am programmatically adding subviews to the piano_note view and activating the NSLayoutConstraints to get it into place (otherwise it shows up way out of position) like so:
pianoNoteDisplayed.image = nil
if !notesAlreadyAttempted.contains(currentUserAnswer) {
let wrongNoteImageName = "large_\(currentUserAnswer)_wrong"
let wrongNoteImage = UIImage(named: wrongNoteImageName)
let wrongNoteImageView = UIImageView(image: wrongNoteImage!)
wrongNoteImageView.translatesAutoresizingMaskIntoConstraints = false
pianoNoteDisplayed.addSubview(wrongNoteImageView)
NSLayoutConstraint.activate([
wrongNoteImageView.widthAnchor.constraint(equalToConstant: pianoNoteDisplayed!.frame.width),
wrongNoteImageView.heightAnchor.constraint(equalToConstant: pianoNoteDisplayed!.frame.height)
])
}
notesAlreadyAttempted.append(currentUserAnswer)
}
The issue is that the subview is displayed slightly off, and I can't seem to figure out why:
(as you can see, the highlight looks slightly compressed vertically.. the top lands correctly, but the bottom doesn't reach far enough by about 5px)
I have tried centering and constraining the subview in multiple ways, using suggestions from about 5 different answers on stack, and a few other articles I found. The images I am using (the piano background and the overlaying note highlight subview) are identical sizes. I have tried adding more or fewer constraints in the interface builder, and I have tried adding subviews to the original piano_background view instead of the second pianoNoteDisplayed view - same result. Using the pianoNoteDisplayed view itself to display the highlighted note works fine by the way:
And these are displayed using the usual .image method:
pianoNoteDisplayed.image = UIImage(named: "large_\(currentCorrectAnswer)_right")
Any suggestions for how to troubleshoot the issue further?
First of all, as far as I understood pianoNoteDisplayed doesn't need to be an UIImageView.
Secondly, if you align piano_background and pianoNoteDisplayed by top, leading, trailing and bottom edges, one will be exactly on top of other. Or you could set them equal height, width and center positions.
The problem with your current set of constraints is that piano_background's Y position is determined by Safe Area and therefore might defer from pianoNoteDisplayed's Y position.
Try using this function:
func addSameSize(subview: UIView, onTopOf superview: UIView) {
superview.addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
subview.centerXAnchor.constraint(equalTo: superview.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: superview.centerYAnchor).isActive = true
subview.widthAnchor.constraint(equalTo: superview.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: superview.heightAnchor).isActive = true
subview.contentMode = superview.contentMode
}
E.g.
addSameSize(subview: wrongNoteImageView, onTopOf: pianoNoteDisplayed)
It will add your image views exactly aligned on top of pianoNoteDisplayed view

Swift: SKConstraint for scale? (or equivalent) Stuttering

At the minute I'm using the SKConstraint.positionX(rangex, y: rangey) to confine my SKCameraNode within the game board I've created. This is nice because when you hit the boundary there's no stuttering. But my current way to cap the scale of the camera creates a stutter as it hits the bound as it goes past and pings back.
#objc func zoomedView(_ sender:UIPinchGestureRecognizer) {
if newCamera.xScale > 0.148{
let pinch = SKAction.scale(by: 1/sender.scale, duration: 0.0)
newCamera.run(pinch)
sender.scale = 1.0
} else {newCamera.setScale(0.148)}
}
Is there an SKConstraint for scale (or equivalent) which is a better way to stop this stutter? Thanks :)
There is no direct SKConstraint equivalent for scale, however the reason you're experiencing the stuttering is as you go over the bound it snaps back the next time the function is called, rather before a frame is rendered, so theoretically you could zoom in massively instantaneously, and stay there until you activate the zoom function again.
A way to create an equivalent is to put the code checking whether the scale is greater than x in the rendering loop as outlined here.
So if you were to check at the last possible moment:
override func didFinishUpdate() {
if newCamera.xScale < 0.148{
newCamera.setScale(0.148)
} else if newCamera.xScale > 10{
newCamera.setScale(10)
}
}

iOS Autolayout - How to set two different distances between views, depends on the screen height

I know I'm missing something, because this has to be something easy to achieve.
My problem is that I have in my "loading screen" (the one that appears right after the splash) an UIImageView with two different images for 3.5" and 4" size screen. In a certain place of that images, I put one UIActivityIndicator, to tell the user that the app is loading something in the background. That place is not the same for both images, because one of them is obviously higher that the other, so I want to set an autolayout constraint that allows me to put that activity indicator at different heights, depends on if the app is running in an iPhone 5 or not.
Without Autolayout, I'd set the frame.origin.y of the view to 300 (for example), and then in the viewDidLoad method of the ViewController, I'd ask if the app is running in an iPhone 5, so I'd change the value to, for example, 350. I have no idea how to do this using Autolayout and I think it has to be pretty simple.
You can create an NSLayoutConstraint outlet on your view controller and connect the outlet to the activity indicator's Y constraint in your xib or storyboard. Then, add an updateViewContraints method to your view controller and update the constraint's constant according to the screen size.
Here's an example of updateViewConstraints:
- (void)updateViewConstraints {
[super updateViewConstraints];
self.activityIndicatorYConstraint.constant =
[UIScreen mainScreen].bounds.size.height > 480.0f ? 200 : 100;
}
Of course you will want to put in your appropriate values instead of 200 and 100. You might want to define some named constants. Also, don't forget to call [super updateViewConstraints].
The problem of #Rob answer's is you should do a lot of code for each constraint.
So to resolve that, just add ConstraintLayout class to your code and modify constraint constant value for the device that you want in the IB :
//
// LayoutConstraint.swift
// MyConstraintLayout
//
// Created by Hamza Ghazouani on 19/05/2016.
// Copyright © 2016 Hamza Ghazouani. All rights reserved.
//
import UIKit
#IBDesignable
class LayoutConstraint: NSLayoutConstraint {
#IBInspectable
var 📱3¨5_insh: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxY == 480 {
constant = 📱3¨5_insh
}
}
}
#IBInspectable
var 📱4¨0_insh: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxY == 568 {
constant = 📱4¨0_insh
}
}
}
#IBInspectable
var 📱4¨7_insh: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxY == 667 {
constant = 📱4¨7_insh
}
}
}
#IBInspectable
var 📱5¨5_insh: CGFloat = 0 {
didSet {
if UIScreen.main.bounds.maxY == 736 {
constant = 📱5¨5_insh
}
}
}
}
Don't forgot to inherit your class constraint from ConstraintLayout
I will add the objective-c version soon
The basic tool in Auto Layout to manage UI objects' position is the Constraints. A constraint describes a geometric relationship between two views. For example, you might have a constraint that says:
“The right edge of progress bar is connected to the left edge of a lable 40 points of empty space between them.”
This means using AutoLayout you can't do conditional position setting based on UIDevice's mode, rather you can create a view layout which modifies itself if eg. the app runs on 3.5' full screen (IPhone4) or 4' full screen (IPhone5) based on the constraints.
So options for your problem using Constraints:
1) find a view on your layout which can be used to create a constraint to position the progressbar relatively. (select the view and the progressbar using CMD button, then use Editor/Pin/Vertical Spacing menu item to create a vertical constraint between the 2 objects)
2) create an absolute constraint to stick the progressbar's position to screen edge (keeping space) or centrally
I found helpful this tutorial about AutoLayout which might be beneficial for you also:
http://www.raywenderlich.com/20881/beginning-auto-layout-part-1-of-2
Pls note: autolayout only works from IOS 6.
The new way, Without writing a single line!
No need to write device based conditions like these :-
if device == iPhoneSE {
constant = 44
} else if device == iPhone6 {
constant = 52
}
I created a library Layout Helper so now you can update constraint for each device without writing a single line of code.
Step 1
Assign the NSLayoutHelper to your constraint
Step 2
Update the constraint for the device you want
Step 3
Run the app and see the MAGIC
I generally always try to stay in Interface Builder for setting up constraints. Diving in code to have more control is usually useful if you have completely different layouts on iPhone 4 and 6 for example.
As mentioned before, you can't have conditionals in Interface Builder, that's when linking a constraint to your view controller really comes handy.
Here's a short explanation on 3 approaches to solve Auto Layout issues for different screen sizes: http://candycode.io/how-to-set-up-different-auto-layout-constraints-for-different-screen-sizes/