让NSWindow在汽车布局世界中正确调整大小

时间:2016-10-25 00:03:17

标签: macos cocoa autolayout nswindow nsscrollview

我在使用自动布局滚动视图进行垂直窗口调整时遇到问题。

我想要什么

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

  1. 通常情况下,窗口会自动将其高度调整为正好 匹配其内容(#2除外)。
  2. 用户可以选择手动调整窗口大小,使其小于窗口大小 内容,在这种情况下,嵌入的滚动视图将滚动 内容。一旦缩短,窗口将保持该高度,直到手动 调整大小或内容再次等于或小于窗口。
  3. 如果内容增长到窗口底部的点 突然进入码头或屏幕边界,窗户的高度被固定 到那个高度,就好像用户调整了它一样。
  4. 我得到了什么

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

    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

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

    自动布局功能很棒。窗口自动调整大小以匹配内容的大小。如果内容的高度发生变化,则窗口的高度将更改为匹配。真棒。

    我无法开展工作

    我没有运气让NSWindow让我手动调整它的高度。调整大小指示符(当悬停在边缘上时)表明我可以改变窗口的宽度,但不能改变其高度。

    我最初想过"哦,在其中一个滚动视图中,它必须是压缩阻力优先级。"但我发现压缩阻力或拥抱优先级的组合不会改变这种行为。我已经尝试设置滚动视图本身的优先级,并在documentView上(这对我没有意义,但我还是尝试过)。我在我能想到的每一个组合中尝试了749,499,49和1的值。

    我已经搜索了似乎已经发布的所有问题,但大多数已发布的问题似乎都在解决不同的问题。

    我添加了" Dump"用于记录垂直约束的按钮。列出的所有内容似乎都符合预期,除了少数我不理解的NSAutoresizingMaskLayoutConstraint对象。但是,这些似乎是文档视图和剪辑视图之间的约束,因此我认为这些是自动创建的,并且不是问题的一部分。

    <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是一个很大的障碍。

    背景

    我正在为我的应用程序进行重大改版。它是一个非常大的应用程序,所以我开始创建一组测试项目来测试一些新的UI和技术。

    第一项重大任务是将所有内容转换为自动布局。大部分看起来都相当顺利,我期待着汽车布局的诸多好处。

2 个答案:

答案 0 :(得分:1)

没有完全自动化的方法来实现你的目标#2。你想要两种不同的模式。在一种模式中,窗口总是足够大以容纳内容。如果它增长,窗口会增长;如果缩小窗口缩小。在另一种模式中,允许窗口小于适合内容所需的窗口,因为用户以这种方式调整窗口大小。在这种情况下,如果内容增长,窗口不会增长;如果它收缩,窗口不会收缩,除非内容变得足够小以至于它都适合然后它切换回第一模式。

自动布局实际上并不是这样的模式,至少不是自动的。您必须检测模式更改并以编程方式修改约束以实现两种行为模式。

您显然在文档视图和剪辑视图之间创建了约束,以保持其顶部和底部重合。这实质上强制剪辑视图,然后滚动视图与文档视图一样大。滚动视图永远不会滚动,因为它永远不会太小而无法显示所有内容。

我认为您可能需要两个底部间距约束。一个约束是所需(1000)优先级的不等式。您想表示剪辑视图永远不应该大于文档视图。文档视图的底部可以大于或等于剪辑视图的底部,但不能小于。

第二个底部间距约束将是相等的(现在为0常量),但优先级略小于NSLayoutPriorityWindowSizeStayPut(500)。这表示您希望剪辑视图和滚动视图足够大以适合内容,除非这会强制窗口增长或阻止用户缩小窗口。

问题在于,如果窗口足够大以适应内容,然后内容增长,则不会强制窗口增长。我所描述的实现了第二种模式。

您可以尝试通过将第二个约束的优先级设置得更高来实现第一个模式。那么问题是不允许用户调整窗口大小。你回到了目前的状况。

我认为您需要做的是通过观察文档视图NSViewFrameDidChangeNotification来注意内容调整大小。请务必通过将其postsFrameChangedNotifications属性设置为true来告诉视图发布该属性。当帧更改时,如果您认为应该处于第一模式,请将第二个约束的优先级设置得更高,在窗口上调用-layoutIfNeeded,然后将优先级设置为低。我认为您可能需要将优先级设置为事件循环的下一轮,因为在剪辑视图出现之后您不清楚是否会收到通知,因此可能需要使用GCD来安排。

那么,你怎么知道你应该进入哪种模式?我不完全确定。我认为它将适用于窗口委托(通常是其控制器)来实现-windowDidEndLiveResize:以了解用户何时完成窗口大小调整。我认为调整大小的用户将是一个实时调整大小,同时以编程方式调整大小或自动调整大小不会。

如果是用户调整了窗口的大小,则需要知道用户是否使窗口增长,以便所有内容都适合,或者如果用户将其调整为小于该值。为此,您可以将文档视图边界的高度与剪辑视图的documentVisibleRect进行比较。

答案 1 :(得分:1)

感谢您做出彻底而周到的回应。

  

您显然在文档视图和剪辑视图之间创建了约束,以保持其顶部和底部重合。

嗯,我没有,但IB确实做到了。 ;)

所以第一步是编辑剪辑视图约束,从&#34;等于0&#34;更改clipView.bottom-0-documentView.bottom约束。到&#34;小于或等于0&#34;。这允许剪辑视图(垂直)小于文档视图,最终允许用户垂直调整窗口大小。

然后我开始提出其他建议,添加一些额外的约束来固定文档的高度,并修改其active属性或更改其priority

然而,最终,我的路线略有不同。问题是,当你要求窗口的内容增长很多,或者当它靠近屏幕的底部时,它的行为就是......好吧,很奇怪。

相反,我创造了一个&#34;粘性&#34;窗口的模式。设置后,文档视图增长,我手动计算窗口的新框架。我这样做是因为当窗口靠近屏幕的底部和/或顶部时,我可以控制窗口的大小调整。

警告

我发现很难对所有这些技术造成隐患。每当调整帧大小时都会发送NSViewFrameDidChangeNotification。在自动布局期间,这可能会发生。如果您观察到此通知并立即调整窗口大小,内容大小或约束,则自动布局会非常沮丧并引发令人讨厌的“循环”#34;和&#34;递归&#34;布局警告(有时也无法正确调整大小)。解决方案是简单地将窗口大小修复程序包装在一个块中,并在所有自动布局逻辑完成后将其排队以在主线程上执行。

这是完成的,有效的测试项目(带评论和备注):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