次の方法で共有


最新の macOS アプリの構築

この記事では、Xamarin.Mac で最新の macOS アプリを構築するために開発者が使用できるヒント、機能、手法について説明します。

モダン ビューを使用した最新の外観のビルド

最新の外観には、次に示すアプリの例のような最新のウィンドウとツール バーの外観が含まれます。

最新の Mac アプリ 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 で有効にすることもできます。

Xcode の Interface Builder でのメイン ストーリーボードの編集

フルサイズのコンテンツ ビューを使用する場合、開発者は、特定のコンテンツ (ラベルなど) がタイトル領域とツール バー領域の下に入り込まないように、コンテンツをオフセットする必要がある場合があります。

この問題を複雑にするために、タイトルとツール バーの領域は、ユーザーが現在実行しているアクション、ユーザーがインストールした macOS のバージョン、アプリが実行されている Mac ハードウェアに基づいて、高さを動的にすることができます。

その結果、ユーザー インターフェイスをレイアウトする場合に、オフセットをハード コーディングするだけでは機能しません。 開発者は動的なアプローチを行う必要があります。

Apple には、NSWindow クラスの [監視可能なキー値]ContentLayoutRect プロパティに、コードで現在のコンテンツ領域を取得する機能が含まれています。 開発者はこの値を使用して、コンテンツ領域が変更された場合に必要な要素を手動で配置できます。

より良い解決策は、自動レイアウト クラスとサイズ クラスを使用して、UI 要素をコードまたは Interface Builder に配置することです。

次の例のようなコードを使用して、アプリのビュー コントローラーで自動レイアウト クラスとサイズ クラスを使用して 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 ウィンドウには、ウィンドウの上端に沿って実行される標準タイトル バーがあります。 ウィンドウにツール バーもある場合は、次のタイトル バー領域の下に表示されます。

標準 Mac ツール バー

効率化されたツール バーを使用すると、タイトル領域が消え、ツールバーはウィンドウの [閉じる]、[最小化]、[最大化]のそれぞれのボタンに合わせてタイトル バーの位置に移動します。

合理化された Mac ツール バー

効率化されたツール バーは、NSViewControllerViewWillAppear メソッドをオーバーライドし、次のようにすることで有効になります。

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

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

この効果は通常、マップ、カレンダー、メモ、システム環境設定のような [シューボックス アプリケーション] (1 つのウィンドウ アプリ) に使用されます。

アクセサリ ビュー コントローラーの使用

アプリのデザインによっては、開発者は、タイトル領域またはツール バー領域のすぐ下に表示されるアクセサリ ビュー コントローラーでタイトル バー領域を補完し、ユーザーが現在関与しているアクティビティに基づいた状況依存のコントロールをユーザーに提供する必要がある場合があります。

アクセサリ ビュー コントローラーの例

アクセサリ ビュー コントローラーは、開発者が介入しなくても、システムによって自動的にぼかされ、サイズ変更されます。

アクセサリ ビュー コントローラーを追加するには、次の操作を行います。

  1. ソリューション エクスプローラーMain.storyboard ファイルをダブルクリックして、編集用に開きます。

  2. カスタム ビュー コントローラーをウィンドウの階層にドラッグします。

    新しいカスタム ビュー コントローラーの追加

  3. アクセサリ ビューの UI をレイアウトします。

    新しいビューのデザイン

  4. アクセサリ ビューを [アウトレット] として公開し、その UI のその他の [アクション] または [アウトレット] として公開します。

    必要な 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 は完全にローカライズされるようになったため、Left プロパティと RightNSLayoutAttribute プロパティは非推奨になり、LeadingTrailing に置き換える必要があります。

タブ付きウィンドウの使用

さらに、macOS システムでは、アクセサリ ビュー コントローラーがアプリのウィンドウに追加される場合があります。 たとえば、複数のアプリのウィンドウを 1 つの仮想ウィンドウにマージするタブ付きのウィンドウを作成するには、次の操作を行います。

タブ付き Mac ウィンドウの例

通常、開発者は、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 に組み込まれている高性能なグラフィックス レンダリング エンジンです。 Core Animation は、コンピューターの速度を低下させる可能性がある CPU でグラフィックス操作を実行するのではなく、最新の macOS ハードウェアで利用できる GPU (グラフィックス処理装置) を利用するように最適化されています。

Core Animation によって提供される CALayer は、高速で滑らかなスクロールやアニメーションなどのタスクに使用できます。 Core Animation を十分に活用するには、アプリのユーザー インターフェイスを複数のサブビューとレイヤーで構成する必要があります。

CALayer オブジェクトには、開発者がユーザーに対して画面上に表示される内容を制御できるように、次のようなプロパティがいくつか用意されています。

  • Content - レイヤーの内容を提供する NSImage または CGImage です。
  • BackgroundColor - レイヤーの背景色を CGColor に設定します
  • BorderWidth - 境界線の幅を設定します。
  • BorderColor - 境界線の色を設定します。

アプリの UI でコア グラフィックスを利用するには、レイヤーに基づくビューを使用する必要があります。Apple は、開発者が常にウィンドウのコンテンツ ビューで有効にする必要があることを提案します。 これにより、すべての子ビューでレイヤー バッキングも自動的に継承されます。

さらに、Apple は、サブレイヤーとして新しい CALayer を追加するのではなく、レイヤーに基づくビューを使用することを提案します。これは、システムが必要な設定 (Retina ディスプレイで必要な設定など) を自動的に処理するためです。

NSViewWantsLayertrue に設定するか、Xcode の Interface Builder の [ Effects Inspector の表示]Core Animation レイヤーをチェックすることで、レイヤー バッキングを有効にできます。

[効果の表示] インスペクター

レイヤーを使用したビューの再描画

Xamarin.Mac アプリでレイヤーに基づくビューを使用する場合のもう 1 つの重要な手順は、NSViewLayerContentsRedrawPolicyNSViewControllerOnSetNeedsDisplay に設定することです。 次に例を示します。

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

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

開発者がこのプロパティを設定しない場合、ビューはフレームの原点が変更されるたびに再描画されます。これはパフォーマンス上の理由から望ましくありません。 ただし、このプロパティが OnSetNeedsDisplay に設定されている場合、コンテンツを強制的に再描画するには、開発者が手動で NeedsDisplaytrue に設定する必要があります。

ビューがダーティとしてマークされている場合、システムはビューの WantsUpdateLayer プロパティをチェックします。 true が返された場合は UpdateLayer メソッドが呼び出され、それ以外の場合はビューの DrawRect メソッドが呼び出され、ビューの内容が更新されます。

Apple には、必要に応じてビューの内容を更新するための次のような推奨事項があります。

  • Apple はパフォーマンスを大幅に向上させるため、可能な限り DrawRect よりも UpdateLater を優先的に使用することをお勧めします。
  • 似た外観の 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
    }
}

最新のドラッグ アンド ドロップの使用

最新のドラッグ アンド ドロップ エクスペリエンスをユーザーに提供するには、開発者はアプリのドラッグ アンド ドロップ操作でドラッグ フロックを採用する必要があります。 ドラッグ フロックとは、ドラッグする個々のファイルや項目が、最初は個々の要素として表示され、ユーザーがドラッグ操作を続けると、フロック (項目数をカウントしてカーソルの下にグループ化されること) になることです。

ユーザーがドラッグ操作を終了すると、個々の要素がロック解除され、元の場所に戻ります。

次のコード例では、カスタム ビューでドラッグ フロックを有効にしています。

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 メソッドにドラッグされる各項目を送信することによって達成されました。

NSTableView または NSOutlineView を操作する場合は、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
    }
}

これにより、すべての行を単一グループとしてペーストボードに書き込む古いメソッドの WriteRowsWith とは対照的に、開発者がドラッグされるテーブルのすべての項目に個別の NSDraggingItem を提供できるようになります。

NSCollectionViews を操作する場合、ドラッグを開始したら WriteItemsAt メソッドではなく PasteboardWriterForItemAt メソッドをもう一度使用します。

開発者は常に、大きなファイルをペースボードに配置しないようにする必要があります。 macOS Sierra の新機能である File Promises により、開発者は新しい NSFilePromiseProvider クラスと NSFilePromiseReceiver クラスを使用して、ユーザーがドロップ操作を完了したときに実行される、特定のファイルへの参照をペーストボードに配置できます。

最新のイベント追跡の使用

タイトル領域やツール バー領域に追加されたユーザー インターフェイス要素 (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 メソッドを使用して LeftMouseUp イベントと LeftMouseDragged イベントを受け取ります。 LeftMouseUp イベントの場合、UI 要素は通常どおり応答します。 LeftMouseDragged イベントの場合、イベントは NSWindowPerformWindowDrag メソッドに渡され、画面上でウィンドウを移動します。

NSWindow クラスの PerformWindowDrag メソッドを呼び出すと、次のような利点があります。

  • これにより、アプリが応答停止していても (ディープ ループを処理している場合など)、ウィンドウを移動させることができます。
  • スペースの切り替えは想定どおりに機能します。
  • スペース バーは通常どおりに表示されます。
  • ウィンドウのスナップと配置は通常どおりに機能します。

最新のコンテナー ビュー コントロールの使用

macOS Sierra は、以前のバージョンの OS で使用できる既存のコンテナー ビュー コントロールに多くの最新の機能強化を提供します。

テーブル ビューの機能強化

開発者は常に、NSTableView などのコンテナー ビュー コントロールの新しい NSView ベースのバージョンを使用する必要があります。 次に例を示します。

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 クラスに次の 2 つの新しいプロパティを追加しました。

  • ContentInsets - スクロール ビューの上部に適用されるオフセットを定義する NSEdgeInsets オブジェクトを開発者が提供できるようにします。
  • AutomaticallyAdjustsContentInsets- true の場合、スクロール ビューは開発者のために自動的に ContentInsets を処理します。

ContentInsets を使用することで、開発者はスクロール ビューの開始位置を調整し、次のようなアクセサリを含めることができます。

  • メール アプリに表示されているような並べ替えインジケーター。
  • 検索フィールド。
  • [最新の情報に更新] または [更新] ボタン。

モダン アプリでの自動レイアウトとローカライズ

Apple は、開発者が国際化された macOS アプリを簡単に作成できるように、Xcode にいくつかのテクノロジを搭載しています。 Xcode を使用することで、開発者はストーリーボード ファイルのアプリのユーザー インターフェイス デザインからユーザー向けのテキストを分離できるようになり、UI が変更された場合にこの分離を維持するためのツールが用意されています。

詳細については、Apple の国際化とローカライズに関するガイドを参照してください。

基本的な国際化の実装

基本的な国際化を実装することで、開発者はアプリの UI を表す単一のストーリーボード ファイルを提供し、すべてのユーザー向け文字列を分離できます。

開発者がアプリのユーザー インターフェイスを定義する最初のストーリーボード ファイルを作成すると、基本的な国際化 (開発者が話す言語) でビルドされます。

次に、開発者は、複数の言語に翻訳可能なローカライズと基本的な国際化文字列 (ストーリーボード UIデザイン内) をエクスポートできます。

これらのローカライズは後でインポートできるようになり、Xcode でストーリーボード用の言語固有の文字列ファイルが生成されます。

ローカライズをサポートするための自動レイアウトの実装

ローカライズされたバージョンの文字列値はサイズや読み取り方向が大きく異なる可能性があるため、開発者はストーリーボード ファイルにアプリのユーザー インターフェイスを配置し、サイズを変更するために自動レイアウトを使用する必要があります。

Apple では、次の操作を行うことをお勧めします。

  • 固定幅の制約の削除 - すべてのテキストベースのビューは、そのコンテンツに基づいてサイズを変更できるようにする必要があります。 固定幅ビューでは、特定の言語でコンテンツがトリミングされる場合があります。
  • 組み込みコンテンツ サイズの使用 - 既定では、テキストベースのビューはコンテンツに合わせて自動サイズ設定されます。 テキストベースのビューのサイズが正しく設定されていない場合は、Xcode の Interface Builder でそれらを選択し、[編集]>[コンテンツに合わせたサイズの自動調整] を選択します。
  • 先頭および末尾の属性の適用 - テキストの方向はユーザーの言語に基づいて変わる可能性があるため、既存の Right 属性と Left 属性ではなく、新しい Leading 制約属性と Trailing 制約属性を使用してください。 LeadingTrailing は言語の方向に基づいて自動的に調整されます。
  • 隣接するビューにビューをピン留めする - これにより、選択した言語に応じて周囲のビューが変更されると、ビューの位置やサイズが変更されます。
  • Windows の最小/最大サイズを設定しない - 選択した言語のコンテンツ領域のサイズ変更に合わせてウィンドウのサイズを変更できるようにします。
  • 継続的にレイアウト変更をテストする - 開発中、アプリは常に異なる言語でテストする必要があります。 詳細については、Apple の国際化されたアプリのテストに関するドキュメントを参照してください。
  • NSStackViews を使用してビューをピン留めする - NSStackViewsことで、コンテンツを予測可能な方法でシフトおよび拡大でき、選択した言語に基づいてコンテンツのサイズを変更できます。

Xcode の Interface Builder でのローカライズ

Apple は、Xcode の Interface Builder で開発者がローカライズをサポートするためにアプリの UI を設計または編集する場合に使用できるいくつかの機能を提供しています。 Attribute Inspector[テキストの方向] セクションでは、選択されたテキストベースのビュー (NSTextField など) で方向を使用し、更新する方法に関するヒントを開発者が提供できるようにします。

[テキストの方向] オプション

テキストの方向には、次の 3 つの値が考えられます。

  • 自然 - レイアウトは、コントロールに割り当てられた文字列に基づいています。
  • 左から右 - レイアウトは常に左から右に強制されます。
  • 右から左 - レイアウトは常に右から左に強制されます。

レイアウトには、次の 2 つの値が考えられます。

  • 左から右 - レイアウトは常に左から右です。
  • 右から左 - レイアウトは常に右から左です。

通常、特定の配置が要求されない限り、これらの設定を変更する必要はありません。

ミラー プロパティは、特定のコントロール プロパティ (セル イメージの位置など) を反転させるようシステムに指示します。 次の 3 つの値を指定できます。

  • 自動 - 位置は、選択した言語の方向に基づいて自動的に変更されます。
  • 右から左へのインターフェイス - 位置は、右から左に基づく言語でのみ変更されます。
  • なし - 位置は変更されません。

開発者がテキストベースのビューのコンテンツに [中央揃え][両端揃え][全画面] の配置を指定した場合、選択された言語に基づいてこれらが反転されることはありません。

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 では、いくつかのパラメーター (Title、Image、Action など) を受け取り、自動的に正しくミラー化する新しい便利なコンストラクター (静的 CreateButton メソッド経由) がいくつか追加されています。 次に例を示します。

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

システム外観の使用

最新の macOS アプリでは、イメージの作成、編集、またはプレゼンテーション アプリに適した新しいダーク インターフェイスの外観を採用できます。

ダーク Mac ウィンドウ UI の例

これは、ウィンドウが表示される前に 1 行のコードを追加することでできます。 次に例を示します。

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 フローと階層を視覚化して設計できます。

コントローラーを使用すると、開発者は要素を 1 つの構成単位に収集できるようになり、セグエはビュー階層全体を移動するために必要な一般的な "接着コード" を抽象化して削除できるようになります。

Xcode の Interface Builder での UI の編集

詳細については、ストーリーボードの概要に関するドキュメントを参照してください。

ストーリーボードで定義された特定のシーンがビュー階層の前のシーンのデータを必要とする場合が多くあります。 Apple は、シーン間で情報を受け渡しするために、次のような提案をしています。

  • データの依存関係は、常に階層を下方向にカスケードする必要があります。
  • UI の柔軟性が制限されるため、UI 構造の依存関係はハードコーディングしないでください。
  • C# インターフェイスを使用して、汎用データの依存関係を提供します。

セグエのソースとして機能するビュー コントローラーは、PrepareForSegue メソッドをオーバーライドし、セグエを実行する前に必要な初期化 (データの受け渡しなど) を行い、ターゲット ビュー コントローラーを表示することができます。 次に例を示します。

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;
    }
}

詳細については、Microsoft のセグエのドキュメントを参照してください。

アクションの伝達

macOS アプリの設計に基づいて、UI コントロールのアクションに最適なハンドラーが UI 階層の他の場所にある場合があります。 これは通常、アプリの UI の残りの部分とは別に、独自のシーンに存在するメニューとメニュー項目に当てはまります。

この状況に対処するために、開発者はカスタム アクションを作成し、アクションを応答側のチェーンに渡すことができます。 詳細については、カスタム ウィンドウ アクションの操作に関するドキュメントを参照してください。

Mac の最新機能

Apple は、開発者が Mac プラットフォームを最大限に活用できるよう、macOS Sierra に次のようなユーザー向け機能を導入しました。

  • NSUserActivity - これにより、アプリはユーザーが現在関与しているアクティビティを記述できます。 NSUserActivity は当初、HandOff をサポートするために作成され、ユーザーのデバイスのいずれかで開始されたアクティビティを別のデバイスで取得して続行することができました。 NSUserActivity は、macOS でも iOS の場合と同じように動作します。詳細については、ハンドオフの概要に関する iOS のドキュメントを参照してください。
  • Mac の Siri - Siri は、現在のアクティビティ (NSUserActivity) を使用して、ユーザーが発行できるコマンドにコンテキストを提供します。
  • 状態の復元 - macOS でユーザーがアプリを終了し、後で再起動すると、アプリは自動的に以前の状態に戻ります。 開発者は、状態の復元 API を使用して、ユーザー インターフェイスがユーザーに表示される前に、一時的な UI 状態をエンコードして復元できます。 アプリが NSDocument ベースの場合、状態の復元は自動的に処理されます。 NSDocument ベースでないアプリで状態の復元を有効にするには、NSWindow クラスの Restorabletrue に設定します。
  • クラウド上のドキュメント - macOS Sierra 以前では、アプリケーションはユーザーの iCloud Drive 内のドキュメントを操作することを明示的にオプトインする必要がありました。 macOS Sierra では、ユーザーの [デスクトップ] フォルダーと [ドキュメント] フォルダーは、システムによって自動的に iCloud Drive と同期される場合があります。 その結果、ユーザーのコンピューターの空き領域を確保するために、ドキュメントのローカル コピーが削除される場合があります。 NSDocument ベースのアプリは、この変更を自動的に処理します。 その他のアプリは、NSFileCoordinator を使用してドキュメントの読み取り/書き込みを同期する必要があります。

まとめ

この記事では、Xamarin.Mac で最新の macOS アプリを構築するために開発者が使用できるヒント、機能、手法について説明しました。