I am trying to create a vertically and horizontally centered stack view with dynamic height content. Something like this:
I and I am almost there, but the problem is that when I add a button which height should be dynamic (scale with label) I get this: (Note the overlapping buttons).
Here's what I currently have:
Without that button with the long label, the view looks good. Now I've read about the concepts of how the auto layout works, that It needs to know the height of the content to center it. But about the cases when the content height is unknown?
Setting the titleLabel of a UIButton to multi-line (changing Line Break to Word Wrap) doesn't change the intrinsicContentSize of the button, so you have to do it yourself.
Here is one approach. Subclass UIButton like this:
class CheckboxButton: UIButton {
override var intrinsicContentSize: CGSize {
return titleLabel?.intrinsicContentSize ?? CGSize.zero
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = titleLabel?.frame.size.width ?? 0
super.layoutSubviews()
}
}
You can confirm the sizing is being done correctly by giving your button a background color (during development) to clearly see its bounds / frame.
After that, the spacing should be handled correctly by auto-layout.
Related
I'm attempting to implement an animation that shows/hides a view in a horizontal arrangement. I'd like this to happen with slide, and with no opacity changes. I'm using auto-layout everywhere.
Critically, the total width of the containing view changes with the window. So, constant-based animations are not possible (or so I believe, but happy to be proved wrong).
|- viewA -|- viewB -|
My first attempt was to use NSStackView, and animate the isHidden property of an arranged subview. Despite seeming like it might do the trick, I was not able to pull off anything close to what I was after.
My second attempt was to apply two constraints, one to force viewB to be zero width, and a second to ensure the widths are equal. On animation I change the priorities of these constraints from defaultHigh <-> defaultLow.
This results in the correct layout in both cases, but the animation is not working out.
With wantsLayer = true on the containing view, no animation occurs whatsoever. The views just jump to their final states. Without wantsLayer, the views do animate. However, when collapsing, viewA does a nice slide, but viewB instantly disappears. As an experiment, I changed the zero width to a fixed 10.0, and with that, the animation works right in both directions. However, I want the view totally hidden.
So, a few questions:
Is it possible to animate layouts like this with layer-backed views?
Are there other techniques possible for achieving the same effect?
Any ideas on how to achieve these nicely with NSStackView?
class LayoutAnimationViewController: NSViewController {
let containerView: NSView
let view1: ColorView
let view2: ColorView
let widthEqualContraint: NSLayoutConstraint
let widthZeroConstraint: NSLayoutConstraint
init() {
self.containerView = NSView()
self.view1 = ColorView(color: NSColor.red)
self.view2 = ColorView(color: NSColor.blue)
self.widthEqualContraint = view2.widthAnchor.constraint(equalTo: view1.widthAnchor)
widthEqualContraint.priority = .defaultLow
self.widthZeroConstraint = view2.widthAnchor.constraint(equalToConstant: 0.0)
widthZeroConstraint.priority = .defaultHigh
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = containerView
// view.wantsLayer = true
view.addSubview(view1)
view.addSubview(view2)
view.subviewsUseAutoLayout = true
NSLayoutConstraint.activate([
view1.topAnchor.constraint(equalTo: view.topAnchor),
view1.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// view1.trailingAnchor.constraint(equalTo: view2.leadingAnchor),
view2.topAnchor.constraint(equalTo: view.topAnchor),
view2.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
view2.trailingAnchor.constraint(equalTo: view.trailingAnchor),
widthEqualContraint,
widthZeroConstraint,
])
}
func runAnimation() {
view.layoutSubtreeIfNeeded()
self.widthEqualContraint.toggleDefaultPriority()
self.widthZeroConstraint.toggleDefaultPriority()
// self.leadingConstraint.toggleDefaultPriority()
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 3.0
self.view.layoutSubtreeIfNeeded()
}) {
Swift.print("animation complete")
}
}
}
extension LayoutAnimationViewController {
#IBAction func runTest1(_ sender: Any?) {
self.runAnimation()
}
}
Also, some potentially relevant, but so far unhelpful, related questions:
Animating Auto Layout changes concurrently with NSPopover contentSize change
Animating Auto Layout constraints with NSView.layoutSubtreeIfNeeded() not working on macOS High Sierra
Hide view item of NSStackView with animation
I've got a pair of editable text fields in a view using auto layout. (For completeness, these are inside an NSTableRowView in an editable table, but I don't think that should matter.)
I'd like the fields to resize their width based on their content, especially once they are edited.
However, NSTextField is not cooperating. I added some code to log the intrinsicContentSize whenever the edit field ends editing, and the value is always { NSViewNoInstrinsicMetric, 15 }, meaning the field has no intrinsic width.
I've seen a lot of posts about multi-line NSTextField and vertical (height) resizing, wrapping, and so on, but I can't find any questions or answers that apply here. The fields are both set to "use single line", "clip", and "scroll".
I've tried sending the fields updateConstraints and invalidateIntrinsicContentSize messages, but they don't seem to have any effect.
I know I can create a subclass of NSTextField and hack this, but I don't understand why this isn't working in the first place. Or maybe NSTextField simply doesn't have an intrinsic width, but I can't find that documented anywhere.
Note: previous answers to just set field.editable = NO are insufficient; these fields must be editable.
Here's my hack:
//
// Create a special text field subclass that provides an intrinsic width for its content.
// Editable NSTextFields normally do not have an intrinsic width, because I guess that would just be too weird.
// This field returns an intrinsic width when not being edited, and the width of its superview when it is.
//
#interface ResizingPatternTextField : NSTextField
#end
#implementation ResizingPatternTextField
- (BOOL)becomeFirstResponder
{
BOOL willEdit = [super becomeFirstResponder];
if (willEdit)
[self invalidateIntrinsicContentSize];
return willEdit;
}
- (void)textDidEndEditing:(NSNotification*)notification
{
[super textDidEndEditing:notification];
[self invalidateIntrinsicContentSize];
}
- (NSSize)intrinsicContentSize
{
NSSize intrinsiceSize = super.intrinsicContentSize;
if (self.currentEditor!=nil)
{
// The field is currently being edited: return the width of the superview as the intrinsic width
// This should cause the field to expand to it's maximum width, within the constraints of the layout
intrinsiceSize.width = self.superview.bounds.size.width;
}
else
{
// If the field isn't being edited and it's editable: calculate the width of the field ourselves
if (self.editable)
{
NSDictionary* textAttrs = #{ NSFontAttributeName: self.font };
NSSize textSize = [self.stringValue sizeWithAttributes:textAttrs];
// Return an intrinsic size with a little padding, rounded up to the nearest whole integer
intrinsiceSize.width = CGCeiling(textSize.width+7.0);
}
}
return intrinsiceSize;
}
Notes:
The padding value of 7.0 points is fixed (for a "small" control size) and isn't a general solution
CGCeiling is just a macro for ceil()
You have to make sure the hugging, compression resistance, and other layout constraints present a pleasing layout when the intrinsic width is very wide. (You should do this anyway, but here it's extra important.)
I have a UIScrollView inside a UIViewController (subclassed by ImageViewController). The ViewController itself is part of a NavigationController's stack. Now, apart from having a navigation bar, I want the ScrollView to take all of the available room on the screen. The UIImageView inside the scrollview should then fill the available room of the scroll view. You can see the current state at the bottom of this posting.
class ImageViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var imageView: UIImageView?
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
if let image = image {
imageView = UIImageView(image: image)
if let imageView = imageView {
imageView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: image.size)
scrollView.addSubview(imageView)
scrollView.contentSize = image.size
let scaleHeight = scrollView.frame.size.height / scrollView.contentSize.height
let scaleWidth = scrollView.frame.size.width / scrollView.contentSize.width
let minimumScale:CGFloat = min(scaleHeight, scaleWidth)
let maximumScale:CGFloat = max(scaleHeight, scaleWidth)
scrollView.minimumZoomScale = minimumScale
scrollView.maximumZoomScale = maximumScale
scrollView.zoomScale = maximumScale
}
}
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
}
The code leaves me with unnecessary borders (left, right, top). How do I get rid of them?
EDIT: With #Bxtr's suggestion and another stackoverflow thread I was able to remove the borders left and right to the scroll view. After some more digging I found out that by deactivating Adjust Scroll View Insets, the image inside the scroll view can be correctly vertically positioned. Still, I do not get the reason for the vertical misplacement in the first place...
Have you checked the margin/padding values, because it kinda looks so (same size on left and right border). If it is not the case, could you please also post your xml file of the activity so we can have every part of the puzzle to help you ?
scrollView.contentSize = image.size;
you have to tweek this line. You are explicitly setting scroll view content size to the image size. You have to set content size to fit the Width of Screen.
You can use a UIView in UIScrollView, and that UIView contains UIImage.
You need to set constraints properly.
After some more digging I found out that by deactivating Adjust Scroll
View Insets, the image inside the scroll view can be correctly
vertically positioned. Still, I do not get the reason for the vertical
misplacement in the first place...
The reason is that the view controller's automaticallyAdjustsScrollViewInsets property is by default YES, the following is from apple documentation:
automaticallyAdjustsScrollViewInsets
A Boolean value that indicates
whether the view controller should automatically adjust its scroll
view insets.
Default value is YES, which allows the view controller to adjust its
scroll view insets in response to the screen areas consumed by the
status bar, navigation bar, and toolbar or tab bar. Set to NO if you
want to manage scroll view inset adjustments yourself, such as when
there is more than one scroll view in the view hierarchy.
Besides setting automaticallyAdjustsScrollViewInsets = No, you can pin the scrollView to the topLayoutGuide (instead of to the top of the viewController's view) when using autoLayout.
I have a simple view that displays an NSBezierpath. On mouseDown inside the path, the path's fill color sets to yellow and the view redraws. On mouseDown outside the path, the path's fill color sets to blue and the view redraws.
In my storyboard, I have a single window controller with a window content segue to a view controller. The view, customview class HeartView (below) fills the entire view controller.
Everything works fine until the user resizes the window vertically. After that, the view exhibits bizarre behavior: mouseDown no longer works everywhere inside the path, the recolor sometimes happens on mouseDown outside the path, and the path sometimes (but not always) doesn't completely fill. I think something is going on in the superview, but I don't know what.
import Cocoa
class HeartView: NSView {
var mouseLocation : NSPoint = NSZeroPoint
func drawObject(){
//Create an empty Bezier path
let aBezier : NSBezierPath = NSBezierPath()
aBezier.moveToPoint(CGPoint(x: 176.95,y: 44.90))
aBezier.curveToPoint(CGPoint(x: 166.71,y: 145.89),
controlPoint1: CGPoint(x: 76.63,y: 76.78),
controlPoint2: CGPoint(x: 82.59,y: 206.70))
aBezier.curveToPoint(CGPoint(x: 176.95,y: 44.90),
controlPoint1: CGPoint(x: 237.55,y: 224.76),
controlPoint2: CGPoint(x: 276.83,y: 95.98))
aBezier.closePath()
if (aBezier.containsPoint(NSMakePoint(mouseLocation.x, mouseLocation.y))){
NSColor.yellowColor().setFill()
NSColor.greenColor().setStroke()
} else {
NSColor.blueColor().setFill()
NSColor.orangeColor().setStroke()
}
aBezier.fill()
aBezier.lineWidth = 2.0
aBezier.stroke()
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
drawObject()
}
override func mouseDown(theEvent: NSEvent) {
mouseLocation.x = theEvent.locationInWindow.x
mouseLocation.y = theEvent.locationInWindow.y
self.setNeedsDisplayInRect(self.frame)
}
}
I found the answer in Lucas Derraugh's video on Mouse Events (Cocoa Programming L27). Turns out, I was capturing the mouseDown event in the superview's coordinate system. In the mouseDown event, I used "locationInWindow," which is what caused the strange behavior. I changed the method to:
override func mouseDown(theEvent: NSEvent) {
var viewPoint:NSPoint = self.convertPoint(theEvent.locationInWindow, fromView: nil)
mouseLocation.x = viewPoint.x
mouseLocation.y = viewPoint.y
self.needsDisplay = true
}
to convert from the window's coordinate system to the view's. Things now work well after any window resize event.
I have found an issue with UITextField. I have created a subclass of uitexfield that allows the user to click on the text to start editing and then rotate and resize the text.
If you resize the textfield by making the height larger the centered text moves to the right even though the width of the textfield does not increase. I have investigated this and found that uitextfield has an internval view of the type UIFieldEditor which in turn has a _UIFieldEditorContentView view. UIFieldEditor seems to be a subclass of UIScrollView and the contentsize of this scrollview becomes much larger than the size of uitextview. When the textview increases it's height the scrollviews contentsize width increases. I guess this might be an internal autolayout issue.
I have added a demo project here that demonstrates the issue. CLick on the text to start edit, then drag the resize icon so that the height increases and you will see the issue.
https://github.com/permagnus/UITextField-Resize-Issue-Demo
Screenhots from revealapp:
Incorrect size of underlying view in scrollview: https://github.com/permagnus/UITextField-Resize-Issue-Demo/blob/master/Screenshots/screenshot-showing-incorrect-size.png
The actuall size of the uitextfield: https://github.com/permagnus/UITextField-Resize-Issue-Demo/blob/master/Screenshots/screenshot-showing-textfield-size.png
Any ideas on how to fix this issue?
I found two ways to fix the issue:
The problems lies within the underlying scrollview. One way is to find the scrollview and se how much offseted it is and compensate for the wrong offset:
- (CGRect)editingRectForBounds:(CGRect)bounds
{
CGRect editRect = [super editingRectForBounds:bounds];
UIScrollView *scrollView = [self findScrollViewFromView:self];
if(scrollView)
{
float diff = (self.bounds.size.width - scrollView.contentSize.width)/2;
return CGRectInset(editRect, diff, 0);
}
return editRect;
}
- (UIScrollView *)findScrollViewFromView:(UIView *)view
{
if([view isKindOfClass:[UIScrollView class]])
{
return (UIScrollView *) view;
}
for(UIView *v in view.subviews)
{
UIScrollView *scrollView = [self findScrollViewFromView:v];
if(scrollView)
{
return scrollView;
}
}
return nil;
}
I also contacted Apple Support to get their point of the problem. They confirmed that this probably is a bug and I have submitted it as one. Their solution is the following:
Field editor is only activated for current editing session, so you can
end the editing session of the text field (by calling
resignFirstResponder) before resizing it (in touchesBegan... ?). In
your scenario, I guess keeping the editing session might not be
necessary.
If you really need to keep the editing session, one solution (ugly) I
can see is to reset the text and make sure the cursor is at the
beginning of document:
self.text = [self.text copy];
UITextPosition *beginningOfDocument = [self positionFromPosition:self.beginningOfDocument offset:0];
self.selectedTextRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:beginningOfDocument];
Both of these solutions are shitty hacks and are not recommended so you use these at your own risk :)