在自动布局环境中正确调整NSWindow的大小 [英] Getting NSWindow resizing right in an auto layout world

查看:232
本文介绍了在自动布局环境中正确调整NSWindow的大小的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在使用自动布局滚动视图来调整垂直窗口大小时遇到​​问题.

I'm having problem getting vertical window resizing working with an auto layout scroll view.

我想要的

我想尽可能地复制我应用程序当前的窗口大小调整行为.窗口的宽度是灵活的,但是窗口的高度通常应该跟踪内容的高度.具体来说:

I would like to replicate, as closely as possible, my app's current window resizing behaviour. The width of the window is flexible, but the height of the window should generally track the height of the contents. Specifically:

  1. 通常,窗口会自动将其高度调整为恰好
    匹配其内容(#2除外).
  2. 用户可以选择手动调整窗口大小,使其小于窗口的大小 内容,在这种情况下,嵌入式滚动视图将滚动 内容.一旦变短,窗口将保持该高度,直到手动 调整大小或内容再次等于或小于窗口.
  3. 如果内容增长到窗口底部的程度 撞到基座或屏幕边界时,固定了窗口的高度 达到该高度,就像用户调整了它的大小一样.
  1. Normally, the window automatically resizes its height to exactly
    match its contents (with the exception of #2).
  2. The user can choose to manually resize the window so it's smaller than its contents, in which case the imbedded scroll view will scroll the contents. Once made shorter, the window remains that height until manually resized or the contents becomes equal or smaller than the window again.
  3. If the contents grows to the point that the bottom of the window bumps into the dock or screen bounds, the window's height is pinned to that height, just as if the user has resized it.

我得到了什么

我创建了一个窗口,其中包含一组代表子视图,这些子视图模仿了我的应用程序的基本需求.窗口的层次结构很简单:

I created a window with a set of representative subviews that mimic the basic needs of my app. The window's hierarchy is simple:

Window
    NSView (contentView)
        NSScrollView
            NSClipView (NSScrollView.contentView)
                NSView (NSScrollView.documentView)
                    A bunch of standard and custom subviews with constraints

您可以在此处下载测试项目(macOS 10.12/Xcode 8): http://mbx.cm/t/4FUGY

You can download the test project here (macOS 10.12/Xcode 8): http://mbx.cm/t/4FUGY

我已经组织了各个子视图,因此它们具有灵活的宽度,但是约束恰好定义了一个可能的高度.父滚动视图将填充窗口的内容视图.

I've organized the various subviews so they have a flexible width, but the constraints define exactly one possible height. The parent scroll view fills the content view of the window.

自动布局的东西效果很好.窗口会自动调整大小以匹配内容的大小.如果内容的高度发生变化,则窗口的高度将发生变化以匹配.很棒.

The auto layout stuff works great. The window automatically resizes to match the size of the contents. If the height of the contents changes, the height of the window changes to match. Awesome.

我无法上班的地方

我没有运气让NSWindow让我手动调整其高度.调整大小指示器(将鼠标悬停在边缘上时)显示我可以更改窗口的宽度,但不能更改其高度.

I have had no luck getting NSWindow to let me manually resize its height. The resize indicator (when hovering over the edge) shows that I can alter the window's width, but not its height.

我最初以为哦,在其中一个滚动视图中,这必须是抗压缩性的优先事项."但是我找不到抗压性或拥抱优先级的组合会改变这种行为.我尝试过设置滚动视图本身以及在documentView上的优先级(这对我来说没有意义,但无论如何我还是尝试过).我以我能想到的每种组合尝试了值749、499、49和1.

I initially thought "Oh, that's got to be a compression resistance priority in one of the scroll views." But I can find no combination of compression resistance or hugging priorities that will alter this behavior. I've tried setting the priorities of the scroll view itself, and on the documentView (which makes no sense to me, but I tried anyway). I tried the values 749, 499, 49, and 1 in every combination I could think of.

我已经搜索了似乎已发布的每个问题,但是大多数发布的问题似乎正在解决不同的问题.

I've searched for every problem that seemed released to this, but most of the posted questions seemed to be addressing different problems.

我添加了一个转储"按钮来记录垂直约束.除了少数我不理解的NSAutoresizingMaskLayoutConstraint对象,列出的所有内容似乎都与预期的一样.但是,这些似乎是文档视图和剪辑视图之间的约束,因此我认为这些约束是自动创建的,而不是问题的一部分.

I added a "Dump" button to log the vertical constraints. Everything listed appears to be as expected, except for a handful of NSAutoresizingMaskLayoutConstraint objects that I don't understand. However, these appear to be constraints between the document view and the clip view, so I assume those are automatically created and aren't part of the problem.

<NSLayoutConstraint:0x608000082580 PartBoxView:0x608000140840'Part B'.height == 96   (active)>
<NSLayoutConstraint:0x608000083de0 V:[PartBoxView:0x608000140840'Part B']-(0)-|   (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083e80 V:[PartBoxView:0x608000140370'Part A']-(NSSpace(8))-[PartBoxView:0x608000140840'Part B']   (active)>
<NSLayoutConstraint:0x608000082da0 V:|-(0)-[NSScrollView:0x6080001c10e0]   (active, names: '|':NSView:0x608000121400 )>
<NSLayoutConstraint:0x608000083430 V:|-(0)-[NSView:0x6080001212c0]   (active, names: '|':NSClipView:0x10040e2a0 )>
<NSLayoutConstraint:0x608000081e00 V:[NSScrollView:0x6080001c10e0]-(0)-|   (active, names: '|':NSView:0x608000121400 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082b20 h=-&- v=-&- NSView:0x608000121400.minY == 0   (active, names: '|':NSThemeFrame:0x102504ea0'Window' )>
<NSAutoresizingMaskLayoutConstraint:0x6500000823f0 h=-&- v=-&- NSClipView:0x10040e2a0.minY == 1   (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083480 V:[NSView:0x6080001212c0]-(0)-|   (active, names: '|':NSClipView:0x10040e2a0 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082440 h=-&- v=-&- V:[NSClipView:0x10040e2a0]-(1)-|   (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083d40 V:|-(0)-[PartBoxView:0x608000140370'Part A']   (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083c50 V:[NSImageView:0x608000161bc0]-(20)-|   (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083bb0 V:|-(20)-[NSImageView:0x608000161bc0]   (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083660 NSImageView:0x608000161bc0.height == 64   (active)>

我希望有自动排版经验的人能告诉我如何使#2正常工作.我很确定#3将需要一些自定义代码,但是#2是最大的障碍.

I'm hoping someone with auto layout experience can tell me how to get #2 working. I'm pretty sure #3 will require some custom code, but #2 is the big hurdle.

背景

我正在对我的应用进行重大改版.这是一个很大的应用程序,因此我首先创建了一组测试项目来测试一些新的UI和技术.

I'm in the process of giving my app a major facelift. It's a pretty big app, so I started by creating a set of test projects to test out some of the new UI and techniques.

首要任务是将所有内容转换为自动版式.大多数情况看起来会相当顺利,我期待自动布局的许多好处.

The first big undertaking is converting everything to auto layout. Most of it looks like it will be fairly smooth, and I'm looking forward to some of the many benefits of auto layout.

推荐答案

肯,

感谢您的彻底和周到的答复.

Thanks for the thorough and thoughtful response.

您显然已经在文档视图和剪辑视图之间创建了约束,以使它们的顶部和底部保持一致.

You have apparently created constraints between the document view and the clip view to keep their tops and bottoms coincident.

好吧,我没有,但是IB当然有. ;)

Well, I didn't, but IB certainly did. ;)

因此,第一步是编辑剪辑视图约束,将clipView.bottom-0-documentView.bottom约束从等于0"更改为小于或等于0".这样可以使剪辑视图(垂直)小于文档视图,最终使用户可以垂直调整窗口大小.

So the first step was to edit the clip view constraints, changing the clipView.bottom-0-documentView.bottom constraint from "equals 0" to "less than or equal to 0". That permits the clip view to be (vertically) smaller than the document view, ultimately allowing the user to resize the window vertically.

然后,我从您的其他建议开始,添加一些其他约束以将文档的高度固定在文档上,并修改其active属性或更改其priority.

I then started with your other suggestions, adding some additional constraints to pin the height to the document and either modifying its active property or changing its priority.

但是,最终,我走了一条稍微不同的路线.问题是,当您要求窗口的内容增长很多,或者当它靠近屏幕底部时,它的行为是……很奇怪.

Ultimately, however, I went a slightly different route. The problem is that when you ask the window's contents to grow a lot, or when it's close to the bottom of the screen, its behavior is ... well, weird.

相反,我为窗口创建了粘滞"模式.设置后,文档视图会增加,我将为窗口手动计算一个新框架.之所以这样做,是因为我可以控制窗口在屏幕底部和/或顶部附近时如何调整窗口大小.

Instead, I created a "sticky" mode for the window. When set, and the document view grows, I manually calculate a new frame for the window. I do this because I can control how the window resizes when it is near the bottom, and/or top, of the screen.

警告

我发现所有这些技术都存在隐患的艰难方法.每当调整帧大小时,都会发送NSViewFrameDidChangeNotification.这可能会在自动布局期间发生.如果您观察到此通知,并立即调整窗口大小,内容大小或约束,则自动布局会很烦人,并会发出讨厌的圆形"和递归"布局警告(有时还会无法正确调整大小).解决方案是,在所有自动布局逻辑都完成后,将窗口大小修复程序简单地包装在一个块中,并使其排队在主线程上执行.

I discovered the hard way that there's a hidden danger to all of these techniques. The NSViewFrameDidChangeNotification is sent whenever the frame is resized. This can happen during auto layout. If you observe this notification and immediately adjust the window size, content size, or constraints, auto layout gets very upset and throws nasty "circular" and "recursive" layout warnings (it also sometimes fails to resize properly). The solution was to simply wrap up the window size fix in a block and queue it to execute on the main thread, after all of the auto layout logic has finished.

这是完成,工作,测试的项目(带有注释和注释): http://mbx.cm/t/Zjdml

Here's the finished, working, test project (with comments and notes): http://mbx.cm/t/Zjdml

以下是相关代码:

@interface ViewController ()
{
  BOOL windowSizeSticky;      // a change in the content size should resize the window to match
}

- (void)documentSizeChangedNotification:(NSNotification*)notification;

@end


@implementation ViewController

- (void)dealloc
{
  self.view.window.delegate = nil;
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)viewDidLoad
{
  [super viewDidLoad];

  // Enable the document view to post size change notifications
  NSView* docView = self.scrollView.documentView;
  docView.postsFrameChangedNotifications = YES;
  // Subscribe to those changes
  [[NSNotificationCenter defaultCenter] addObserver:self
                       selector:@selector(documentSizeChangedNotification:)
                         name:NSViewFrameDidChangeNotification
                         object:docView];
  // Queue up an initial evaluation so windowSizeSticky is set correctly
  dispatch_async(dispatch_get_main_queue(), ^{
    [self documentSizeChangedNotification:nil];
    });
}

- (void)viewWillAppear
{
  // Make this controller the window's delegate
  self.view.window.delegate = self;

  [super viewWillAppear];
}

- (void)windowDidEndLiveResize:(NSNotification *)notification
{
  // Whenever the user resizes the window, reevaluate the windowSizeSticky mode
  NSView* documentView = self.scrollView.documentView;
  NSClipView* clipView = (NSClipView*)(documentView.superview);
  NSRect docVisible = clipView.documentVisibleRect;
  NSRect docFrame = documentView.frame;

  // Update the "sticky" mode depending on whether the window now displays all, or only a portion, of the contents
  windowSizeSticky = (docVisible.size.height==docFrame.size.height);
}

- (void)documentSizeChangedNotification:(__unused NSNotification *)notification
{
  NSView* documentView = self.scrollView.documentView;
  NSWindow* window = documentView.window;
  if (!window.inLiveResize)   // Suppress this logic while the user is manually resizing the window
    {
    dispatch_async(dispatch_get_main_queue(), ^{
      // Do this the next time the main loop is idle
      // This notification can be sent during auto layout, and we don't want to attempt to resize
      //  the window in the middle of an auto layout calculation.

      // The geometry of the document view has changed; check to see if the window needs resizing
      NSClipView* clipView = (NSClipView*)(documentView.superview);
      NSRect docVisible = clipView.documentVisibleRect;
      NSRect docFrame = documentView.frame;   // The doc's frame is in the clip view's coordinate system
      if (docVisible.size.height==docFrame.size.height)
        {
        // All of the document is (vertically) visible in the clip view
        // That means the window is displaying all of its contents
        // Whenever this happens, switch to "sticky" mode so future changes in content will make the window grow
        windowSizeSticky = YES;
        }
      else if (windowSizeSticky && docVisible.size.height < docFrame.size.height)
        {
        // The content is now taller than the view port of the scroll view & the window is "sticky"
        // Try to make the window taller so all of the content is exposed
        NSRect windowFrame = window.frame;
        CGFloat addHeight = docFrame.size.height-docVisible.size.height;

        NSRect contentRect = [window contentRectForFrameRect:windowFrame];
        contentRect.size.height += addHeight;

        // Calculate an ideal window frame, then adjust the existing frame so it's as close as we can get
        NSRect targetFrame = [window frameRectForContentRect:contentRect];
        CGFloat deltaY = targetFrame.size.height-windowFrame.size.height;
        if (deltaY >= 1.0)
          {
          // The window needs to be taller
          // Make it tall enough to display all of the content, keeping its title bar where it is
          windowFrame.origin.y -= deltaY;
          windowFrame.size.height += deltaY;

          // Screen bounds check...
          NSRect visibleFrame = window.screen.visibleFrame;
          if (visibleFrame.origin.y>windowFrame.origin.y)
            {
            // The bottom of the window is now below the visible area of the screen
            // Move the whole window up so it's back on the screen
            windowFrame.origin.y = visibleFrame.origin.y;
            if (visibleFrame.origin.y+visibleFrame.size.height < windowFrame.origin.y+windowFrame.size.height)
              {
              // The top of the window is now off the top of the screen
              // Shorten the window so it's entirely within the screen
              windowFrame.size.height = visibleFrame.size.height;
              // This also means "sticky" mode is off, since we had to size the window to something smaller
              //  than its contents.
              windowSizeSticky = NO;
              }
            }
          [window setFrame:windowFrame
               display:NO
               animate:NO/* be consistent; constraints doesn't animate when getting shorter */];
          }
        }
      // else { window is not sticky OR its contents doesn't exceed the height of the window: do nothing }
      });
    }
}

@end

这篇关于在自动布局环境中正确调整NSWindow的大小的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆