生成新式 macOS 应用

本文介绍了开发人员可以用来在 Xamarin.Mac 中生成新式 macOS 应用的几个提示、功能和技术。

使用新式视图构建新式外观

新式外观将包括新式窗口和工具栏外观,例如如下所示的示例应用:

An example of a modern Mac app UI

启用全尺寸内容视图

为了在 Xamarin.Mac 应用中实现此外观,开发人员希望使用全尺寸内容视图,这意味着内容在工具和标题栏区域下扩展,并且将由 macOS 自动模糊。

若要在代码中启用此功能,请为 NSWindowController 创建一个自定义类,使其如下所示:

using System;
using Foundation;
using AppKit;

namespace MacModern
{
    public partial class MainWindowController : NSWindowController
    {
        #region Constructor
        public MainWindowController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void WindowDidLoad ()
        {
            base.WindowDidLoad ();

            // Set window to use Full Size Content View
            Window.StyleMask = NSWindowStyle.FullSizeContentView;
        }
        #endregion
    }
}

还可以在 Xcode 的 Interface Builder 中启用此功能,方法是选择“窗口”并选中“全尺寸内容视图”:

Editing the main storyboard in Xcode's Interface Builder

使用“全尺寸内容视图”时,开发人员可能需要偏移标题和工具栏区域下的内容,以便特定内容(如标签)不会在内容下方滑动。

更复杂的是,标题和工具栏区域的动态高度可能基于用户当前执行的操作、用户安装的 macOS 版本和/或应用运行的 Mac 硬件。

因此,在布置用户界面时,简单地对偏移量进行硬编码是行不通的。 开发人员需要采用动态方法。

Apple 已经包含了 NSWindow 类的 Key-Value ObservableContentLayoutRect 属性,以在代码中获取当前内容区域。 当内容区域发生更改时,开发人员可以使用此值手动定位所需的元素。

更好的解决方案是使用 Auto Layout 和 Size 类在代码或 Interface Builder 中定位 UI 元素。

以下示例中的类似代码可用于在应用的视图控制器中使用 AutoLayout 和 Size 类定位 UI 元素:

using System;
using AppKit;
using Foundation;

namespace MacModern
{
    public partial class ViewController : NSViewController
    {
        #region Computed Properties
        public NSLayoutConstraint topConstraint { get; set; }
        #endregion

        ...

        #region Override Methods
        public override void UpdateViewConstraints ()
        {
            // Has the constraint already been set?
            if (topConstraint == null) {
                // Get the top anchor point
                var contentLayoutGuide = ItemTitle.Window?.ContentLayoutGuide as NSLayoutGuide;
                var topAnchor = contentLayoutGuide.TopAnchor;

                // Found?
                if (topAnchor != null) {
                    // Assemble constraint and activate it
                    topConstraint = topAnchor.ConstraintEqualToAnchor (topAnchor, 20);
                    topConstraint.Active = true;
                }
            }

            base.UpdateViewConstraints ();
        }
        #endregion
    }
}

此代码为将应用于标签 (ItemTitle) 的顶级约束创建存储,以确保其不会滑到标题和工具栏区域下:

public NSLayoutConstraint topConstraint { get; set; }

通过重写视图控制器的 UpdateViewConstraints 方法,开发人员可以测试是否已生成所需的约束,并根据需要创建它。

如果需要生成新的约束,则访问需要约束的控件的窗口的 ContentLayoutGuide 属性,并将其强制转换为 NSLayoutGuide

var contentLayoutGuide = ItemTitle.Window?.ContentLayoutGuide as NSLayoutGuide;

将访问 NSLayoutGuide 的 TopAnchor 属性,如果可用,则它用于生成具有所需偏移量的新约束,并使新约束处于活动状态以应用它:

// Assemble constraint and activate it
topConstraint = topAnchor.ConstraintEqualToAnchor (topAnchor, 20);
topConstraint.Active = true;

启用简化工具栏

普通 macOS 窗口包括一个标准标题栏,沿着窗口的顶部边缘延伸。 如果窗口还包括工具栏,则会在此标题栏区域下显示:

A standard Mac Toolbar

使用简化工具栏时,标题区域会消失,工具栏将向上移动到标题栏的位置,与窗口的“关闭”、“最小化”和“最大化”按钮一行:

A streamlined Mac Toolbar

通过重写 NSViewControllerViewWillAppear 方法并使其如下所示,可以启用简化工具栏:

public override void ViewWillAppear ()
{
    base.ViewWillAppear ();

    // Enable streamlined Toolbars
    View.Window.TitleVisibility = NSWindowTitleVisibility.Hidden;
}

此效果通常用于 Shoebox Applications(一个窗口应用),如地图、日历、便笺和系统首选项。

使用附件视图控制器

根据应用的设计,开发人员可能还想使用显示在标题/工具栏区域正下方的附件视图控制器来补充标题栏区域,以便根据用户当前从事的活动为用户提供上下文相关的控件:

An example Accessory View Controller

系统会自动模糊和调整附件视图控制器的大小,而无需开发人员干预。

若要添加附件视图控制器,请执行以下操作:

  1. 在“解决方案资源管理器”中,双击 Main.storyboard 文件,将其打开进行编辑。

  2. 自定义视图控制器拖到窗口的层次结构中:

    Adding a new Custom View Controller

  3. 布局附件视图的 UI:

    Designing the new view

  4. 将附件视图公开为输出口或任何其他操作或其 UI 的输出口

    Adding the required OUtlet

  5. 保存更改。

  6. 返回到 Visual Studio for Mac 以同步更改。

编辑 NSWindowController,使其如下所示:

using System;
using Foundation;
using AppKit;

namespace MacModern
{
    public partial class MainWindowController : NSWindowController
    {
        #region Constructor
        public MainWindowController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void WindowDidLoad ()
        {
            base.WindowDidLoad ();

            // Create a title bar accessory view controller and attach
            // the view created in Interface Builder
            var accessoryView = new NSTitlebarAccessoryViewController ();
            accessoryView.View = AccessoryViewGoBar;

            // Set the location and attach the accessory view to the
            // titlebar to be displayed
            accessoryView.LayoutAttribute = NSLayoutAttribute.Bottom;
            Window.AddTitlebarAccessoryViewController (accessoryView);

        }
        #endregion
    }
}

此代码的关键点是视图设置为 Interface Builder 中定义的自定义视图,并将其公开为输出口

accessoryView.View = AccessoryViewGoBar;

以及定义附件显示位置LayoutAttribute

accessoryView.LayoutAttribute = NSLayoutAttribute.Bottom;

由于 macOS 现已完全本地化,因此已弃用 LeftRightNSLayoutAttribute 属性,应替换为 LeadingTrailing

使用选项卡式窗口

此外,macOS 系统还可以将附件视图控制器添加到应用的窗口。 例如,若要创建选项卡式窗口,其中多个应用的窗口合并到一个虚拟窗口中:

An example of a tabbed Mac Window

通常,开发人员需要在 Xamarin.Mac 应用中采取有限的操作并使用选项卡式窗口,系统会自动处理它们,如下所示:

  • 调用 OrderFront 方法时,窗口将自动变成选项卡式。
  • 调用 OrderOut 方法时,窗口将自动取消选项卡式。
  • 在代码中,所有选项卡式窗口仍然被视为“可见”,但任何非最前面的选项卡都被系统使用 CoreGraphics 隐藏。
  • 使用 NSWindowTabbingIdentifier 属性将窗口组合到选项卡中。
  • 如果它是基于 NSDocument 的应用,则会自动启用其中的一些功能(例如添加到选项卡栏的加号按钮),而无需执行任何开发人员操作。
  • 非基于 NSDocument 的应用可以通过重写 NSWindowsControllerGetNewWindowForTab 方法,在选项卡组中启用“加号”按钮来添加新文档。

将所有部分组合在一起,想要使用基于系统的选项卡式窗口的应用的 AppDelegate 可能如下所示:

using AppKit;
using Foundation;

namespace MacModern
{
    [Register ("AppDelegate")]
    public class AppDelegate : NSApplicationDelegate
    {
        #region Computed Properties
        public int NewDocumentNumber { get; set; } = 0;
        #endregion

        #region Constructors
        public AppDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void DidFinishLaunching (NSNotification notification)
        {
            // Insert code here to initialize your application
        }

        public override void WillTerminate (NSNotification notification)
        {
            // Insert code here to tear down your application
        }
        #endregion

        #region Custom Actions
        [Export ("newDocument:")]
        public void NewDocument (NSObject sender)
        {
            // Get new window
            var storyboard = NSStoryboard.FromName ("Main", null);
            var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;

            // Display
            controller.ShowWindow (this);
        }
        #endregion
    }
}

其中,NewDocumentNumber 属性跟踪创建的新文档数量,NewDocument 方法会创建一个新文档并显示它。

然后,NSWindowController 如下所示:

using System;
using Foundation;
using AppKit;

namespace MacModern
{
    public partial class MainWindowController : NSWindowController
    {
        #region Application Access
        /// <summary>
        /// A helper shortcut to the app delegate.
        /// </summary>
        /// <value>The app.</value>
        public static AppDelegate App {
            get { return (AppDelegate)NSApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Constructor
        public MainWindowController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Public Methods
        public void SetDefaultDocumentTitle ()
        {
            // Is this the first document?
            if (App.NewDocumentNumber == 0) {
                // Yes, set title and increment
                Window.Title = "Untitled";
                ++App.NewDocumentNumber;
            } else {
                // No, show title and count
                Window.Title = $"Untitled {App.NewDocumentNumber++}";
            }
        }
        #endregion

        #region Override Methods
        public override void WindowDidLoad ()
        {
            base.WindowDidLoad ();

            // Prefer Tabbed Windows
            Window.TabbingMode = NSWindowTabbingMode.Preferred;
            Window.TabbingIdentifier = "Main";

            // Set default window title
            SetDefaultDocumentTitle ();

            // Set window to use Full Size Content View
            // Window.StyleMask = NSWindowStyle.FullSizeContentView;

            // Create a title bar accessory view controller and attach
            // the view created in Interface Builder
            var accessoryView = new NSTitlebarAccessoryViewController ();
            accessoryView.View = AccessoryViewGoBar;

            // Set the location and attach the accessory view to the
            // titlebar to be displayed
            accessoryView.LayoutAttribute = NSLayoutAttribute.Bottom;
            Window.AddTitlebarAccessoryViewController (accessoryView);

        }

        public override void GetNewWindowForTab (NSObject sender)
        {
            // Ask app to open a new document window
            App.NewDocument (this);
        }
        #endregion
    }
}

其中静态 App 属性提供了访问 AppDelegate 的快捷方式。 SetDefaultDocumentTitle 方法根据创建的新文档数量设置新的文档标题。

以下代码告知 macOS 应用更喜欢使用选项卡,并提供一个字符串来允许将应用的窗口组合到选项卡:

// Prefer Tabbed Windows
Window.TabbingMode = NSWindowTabbingMode.Preferred;
Window.TabbingIdentifier = "Main";

以下替代方法会将加号按钮添加到选项卡栏,该按钮将在用户单击时创建新文档:

public override void GetNewWindowForTab (NSObject sender)
{
    // Ask app to open a new document window
    App.NewDocument (this);
}

使用 Core Animation

Core Animation 是一个内置于 macOS 中的高性能图形渲染引擎。 核心动画经过优化,利用新式 macOS 硬件中提供的 GPU(图形处理单元),而不是在 CPU 上运行图形操作(这会降低计算机的速度)。

Core Animation 提供的 CALayer 可用于快速流畅的滚动和动画等任务。 应用的用户界面应由多个子视图和图层组成,以充分利用 Core Animation。

CALayer 对象提供了多个属性,使开发人员能够控制屏幕上向用户显示的内容,例如:

  • Content - 可以是提供图层内容的 NSImageCGImage
  • BackgroundColor - 将图层的背景色设置为 CGColor
  • BorderWidth - 设置边框宽度。
  • BorderColor - 设置边框颜色。

若要在应用的 UI 中使用 Core Graphics,它必须使用基于图层的视图,Apple 建议开发人员始终在窗口的内容视图中启用该视图。 这样,所有子视图也会自动继承层支持。

此外,Apple 建议使用基于图层的视图,而不是将新的 CALayer 添加为子图层,因为系统将自动处理多个所需的设置(例如 Retina Display 所需的设置)。

可以通过将 NSViewWantsLayer 设置为 true 或在 Xcode 的 Interface Builder 中的“视图效果检查器”下通过检查“Core Animation 图层”来启用图层支持:

The View Effects Inspector

使用图层重新绘制视图

在 Xamarin.Mac 应用中使用基于图层的视图时,另一个重要步骤是在 NSViewController 中将 NSViewLayerContentsRedrawPolicy 设置为 OnSetNeedsDisplay。 例如:

public override void ViewWillAppear ()
{
    base.ViewWillAppear ();

    // Set the content redraw policy
    View.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;
}

如果开发人员未设置此属性,则每当视图的帧源发生更改时,都会重新绘制视图,出于性能原因不需要这样做。 但是,如果将此属性设置为 OnSetNeedsDisplay,则开发人员必须手动将 NeedsDisplay 设置为 true,才能强制重新绘制内容。

当视图标记为脏时,系统会检查视图的 WantsUpdateLayer 属性。 如果返回 true,则调用 UpdateLayer 方法,否则将调用视图的 DrawRect 方法来更新视图的内容。

Apple 提供以下建议,用于在需要时更新视图内容:

  • Apple 更喜欢尽可能使用 UpdateLater 而不是 DrawRect,因为它可提供显著的性能提升。
  • 对类似 UI 元素使用相同的 layer.Contents
  • Apple 还希望开发人员尽可能使用标准视图(如 NSTextField)撰写 UI。

若要使用 UpdateLayer,请为 NSView 创建自定义类,并使代码如下所示:

using System;
using Foundation;
using AppKit;

namespace MacModern
{
    public partial class MainView : NSView
    {
        #region Computed Properties
        public override bool WantsLayer {
            get { return true; }
        }

        public override bool WantsUpdateLayer {
            get { return true; }
        }
        #endregion

        #region Constructor
        public MainView (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void DrawRect (CoreGraphics.CGRect dirtyRect)
        {
            base.DrawRect (dirtyRect);

        }

        public override void UpdateLayer ()
        {
            base.UpdateLayer ();

            // Draw view
            Layer.BackgroundColor = NSColor.Red.CGColor;
        }
        #endregion
    }
}

使用新式拖放

若要为用户提供新式拖放体验,开发人员应在应用的拖放操作中采用 Drag Flocking。 Drag Flocking 是指当用户继续拖动操作时,每个被拖动的文件或项目最初显示为聚集的单个元素(在光标下按项目数分组)。

如果用户终止拖动操作,则各个元素将取消聚集,并返回到其原始位置。

以下示例代码会在自定义视图上启用 Drag Flocking:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace MacModern
{
    public partial class MainView : NSView, INSDraggingSource, INSDraggingDestination
    {
        #region Constructor
        public MainView (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void MouseDragged (NSEvent theEvent)
        {
            // Create group of string to be dragged
            var string1 = new NSDraggingItem ((NSString)"Item 1");
            var string2 = new NSDraggingItem ((NSString)"Item 2");
            var string3 = new NSDraggingItem ((NSString)"Item 3");

            // Drag a cluster of items
            BeginDraggingSession (new [] { string1, string2, string3 }, theEvent, this);
        }
        #endregion
    }
}

通过将被拖动的每个项目作为数组中的一个单独元素发送到 NSViewBeginDraggingSession 方法,可以实现聚集效果。

使用 NSTableViewNSOutlineView 时,请使用 NSTableViewDataSource 类的 PastboardWriterForRow 方法启动拖动操作:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace MacModern
{
    public class ContentsTableDataSource: NSTableViewDataSource
    {
        #region Constructors
        public ContentsTableDataSource ()
        {
        }
        #endregion

        #region Override Methods
        public override INSPasteboardWriting GetPasteboardWriterForRow (NSTableView tableView, nint row)
        {
            // Return required pasteboard writer
            ...

            // Pasteboard writer failed
            return null;
        }
        #endregion
    }
}

这允许开发人员为表中正在拖动的每个项提供单独的 NSDraggingItem,而不是将所有行作为一个组写入粘贴板的旧方法 WriteRowsWith

使用 NSCollectionViews 时,再次使用 PasteboardWriterForItemAt 方法,而不是拖动开始时的 WriteItemsAt 方法。

开发人员应始终避免将大型文件放在粘贴板上。 对于 macOS Sierra 来说,File Promises 是一项新功能,它允许开发人员在粘贴板上放置对给定文件的引用,稍后当用户使用新的 NSFilePromiseProviderNSFilePromiseReceiver 类完成拖动操作时,将实现这些引用。

使用新式事件跟踪

对于已添加到标题或工具栏区域的用户界面元素(如 NSButton),用户应该能够单击元素并让它照常触发事件(例如显示弹出窗口)。 但是,由于该项也位于标题或工具栏区域中,用户应能够单击并拖动元素以移动窗口。

若要在代码中完成此操作,请为元素(如 NSButton)创建自定义类并替代 MouseDown 事件,如下所示:

public override void MouseDown (NSEvent theEvent)
{
    var shouldCallSuper = false;

    Window.TrackEventsMatching (NSEventMask.LeftMouseUp, 2000, NSRunLoop.NSRunLoopEventTracking, (NSEvent evt, ref bool stop) => {
        // Handle event as normal
        stop = true;
        shouldCallSuper = true;
    });

    Window.TrackEventsMatching(NSEventMask.LeftMouseDragged, 2000, NSRunLoop.NSRunLoopEventTracking, (NSEvent evt, ref bool stop) => {
        // Pass drag event to window
        stop = true;
        Window.PerformWindowDrag (evt);
    });

    // Call super to handle mousedown
    if (shouldCallSuper) {
        base.MouseDown (theEvent);
    }
}

此代码使用 UI 元素所附加的 NSWindowTrackEventsMatching 方法来截获 LeftMouseUpLeftMouseDragged 事件。 对于 LeftMouseUp 事件,UI 元素会正常响应。 对于 LeftMouseDragged 事件,该事件将传递给 NSWindowPerformWindowDrag 方法,以在屏幕上移动窗口。

调用 NSWindow 类的 PerformWindowDrag 方法具有以下优势:

  • 它允许窗口移动,即使应用挂起(例如处理深层循环时)。
  • 空间切换将按预期工作。
  • 空格栏将正常显示。
  • 窗口贴靠和对齐正常工作。

使用新式容器视图控件

macOS Sierra 对以前版本的操作系统中现有的容器视图控件进行了许多新式改进。

表视图增强功能

开发人员应始终使用新的基于 NSView 的容器视图控件版本,如 NSTableView。 例如:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace MacModern
{
    public class ContentsTableDelegate : NSTableViewDelegate
    {
        #region Constructors
        public ContentsTableDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
        {
            // Build new view
            var view = new NSView ();
            ...

            // Return the view representing the item to display
            return view;
        }
        #endregion
    }
}

这允许将自定义表行操作附加到表中的给定行(如向右轻扫以删除该行)。 若要启用此行为,请替代 NSTableViewDelegateRowActions 方法:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace MacModern
{
    public class ContentsTableDelegate : NSTableViewDelegate
    {
        #region Constructors
        public ContentsTableDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
        {
            // Build new view
            var view = new NSView ();
            ...

            // Return the view representing the item to display
            return view;
        }

        public override NSTableViewRowAction [] RowActions (NSTableView tableView, nint row, NSTableRowActionEdge edge)
        {
            // Take action based on the edge
            if (edge == NSTableRowActionEdge.Trailing) {
                // Create row actions
                var editAction = NSTableViewRowAction.FromStyle (NSTableViewRowActionStyle.Regular, "Edit", (action, rowNum) => {
                    // Handle row being edited
                    ...
                });

                var deleteAction = NSTableViewRowAction.FromStyle (NSTableViewRowActionStyle.Destructive, "Delete", (action, rowNum) => {
                    // Handle row being deleted
                    ...
                });

                // Return actions
                return new [] { editAction, deleteAction };
            } else {
                // No matching actions
                return null;
            }
        }
        #endregion
    }
}

静态 NSTableViewRowAction.FromStyle 用于创建以下样式的新表行操作:

  • Regular - 执行标准非破坏性操作,例如编辑行的内容。
  • Destructive - 执行破坏性操作,例如从表中删除行。 这些操作将以红色背景呈现。

滚动视图增强功能

直接使用滚动视图 (NSScrollView) 或作为其他控件(如 NSTableView)的一部分使用时,滚动视图的内容可以使用新式外观和视图在 Xamarin.Mac 应用中的标题和工具栏区域下滑动。

因此,滚动视图内容区域中的第一项可能会被标题和工具栏区域部分遮盖。

若要解决此问题,Apple 向 NSScrollView 类添加了两个新属性:

  • ContentInsets - 允许开发人员提供一个 NSEdgeInsets 对象,定义将应用于滚动视图顶部的偏移量。
  • AutomaticallyAdjustsContentInsets - 如果为 true,则滚动视图将自动为开发人员处理 ContentInsets

通过使用 ContentInsets,开发人员可以调整滚动视图的开始,以允许包含附件,例如:

  • 类似于邮件应用程序中显示的排序指示器。
  • 搜索字段。
  • “刷新”或“更新”按钮。

新式应用中的自动布局和本地化

Apple 在 Xcode 中包含多项技术,使开发人员能够轻松创建国际化的 macOS 应用。 Xcode 现在允许开发人员在其情节提要文件中将面向用户的文本与应用的用户界面设计分开,并提供在 UI 更改时保持这种分离的工具。

有关详细信息,请参阅 Apple 的国际化和本地化指南

实现基本国际化

通过实现基本国际化,开发人员可以提供一个单独的情节提要文件来表示应用的 UI,并分离出所有面向用户的字符串。

当开发人员创建定义应用用户界面的初始情节提要文件时,它们将以基本国际化(开发人员所说的语言)生成。

接下来,开发人员可以导出本地化和基本国际化字符串(在情节提要 UI 设计中),这些字符串可以翻译成多种语言。

稍后可以导入这些本地化,Xcode 将为情节提要生成特定于语言的字符串文件。

实现“自动布局”以支持本地化

由于字符串值的本地化版本可能具有截然不同的大小和/或读取方向,开发人员应使用“自动布局”在情节提要文件中定位和调整应用的用户界面。

Apple 建议执行以下操作:

  • 删除固定宽度约束 - 应允许所有基于文本的视图根据其内容调整大小。 固定宽度视图可能采用特定语言裁剪其内容。
  • 使用固有内容大小 - 默认情况下,基于文本的视图将自动调整大小以适应其内容。 对于未正确调整大小的基于文本的视图,请在 Xcode 的 Interface Builder 中选择它们,然后选择“编辑”>“调整大小以适应内容”。
  • 应用前导和尾随属性 - 由于文本的方向可以根据用户的语言更改,因此请使用新的 LeadingTrailing 约束属性,而不是现有的 RightLeft 属性。 LeadingTrailing 将根据语言方向自动调整。
  • 将视图固定到相邻视图 - 这允许视图在周围的视图发生更改时重新定位和调整大小,以响应所选语言。
  • 不设置窗口最小和/或最大大小 - 允许窗口更改大小,因为所选语言会调整其内容区域的大小。
  • 持续测试布局更改 - 在应用开发期间,应以不同的语言不断测试。 有关更多详细信息,请参阅 Apple 的测试国际化应用文档。
  • 使用 NSStackViews 将视图固定在一起 - NSStackViews 允许其内容以可预测的方式移动和增长,并根据所选语言更改内容大小。

在 Xcode 的 Interface Builder 中本地化

Apple 在 Xcode 的 Interface Builder 中提供了多个功能,开发人员在设计或编辑应用的 UI 以支持本地化时可以使用这些功能。 “属性检查器”的“文本方向”部分允许开发人员提供有关如何在所选基于文本的视图(如 NSTextField)上使用和更新方向的提示:

The Text Direction options

文本方向”有三个可能的值:

  • 自然 - 布局基于分配给控件的字符串。
  • 从左到右 - 布局始终强制为从左到右。
  • 从右到左 - 布局始终强制为从右到左。

布局”有两个可能的值:

  • 从左到右 - 布局始终从左到右。
  • 从右到左 - 布局始终从右到左。

通常,除非需要特定的对齐方式,否则不应更改这些内容。

Mirror 属性指示系统翻转特定控件属性(如单元格图像位置)。 它具有三个可能值:

  • 自动 - 位置将根据所选语言的方向自动更改。
  • 在从右到左界面中 - 位置将仅以从右到左的语言更改。
  • 从不 - 位置永远不会改变。

如果开发人员对基于文本的视图的内容指定了“居中”、“两端对齐”或“完整”对齐方式,则永远不会根据所选语言翻转这些内容。

在 macOS Sierra 之前,在代码中创建的控件不会自动镜像。 开发人员必须使用如下所示的代码来处理镜像:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    // Setting a button's mirroring based on the layout direction
    var button = new NSButton ();
    if (button.UserInterfaceLayoutDirection == NSUserInterfaceLayoutDirection.LeftToRight) {
        button.Alignment = NSTextAlignment.Right;
        button.ImagePosition = NSCellImagePosition.ImageLeft;
    } else {
        button.Alignment = NSTextAlignment.Left;
        button.ImagePosition = NSCellImagePosition.ImageRight;
    }
}

其中,AlignmentImagePosition 是基于控件的 UserInterfaceLayoutDirection 设置的。

macOS Sierra 添加了多个新的便利构造函数(通过静态 CreateButton 方法),这些构造函数采用多个参数(如 Title、Image 和 Action),并将自动正确镜像。 例如:

var button2 = NSButton.CreateButton (myTitle, myImage, () => {
    // Take action when the button is pressed
    ...
});

使用“系统外观”

新式 macOS 应用可以采用适用于图像创建、编辑或演示文稿应用的新深色界面外观:

An example of a dark Mac Window UI

这可以通过在显示窗口之前添加一行代码来完成。 例如:

using System;
using AppKit;
using Foundation;

namespace MacModern
{
    public partial class ViewController : NSViewController
    {
        ...

        #region Override Methods
        public override void ViewWillAppear ()
        {
            base.ViewWillAppear ();

            // Apply the Dark Interface Appearance
            View.Window.Appearance = NSAppearance.GetAppearance (NSAppearance.NameVibrantDark);

            ...
        }
        #endregion
    }
}

NSAppearance 类的静态 GetAppearance 方法用于从系统获取命名外观(在本例中为 NSAppearance.NameVibrantDark)。

Apple 对使用“系统外观”有以下建议:

  • 首选命名颜色而不是硬编码值(如 LabelColorSelectedControlColor)。
  • 尽可能使用系统标准控件样式。

使用“系统外观”的 macOS 应用将自动为已从“系统首选项”应用启用辅助功能的用户正确工作。 因此,Apple 建议开发人员始终在其 macOS 应用中使用“系统外观”。

使用情节提要设计 UI

情节提要允许开发人员不仅设计构成应用用户界面的各个元素,还可以可视化和设计给定元素的 UI 流和层次结构。

控制器允许开发人员将元素收集到一个组成单元中,Segue 会提取并删除在整个视图层次结构中移动所需的典型“胶水代码”:

Editing the UI in Xcode's Interface Builder

有关详细信息,请参阅我们的情节提要简介文档。

在许多情况下,情节提要中定义的给定场景将需要视图层次结构中先前场景的数据。 对于在场景之间传递信息,Apple 有以下建议:

  • 数据依赖项应始终通过层次结构向下级联。
  • 避免对 UI 结构依赖项进行硬编码,因为这会限制 UI 的灵活性。
  • 使用 C# 接口提供泛型数据依赖项。

充当 Segue 源的视图控制器可以替代 PrepareForSegue 方法,并在执行 Segue 以显示目标视图控制器之前执行所需的任何初始化(例如传递数据)。 例如:

public override void PrepareForSegue (NSStoryboardSegue segue, NSObject sender)
{
    base.PrepareForSegue (segue, sender);

    // Take action based on Segue ID
    switch (segue.Identifier) {
    case "MyNamedSegue":
        // Prepare for the segue to happen
        ...
        break;
    }
}

有关详细信息,请参阅我们的 Segue 文档。

传播操作

根据 macOS 应用的设计,有时,UI 控件上操作的最佳处理程序可能位于 UI 层次结构中的其他位置。 这通常适用于位于其自己的场景中的菜单和菜单项,独立于应用的 UI 的其余部分。

若要处理这种情况,开发人员可以创建一个自定义操作并将操作传递到响应方链。 有关详细信息,请参阅我们的使用自定义窗口操作文档。

新式 Mac 功能

Apple 在 macOS Sierra 中包括了多个面向用户的功能,使开发人员能够充分利用 Mac 平台,例如:

  • NSUserActivity - 这允许应用描述用户当前参与的活动。 NSUserActivity 最初是为了支持 HandOff 而创建的,其中,在用户的一个设备上启动的活动可以被拾取并在另一台设备上继续。 NSUserActivity 在 macOS 中的工作方式与在 iOS 中相同,因此请参阅我们的 Handoff 简介 iOS 文档以了解更多详细信息。
  • Mac 上的 Siri - Siri 使用当前活动 (NSUserActivity) 为用户可以发出的命令提供上下文。
  • 状态还原 - 当用户在 macOS 上退出应用后再重新启动应用时,应用将自动返回到其以前的状态。 开发人员可以使用状态还原 API 在向用户显示用户界面之前对暂时性 UI 状态进行编码和还原。 如果应用基于 NSDocument,则会自动处理状态还原。 若要为非基于 NSDocument 的应用启用状态还原,请将 NSWindow 类的 Restorable 设置为 true
  • 云中的文档 - 在 macOS Sierra 之前,应用必须显式选择使用用户的 iCloud 云盘中的文档。 在 macOS Sierra 中,用户的 DesktopDocuments 文件夹可由系统自动与其 iCloud 云盘同步。 因此,可能会删除文档的本地副本,以释放用户计算机上的空间。 基于 NSDocument 的应用将自动处理此更改。 所有其他应用类型都需要使用 NSFileCoordinator 来同步文档的读取和写入。

总结

本文介绍了开发人员可以用来在 Xamarin.Mac 中生成新式 macOS 应用的几个提示、功能和技术。