Xamarin.iOS 中的统一情节提要

iOS 8 新增了一个用于创建用户界面的机制——统一的情节提要,使用更简便。 有了统一的情节提要来覆盖所有不同的硬件屏幕大小,就可以以“一次设计,多次使用”的方式创建快速且响应迅速的视图。

开发人员不需要再为 iPhone 和 iPad 设备单独创建特定的情节提要,而是可以使用通用接口灵活地设计应用,然后再针对不同的大小类自定义该接口。 这样就可以根据每种外形规格的优势调整应用,优化每个用户界面以提供最佳体验。

大小类

在 iOS 8 之前,开发人员使用 UIInterfaceOrientationUIInterfaceIdiom 来区分横屏和竖屏模式,区分 iPhone 和 iPad 设备。 在 iOS8 中,使用“大小类”来确定方向和设备。

设备由“大小类”在纵轴和横轴两个方向上定义,iOS 8 中有两种大小类:

  • 常规 - 这是针对大屏幕(如 iPad)或小工具,它给人留下大尺寸(如 UIScrollView)的印象
  • 小巧 - 适用于较小的设备(如 iPhone)。 此大小会考虑设备的方向。

如果两个概念一起使用,则结果是一个 2 x 2 网格,该网格定义可用于两种不同方向的不同可能大小,如下图所示:

A 2 x 2 grid that defines the different possible sizes that can be used in Regular and Compact orientations

开发人员可以创建视图控制器,该控制器使用四种可能性中的任意一种生成不同布局(如上图所示)。

iPad 大小类

由于屏幕大小,iPad 在横竖两个方向上都具有常规类大小。

iPad Size Classes

iPhone 大小类

iPhone 根据设备的方向具有不同的大小类:

iPhone Size Classes

  • 当设备处于竖屏模式下,屏幕横向上具有小巧类,竖向上具有常规
  • 当设备处于横屏模式下,屏幕在横竖两个方向上的类与竖屏模式下的相反。

iPhone 6 Plus 大小类

当处于竖屏时,大小与更早些的 iPhone 相同,但在横屏时不同:

iPhone 6 Plus Size Classes

因为 iPhone 6 Plus 的屏幕够大,在横屏模式下能够具有常规宽度大小类。

支持新屏幕缩放

iPhone 6 Plus 使用全新的 Retina HD 显示,屏幕缩放因子为 3.0(是初代 iPhone 屏幕分辨率的三倍)。 若要在这些设备上提供最佳体验,请包含为这一屏幕缩放设计的新图稿。 在 Xcode 6 及更高版本中,资产目录可包含 1 倍、2 倍和 3 倍大小的图像;只需添加新的图像资产,在 iPhone 6 Plus 运行时,iOS 便会选择正确的资产。

iOS 中的图像加载行为还能识别图像文件上的 @3x 后缀。 例如,如果开发人员在应用程序包中包含某个图像资产(采用的分辨率),且包含以下文件名:MonkeyIcon.pngMonkeyIcon@2x.pngMonkeyIcon@3x.png。 在 iPhone 6 Plus 上,如果开发人员使用以下代码加载某个图像,则系统会自动使用 MonkeyIcon@3x.png 图像:

UIImage icon = UIImage.FromFile("MonkeyImage.png");

动态启动屏幕

启动屏幕文件显示为初始屏幕,iOS 应用将会启动,向用户提供反馈说应用实际上正在启动。 在 iOS 8 之前,开发人员必须包含多个 Default.png 图像资产以供应用可能会在其上运行的每种设备类型、方向和屏幕分辨率。

iOS 8 中一项新增功能是,开始人员可以在 Xcode 中使用单个的原子 .xib 文件,它使用自动布局和大小类来为每个设备、分辨率和方向创建可使用的动态启动屏幕。 此功能不仅能减少开发人员创建和维护所有需要图像资产所需的工作量,还能为应用的安全程序包“瘦身”。

特征

特征是可用于确定布局如何随其所在环境更改而更改的属性。 它们能一组属性(基于 UIUserInterfaceSizeClassHorizontalSizeClassVerticalSizeClass),以及接口习语(UIUserInterfaceIdiom)和显示缩放所组成。

上述所有状态都包装在 Apple 称作“特征集合”(UITraitCollection)的容器里,其中不仅包含属性,还有这些属性的值。

特征环境

特征环境是 iOS 8 中新增的一个接口,能够返回以下对象的特征集合:

  • 屏幕(UIScreens)。
  • 窗口(UIWindows)。
  • 视图控制器(UIViewController)。
  • 视图(UIView)。
  • 演示文稿控制器(UIPresentationController)。

开发人员使用特征环境返回的特征集合来确定用户界面应该如何布局。

所有特征环境的层次结构都如下图所示:

The Trait Environments hierarchy diagram

上述每种特征环境具有特征集合默认会从父环境流向子环境。

除了获取当前的特征集合以外,特征环境还具有 TraitCollectionDidChange 方法,可以在视图或视图控制器子类中重写此方法。 当这些特征发生更改时,开发人员可以使用此方法根据特征修改 UI 元素中的任意元素。

典型特征集合

本部分将介绍用户在使用 iOS 8 时将体验的典型特征集合类型。

下面是开发人员在 iPhone 上可能看到的典型特征集合:

属性 Value
HorizontalSizeClass 精简
VerticalSizeClass 常规
UserInterfaceIdom 手机
DisplayScale 2.0

上面的集合表示完全限定的特征集合,因为它具有其所有特征属性的值。

特征集合的其中一些值也有可能缺失(Apple 称作“未指定”):

属性 Value
HorizontalSizeClass 精简
VerticalSizeClass 未指定
UserInterfaceIdom 未指定
DisplayScale 未指定

但通常,当开发人员向特征环境询问其特征集合时,它会返回完全限定的集合,如上面的示例所示。

如果特征环境(如视图或视图控制器)不在当前视图层次结构内,一个或多个特征属性可能会向开发人员返回未指定的值。

如果开发人员使用 Apple 提供的其中一种创建方法(如 UITraitCollection.FromHorizontalSizeClass)来创建新的集合,则也会获取部分限定的特征集合。

对多个特征集合可以执行的一项操作是将它们互相进行比较,该操作涉及询问一个特征集合是否包含另一个。 “包含”的含义是,对于第二个集合中指定的任何特征,其值必须与第一个集合中的值完全匹配。

若要测试两个特征,请使用 Contains 方法,即 UITraitCollection 传递要测试的特征的值。

开发人员可以在代码中手动执行比较,以确定如何布局视图或视图控制器。 但是,UIKit 在内部使用此方法提供它的其中一些功能,例如在外观代理中。

外观代理

较早版本的 iOS 中已引入了外观代理,以允许开发人员自定义其视图的属性。 它在 iOS 8 中得到扩展以支持特征集合。

外观代理现在包含新的方法 AppearanceForTraitCollection,该方法为传入的给定特征集合返回新的外观代理。 开发人员对此外观代理进行的任何自定义都只会在遵从指定的特征集合的视图上生效。

通常,开发人员会将部分指定的特征集合传入 AppearanceForTraitCollection 方法(例如,刚刚指定横向大小类为“小巧”的特征集合),以便可在横向为小巧的应用中自定义任何视图。

UIImage

Apple 为其添加特征集合的另一个类是 UIImage。 过去,开发人员必须指定将会包含在应用(如图标)中的任何已设置位图的图形资产的 @1X 和 @2x 版本。

iOS 8 对功能进行了扩展,现在允许开发人员在基于特征集合的图像目录中包含某个图像的多个版本。 例如,开发人员可以包含一张较小的图像供小巧特征类使用;任何其他集合则使用完整大小的图像。

UIImageView 类中使用其中一个图像时,图像视图会自动为其特征集合显示正确版本的图像。 如果特征环境更改(例如,用户将设备从竖屏切换为横屏),则图像视图会自动选择新的图像大小以匹配新的特征集合,并更改其大小以匹配正在显示的图像的当前版本的大小。

UIImageAsset

Apple 在 iOS 8 中新增了名为“UIImageAsset”的一个新类,为开发人员在图像选择上提供更大的控制权。

图像资产包装图像所有的不同版本,允许开发人员询问匹配传入的特征集合的特定图像。 可在不中断运行的情况下,向图像资产添加或从其中删除图像。

有关图像资产的更多信息,请参阅 Apple 的 UIImageAsset 文档。

组合多个特征集合

开发人员可以对特征集合执行的另一项操作是一次添加两个特征集合,此操作会让两个集合组合在一起,其中一个集合中未指定的值替代为另一个集合中指定的值。 使用 UITraitCollection 类的 FromTraitsFromCollections 方法完成此操作。

如上所述,如果其中一个特征集合中有任意特征未指定,而另一个集合中为已指定,则会将相应值设置为指定的版本。 但是,如果给定的指定值有多个版本,则最近一个特征集合中的值将会是其使用的值。

自适应视图控制器

本部分将介绍 iOS 视图和视图控制器如何运用特征和大小类的概念,使其在开发人员的应用中自动自适应的能力更强。

拆分视图控制器

iOS 8 中变化最大的视图控制器类之一就是 UISplitViewController 类。 过去,开发人员通常会在应用的 iPad 版本上使用“拆分视图控制器”,然后必须为应用的 iPhone 版本的视图层次结构提供完全不同的版本。

在 iOS 8 中,iPad 和 iPhone 两个平台上均可以使用 UISplitViewController 类,它允许开发人员创建一个视图控制器层次结构,即可在 iPhone 和 iPad 上同时使用。

当 iPhone 处于横屏时,拆分视图控制器会并排显示其视图,与在 iPad 上的显示方式相同。

重写特征

如下图所示(iPad 处于横屏方向时的拆分视图控制器),特征环境从父容器向下级联到子容器:

A Split View Controller on an iPad in the landscape orientation

由于 iPad 在横向和竖向两个方向上都具有常规大小类,因此,拆分视图会同时显示主视图和详细信息视图。

在横竖两个方向的大小类均为“小巧”的 iPhone 上,拆分视图控制器则仅显示详细信息视图,如下图所示:

The Split View Controller only displays the detail view

如果开发人员希望在 iPhone 处于横屏方向时,在应用中同时显示主视图和详细信息视图,则必须为拆分视图控制器插入一个父容器并重写特征集合。 如下图所示:

The developer must insert a parent container for the Split View Controller and override the Trait Collection

UIView 设置为拆分视图控制器的父级,在视图上调用 SetOverrideTraitCollection 方法传入新的特征集合并以拆分视图控制器作为目标。 新的特征集合重写 HorizontalSizeClass,将其设置为 Regular,以便拆分视图控制器能够在 iPhone 处于横屏方向时同时显示主视图和详细信息视图。

请注意,VerticalSizeClass 设置为 unspecified,则允许将新的特征集合添加到父级的特征集合,导致子级拆分视图控制器的是 Compact VerticalSizeClass

特征更改

本部分将详细介绍特征集合在特征环境发生更改时如何转换。 例如,当设备从竖屏旋转为横屏时。

The portrait to landscape Trait Changes overview

首先,iOS 8 进行一些设置,为即将发生的转换做好准备。 接下来,系统为生成转换状态的动画。 最后,iOS 8 清理转换期间所需的任何临时状态。

iOS 8 提供多个回叫供开发人员用于参与特征更改,如下表所示:

阶段 回调 说明
安装
  • WillTransitionToTraitCollection
  • TraitCollectionDidChange
  • 在将特征集合设置为其新的值之前,在特征更改开始时调用此方法。
  • 当特征集合已更改,但在任何动画还没发生之前调用此方法。
动画 WillTransitionToTraitCollection 传递给此方法的特征协调器具有 AnimateAlongside 属性,该属性允许开发人员添加会与默认动画一起执行的动画。
清理 WillTransitionToTraitCollection 为开发人员提供一种方法,在转换发生之后包含其自己的清理代码。

WillTransitionToTraitCollection 方法非常适合生成视图控制器以及特征集合更改的动画。 仅在视图控制器(UIViewController)上提供 WillTransitionToTraitCollection 方法,UIViews 等其他特征环境上不提供。

TraitCollectionDidChange 非常适合与 UIView 类一起使用,其中开发人员希望特征发生更改时更新 UI。

折叠拆分视图控制器

接下来,让我们来看看当拆分视图控制器从两列折叠为一列视图时会发生什么。 作为此更改的一部分,需要执行以下两个过程:

  • 默认情况下,拆分视图控制器将在折叠发生后使用主视图控制器作为视图。 开发人员可通过重写 UISplitViewControllerDelegateGetPrimaryViewControllerForCollapsingSplitViewController 方法,并提供希望在折叠状态下显示的任何视图控制器来重写此行为。
  • 辅助视图控制器必须合并到主视图控制器中。 在这一步,开发人员通常并不需要采取任何操作;拆分视图控制器包含自动根据硬件设备处理此阶段。 但在某些情况下,开发人员可能需要与此更改进行交互。 调用 UISplitViewControllerDelegateCollapseSecondViewController 方法允许发生折叠时显示的是主视图控制器,而不是详细信息视图。

展开拆分视图控制器

接下来,让我们来详细了解当拆分视图控制器由折叠状态变为展开时会发生什么。 同样需要执行以下两个阶段:

  • 首先,定义新的主视图控制器。 默认情况下,拆分视图控制器将自动使用折叠视图中的主视图控制器。 开发人员同样可以使用 UISplitViewControllerDelegateGetPrimaryViewControllerForExpandingSplitViewController 方法重写此行为。
  • 选择主视图控制器后,必须重新创建辅助视图控制器。 同样地,拆分视图控制器包含自动根据硬件设备处理此阶段。 开发人员可通过调用 UISplitViewControllerDelegateSeparateSecondaryViewController 方法来重写此行为。

在拆分视图控制器中,主视图通过实现 UISplitViewControllerDelegateCollapseSecondViewControllerSeparateSecondaryViewController 方法,在展开和折叠视图的过程中均起到作用。 UINavigationController 实现这些方法以自动推送和弹出辅助视图控制器。

显示视图控制器

Apple 对 iOS 8 所做的另一项更改是开发人员显示视图控制器的方式。 过去,如果应用具有叶视图控制器(例如,表视图控制器),而开发人员展示的是另一个控制器(例如,响应点击单元格的用户),应用会通过控制器层次结构返回到“导航视图控制器”,并调用 PushViewController 方法以显示新的视图。

导航控制器与其运行所处的环境之间呈现出非常紧密的耦合。 在 iOS 8 中,Apple 通过提供两个新方法来分离这一点:

  • ShowViewController - 根据其环境进行调整,以显示新的视图控制器。 例如,在 UINavigationController 中,只会将新视图推送到堆栈上。 在拆分视图控制器中,新的视图控制器将会在左侧显示为新的主视图控制器。 如果没有容器视图控制器,则新视图将会显示为模式视图控制器。
  • ShowDetailViewController - 与 ShowViewController 的工作方式类似,但是在拆分视图控制器上实现,以将详细信息视图替换为传入的新的视图控制器。 如果拆分视图控制器被折叠(在 iPhone 应用中可能会看到这种情况),则调用会重定向到 ShowViewController 方法,新视图将会显示为主视图控制器。 同样地,如果没有容器视图控制器,则新视图将会显示为模式视图控制器。

这些方法的工作原理是从叶视图控制器开始,然后沿着层次结构向上,直到找到合适的容器视图控制器来处理新视图的显示。

开发人员可以在其自定义视图控制器中实现 ShowViewControllerShowDetailViewController,以获取与 UINavigationControllerUISplitViewController 提供的相同的自动化功能。

工作原理

在本部分中,我们将介绍如何在 iOS 8 中实现这些方法。 首先,让我们来了解新的 GetTargetForAction 方法:

The new GetTargetForAction method

此方法会遍历层次结构链,直至找到正确的容器视图控制器。 例如:

  1. 如果调用的是 ShowViewController 方法,则实现此方法的链中的第一个视图控制器为导航控制器,因此用作新视图的父级。
  2. 如果调用的是 ShowDetailViewController 方法,则拆分视图控制器是实现它的第一个视图控制器,因此用作父级。

GetTargetForAction 方法的工作原理:查找实现给定操作的视图控制器,然后询问该视图控制器是否希望接收该操作。 由于此方法是公开的,开发人员可以创建功能与内置的 ShowViewControllerShowDetailViewController 方法类似的自定义方法。

自适应演示文稿

在 iOS 8 中,Apple 还将弹出式演示文稿(UIPopoverPresentationController)也做成了自适应。 因此,弹出式演示文稿视图控制器会自动在常规大小类中显示正常的弹出式视图,但在横向为小巧大小类时(例如,在 iPhone 上)会显示全屏。

为了适应统一的情节提要系统内的更改,已创建一个新的控制器对象来管理显示的视图控制器 - UIPresentationController。 从显示视图控制器直到其消失,系统会创建此控制器。 由于属于管理类,因此可将其视为视图控制器上的超级类,因为它响应影响视图控制器的设备更改(如方向),然后馈送回演示文稿控制器控制的视图控制器中。

当开发人员使用 PresentViewController 方法显示视图控制器时,将演示文稿过程的管理交给 UIKit。 UIKit 处理创建的样式的正确控制器等等,唯一的例外是当视图控制器具有设置为 UIModalPresentationCustom 的样式时。 在这里,应用可以提供其自己的 PresentationController,而无需使用 UIKit 控制器。

自定义演示文稿样式

借助自定义演示文稿样式,开发人员可以选择使用自定义演示文稿控制器。 可以使用此自定义控制器修改其应用到的视图的外观和行为。

使用大小类

本文随附的自适应照片 Xamarin 项目提供了在 iOS 8 统一接口应用中使用大小类和自适应视图控制器的工作示例。

虽然应用完全从代码创建其 UI,但与使用 Xcode 的 Interface Builder 来创建统一的情节提要不同,采用的技术是相同的。

接下来,让我们详细了解自适应照片项目如何在 iOS 8 中实现多个“大小类”功能,打造一款自适应应用。

调整特征环境更改

当在 iPhone 上运行自适应照片应用时,如果用户将设备从竖屏旋转为横屏,则拆分视图控制器会同时显示主视图和详细信息视图:

The Split View Controller will display both the master and details view as seen here

此操作是通过重写视图控制器的 UpdateConstraintsForTraitCollection 方法,并根据 VerticalSizeClass 的值调整约束来实现的。 例如:

public void UpdateConstraintsForTraitCollection (UITraitCollection collection)
{
    var views = NSDictionary.FromObjectsAndKeys (
        new object[] { TopLayoutGuide, ImageView, NameLabel, ConversationsLabel, PhotosLabel },
        new object[] { "topLayoutGuide", "imageView", "nameLabel", "conversationsLabel", "photosLabel" }
    );

    var newConstraints = new List<NSLayoutConstraint> ();
    if (collection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact) {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("[imageView]-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:|[topLayoutGuide][imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.Add (NSLayoutConstraint.Create (ImageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
            View, NSLayoutAttribute.Width, 0.5f, 0.0f));
    } else {
        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[nameLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[conversationsLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("|-[photosLabel]-|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));

        newConstraints.AddRange (NSLayoutConstraint.FromVisualFormat ("V:[topLayoutGuide]-[nameLabel]-[conversationsLabel]-[photosLabel]-20-[imageView]|",
            NSLayoutFormatOptions.DirectionLeadingToTrailing, null, views));
    }

    if (constraints != null)
        View.RemoveConstraints (constraints.ToArray ());

    constraints = newConstraints;
    View.AddConstraints (constraints.ToArray ());
}

添加转换动画

当自适应照片应用中的拆分视图控制器从折叠变为展开时,会通过重写视图控制器的 WillTransitionToTraitCollection 方法向默认动画添加动画。 例如:

public override void WillTransitionToTraitCollection (UITraitCollection traitCollection, IUIViewControllerTransitionCoordinator coordinator)
{
    base.WillTransitionToTraitCollection (traitCollection, coordinator);
    coordinator.AnimateAlongsideTransition ((UIViewControllerTransitionCoordinatorContext) => {
        UpdateConstraintsForTraitCollection (traitCollection);
        View.SetNeedsLayout ();
    }, (UIViewControllerTransitionCoordinatorContext) => {
    });
}

重写特征环境

如上所示,当 iPhone 设备处于横屏视图时,自适应照片应用强制拆分视图控制器同时显示主视图和详细信息视图。

这是通过在视图控制器中使用以下代码实现的:

private UITraitCollection forcedTraitCollection = new UITraitCollection ();
...

public UITraitCollection ForcedTraitCollection {
    get {
        return forcedTraitCollection;
    }

    set {
        if (value != forcedTraitCollection) {
            forcedTraitCollection = value;
            UpdateForcedTraitCollection ();
        }
    }
}
...

public override void ViewWillTransitionToSize (SizeF toSize, IUIViewControllerTransitionCoordinator coordinator)
{
    ForcedTraitCollection = toSize.Width > 320.0f ?
         UITraitCollection.FromHorizontalSizeClass (UIUserInterfaceSizeClass.Regular) :
         new UITraitCollection ();

    base.ViewWillTransitionToSize (toSize, coordinator);
}

public void UpdateForcedTraitCollection ()
{
    SetOverrideTraitCollection (forcedTraitCollection, viewController);
}

展开和折叠拆分视图控制器

接下来,让我们了解如何在 Xamarin 中实现拆分视图控制器的展开和折叠行为。 当在 AppDelegate 中创建拆分视图控制器时,会分配其委托以处理以下更改:

public class SplitViewControllerDelegate : UISplitViewControllerDelegate
{
    public override bool CollapseSecondViewController (UISplitViewController splitViewController,
        UIViewController secondaryViewController, UIViewController primaryViewController)
    {
        AAPLPhoto photo = ((CustomViewController)secondaryViewController).Aapl_containedPhoto (null);
        if (photo == null) {
            return true;
        }

        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            var viewControllers = new List<UIViewController> ();
            foreach (var controller in ((UINavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containsPhoto");

                if ((bool)method.Invoke (controller, new object[] { null })) {
                    viewControllers.Add (controller);
                }
            }

            ((UINavigationController)primaryViewController).ViewControllers = viewControllers.ToArray<UIViewController> ();
        }

        return false;
    }

    public override UIViewController SeparateSecondaryViewController (UISplitViewController splitViewController,
        UIViewController primaryViewController)
    {
        if (primaryViewController.GetType () == typeof(CustomNavigationController)) {
            foreach (var controller in ((CustomNavigationController)primaryViewController).ViewControllers) {
                var type = controller.GetType ();
                MethodInfo method = type.GetMethod ("Aapl_containedPhoto");

                if (method.Invoke (controller, new object[] { null }) != null) {
                    return null;
                }
            }
        }

        return new AAPLEmptyViewController ();
    }
}

SeparateSecondaryViewController 方法测试是否看到显示一张照片,并根据该状态执行操作。 如果未显示照片,则会折叠辅助视图控制器,以便显示主视图控制器。

展开拆分视图控制器时使用 CollapseSecondViewController 方法查看堆栈上是否存在任何照片,如果存在,则折叠回到该视图。

在不同视图控制器之间移动

接下来,让我们了解自适应照片应用如何在视图控制器之间移动。 在 AAPLConversationViewController 类中,当用户选择表中的一个单元格时,会调用 ShowDetailViewController 方法显示详细信息视图:

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
    var photo = PhotoForIndexPath (indexPath);
    var controller = new AAPLPhotoViewController ();
    controller.Photo = photo;

    int photoNumber = indexPath.Row + 1;
    int photoCount = (int)Conversation.Photos.Count;
    controller.Title = string.Format ("{0} of {1}", photoNumber, photoCount);
    ShowDetailViewController (controller, this);
}

显示披露指示器

自适应照片应用中,有会根据对特征环境的更改隐藏或显现很多地方的披露指示器。 使用以下代码进行处理:

public bool Aapl_willShowingViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

public bool Aapl_willShowingDetailViewControllerPushWithSender ()
{
    var selector = new Selector ("Aapl_willShowingDetailViewControllerPushWithSender");
    var target = this.GetTargetViewControllerForAction (selector, this);

    if (target != null) {
        var type = target.GetType ();
        MethodInfo method = type.GetMethod ("Aapl_willShowingDetailViewControllerPushWithSender");
        return (bool)method.Invoke (target, new object[] { });
    } else {
        return false;
    }
}

使用上述详细信息中介绍的 GetTargetViewControllerForAction 方法能实现。

当表视图控制器显示的是数据时,会使用上述实现的方法查看是否会发生推送,以及是否根据情况显示或隐藏披露指示器:

public override void WillDisplay (UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
    bool pushes = ShouldShowConversationViewForIndexPath (indexPath) ?
         Aapl_willShowingViewControllerPushWithSender () :
         Aapl_willShowingDetailViewControllerPushWithSender ();

    cell.Accessory = pushes ? UITableViewCellAccessory.DisclosureIndicator : UITableViewCellAccessory.None;
    var conversation = ConversationForIndexPath (indexPath);
    cell.TextLabel.Text = conversation.Name;
}

新的 ShowDetailTargetDidChangeNotification 类型

Apple 添加了一个新的通知类型,用于在拆分视图控制器 ShowDetailTargetDidChangeNotification 中使用大小类和特征环境。 每当拆分视图控制器的目标详细信息视图更改时(例如,展开或折叠该控制器),都会发送此通知。

当详细信息视图控制器更改时,自适应照片应用使用此通知更新披露指示器的状态:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();
    TableView.RegisterClassForCellReuse (typeof(UITableViewCell), AAPLListTableViewControllerCellIdentifier);
    NSNotificationCenter.DefaultCenter.AddObserver (this, new Selector ("showDetailTargetDidChange:"),
        UIViewController.ShowDetailTargetDidChangeNotification, null);
    ClearsSelectionOnViewWillAppear = false;
}

深入了解自适应照片应用,查看大小类、特征集合和自适应视图控制器可用于在 Xamarin.iOS 中打造统一的应用。

统一的情节提要

在 iOS 8 中,统一的情节提要通过以多个大小类为目标,允许开发人员通过以多个大小类为目标,创建一个可同时在 iPhone 和 iPad 上显示的统一情节提要。 通过使用统一情节提要,开发人员可减少 UI 特定代码的编写量,并且只需设计一个接口来进行创建和维护。

统一情节提要主要具有以下优势:

  • 为 iPhone 和 iPad 使用同一个情节提要文件。
  • 向后部署到 iOS 6 和 iOS 7。
  • 预览不同设备、方向和 OS 版本的布局。

启用大小类

默认情况下,任何新的 Xamarin.iOS 项目都将使用大小类。 若要使用较旧项目中某个情节提要内的大小类和自适应 Segues,则必须先转换为 Xcode 6 统一情节提要格式,并且在 Xcode 文件检查器中为你的情节提要选中“使用大小类”复选框。

动态启动屏幕

启动屏幕文件显示为初始屏幕,iOS 应用将会启动,向用户提供反馈说应用实际上正在启动。 在 iOS 8 之前,开发人员必须包含多个 Default.png 图像资产以供应用可能会在其上运行的每种设备类型、方向和屏幕分辨率。 例如 Default@2x.pngDefault-Landscape@2x~ipad.pngDefault-Portrait@2x~ipad.png 等。

考虑到新的 iPhone 6 和 iPhone 6 Plus 设备(以及即将推出的 Apple Watch),以及所有现有的 iPhone 和 iPad 设备,这表示必须创建和维护 Default.png 启动屏幕图像资产各种不同的大小、方向和分辨率。 此外,这些文件可能相当大,会“膨胀”可交付的应用程序包,增加从 iTunes App Store 下载该应用所需的时间(可能使其无法通过移动网络传送),并会增加最终用户设备上所需的存储量。

iOS 8 中一项新增功能是,开始人员可以在 Xcode 中使用单个的原子 .xib 文件,它使用自动布局和大小类来为每个设备、分辨率和方向创建可使用的动态启动屏幕。 此功能不仅能减少开发人员创建和维护所有需要图像资产所需的工作量,还能为应用的安全程序包大幅“瘦身”。

动态启动屏幕具有以下限制和注意事项:

  • 仅使用 UIKit 类。
  • 使用属于 UIViewUIViewController 对象的单一根视图。
  • 不要与应用的代码建立任何连接(请勿添加操作出口)。
  • 不要添加 UIWebView 对象。
  • 不要使用任何自定义类。
  • 不要使用运行时属性。

请将上述指导准则记在心中。接下来,让我们了解如何将动态启动屏幕添加到现有的 Xamarin iOS 8 项目。

请执行以下操作:

  1. 打开 Visual Studio for Mac 并加载解决方案,以添加动态启动屏幕。

  2. 在“解决方案资源管理器”中,右键单击 MainStoryboard.storyboard 文件,然后选择“打开方式>Xcode Interface Builder

    Open With Xcode Interface Builder

  3. 在 Xcode 中,依次选择“文件”>“新建”>“文件…”:

    Select File / New

  4. 依次选择“iOS”>“用户界面”>“启动屏幕”,然后单击“下一步”按钮:

    Select iOS / User Interface / Launch Screen

  5. 给文件 LaunchScreen.xib 命名,然后单击“创建”按钮:

    Name the file LaunchScreen.xib

  6. 通过添加图形元素并使用布局约束为给定设备、方向和屏幕大小定位启动屏幕的设计:

    Editing the design of the launch screen

  7. 保存对 LaunchScreen.xib 的更改。

  8. 选择“应用目标”,然后选择“通用”选项卡:

    Select the Applications Target and the General tab

  9. 单击“选择 Info.plist”按钮,为 Xamarin 应用选择 Info.plist,然后单击“选择”按钮:

    Select the Info.plist for the Xamarin app

  10. 在“应用图标和启动图像”部分中,打开“启动屏幕文件”下拉列表,选择上面创建的 LaunchScreen.xib

    Choose the LaunchScreen.xib

  11. 保存对文件的更改,然后返回 Visual Studio for Mac。

  12. 等待 Visual Studio for Mac 完成与 Xcode 的同步更改。

  13. 在“解决方案资源管理器”中,右键单击“资源”文件夹,然后依次选择“添加”>“添加文件…”:

    Select Add / Add Files...

  14. 选择前面创建的 LaunchScreen.xib 文件,然后单击“打开”按钮:

    Select the LaunchScreen.xib file

  15. 构建应用程序。

测试动态启动屏幕

在 Visual Studio for Mac 中,选择 iPhone 4 Retina 模拟器并运行应用。 动态启动屏幕将以正确的格式和方向显示:

The Dynamic Launch Screen displayed in the vertical orientation

在 Visual Studio for Mac 中停止应用,并选择 iPad iOS 8 设备。 运行应用并正确设置此设备和方向的启动屏幕格式:

The Dynamic Launch Screen displayed in the horizontal orientation

返回到 Visual Studio for Mac,并阻止应用运行。

使用 iOS 7

为了保持与 iOS 7 的向下兼容性,只需在 iOS 8 应用中像往常一样包括常规 Default.png 图像资产。 iOS 将返回到以前的行为,并在 iOS 7 设备上运行时将这些文件用作启动屏幕。

若要查看如何在 Xamarin 中实现动态启动屏幕,请查看附加到本文档的动态启动屏幕示例 iOS 8 应用。

总结

本文快速介绍了大小类,以及它们如何影响 iPhone 和 iPad 设备上的布局。 其中介绍了特征、特征环境和特征集合如何使用大小类创建统一接口。 简要介绍了自适应视图控制器,以及它们如何使用统一接口内部的大小类。 介绍了从 Xamarin iOS 8 应用中的 C# 代码完全实现大小类和统一接口。

最后,本文介绍了创建单个动态启动屏幕的基本知识,该屏幕将在每个 iOS 8 设备上显示为启动屏幕。