Xamarin-IOS-开发秘籍-全-

Xamarin IOS 开发秘籍(全)

原文:zh.annas-archive.org/md5/9b536329a9548b24c26b6d64c87185e5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书将为您提供使用 C#编程语言开发并部署丰富强大的 iPhone 和 iPad 应用的所需所有技能。Xamarin.iOS,之前被称为 MonoTouch,已经确立为一个强大的软件开发工具包,它将 iOS 开发带到了.NET 程序员面前。书中包含易于理解且详细的示例,将成为您 iOS 开发旅程中的最佳伴侣。

本书涵盖内容

第一章, 开发工具,教您如何安装和使用创建您的第一个 iOS 应用所需的开发工具。从那里开始,您将创建并调试您的第一个 Xamarin.iOS 项目。

第二章, 用户界面 – 视图,讨论了 iOS SDK 的基本用户界面组件。详细介绍了最常用的视图和控制组件,我们将通过一系列示例项目熟悉该平台。我们还将讨论与标准.NET 组件的相似之处和不同之处。

第三章, 用户界面 – 视图控制器,向您介绍视图控制器,这些对象负责提供您的应用与用户之间的交互机制。通过简单的逐步过程进行解释,您将开始创建可以在 iPhone 和 iPad 设备上运行的应用程序。

第四章, 数据管理,涵盖了 iOS 平台上的数据管理实践以及如何使用 C#的便利性来高效地使用它们。您将学习如何管理本地区 SQLite 数据库文件,同时也会学习如何使用 iCloud 在不同设备间存储数据。

第五章, 显示数据,专注于数据管理的另一个重要部分。通过一系列简单而完整的项目,您将了解可用于在 iPhone 屏幕上显示数据的组件,这些屏幕比计算机屏幕小。以用户友好的方式显示各种类型的数据对于移动设备至关重要,到您阅读完本章时,您在这方面的技能一定会更加熟练。

第六章, 网络服务,指导您通过.NET SOAP、WCF 和 REST 服务创建将用户与世界连接起来的应用。如果没有 Xamarin.iOS,这些强大的.NET 特性将不会成为 iOS 开发的一部分。

第七章, 多媒体资源,将教会您如何通过设备的硬件创建捕捉、再现和管理多媒体内容的应用程序。您不仅将学习如何使用相机捕捉图像和视频,还将学习如何播放和录制音频。

第八章,集成 iOS 功能,将指导你如何整合平台的原生应用程序和组件。你将学习如何在你的应用程序中提供电子邮件、短信和通讯录功能,以及如何使用原生日历来创建事件。

第九章,与设备硬件交互,讨论了通过设备的传感器创建完全了解其周围环境的应用程序。你将学习根据设备方向调整用户界面,并响应加速度计和陀螺仪事件。

第十章,位置服务和地图,是使用内置位置服务创建向用户提供位置信息的应用程序的详细指南。你不仅将学习如何使用 GPS 硬件,还将学习如何在地图上显示和布局信息。

第十一章,图形和动画,介绍了 2D 图形和动画。你将学习如何动画化组件并在屏幕上绘制简单的图形。到本章结束时,你将创建一个简单的手指绘画应用程序。

第十二章,多任务,将指导你如何在 iOS 应用程序中实现多任务处理的细节。这通过在幕后执行代码,极大地增强了用户体验。

第十三章,本地化,讨论了如何在应用程序中提供本地化内容。你将学习如何准备你的应用程序以面向全球用户。

第十四章,部署,不仅将指导你完成将完成的应用程序部署到设备上的必要步骤,还将指导你准备和分发它到 App Store。

第十五章,高级功能,介绍了在较新 iOS 版本中引入的一些关键功能,例如通过 iOS 7 的 UIKit Dynamics 实现将物理效果应用于用户界面组件,自定义视图控制器之间的动画过渡,等等!

你需要为本书准备的东西

本书的最低要求是运行至少 Mac OS X Lion(10.7.)的 Mac 电脑。几乎你将使用本书创建的所有项目都可以在 iOS 模拟器上运行。然而,一些项目将需要设备才能正常工作。你将在第一章,开发工具*中找到所有适当的详细信息。

本书面向的对象

这本书对于没有 iOS 开发经验的 C# 和 .NET 开发者来说是必不可少的,但它也适用于想要过渡到 Xamarin.iOS 和 C# 语言的 Objective-C 开发者,以创建完整、引人入胜的 iPhone、iPod 和 iPad 应用程序并将它们部署到 App Store。

惯例

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。

文本中的代码单词、食谱名称、脚本、数据库表名、文件夹名称、文件名、文件扩展名和路径名如下所示:"Register 属性用于将类暴露给底层的 Objective-C 运行时。"

代码块如下设置:

using System;
using System.Collections.Generic;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

EKEvent newEvent = EKEvent.FromStore(evStore);
newEvent.StartDate = DateTime.Now.AddDays(1);
newEvent.EndDate = DateTime.Now.AddDays(1.1);
newEvent.Title = "Xamarin event!";

任何命令行输入或输出都如下所示:

cd <code_directory>/CH06_code/WcfService/WcfService
./start_wcfservice.sh

新术语重要词汇以粗体显示。你会在屏幕上、菜单或对话框中看到的单词,例如,在文本中如下所示:"转到面板,并从下拉列表中选择对象。"

注意

警告或重要注意事项如下所示。

小贴士

小技巧和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或者可能不喜欢什么。读者的反馈对我们开发你真正能从中获得最大收益的标题非常重要。

要向我们发送一般反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件的主题中提及书名。

如果你在一个主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。

下载示例代码

你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便直接将文件通过电子邮件发送给你。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。

盗版

在互联网上,版权材料的盗版问题是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。

问题

如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 开发工具

在本章中,我们将涵盖:

  • 安装必备条件

  • 使用 Xamarin Studio 创建 iOS 项目

  • Interface Builder

  • 创建 UI

  • 使用 Outlets 访问 UI

  • 向控件添加操作

  • 编译 iOS 项目

  • 调试我们的应用程序

简介

专业人士最关心的一件重要事情是完成他们工作所需的工具。就像木匠需要凿子来刮木料,或者摄影师需要相机来捕捉光线一样,我们开发者需要某些工具,没有这些工具我们无法工作。

在本章中,我们将提供有关开发 iOS 应用程序(Apple 的移动设备操作系统)所需的 IDEs(集成开发环境)和 SDKs(软件开发工具包)的信息。我们将描述每个工具在开发周期中的作用,并介绍开发我们的第一个应用程序所必需的功能。

以下是需要使用 Xamarin.iOS 开发应用程序的工具:

  • 至少运行 Lion (10.7.*) 操作系统的 Apple Mac 计算机:我们需要的必需程序不能安装在其他计算机平台上。

    注意

    Xamarin 还为其产品提供 Visual Studio 开发集成。编译、测试、调试和分发应用程序仍需要一台 Mac 计算机。更多信息可以在 Xamarin 网站上找到,链接为 docs.xamarin.com/guides/ios/getting_started/introduction_to_xamarin_ios_for_visual_studio/

  • 最新 iOS SDK:要能够下载 iOS SDK,开发者必须注册为 Apple 开发者。iOS SDK 包括两个基本组件,以及其他内容:

    • Xcode:这是 Apple 使用 Objective-C 编程语言为 iOS 和 Mac 开发原生应用程序的 IDE。

    • iOS 模拟器:这是一个在计算机上调试 iOS 应用程序的基本程序,无需设备。请注意,许多 iOS 功能在模拟器上无法工作。因此,如果应用程序使用这些功能,则需要设备。

    注意

    注册和 SDK 下载均可在 Apple 开发者门户(developer.apple.com)免费进行。如果我们想在设备上运行和调试我们的应用程序或在 App Store 上分发它们,我们需要注册 iOS 开发者计划,这需要订阅费。

  • Xamarin 安装程序:Xamarin 提供了一个包含所有必要工具的安装包。此包包括 Xamarin.iOS SDK 和用于使用 C# 开发 iOS 应用程序的 IDE——Xamarin Studio。下载 Xamarin 安装程序需要免费注册,可以通过点击链接 xamarin.com/download 获取。

本章还将描述如何使用 Xamarin Studio 创建我们的第一个 iPhone 项目,使用 Xcode 构建其 UI,以及如何在我们的代码中访问应用程序的用户界面,使用OutletsActions的概念。

最后但同样重要的是,我们将学习如何编译我们的应用程序,可用的编译选项以及如何在模拟器上进行调试。

安装先决条件

本节提供了有关如何下载和安装使用 Xamarin.iOS 进行开发的必要工具的信息。

准备工作

我们需要在我们的计算机上下载并安装所有必要的组件。首先,需要在 developer.apple.com 上注册为 Apple 开发者。注册是免费且简单的,并提供访问所有必要的发展资源。通过电子邮件确认注册后,我们可以登录并从地址 developer.apple.com/devcenter/ios/index.action#downloads 下载 iOS SDK。在撰写本文时,Xcode 的最新版本是 5.0.1,iOS SDK 的最新版本是 7.0.3。

如何操作...

为了使我们的计算机准备好进行 iOS 开发,我们需要按照以下顺序下载和安装必要的组件:

  • Xcode 和 iOS SDK: 需要登录 Mac App Store。您可以在 App Store 中搜索 Xcode,或者在 iOS 开发者门户的下载部分点击下载 Xcode按钮。下载完成后,按照屏幕上的说明安装 Xcode。以下截图显示了 Mac App Store 中的 Xcode:如何操作...

  • Xamarin 入门版: 从 Xamarin 网站 xamarin.com/download 下载并运行 Xamarin 入门版。按照屏幕上的说明安装 Xamarin Studio 和 Xamarin.iOS。

    注意

    Xamarin 入门版是免费的,但有一些限制,例如最大应用程序包大小的限制,并且没有 Visual Studio 支持。然而,它支持部署到设备和 App Store。在撰写本文时,本书中显示的所有配方都完全由入门版支持,除了第六章中的使用 WCF 服务配方,Web 服务。需要商业或企业版才能支持 WCF。

它是如何工作的...

现在我们已经准备好了,让我们看看每个组件需要什么。

Xcode

Xcode 是苹果为 iOS 和 Mac 平台开发应用程序的 IDE。它针对 Objective-C 编程语言,这是使用 iOS SDK 进行编程的主要语言。由于 Xamarin.iOS 是 C#语言的 SDK,人们可能会问我们为什么需要它。除了提供各种调试 iOS 应用的工具外,Xcode 还为我们提供了组织者窗口。如以下截图所示,我们可以使用它来查看设备的控制台日志、安装和管理必要的配置文件,甚至查看设备的崩溃日志。要打开组织者窗口,请转到菜单栏上的窗口 | 组织者,或在键盘上按Cmd + Shift + 2

Xcode

接口构建器

第二个组件是接口构建器。这是一个用户界面设计器,它以前是一个独立的应用程序。从 Xcode 4.0 开始,它被集成到 IDE 中。接口构建器提供了构建应用程序用户界面所需的所有必要功能。它也与.NET 开发者习惯的内容有很大不同。

iOS 模拟器

第三部分是 iOS 模拟器。正如其名称所暗示的那样:一个设备模拟器,我们可以用它来运行我们的应用,而无需实际设备。iOS 模拟器最重要的特点是它可以选择模拟较旧的 iOS 版本(如果它们已安装在计算机上),包括 iPhone 和 iPad 界面以及设备方向。然而,模拟器缺少一些依赖于硬件的设备功能,例如指南针或加速度计。使用这些功能的应用程序必须在实际设备上进行测试和调试。

Xamarin.iOS 是允许.NET 开发者使用 C#编程语言为 iOS 开发应用程序的 SDK。所有对 Objective-C 开发者可用的 API 也通过 Xamarin.iOS 对 C#开发者可用。它不是一个具有自己 API 的独立框架,例如用户界面。Xamarin.iOS 程序员可以使用与 Objective-C 程序员相同的 UI 元素,同时还可以享受 C#带来的额外好处,如泛型、LINQ 以及使用 async/await 的异步编程。

还有更多...

使用 Xamarin.iOS 开发的应用程序与使用原生 Objective-C 编程语言开发的所有其他应用程序进入 App Store 的机会相同。这意味着如果一个应用不符合苹果对应用接受度的严格政策,它将失败,无论它是用 Objective-C 还是 C#编写的。Xamarin.iOS 团队在创建一个 SDK 方面做得很好,让开发者只需担心设计和代码的最佳实践,而无需担心其他任何事情。

有用链接

以下是一些有用的链接,您可以浏览:

更新

Xamarin Studio 有一个检查可用更新的功能。每次程序启动时,它都会检查 Xamarin.iOS 的更新。它可以关闭,但这样做不建议,因为它有助于保持与最新版本的同步。它可以在 Xamarin Studio | 检查更新 下找到。

参见

  • 编译 iOS 项目调试我们的应用程序 菜谱

  • 在第十四章 部署为 App Store 准备我们的应用 菜谱中,第十四章

使用 Xamarin Studio 创建 iOS 项目

在这个菜谱中,我们将讨论如何使用 Xamarin Studio 创建我们的第一个 iOS 项目。

准备中...

现在我们已经安装了所有先决条件,我们将讨论如何使用 Xamarin Studio 创建我们的第一个 iOS 项目。

启动 Xamarin Studio。它可以在 应用程序 文件夹中找到。Xamarin Studio 的默认项目位置是 /Users/{你的用户名}/Projects。如果硬盘上不存在,则在创建我们的第一个项目时创建。如果您想更改文件夹,请从菜单栏选择 Xamarin Studio | 首选项。在左侧面板中选择 加载/保存,在 默认解决方案 位置 字段中输入项目首选位置,然后单击 确定

准备中...

如何做...

当启动 Xamarin Studio 时,首先加载的是其起始页面。执行以下步骤以使用 Xamarin Studio 创建 iOS 项目:

  1. 从菜单栏导航到 文件 | 新建 | 解决方案...。将出现一个窗口,提供给我们可用的项目选项。

  2. 在此窗口左侧的面板中,转到 C# | iOS | iPhone。iPhone 项目模板将在中间面板中显示。

  3. 选择 单视图应用程序

  4. 最后,输入 MyFirstiOSProject 作为 解决方案名称 并单击 确定。以下截图显示了 新建解决方案 窗口:如何做...

就这么简单。您已经创建了您的第一个 iPhone 项目。您可以构建并运行它;iOS 模拟器将启动,但屏幕仍然是空白浅灰色。

注意

项目模板可能与前面截图中的不同。

工作原理...

让我们看看幕后发生了什么。

当 Xamarin Studio 创建一个新的 iOS 项目时,它会创建一系列文件。解决方案文件可以在 Xamarin Studio 窗口左侧的 解决方案 面板中查看。如果 解决方案 面板不可见,可以通过从菜单栏选择 视图 | 面板 | 解决方案 来激活它。

下面的截图显示了构成 iPhone 项目的关键文件:

如何工作...

MyFirstiOSProjectViewController.xib

MyFirstiOSProjectViewController.xib 是包含应用程序视图的文件。XIB 文件基本上是 Xcode 可以读取的具有特定结构的 XML 文件。这些文件包含有关用户界面的信息,例如它包含的控件类型、它们的属性和出口。

注意

如果双击 MyFirstiPhoneProjectViewController.xib 或任何具有 .xib 后缀的文件,Xamarin Studio 将自动在 Xcode 的 Interface Builder 中打开该文件。

当我们使用 Interface Builder 创建一个新的界面并保存时,它将以 XIB 格式保存。

MyFirstiOSProjectViewController.cs

MyFirstiOSProjectViewController.cs 是实现视图功能的文件。以下是创建文件时的内容:

using System;
using System.Drawing;
using MonoTouch.Foundation;
using MonoTouch.UIKit;

namespace MyFirstiOSProject
{
    public class MyFirstiOSProjectViewController : UIViewController
    {
        public MyFirstiOSProjectViewController () : base ("MyFirstiOSProjectViewController", null)
        {
        }

        public override void DidReceiveMemoryWarning ()
        {
            // Releases the view if it doesn't have a superview.base.DidReceiveMemoryWarning ();

            // Release any cached data, images, etc that aren't in use.
        }

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

            // Perform any additional setup after loading the view, typically from a nib.
        }
}
}

注意

Xamarin.iOS 以前被称为 MonoTouch。为了确保代码兼容性,命名空间没有被重命名。

此文件中的代码包含与将要加载的视图相对应的类,以及一些默认方法,这些方法会被覆盖。这些是我们创建视图控制器时将更频繁使用的方法。以下是每个方法的简要描述:

  • ViewDidLoad:当控制器的视图加载时,会调用此方法。这是我们用来初始化任何附加组件的方法。

  • DidReceiveMemoryWarning:当应用程序收到内存警告时,会调用此方法。此方法负责释放当时不需要的资源。

MyFirstiOSProjectViewController.designer.cs

MyFirstiOSProjectViewController.designer.cs 是一个文件,它以 C# 代码的形式保存了我们主窗口的类信息。Xamarin Studio 为项目中添加的每个 XIB 创建一个 .designer.cs 文件。每次我们通过 Interface Builder 保存 XIB 中的更改时,该文件都会自动生成。Xamarin Studio 会处理这些更改,以确保我们在界面中做出的更改能够立即反映在代码中。我们不得直接修改此文件,因为当相应的 XIB 通过 Interface Builder 保存时,这些更改将会丢失。此外,如果通过 Interface Builder 没有保存任何内容,并且手动修改了它,很可能会导致编译错误。

创建新项目时文件的内容如下:

//
// This file has been generated automatically by MonoDevelop to store outlets and
// actions made in the Xcode designer. If it is removed, they will be lost.
// Manual changes to this file may not be handled correctly.
//
using MonoTouch.Foundation;

namespace MyFirstiOSProject
{
    [Register ("MyFirstiOSProjectViewController")]
    partial class MyFirstiOSProjectViewController
    {
        void ReleaseDesignerOutlets ()
        {
        }
    }
}

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。

此文件包含 MyFirstiOSProjectViewController 类的其他部分声明。它被 Register 属性装饰。

Register 属性用于将类暴露给底层 Objective-C 运行时。字符串参数声明我们的类将通过什么名称暴露给运行时。它可以是我们想要的任何名称,但始终将其设置为我们的 C# 类的名称是一个好习惯。该属性在 Xamarin.iOS 的内部使用得非常频繁,因为它将所有原生 NSObject 类与其 C# 对应项绑定在一起。

注意

NSObject 是一个根类或基类。它在 .NET 世界中相当于 System.Object。两者之间的唯一区别是,所有 .NET 对象都继承自 System.Object,但在 Objective-C 中,大多数而不是所有 Objective-C 对象都继承自 NSObject。所有从 NSObject 继承的本地对象的 C# 对应项也继承自其 Xamarin.iOS NSObject 对应项。

AppDelegate.cs

AppDelegate.cs 文件包含 AppDelegate 类。文件的以下内容列出:

using System;
using System.Collections.Generic;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;

namespace MyFirstiOSProject
{
    // The UIApplicationDelegate for the application. This classis responsible for launching the
    // User Interface of the application, as well as listening(and optionally responding) to
    // application events from iOS.
    [Register ("AppDelegate")]
    public partial class AppDelegate : UIApplicationDelegate
    {
        // class-level declarations
        UIWindow window;
        MyFirstiOSProjectViewController viewController;
        //
        // This method is invoked when the application has loaded and is ready to run. In this
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching (UIApplication app,NSDictionary options)
        {
            window = new UIWindow (UIScreen.MainScreen.Bounds);

            viewController = new MyFirstiOSProjectViewController ();
            window.RootViewController = viewController;
            window.MakeKeyAndVisible ();

            return true;
        }
    }
}

第一部分对 .NET 开发者来说很熟悉,包括适当的 using 指令,用于导入使用所需的命名空间。考虑以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;

前三个 using 指令允许我们使用 .NET 世界中特定且熟悉的命名空间,与 Xamarin.iOS 一起使用。

注意

SystemSystem.Collections.GenericSystem.Linq:尽管这三个命名空间提供的功能几乎与它们知名的 .NET 对应项相同,但它们包含在专门为与 Xamarin.iOS 一起使用而创建的程序集中,并且当然与它一起分发。使用 .NET 编译的程序集不能直接用于 Xamarin.iOS 项目。

MonoTouch.Foundation 命名空间是对原生 Objective-C Foundation 框架的包装,其中包含提供基本功能类的封装。这些对象的名称与在原生 Foundation 框架中找到的相同 NS 前缀相同。例如,NSObjectNSStringNSValue 等等。除了带有 NS 前缀的对象外,MonoTouch.Foundation 命名空间还包含用于绑定到原生对象的全部属性,例如我们之前看到的 OutletRegister 属性。MonoTouch.UIKit 命名空间是对原生 Objective-C UIKit 框架的包装。正如其名称所暗示的,该命名空间包含提供界面功能的类、代理和事件。几乎所有对象的名称都共享相同的 UI 前缀。此时应该清楚,这两个命名空间对于所有 Xamarin.iOS 应用程序都是必不可少的,并且它们的对象将被频繁使用。

该类继承自 UIApplicationDelegate 类,使其成为我们应用程序的代理对象。

注意

Objective-C 世界中委托对象的概念与 C# 中的 delegate 有所不同。它将在第二章(Chapter 2,用户界面 – 视图)中详细解释。

AppDelegate 类包含两个字段和一个方法:

UIWindow window;MyFirstiOSProjectViewController viewController; 
//..
public override bool FinishedLaunching (UIApplication app, NSDictionary options) {

UIWindow 对象定义了我们应用程序的主窗口,而 MyFirstiOSProjectViewController 对象是用于保存应用程序视图控制器的变量。

注意

iOS 应用通常只有一个 UIWindow 类型的窗口。UIWindow 是应用程序启动时首先显示的控制项,后续的每个视图都按层次结构添加在其下方。

如其名称所示,FinishedLaunching 方法在应用程序完成初始化过程时被调用。这是我们向用户展示用户界面的方法。此方法的实现必须轻量级;如果它不能在调用后及时返回,iOS 将终止应用程序。这通过防止开发者在初始化时执行复杂和长时间运行的任务(例如连接到网络服务以接收数据)来为用户提供更快的用户界面加载时间。app 参数是应用程序的 UIApplication 对象,也可以通过 static 属性 UIApplication.SharedApplication 访问。options 参数可能包含有关应用程序启动方式的信息,但目前我们不需要它。

此类型项目的 FinishedLaunching 方法的默认实现如下:

  • UIWindow 对象使用屏幕大小初始化如下:

    window = new UIWindow (UIScreen.MainScreen.Bounds);
    
  • 视图控制器初始化并设置为窗口的根视图控制器如下:

    viewController = new MyFirstiPhoneProjectViewController();
    window.RootViewController = viewController;
    window.MakeKeyAndVisible ();
    return true;
    

通过 window.MakeKeyAndVisible() 调用在屏幕上显示窗口,并返回方法。此方法必须在 FinishedLaunching 方法内部调用,否则应用程序的用户界面将不会按预期向用户展示。最后但同样重要的是,return true 行通过标记其执行完成来返回方法。

Main.cs

Main.cs 文件内部,程序的运行时生命周期从以下代码开始:

namespace MyFirstiOSProject
{
    public class Application
    {
        // This is the main entry point of the application.
        static void Main (string[] args)
        {
            // if you want to use a different Application Delegate class from "AppDelegate"
            // you can specify it here.
            UIApplication.Main (args, null, "AppDelegate");
        }
    }
}

它与以下 .NET System.Windows.Forms 应用程序中的调用非常相似:

Application.Run(new Form1());

UIApplication.Main 方法启动消息循环或运行循环,该循环负责通过 AppDelegate 类的事件处理器将通知分发给应用程序。事件处理器如 FinishedLaunchingReceiveMemoryWarningDidEnterBackground 只是这些通知中的一些。除了通知分发机制外,UIApplication 对象还包含所有存在的 UIWindow 对象的列表,通常只有一个。iOS 应用必须有一个 UIApplication 对象,或者是从它继承的类,并且该对象必须有一个相应的 UIApplicationDelegate 对象。这就是我们之前看到的 AppDelegate 类实现。

Info.plist

Info.plist文件基本上是应用程序的设置文件。它具有简单的属性值结构,定义了 iOS 应用程序的各种设置,例如它支持的朝向、图标、支持的 iOS 版本、它可以安装的设备等等。如果我们双击 Xamarin Studio 中的此文件,它将在专门为此文件设计的嵌入式编辑器中打开。新项目中的文件如下截图所示:

Info.plist

我们还可以通过iOS 应用程序下的项目选项窗口访问Info.plist

更多内容...

Xamarin Studio 为开发 iOS 应用程序提供了许多不同的项目模板。以下是一个列表,描述了每个项目模板的用途:

  • 空项目:这是一个没有任何视图的空项目。

  • 实用应用程序:这是一种特殊的 iOS 应用程序,提供一屏功能,在许多情况下,还提供另一屏进行配置。

  • 主从应用程序:这种类型的项目创建了一个支持通过多个屏幕导航的模板。它包含两个视图控制器。

  • 单视图应用程序:这种模板类型是我们在这个配方中使用的。

  • 标签页应用程序:这是一个添加标签栏控制器并管理类似标签界面的两个视图控制器的模板。

  • OpenGL 应用程序:这是一个用于创建 OpenGL 应用程序或游戏的模板。

这些模板适用于 iPhone、iPad 和通用(iPhone 和 iPad)项目。它们也适用于 Interface Builder 的故事板应用设计。

注意

除非另有说明,所有针对 iPhone 的项目模板也适用于 iPod Touch。

Xamarin.iOS 组件列表

Xamarin.iOS 支持的组件可以在ios.xamarin.com/Documentation/Assemblies找到。

参见

  • 创建 UI通过 Outlets 访问 UI的配方

  • 第二章中的添加和自定义视图配方,用户界面 – 视图

Interface Builder

在这个配方中,我们将查看 Xcode 的 Interface Builder。由于我们无法使用 Xcode 编写代码,Xamarin Studio 提供了一个透明的方式来与 Xcode 进行用户界面文件的通信。

如何操作...

让我们通过以下步骤查看 Interface Builder:

  1. 如果你已经安装了 iOS SDK,那么你的电脑上已经安装了带有 Interface Builder 的 Xcode。前往 Xamarin Studio 并打开我们之前创建的项目MyFirstiOSProject

  2. 在左侧的解决方案面板中双击MyFirstiOSProjectViewController.xib。Xamarin Studio 将启动 Xcode 并在 Interface Builder 中加载文件。

  3. 在 Xcode 窗口的右上角工具栏中,选择适当的编辑器和查看选项,如下面的截图所示:如何操作...

  4. 以下截图展示了打开 XIB 文件时的接口构建器外观:如何操作...

它是如何工作的...

现在我们已经用我们的应用程序视图控制器加载了接口构建器,让我们熟悉一下它。

用户界面设计师直接连接到 Xcode 项目。当我们添加一个对象时,Xcode 会自动生成代码以反映我们所做的更改。Xamarin Studio 会为我们处理这些,因此当我们双击 XIB 文件时,它会自动创建一个临时 Xcode 项目。这使我们能够对用户界面进行我们想要的更改。因此,我们只需为我们的应用程序设计用户界面即可。

接口构建器分为三个区域。以下是每个区域的简要描述:

  • 导航区域:在这个区域,我们可以看到 Xcode 项目中的文件。

  • 编辑区域:这个区域是我们设计用户界面的地方。编辑区域分为两个部分。左边的是设计师,右边的是辅助编辑器。在辅助编辑器内部,我们可以看到与设计师中选定的项目对应的底层 Objective-C 源代码文件。尽管我们不需要编辑 Objective-C 源代码,但稍后我们还需要辅助编辑器。

  • 实用区域:这个区域包含检查器和库窗格。检查器窗格是我们配置每个对象的地方,库窗格是我们查找对象的地方。

还有更多...

我们在接口构建器中看到了 XIB 文件的外观,但关于这些文件还有更多内容。我们之前提到,XIB 文件是 Interface Builder 可以读取的 XML 文件,其中包含适当的信息。问题是,当项目中进行编译时,编译器会将 XIB 文件编译成其二进制等效文件,即 NIB 文件。XIB 和 NIB 文件包含相同的信息。它们之间的唯一区别是,XIB 文件以人类可读的形式存在,而 NIB 文件则不是。例如,当我们编译我们创建的项目时,MyFirstiOSProjectViewController.xib文件将在输出文件夹中变为MyFirstiOSProjectViewController.nib。除了二进制转换之外,编译器还会对 NIB 文件进行压缩。因此,NIB 文件的大小将比 XIB 文件小得多。

关于 XIB 文件,还有更多内容。我们之前提到,XIB 文件是 Interface Builder 可以读取的 XML 文件,其中包含适当的信息。问题是,当项目中进行编译时,编译器会将 XIB 文件编译成其二进制等效文件,即 NIB 文件。XIB 和 NIB 文件包含相同的信息。它们之间的唯一区别是,XIB 文件以人类可读的形式存在,而 NIB 文件则不是。例如,当我们编译我们创建的项目时,MyFirstiOSProjectViewController.xib文件将在输出文件夹中变为MyFirstiOSProjectViewController.nib。除了二进制转换之外,编译器还会对 NIB 文件进行压缩。因此,NIB 文件的大小将比 XIB 文件小得多。

当应用程序启动时,iOS 会将 NIB 文件作为一个整体加载到内存中,并且其中的所有对象都会被实例化。因此,保留在 NIB 文件中不总是会被使用的对象是浪费内存的。此外,请记住,你正在为移动设备开发,其可用资源与桌面计算机的资源无法匹敌,无论其功能如何。

自 iOS 5 开始,苹果引入了故事板功能,该功能简化了用户界面设计。

更多信息

你可能已经注意到,在检查器面板的属性选项卡中,有一个名为模拟度量的部分。该部分下的选项帮助我们直接在设计师区域看到我们的界面在设备状态栏、工具栏或导航栏下的样子。尽管这些选项保存在 XIB 文件中,但它们与实际运行时的应用程序无关。例如,如果我们将状态栏选项设置为,并不意味着我们的应用程序将没有状态栏启动。

注意

状态栏是显示在设备屏幕顶部的一部分,它向用户显示某些信息,例如当前时间、电池状态和 iPhone 上的运营商名称。

相关内容

  • 创建 UI使用连接访问 UI向控件添加动作的配方

  • 第二章(ch02.html "第二章。用户界面 – 视图")中的添加和自定义视图配方,用户界面 – 视图

  • 第三章(ch03.html "第三章。用户界面 – 视图控制器")中的使用视图控制器加载视图配方,用户界面 – 视图控制器

创建 UI

在这个配方中,我们将学习如何在用户界面中添加和管理控件。

准备工作

让我们在界面中添加一些控件。首先,在 Xamarin Studio 中创建一个新的 iPhone 单视图应用程序项目,并将其命名为ButtonInput。当它打开时,双击ButtonInputViewController.xib解决方案面板中,以 Interface Builder 打开它。

如何实现...

现在我们有一个新项目,并且 Interface Builder 已经打开了ButtonInputViewController.xib文件,我们将向其中添加一些控件。

添加标签

执行以下步骤:

  1. 如果尚未选择,请转到面板并从下拉列表中选择对象

  2. 选择标签对象。将标签拖放到设计师中视图的灰色区域,位于上半部分。

  3. 从左侧和右侧选择并调整标签对象的大小,使其与当接近视图边缘时出现的虚线对齐。

  4. 再次,当标签对象被选中时,转到检查器面板,选择属性选项卡,然后在布局部分单击中间对齐按钮。

恭喜你,你已经在应用程序的主视图中添加了标签

添加按钮

我们将执行类似的步骤在我们的界面中添加按钮,使用以下步骤:

  1. 再次,在面板中,在对象部分,选择按钮对象。它位于标签对象旁边。将其拖放到视图的下半部分。将其中心与之前添加的标签的中心对齐。

  2. 当两个控件的中心几乎对齐时,会出现一条虚线,按钮对象会自动吸附到它上面。

  3. 按钮对象调整到与标签相同的宽度。由于标签具有透明背景,您无法确切地看到它的宽度,当您调整大小时会显示三条虚线,这时您就会知道按钮对象具有相同的宽度。

  4. 现在,让我们向按钮添加一些文本。选择它并转到检查器面板。

  5. 标题字段的属性选项卡中,输入请在此处点击!

  6. 在添加按钮后,通过在菜单栏中导航到文件 | 保存来保存文档。主视图现在应该看起来像以下截图所示:添加按钮

它是如何工作的...

如您所见,尽管 Interface Builder 的一些概念似乎很难,但它使用起来相当简单。它还提供了大量的反馈。当我们拖动对象时,会出现基本作为正确定位控件的对齐点的指南。此外,当我们调整控件大小时,我们会在其旁边看到其尺寸。

您还可以通过修改检查器面板中大小选项卡中的值来调整控件的大小和位置。大小选项卡中的另一个有用功能是自动调整大小自动调整大小为控件提供布局选项,当我们要使我们的应用支持不同的设备方向时,它非常有用。您可以选择一个控件,然后点击自动调整大小部分中左边的方框外部或内部的线条。旁边的图像会动画显示,给您一个印象,当布局改变时控件将如何表现。

还有更多...

现在,让我们尝试在 iOS 模拟器上运行应用。回到 Xamarin Studio,如果尚未选择,请选择项目配置调试。根据您安装的 iOS SDK 版本,以下截图显示了可用的调试目标选项:

还有更多...

选择您喜欢的目标并点击运行按钮。当编译完成后,iOS 模拟器将自动启动并运行我们刚刚创建的应用。您甚至可以用鼠标点击按钮,并看到它做出响应。当然,我们的应用目前还没有其他功能。

设置按钮的标题

设置按钮或标签的标题可以通过简单地双击它并输入首选标题来完成。这样做,并观察 Interface Builder 如何响应以显示要执行的操作。

相关内容

  • 编译 iOS 项目调试我们的应用配方

  • 第二章中的 使用按钮接收用户输入 菜单,用户界面 – 视图

使用出口访问 UI

在本菜谱中,我们将讨论出口的概念及其在 Xamarin.iOS 中的使用。

准备工作

在前面的菜谱中,我们学习了如何添加控件来形成我们应用的基本界面。在本菜谱中,我们将讨论如何在代码中访问和使用这些控件。启动 Xamarin Studio 并打开我们之前创建的ButtonInput项目。通过在解决方案面板中双击它来在 Interface Builder 中打开项目的ButtonInputViewController.xib

如何操作...

执行以下步骤以使用出口访问 UI:

  1. 在辅助编辑器中,选择ButtonInputViewController.h文件,按Ctrl键,并将其从标签拖动到 Objective-C 源文件中,如图所示:如何操作...

  2. 当你释放光标时,会出现一个上下文窗口,类似于以下截图所示:如何操作...

  3. 在上下文窗口的名称字段中输入labelStatus,然后点击连接

  4. 对于按钮也做同样的操作,并将其命名为buttonTap。通过在菜单栏中导航到文件 | 保存或按键盘上的Cmd + S来保存 Interface Builder 文档。

  5. 在 Xamarin Studio 中,在ButtonInputViewController类的ViewDidLoad方法中输入以下代码:

    // Create and hook a handler to our button's TouchUpInside event
    // through its outlet
    this.buttonTap.TouchUpInside += delegate(object sender, EventArgs e) {
      this.labelStatus.Text = "Button tapped!";
    };
    

    这段代码片段为按钮的TouchUpInside事件添加了一个处理程序。这个事件类似于System.Windows.FormsButton控制的Clicked事件。它还展示了匿名方法的使用,这显示了 Xamarin.iOS 如何为.NET 开发者提供 C#功能。

就这样!我们的应用现在已经准备好了具有功能控件。在模拟器上编译并运行它。当你点击按钮时,你会看到标签文本的变化。

它是如何工作的...

出口机制基本上是一种将 Interface Builder 对象与代码连接起来的方法。这是必要的,因为这是我们访问使用 Interface Builder 创建的用户界面对象的唯一方式。这就是 Interface Builder 的工作原理,它不仅仅是一个 Xamarin.iOS 的解决方案。一个对象的出口提供了一个变量,这样我们就可以在项目中使用它。Xamarin.iOS 使开发者的生活变得更加容易,因为当我们创建 Interface Builder 中的出口并将它们连接起来时,Xamarin Studio 会在后台自动生成与这些出口相关的代码。这就是ButtonInputViewController.designer.cs文件添加的内容,为我们提供了访问我们创建的控件的方法:

[Outlet]
MonoTouch.UIKit.UILabel labelStatus { get; set; }

[Outlet]
MonoTouch.UIKit.UIButton buttonTap { get; set; }

这些属性为我们提供了访问控件的方法。它们被Outlet属性装饰。您可以看到属性名称与我们为出口输入的名称完全相同。这非常重要,因为我们只需要为出口提供一次名称,我们不必担心在代码的不同部分重复相同的命名约定。此外,请注意,控件变量的类型与我们在用户界面中拖放的控制类型完全相同。这些信息存储在 XIB 文件中,Xamarin Studio 相应地读取这些信息。

还有更多...

要删除出口,您首先必须断开它们。例如,要删除buttonTap出口,请按Ctrl并单击按钮。在出现的面板中,单击出口旁边的x按钮,如图下所示。这将断开出口。

还有更多...

在此之后,从 Objective-C 源文件中删除以下代码:

@property (retain, nonatomic) IBOutlet UIButton *buttonTap;

当您保存文档时,出口将从 Xamarin Studio 项目中删除。

通过代码添加出口

添加出口的另一种方法是创建一个 C#类中的属性,并用Outlet属性装饰它:

[Outlet]
UIButton ButtonTap { get;	set; }

当您在界面构建器中打开 XIB 文件时,出口将被添加到用户界面中。然而,您仍然需要将其连接到相应的控件。最简单的方法是按Ctrl,单击出口对应的控件,然后从新引用出口拖动到设计区域左侧的文件所有者对象,如图下所示:

通过代码添加出口

当您释放鼠标时,从出现的迷你上下文菜单中选择ButtonTap出口。

注意

注意,是 Xamarin Studio 监视在界面构建器中做出的更改,而不是相反。当在 Xamarin 项目中做出更改时,请确保始终从 Xamarin Studio 打开 XIB 文件。

参见

  • 界面构建器创建用户界面向控件添加动作的食谱

  • 在第二章用户界面 – 视图中的添加和自定义视图食谱,用户界面 – 视图

向控件添加动作

在这个食谱中,我们讨论了动作的概念及其与 Xamarin.iOS 的用法。

准备工作

在这个食谱中,我们将讨论如何使用动作与用户界面的控件。

  1. 在 Xamarin Studio 中创建一个新的 iPhone 单视图应用程序项目,并将其命名为ButtonInputAction

  2. 在界面构建器中打开ButtonInputActionViewController.xib,并添加与之前食谱中ButtonInput项目相同的控件、出口和连接。现在不要在项目中添加任何代码。

如何做...

向界面对象添加动作与添加出口类似,如下所示:

  1. 在 Interface Builder 中,按Ctrl并从按钮拖动到源代码文件。

  2. 在将显示的上下文窗口中,将连接字段从Outlets更改为Action

  3. 名称字段中输入OnButtonTap,如果尚未选择,请选择触摸内部

  4. 点击连接按钮并保存文档。

  5. ButtonInputActionViewController类中,添加以下方法:

    partial void OnButtonTap(NSObject sender)
    {
    
      this.labelStatus.Text = "Button tapped!";
    
    }
    

应用程序已经准备好了!在模拟器中编译并运行它。点击按钮,您会看到标签中的文本改变,就像我们在上一个应用程序中创建的那样。

它是如何工作的...

Objective-C 中的动作相当于 C#中的控制事件。它们负责传递各种对象的信号通知。在这个例子中,我们不是在按钮的TouchUpInside事件上连接一个处理程序,而是为它添加了一个动作。如您可能已经注意到的,我们添加的作为动作处理程序的方法被声明为partial;这是因为 Xamarin Studio 已经为我们声明了一个部分方法声明。这是我们在 Interface Builder 中保存文档时产生的代码:

[Action ("OnButtonTap:")]
partial void OnButtonTap (MonoTouch.Foundation.NSObject sender);

方法的部分声明被标记为Action属性。这是来自MonoTouch.Foundation命名空间的一个属性,它允许我们将方法暴露为 Objective-C 动作。您可以看到,传递给属性的字符串参数与我们输入到 Interface Builder 中的动作名称完全相同,只是在后面加了一个冒号(:)。

注意

Objective-C 中的冒号表示参数的存在。例如,doSomethingdoSomething;不同。区别在于第一个不接受任何参数,而第二个接受一个参数。

如何工作...

动作名称末尾的冒号表示有一个参数,在这种情况下,参数是MonoTouch.Foundation.NSObject发送者。这就是我们在模拟器中点击按钮时我们的应用程序看起来像什么:

更多...

上一节中的示例只是为了展示如何在 Xamarin.iOS 项目中实现动作。用动作替换事件基本上是开发者的决定。

参见

  • Interface Builder创建 UI使用 Outlets 访问 UI菜谱

编译 iOS 项目

在这个菜谱中,我们将讨论如何使用 Xamarin.iOS 编译一个项目。

准备工作

Xamarin Studio 提供了许多不同的编译选项。在这个菜谱中,我们将讨论这些选项。我们将使用本章中较早创建的ButtonInput项目。

如何做到这一点...

执行以下步骤以使用 Xamarin.iOS 编译 iOS 项目:

  1. 在 Xamarin Studio 中加载项目后,转到项目 | 按钮输入选项

  2. 在出现的窗口中,从左侧面板的构建部分选择iOS 构建。选择项目配置为调试,平台为iPhoneSimulator

  3. 链接器行为字段中,从组合框中选择链接所有程序集

  4. SDK 版本字段中,如果尚未选择,请选择默认

  5. 现在转到左侧面板上的iOS 应用

  6. 摘要选项卡中,在应用程序名称字段中输入Button Input,在版本字段中输入1.0。在部署目标组合框中选择版本6.0。以下截图显示了iOS 应用选项窗口:如何操作...

  7. 点击确定按钮,通过菜单栏中的构建 | 构建所有导航来编译项目。

它是如何工作的...

我们为我们的项目设置了一些选项。让我们看看这些选项为编译定制提供了什么:

iOS 构建选项

我们设置的第一项与链接器相关。链接器是由 Xamarin.iOS 团队开发的工具,并在 SDK 中提供。每次编译 Xamarin.iOS 项目时,编译器不仅编译项目,还需要 Xamarin.iOS 框架的所有程序集,以便最终应用能够在设备(或模拟器)上运行。这实际上意味着每个应用都附带其自己的 Xamarin.iOS 框架编译版本。最终的应用程序包大小相当大。这就是链接器的作用所在。它所做的就是删除所有未使用的代码的程序集,以便编译器只编译应用所需和使用的代码。这导致应用程序包的大小大大减小,这在移动应用中是一个宝贵的资产。以下是一些链接器选项:

  • 不链接:当在模拟器上调试时使用此选项。链接器被关闭,所有程序集都按原样编译。这提供了更快的编译时间。

  • 仅链接 SDK 程序集:链接器仅删除 Xamarin.iOS 框架的程序集。项目程序集保持完整。它有效地减少了最终应用的大小。

  • 链接所有程序集:链接器在所有程序集上被激活。这会进一步减少大小。如果代码中使用了反射或序列化,使用此选项时需要小心。代码中通过反射使用的类型和方法对链接器来说是透明的。如果代码中存在这种情况,请使用Preserve属性装饰这些类型或方法。此属性基本上是通知链接器不要参与精简过程。

SDK 版本字段中,我们设置将用于编译应用的 iOS SDK 版本。将其设置为默认将自动选择系统上安装的最高 SDK 版本。

注意

在为模拟器编译时,不建议开启链接器。这是因为编译器没有在 iPhoneSimulator 平台上编译 Xamarin.iOS 程序集,因此它们被直接使用。开启链接器只会导致编译完成所需的时间更长,对最终应用程序包的大小没有影响。

iOS 应用程序选项

在项目选项中构建部分的iOS 应用程序窗口中,我们设置了三个选项:

  • 第一个选项是应用程序名称。这是将在模拟器、设备和 App Store 上显示的应用程序包的名称。正如我们所看到的,我们通常可以在名称中添加空格。

  • 第二个选项,版本,定义了应用程序的版本。当应用程序最终通过 App Store 分发时,它将显示为应用程序的版本。

  • 第三个选项,部署目标,是应用程序可以安装的最小 iOS 版本。

还有更多...

还有两个选项窗口。这些是iOS 包签名iOS IPA 选项。它们将在第十四章的食谱中详细讨论,部署

参见

  • 第十四章(部署)中第十四章. 部署的为 App Store 准备我们的应用程序食谱

调试我们的应用程序

本食谱提供了在模拟器上调试 Xamarin.iOS 应用程序的信息。

准备工作

Xamarin.iOS 与 Xamarin Studio 结合,提供用于在模拟器或设备上调试应用程序的调试器。在本食谱中,我们将了解如何使用调试器调试 Xamarin.iOS 应用程序。打开 Xamarin Studio 并加载ButtonInput项目。确保将项目配置设置为调试 | iPhone

如何操作...

执行以下步骤以调试您的应用程序:

  1. Xamarin Studio 支持断点。要激活一行上的断点,请点击行号左侧的空间来设置它。在ButtonInputViewController.cs文件中的以下行设置断点:

    this.labelStatus.Text = "Button tapped!";
    

    以下截图显示了 Xamarin Studio 中的断点外观:

    如何操作...

  2. 通过点击运行按钮或通过菜单栏导航到运行 | 开始调试来编译和调试项目。Xamarin Studio 的状态将显示消息等待调试器连接…

  3. 当模拟器打开并加载应用程序时,请关注应用程序输出面板中提供的信息。

  4. 点击应用程序按钮。执行将暂停,Xamarin Studio 将以黄色突出显示断点。将鼠标移至断点行中的labelStatus变量上。将显示一个窗口,其中包含所有已评估变量的成员,如下面的截图所示:如何操作...

  5. 要停止调试,请点击工具栏中的停止按钮。

工作原理...

所使用的调试器被称为软调试器。之所以称为“软调试器”,是因为它依赖于运行时和 Xamarin Studio,两者结合提供统一的调试平台。当调试过程开始时,Xamarin Studio 开始监听来自应用调试信息。在模拟器和设备上调试也是如此。当应用执行时,它开始将信息发送回 Xamarin Studio。然后,它在应用程序输出面板中显示这些信息,该面板会自动激活。调试时的典型应用程序输出包括已加载的程序集信息、开始执行的所有线程,以及如果有,可用的断点。

还有更多...

Console.WriteLine()方法也可以用于调试目的。调试器会处理这个问题,并将方法的输出重定向到 Xamarin Studio 的应用程序输出面板。

调试时的应用性能

当编译用于调试目的时,编译器会产生更大、更慢的代码。这是因为它生成了提供适当调试信息所需的额外代码。这就是为什么在调试应用时,应用的执行速度比简单运行情况慢得多。在生成应用的发布副本之前,请记住使用发布 | iPhone项目配置来编译它,以避免运行时执行缓慢。

FinishedLaunching中的断点

不在FinishedLaunching方法中编写复杂代码的另一个原因是,在大多数情况下,你将无法对其进行调试。如果你在FinishedLaunching中设置断点,应用执行将暂停,但当时间限制达到时,iOS 将终止应用。

参见

  • 在第十四章“部署”中的创建配置文件配方

第二章:用户界面 – 视图

在本章中,我们将涵盖以下主题:

  • 添加和自定义视图

  • 使用按钮接收用户输入

  • 显示图像

  • 显示和编辑文本

  • 使用键盘

  • 显示进度

  • 显示比屏幕更大的内容

  • 在分页内容中导航

  • 显示警报

  • 创建自定义视图

  • 视图样式

简介

应用程序的用户界面UI)对于提供用户与设备(无论是计算机、移动电话还是平板电脑)的便捷通信方式至关重要。在移动设备上,用户界面不仅至关重要,而且是与用户交互的唯一方式。开发者在为移动设备开发应用程序时必须应对各种限制和约束。移动设备的处理能力不匹配桌面 CPU,屏幕也较小,这使得每次选择要显示的信息类型变得更加困难。

在本章中,我们将讨论 iOS 应用程序 UI 的关键组件。我们将了解如何使用和自定义这些组件来创建丰富的应用程序用户界面,并讨论它们与桌面等价物的相似之处和不同之处。以下是一个组件列表:

  • UIView:这是一个可定制的容器,是大多数 iOS 用户界面控件的基础对象

  • UIButton:这是.NET 世界中的按钮等价物

  • UILabel:这是.NET 世界中的标签等价物

  • UIImageView:这是一个允许我们显示和创建基本图像动画的视图

  • UITextView:这是一个允许我们显示可编辑文本的视图

  • UITextField:这与.NET 的 TextBox 控件类似

  • UIProgressView:这显示已知的长度进度

  • UIScrollView:这提供了显示可滚动内容的能力

  • UIPageControl:这为分为不同页面或屏幕的内容提供导航功能

  • UIAlertView:这是 iOS 中用于向用户显示消息框的默认控件

我们还将讨论如何以编程方式创建这些组件的实例,以及如何高效地样式化和使用它们。

添加和自定义视图

在这个菜谱中,我们将讨论如何使用 Xcode 的 Interface Builder 添加和自定义UIView

准备工作

使用 Interface Builder 添加视图是一个简单的任务。让我们首先在 Xamarin Studio 中创建一个新的iPhone Single View Application项目。将项目命名为FirstViewApp,并使用 Interface Builder 打开FirstViewAppViewController.xib文件。

如何做到这一点...

执行以下步骤:

  1. 要将视图添加到项目中,从垫拖动一个UIView对象到主视图。确保它适合整个窗口区域。为了使UIView可访问,为其创建一个出口并将其命名为subView

    注意

    关于出口的概念及其使用方法将在第一章开发工具中详细讨论。

  2. 选择我们刚刚添加的视图并转到检查器面板。选择属性选项卡,并在背景下拉列表中选择深灰色。现在,选择大小选项卡并将视图的高度减少 60 点。保存文档。

  3. 在模拟器上编译并运行应用程序。结果应该看起来像以下截图所示:如何操作...

    模拟器屏幕上的深灰色部分是我们刚刚添加的视图。

它是如何工作的...

我们已经成功创建了一个包含一个视图的应用程序。当然,这个应用程序不提供任何功能。它只是用来展示如何添加视图并显示它。

视图是 iOS 应用程序界面的基本组件。每个视觉用户界面对象都继承自UIView类。这个概念与.NET 中的表单有些不同。视图管理内容绘制,接受其他视图作为子视图,提供自动调整大小功能,可以接受自身及其子视图的触摸事件,并且许多属性甚至可以动画化。甚至UIWindow也继承自UIView。iOS 开发者将最频繁地使用这个类或其继承者。

当使用 Interface Builder 添加的视图在运行时首次实例化时,它使用通过检查器面板的大小选项卡设置的值来设置其Frame属性。Frame属性是RectangleF类型,它定义了视图在其父视图坐标系中的位置(在我们的例子中,是主窗口)以及其大小(以点为单位)。

注意

在 Objective-C 中,UIView 的frame属性是CGRect类型。此类型在 Xamarin.iOS 中未绑定,因此使用了更熟悉的System.Drawing.RectangleF

父视图是一个视图的父视图,而子视图是其子视图。具有相同父视图的视图被称为兄弟视图。

iOS 中的默认坐标系起源于左上角并向底部和右侧延伸。坐标原点始终相同,且不能通过编程方式更改。

iOS 的坐标系在以下图中显示:

它是如何工作的...

当设置Frame属性时,它会调整Bounds属性。Bounds属性定义了视图在其自身坐标系中的位置以及其大小(以点为单位)。它也是RectangleF类型。Bounds属性的默认位置是(0,0),其大小始终与视图的Frame值相同。这两个属性的尺寸是相互关联的,因此当你更改Frame的大小,Bounds的大小也会相应地改变,反之亦然。你可以更改Bounds属性以显示视图的不同部分。

一个视图的框架可以在位置和位置上超出屏幕。也就是说,具有值 (x = -50, y = -50, width = 1500, height = 1500) 的视图框架是完全可接受的,尽管它将不会在 iPhone 的屏幕上完全可见。

更多内容...

另一点需要注意的是,UIView 类继承自 UIResponder 类。UIResponder 类负责响应用和处理事件。当视图被添加到父视图中时,它成为其响应链的一部分。UIView 类公开了 UIResponder 的属性和方法,我们现在感兴趣描述的是以下两个:

  • IsFirstResponder 属性:这个属性返回一个布尔值,表示视图是否是第一个响应者。基本上,它表示视图是否具有焦点。

  • ResignFirstResponder():这将使视图失去焦点。

以编程方式添加视图

如果我们想以编程方式将视图添加到主视图中,我们将使用以下 UIView.AddSubview(UIView) 方法:

this.View.AddSubview(this.subView);

AddSubview 方法将其参数(UIView 类型)添加到调用者的子视图列表中,并将其 Superview 参数设置为调用者。除非使用 AddSubview 方法将其添加到父视图中,否则视图将不会显示。此外,如果视图已经有一个父视图,并且使用其 AddSubview 方法添加到另一个视图中,其 Superview 将更改为新的调用者。这意味着视图一次只能有一个父视图。

注意

当使用 Interface Builder 将视图作为子视图添加时,不需要使用 AddSubview 方法来显示子视图。然而,当以编程方式添加视图时,必须调用 AddSubview 方法。

对于以编程方式从父视图中删除视图,请调用其 RemoveFromSuperview 方法。如果对没有父视图的视图调用此方法,则不会执行任何操作。当我们想要重用要删除的视图时,必须注意。我们必须保留对其的引用,否则它可能会被释放。

视图内容布局

UIView 的另一个重要属性是 ContentModeContentMode 接受 UIViewContentMode 枚举类型的值。此属性设置 UIView 将如何显示其内容,通常是图像。此属性的默认值是 UIViewContentMode.ScaleToFill。这会将内容缩放到适合视图的确切大小,如果需要则拉伸。UIViewContentMode 的可用值将在本章后面的 显示图像 菜谱中详细解释。

参见

  • 创建自定义视图 菜谱

  • 第一章中的 创建 UI 菜谱,开发工具

  • 第一章中的 使用连接访问 UI 菜谱,开发工具

使用按钮接收用户输入

在这个菜谱中,我们将学习如何使用按钮来接收和响应用户输入。

准备工作

我们在第一章,开发工具中使用了按钮,讨论了如何使用 Interface Builder 将控件添加到用户界面。在本食谱中,我们将更详细地描述UIButton类。在 Xamarin Studio 中打开我们在上一个食谱中创建的FirstViewApp项目。增加我们添加的视图的高度,使其在 Interface Builder 中覆盖整个设备屏幕,并保存文档。

如何操作...

执行以下步骤:

  1. 我们将在我们的界面中以编程方式添加一个按钮。此按钮在被点击时会改变我们视图的背景颜色。打开FirstViewAppViewController.cs文件,并在类中输入以下代码:

    UIButton buttonChangeColor;
    private void CreateButton ()
    {
      RectangleF viewFrame = this.subView.Frame;
      RectangleF buttonFrame = new RectangleF (10f, viewFrame.Bottom - 200f, viewFrame.Width - 20f, 50f);
      this.buttonChangeColor = UIButton.FromType (UIButtonType.System);
      this.buttonChangeColor.Frame = buttonFrame;
      this.buttonChangeColor.SetTitle ("Tap to change view color", UIControlState.Normal);
      this.buttonChangeColor.SetTitle ("Changing color...", UIControlState.Highlighted);
      this.buttonChangeColor.TouchUpInside += this.ButtonChangeColor_TouchUpInside;
      this.subView.AddSubview (this.buttonChangeColor);
    }
    bool isYellow;
    private void ButtonChangeColor_TouchUpInside (object sender, EventArgs e)
    {
      if (this.isYellow) {
        this.subView.BackgroundColor = UIColor.LightGray;
        this.isYellow = false;
      }  else {
        this.subView.BackgroundColor = UIColor.Yellow;
        this.isYellow = true;
      }
    }
    
  2. ViewDidLoad方法中,添加以下行:

    this.CreateButton ();
    
  3. 在模拟器上编译并运行应用。当按钮被点击时,结果应类似于以下截图:如何操作...

它是如何工作的...

在本食谱中,我们向用户界面添加了一个按钮。此按钮会改变其父视图的背景颜色。此外,我们没有使用 Interface Builder 就完成了这个任务。

现在我们来看看代码是如何工作的。我们创建以下字段来保存按钮对象:

// A button to change the view's background color
UIButton buttonChangeColor;

CreateButton方法中,我们创建按钮并设置一些属性。该方法如下所示:

// Create the appropriate rectangles for the button's frame
RectangleF viewFrame = this.subView.Frame;
RectangleF buttonFrame = new RectangleF (10f, viewFrame.Bottom - 200f, viewFrame.Width - 20f, 50f);

首先,我们将视图的框架分配给一个名为viewFrame的新变量。然后,我们创建一个名为buttonFrame的新RectangleF对象。此对象将被分配给按钮的Frame属性。现在我们为我们的按钮有了框架,我们可以按照以下代码片段初始化它:

// Create the button.
this.buttonChangeColor = UIButton.FromType (UIButtonType.System);
this.buttonChangeColor.Frame = buttonFrame;

按钮是通过UIButton.FromType(UIButtonType)静态方法初始化的。此方法接受一个UIButtonType类型的参数,并返回 iOS SDK 中包含的预定义按钮类型。这里使用的UIButtonType.System按钮枚举值是默认类型,没有边框或背景。在buttonChangeColor对象初始化后,我们将其框架设置为之前创建的RectangleF值。

现在我们已经为按钮提供了初始化代码,我们将设置其标题(没错,不止一个),如下所示:

// Set the button's titles
this.buttonChangeColor.SetTitle ("Tap to change view color", UIControlState.Normal);
this.buttonChangeColor.SetTitle ("Changing color...", UIControlState.Highlighted);

我们调用了两次UIButton.SetTitle(string, UIControlState)方法。此方法负责为每个给定的按钮状态设置按钮的标题。字符串参数是实际显示的标题。第二个参数是UIControlState类型的枚举。此参数表示应用于控件的不同控件状态。这些控件状态如下:

  • 正常:这是启用控件默认的空闲状态。

  • 高亮:这是控件在触摸抬起事件发生时的状态。

  • 禁用:这是控件被禁用且不接受任何事件的状态。

  • 选中:这是控件被选中的状态。在大多数情况下,此状态不适用。然而,当需要选择状态时,它很有用,例如在UISegmentedControl对象中。

  • 应用:这是为应用程序使用提供的附加控件状态值。

  • 保留:这是供内部框架使用的。

因此,使用UIButton.SetTitle(string, UIControlState)方法,我们设置了按钮在其默认状态下将显示的标题,以及按钮在被点击时将显示的标题。

然后,我们使用以下代码设置按钮的TouchUpInside事件处理程序,并将其作为子视图添加到subView中:

this.buttonChangeColor.TouchUpInside += this.ButtonChangeColor_TouchUpInside;
// Display the button
this.subView.AddSubview (this.buttonChangeColor);

buttonChangeColor_TouchUpInside事件内部,我们根据我们声明的布尔字段更改视图的背景颜色,如下面的代码所示:

if (this.isYellow) {
  this.subView.BackgroundColor = UIColor.DarkGray;
  this.isYellow = false;
} else {
  this.subView.BackgroundColor = UIColor.Yellow;
  this.isYellow = true;
}

这通过将视图的BackgroundColor属性设置为所需的UIColor类实例来完成,如前面突出显示的代码所示。UIColor对象是一个具有许多不同静态方法和属性的类,允许我们创建不同颜色的对象。

当你在模拟器上编译并运行应用程序时,请注意当你点击按钮时视图的颜色变化。同时请注意,当鼠标光标(或设备上的手指)"触摸"按钮时,按钮的标题如何变化。

还有更多...

在这个示例中,我们使用了UIButton.FromType(UIButtonType)静态方法来初始化按钮。以下是UIButtonType枚举标志的简要描述:

  • 系统:这是按钮的默认类型。

  • 自定义:这是一个无边框的透明按钮。在创建具有图像背景的自定义按钮时使用此标志。按钮的标题不是透明的。

  • 圆角矩形:这是具有圆角的默认按钮类型。截至 iOS 7,此类型的UIButton已被弃用。请使用UIButtonType.System代替。

  • 详细披露:这是一个揭示与项目相关额外信息的圆形蓝色按钮。

  • 信息浅色:这是一个带有代表信息显示的字母(i)的浅色按钮。

  • 信息深色:这与 InfoLight 相同;它以深色显示。

  • 联系人添加:这是一个带有白色加号(+)的圆形蓝色按钮。通常,此按钮用于显示要添加到项目中的联系人信息。

改变按钮的外观

对于使用UIButtonType.Custom类型创建自定义按钮,请使用UIButton类的SetBackgroundImageSetImage方法。它们都接受一个UIImage和一个UIControlState参数,以便可以设置不同控件状态的不同图像。在设置按钮的图像时,务必相应地设置UIButton.ContentMode属性,无论是否创建自定义按钮。

SetImageSetBackgroundImage方法提供的功能也可以在 Interface Builder 中检查器垫属性选项卡中的相应图像背景字段中完成。从下拉列表框中选择要设置所需图像的状态,并设置图像文件的路径,如下面的截图所示:

更改按钮外观

参见

  • 添加和自定义视图配方

  • 显示图像配方

  • 创建自定义视图配方

  • 样式化视图配方

  • 第一章开发工具创建 UI配方

显示图像

在这个配方中,我们将学习如何使用UIImageView类在屏幕上显示图像。

准备工作

在这个配方中,我们将了解如何在项目中捆绑和显示图像。需要显示一个图像文件。这里使用的图像文件命名为Toroni.jpg。在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序项目,并将其命名为ImageViewerApp

如何操作...

以下是这个配方的步骤:

  1. 在 Interface Builder 中打开ImageViewerAppViewController.xib文件。

  2. 在其视图中添加一个UIImageView对象。将UIImageView对象与名为imageDisplay的输出连接起来。

  3. 保存文档。

  4. 在 Xamarin Studio 中,进入ImageViewerAppViewController类,输入以下代码:

    public override ViewDidLoad()
    {
      base.ViewDidLoad();
      this.imageDisplay.ContentMode = UIViewContentMode.ScaleAspectFit;
      this.imageDisplay.Image = UIImage.FromFile("Toroni.jpg");
    }
    
  5. 右键点击解决方案垫中的项目,导航到添加 | 添加文件…。选择要显示的图像文件,然后点击打开

  6. 右键点击你刚刚添加的图片文件,导航到构建操作 | 捆绑资源

  7. 最后,在模拟器上编译并运行应用程序。你添加到项目中的图片应该显示在屏幕上,如下面的截图所示:如何操作...

工作原理...

UIImageView类基本上是一个为显示图像而定制的视图。当你在一个项目中添加图片时,其构建操作必须在解决方案垫中设置为捆绑资源;否则,图片将不会被复制到应用程序捆绑包中。幸运的是,Xamarin Studio 足够智能,可以自动处理图像的此设置。

当显示图像时,ContentMode属性非常重要。它设置了UIView(在本例中为UIImageView)对象将如何显示图像。我们将其设置为UIViewContentMode.ScaleAspectFit,以便它将调整大小以适应UIImageView的区域,同时保持宽高比不变。如果ContentMode属性保留为默认的ScaleToFill值,输出将类似于以下截图所示:

工作原理...

要设置UIImageView应显示的图片,我们使用UIImage对象设置其Image属性,如下面的代码所示:

this.imageDisplay.Image = UIImage.FromFile("Toroni.jpg");

ContentMode 属性接受一个名为 UIViewContentMode 的枚举类型。提供的值如下:

  • ScaleToFill: 这是基本 UIView 对象的默认值。它将内容缩放以适应视图的大小,必要时更改宽高比。

  • ScaleAspectFit: 这将内容缩放以适应视图的大小,同时保持其宽高比。视图内容的剩余区域变为透明。

  • ScaleAspectFill: 这会将内容缩放以填充视图的大小,同时保持其宽高比。部分内容可能会被裁剪掉。

  • Redraw: 当视图的边界发生变化时,其内容不会被重新绘制。此值会导致内容被重新绘制。在 CPU 周期内,绘制内容是一项昂贵的操作,因此在使用此值与大量内容一起使用之前请三思。

  • Center: 这将内容放置在视图的中心,同时保持其宽高比。

  • Top, Bottom, Left, Right, TopLeft, TopRight, BottomLeft, 和 BottomRight: 这些将内容在视图中对齐到相应的值。

更多内容...

UIImage 类是表示图像信息的对象。它支持的文件格式如下表所示:

文件格式 文件扩展名
可移植网络图形 (PNG) .png
联合图像专家小组 (JPEG) .jpg, .jpeg
标记图像文件格式 (TIFF) .tiff, .tif
图形交换格式 .gif
Windows 位图格式 .bmp
Windows 图标格式 .ico
Windows 光标 .cur
XWindow 位图 .xbm

注意

UIImageView 类不支持动画 GIF 图像文件。当动画 GIF 被设置为 UIImageViewImage 属性时,只有其第一帧将以静态图像的形式显示。

使用不同屏幕尺寸的图像

为背景创建图像为开发者提供了为他们的应用程序创建丰富和优雅的用户界面的能力。为视图创建背景的首选图像文件格式是 PNG。然而,自从 iPhone 4 发布以来,屏幕分辨率已经提高。为了在应用程序中支持两种屏幕分辨率,iOS SDK 提供了一个简单的解决方案。只需将图像保存为高分辨率,并在扩展名之前添加一个 @2x 后缀。例如,名为 Default.png 的文件的高分辨率版本名称将是 Default@2x.png。此外,无需额外的代码即可使用这两个文件。只需使用 UIImage.FromBundle(string) 静态方法,传递不带扩展名的文件名。以下行代码将根据屏幕分辨率加载适当的文件:

this.imageDisplay = UIImage.FromBundle("Default");

iOS 会根据应用程序运行的设备加载适当的文件。

注意

前面的情况仅适用于 PNG 图像文件。

参见

  • 添加和自定义视图 烹饪法

  • 在 第七章 的 选择图像和视频 烹饪法中,多媒体资源

显示和编辑文本

在这个菜谱中,我们将学习如何使用编辑功能显示简单的文本块。

准备工作

在这个菜谱中,我们将讨论UITextView的使用以及如何使用它显示可编辑文本。在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序项目,并将其命名为TextViewApp

如何做到这一点...

执行以下步骤:

  1. 在 Interface Builder 中打开TextViewAppViewController.xib

  2. 在视图的顶部附近添加一个UIButton,在其下方添加一个UITextView。将两个对象连接到它们的出口。

  3. 保存文档。

  4. 回到 Xamarin Studio,在TextViewAppViewController类中输入以下ViewDidLoad方法:

    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.buttonFinished.Enabled = false;
      this.buttonFinished.TouchUpInside += (sender, e) => {
    
        this.myTextView.ResignFirstResponder();
    
      } ;
      this.myTextView.Delegate = new MyTextViewDelegate(this);
    }
    
  5. 添加以下嵌套类:

    private class MyTextViewDelegate : UITextViewDelegate
    {
    
      public MyTextViewDelegate (TextViewAppViewController parentController)
      {
        this.parentController = parentController;
      }
      private TextViewAppViewController parentController;
    
      public override void EditingStarted (UITextView textView)
      {
        this.parentController.buttonFinished.Enabled = true;
      }
    
      public override void EditingEnded (UITextView textView)
      {
        this.parentController.buttonFinished.Enabled = false;
      }
    
      public override void Changed (UITextView textView)
      {
        Console.WriteLine ("Text changed!");
      }
    
    }
    
  6. 在模拟器上编译并运行应用程序。在文本视图中点击某个位置,键盘将出现。输入一些文本,然后点击完成按钮以隐藏键盘。

它是如何工作的...

UITextView类提供了一个显示可编辑文本块的对象。为了响应文本视图的事件,我们实现了一个从UITextViewDelegate继承的类(如下面的代码所示),它将作为文本视图的代理:

private class MyTextViewDelegate : UITextViewDelegate
{
  public MyTextViewDelegate (TextViewAppViewController parentController)
  {this.parentController = parentController;}
  private TextViewAppViewController parentController;

我们声明了一个接受TextViewAppViewController对象的构造函数,这样我们就可以访问我们的控件并获取我们控制器的实例。

然后,我们重写了UITextViewDelegate类的三个方法,如下面的代码所示:

public override void EditingStarted (UITextView textView)
{
  this.parentController.buttonFinished.Enabled = true;
}

public override void EditingEnded (UITextView textView)
{
  this.parentController.buttonFinished.Enabled = false;
}

public override void Changed (UITextViewtextViewUITextView textView)
{
  Console.WriteLine ("Text changed!");
}

这些方法是在相应事件被触发时会被调用的处理程序。当点击文本视图时,会调用EditingStarted方法。我们在其中启用完成按钮。当我们向文本视图中输入一些文本时,会调用Changed方法,我们可以在 Xamarin Studio 的应用程序输出面板中看到Console.WriteLine方法的输出。最后,当我们点击完成按钮时,键盘隐藏,并调用EditingEnded方法。此方法允许我们禁用按钮。

ViewDidLoad方法中,我们为按钮的TouchUpInside事件分配了一个处理程序,如下面的代码所示:

this.buttonFinished.TouchUpInside += (sender, e) => {
  this.myTextView.ResignFirstResponder ();
};

我们在文本视图的ResignFirstResponder()方法中调用它,这样当按钮被点击时,文本视图将失去焦点,导致键盘隐藏。然后,我们将我们创建的代理的新实例分配给文本视图的Delegate属性,传递TextViewAppViewController对象的实例,如下面的代码所示:

this.myTextView.Delegate = new MyTextViewDelegate (this);

更多内容...

Objective-C 中的委托与 C#中的委托有所不同。尽管在两个世界中,它们最常用的用途都是提供对某种形式的事件通知机制的访问,但在 Objective-C 中,这种机制要复杂一些。C#委托类似于 C 或 C++编程语言中的函数指针。它是一个包含对特定签名的特定方法引用的对象。另一方面,Objective-C 委托是一种符合特定协议的特定类型的对象。它基本上是一个封装了一个或多个方法(以及/或其它成员)的对象,这些方法充当事件处理器。

注意

Objective-C 协议类似于 C#中的接口。

委托对象的概念一开始可能看起来有些复杂,但实际上并不难理解。关于事件通知机制,Xamarin.iOS 通过为大多数对象提供事件(包括这里描述的UITextView)来简化.NET 开发者的工作。

参见

  • 使用键盘菜谱

使用键盘

在这个菜谱中,我们将讨论设备虚拟键盘使用的一些重要方面。

准备工作

在上一个菜谱中,我们讨论了如何编辑文本。在这个菜谱中,我们将讨论一些我们可以做,甚至必须做的事情来有效地使用键盘。在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序项目,并将其命名为KeyboardApp

如何做...

执行以下步骤:

  1. 在 Interface Builder 中打开KeyboardAppViewController.xib文件。

  2. 在视图的下半部分添加一个UITextField对象,并将其连接到一个出口。

  3. 保存文档。

  4. 在 Xamarin Studio 中,在KeyboardAppViewController类中输入以下代码:

    private NSObject kbdWillShow, kbdDidHide;
    public override void ViewDidLoad()
    {
    
      base.ViewDidLoad();
    
      this.emailField.KeyboardType = UIKeyboardType.EmailAddress;
      this.emailField.ReturnKeyType = UIReturnKeyType.Done;
    
      this.kbdWillShow = UIKeyboard.Notifications.ObserveWillShow((s, e) => {RectangleF kbdBounds = e.FrameEnd;
        RectangleF textFrame = this.emailField.Frame;
          textFrame.Y -= kbdBounds.Height;
      this.emailField.Frame = textFrame;
      } );
      this.kbdDidHide = UIKeyboard.Notifications.ObserveDidHide((s, e) => {
        RectangleF kbdBounds = e.FrameEnd;
        RectangleF textFrame = this.emailField.Frame;
        textFrame.Y += kbdBounds.Height;
        this.emailField.Frame = textFrame;
      } );
    
      this.emailField.ShouldReturn = delegate(UITextField textField) {
        return textField.ResignFirstResponder ();
      } ;
    
    }
    
  5. 在模拟器上编译并运行应用程序。点击文本框,观察它向上移动以避免被键盘隐藏。点击键盘上的完成按钮,观察文本框在键盘隐藏时返回到其原始位置。

它是如何工作的...

iOS 中有各种类型的键盘。由于屏幕尺寸有限,无法一次性显示所有按键,因此根据我们需要用户提供的文本输入设置适当的键盘类型是一种良好的做法。在这个项目中,我们将键盘设置为电子邮件地址类型。我们还在以下代码中自定义了返回键的类型,将其设置为完成

this.emailField.KeyboardType = UIKeyboardType.EmailAddress;
this.emailField.ReturnKeyType = UIReturnKeyType.Done;

当键盘显示时,确保它不会遮挡必要的 UI 元素是开发者的责任。在这种情况下,由于我们提供了用户输入文本的能力,我们必须确保文本框是可见的,这样用户才能看到正在输入的内容。为此,我们使用以下代码在默认的通知中心添加了两个观察者:

// Add observers for the keyboard
this.kbdWillShow = UIKeyboard.Notifications.ObserveWillShow((s, e) => {

通知中心是 iOS 提供系统级通知的机制。通常,可以通过NSNotificationCenter.DefaultCenter静态属性访问它。然而,Xamarin.iOS 提供了一些 API,这些 API 简化了我们的操作。在这个菜谱的示例项目中,你可以找到这两个 API 的使用示例。在这个菜谱中,我们使用的是 Xamarin 的 API。

通过调用UIKeyboard.Notifications.ObserveWillShow并将其传递一个处理程序,我们订阅了通知中心,以便在键盘即将显示时得到通知。此处理程序是EventHandler<UIKeyboardEventArgs>类型,UIKeyboardEventArgs参数为我们提供了,包括键盘显示后的框架(如下面的代码所示):

// Get the keyboard's bounds
RectangleF kbdBounds = e.FrameEnd;

然后,我们使用以下代码将文本字段的框架存储在一个变量中:

// Get the text field's frame
RectangleF textFrame = this.emailField.Frame;

我们使用以下代码值来减少框架的Y位置,以便文本字段向上移动:

// Change the y position of the text field frame
textFrame.Y -= kbdBounds.Height;

当新框架设置为emailField(如下面的代码所示)时,它将移动到新位置:

this.emailField.Frame = textFrame;

第二个处理程序是在键盘关闭后需要将文本字段移回原始位置的。它与第一个处理程序几乎相同,除了两个不同之处。使用UIKeyboard.Notifications.ObserveDidHide方法。此方法将在键盘隐藏后触发我们的处理程序。在这个处理程序中,我们只是确保我们将文本字段的定位调整回原来的位置。

ViewDidLoad方法的最后几行代码中设置了UITextField类的ShouldReturn属性。此属性接受一个UITextFieldCondition类型的委托,如下面的代码所示:

this.emailField.ShouldReturn = delegate(UITextField textField) {
  return textField.ResignFirstResponder ();
} ;

当用户在虚拟键盘上点击回车键时,我们添加的处理程序会被调用。在这里,我们调用UITextFieldResignFirstResponder方法,这将隐藏我们的键盘。

更多内容...

类中NSObject类型的两个字段,它们被分配给了我们使用的UIKeyboard.Notifications方法的返回值,包含有关我们添加的观察者的信息。为了移除我们在这里添加的两个观察者,请添加以下代码:

NSNotificationCenter.DefaultCenter.RemoveObserver (this.kbdWillShow);
NSNotificationCenter.DefaultCenter.RemoveObserver (this.kbdDidHide);

注意

在开发使用键盘并支持多个界面方向的应用程序时,必须小心。例如,如果键盘以纵向模式出现,而用户切换到横向模式,则键盘的边界和文本字段的框架都将不同,必须相应地进行调整。

参见

  • 显示和编辑文本 菜谱

  • 在第九章的调整 UI 方向菜谱中,与设备硬件交互

显示进度

在这个菜谱中,我们将讨论如何显示已知长度的进度。

准备工作

在这个菜谱中,我们将讨论UIProgressView控件。此控件提供与.NET 中 ProgressBar 控件类似的功能。在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序项目,并将其命名为ProgressApp

如何实现...

以下是如何使用UIProgressView类的步骤。请注意,在这个菜谱中,我们将使用编程方式添加所有控件,而不使用 Interface Builder。

  1. ProgressAppViewController类文件中添加以下using指令:

    using System.Drawing;
    using System.Threading;
    using System.Threading.Tasks;
    
  2. 在类中添加以下字段:

    UILabel labelStatus;
    UIButton buttonStartProgress;
    UIProgressView progressView;
    float incrementBy = 0f;
    
  3. ViewDidLoad重写中输入以下代码:

    // Initialize the label
    this.labelStatus = new UILabel (new RectangleF (60f, 60f, 200f, 50f));
    this.labelStatus.AdjustsFontSizeToFitWidth = true;
    // Initialize the button
    this.buttonStartProgress = UIButton.FromType (UIButtonType.System);
    this.buttonStartProgress.Frame = new RectangleF (60f, 400f, 200f, 40f);
    
    this.buttonStartProgress.SetTitle ("Tap to start progress!", UIControlState.Normal);
    this.buttonStartProgress.TouchUpInside += delegate {
      // Disable the button
      this.buttonStartProgress.Enabled = false;
      this.progressView.Progress = 0f;
      // Start a progress
      Task.Factory.StartNew(this.StartProgress);
    } ;
    
    // Initialize the progress view
    this.progressView = new UIProgressView (new RectangleF (60f, 200f, 200f, 50f));
    
    // Set the progress view's initial value
    this.progressView.Progress = 0f;
    
    // Set the progress increment value
    // for 10 items
    this.incrementBy = 1f / 10f;
    
    this.View.AddSubview(this.labelStatus);
    this.View.AddSubview(this.buttonStartProgress);
    this.View.AddSubview(this.progressView);
    
  4. 在类中添加以下方法:

    private void StartProgress ()
    {
      float currentProgress = 0f;
      while (currentProgress < 1f)
      {
        Thread.Sleep(1000);
        this.InvokeOnMainThread(delegate {
          // Advance the progress
          this.progressView.Progress += this.incrementBy;
          currentProgress = this.progressView.Progress;
          // Set the label text
          this.labelStatus.Text = string.Format("Current value: { 0}", Math.Round((double)this.progressView.Progress, 2));
          if (currentProgress >= 1f)
          {
            this.labelStatus.Text = "Progress completed!";
            this.buttonStartProgress.Enabled = true;
          }//end if
        } );
      }//end while
    }
    
  5. 在模拟器上编译并运行应用程序。点击按钮并观察进度条填充。

它是如何工作的...

UIProgressView的当前值由其Progress属性表示。其可接受值范围始终从01。因此,当我们初始化它时,我们将其设置为0以确保条形不会填充任何内容。这可以通过以下代码完成:

this.progressView.Progress = 0f;

由于UIProgressView有一个特定的范围,我们需要根据我们需要处理的项数(在这种情况下,10)来分配我们希望它增加的值,使用以下代码:

this.incrementBy = 1f / 10f;

在按钮的TouchUpInside处理程序中,我们禁用按钮并从System.Threading.Tasks中的Task开始我们的进度,如下所示:

this.buttonStartProgress.TouchUpInside += delegate {
  // Disable the button
  this.buttonStartProgress.Enabled = false;
  this.progressView.Progress = 0;
  // Start a progress
  Task.Factory.StartNew(this.StartProgress);
};

StartProgress()方法中,我们启动一个循环来处理需要完成的工作。由于工作在单独的线程上执行,当我们想要更改控件时,必须在主 UI 线程上通过调用InvokeOnMainThread方法来完成,该方法接受一个NSAction类型的参数。NSAction类型参数还可以接受匿名方法,如以下代码所示:

this.InvokeOnMainThread(delegate {
  // Advance the progress
  this.progressView.Progress += this.incrementBy;
  currentProgress = this.progressView.Progress;
  // Set the label text
  this.labelStatus.Text = string.Format("Current value: { 0}", Math.Round((double)this.progressView.Progress, 2));
  if (currentProgress >= 1f)
  {
    this.labelStatus.Text = "Progress completed!";
    this.buttonStartProgress.Enabled = true;
  }//end if
});

更多内容...

进度视图支持两种样式。UIProgressViewStyle.Default(在这个菜谱中使用的那种)和UIProgressViewStyle.Bar。这两种样式之间没有任何功能差异,除了外观。要更改进度视图的样式,将其Style属性设置为之前提到的值之一。

UIProgressView 的高度

设置进度视图的高度没有效果,因为对于控件来说它是恒定的。要创建可变高度的进度条,必须对UIProgressView类进行子类化。

相关内容

  • 使用按钮接收用户输入的菜谱

显示比屏幕更大的内容

在这个菜谱中,我们将学习如何显示超出屏幕边界的内联内容。

准备工作

在这个菜谱中,我们将讨论UIScrollView控件。创建一个新的 iPhone单视图应用程序项目,并将其命名为ScrollApp

如何实现...

创建项目的以下步骤:

  1. 在 Interface Builder 中打开ScrollAppViewController.xib文件。

  2. 在其视图中添加一个UIScrollView对象并将其连接到一个出口。保存文档。

  3. 返回到 Xamarin Studio,在ScrollAppViewController类中添加以下代码:

    // Image view
    UIImageView imgView;
    public override void ViewDidLoad()
    {
      base.ViewDidLoad();
    
      this.imgView = new UIImageView (UIImage.FromFile ("Kastoria.jpg"));
      this.scrollView.ContentSize = this.imgView.Image.Size;
      this.scrollView.ContentOffset = new PointF (200f, 50f);
      this.scrollView.PagingEnabled = true;
      this.scrollView.MinimumZoomScale = 0.25f;
      this.scrollView.MaximumZoomScale = 2f;
      this.scrollView.ViewForZoomingInScrollView = delegate(UIScrollView scroll) {
        return this.imgView;
      } ;
      this.scrollView.ZoomScale = 1f;
    
      this.scrollView.IndicatorStyle = UIScrollViewIndicatorStyle.White;
      this.scrollView.AddSubview (this.imgView);
    
    }
    
  4. 最后,将图像添加到项目中,并将其构建操作设置为BundleResource。iPhone 5S 屏幕大小为 640 x 1136 像素的图像更大为佳。

  5. 在模拟器上编译并运行应用程序。点击并拖动图像以显示不同的部分。通过按下Alt + 左键单击,可以模拟捏合缩放功能。

它是如何工作的...

UIScrollView能够管理超出屏幕大小的内容。滚动视图将显示的内容大小必须在它的ContentSize属性中设置,如下面的代码所示:

this.scrollView.ContentSize = this.imgView.Image.Size;

下面的代码中显示的ContentOffset属性定义了内容在滚动视图边界内的位置:

this.scrollView.ContentOffset = new PointF (200f, 50f);

这意味着图像的(x=200,y=50)点将在UIScrollView的原点(x=0,y=0)处显示。为了提供内容的缩放功能,我们首先设置MinimumZoomScaleMaximumZoomScale属性,如下面的代码所示:

this.scrollView.MinimumZoomScale = 0.25f;
this.scrollView.MaximumZoomScale = 2f;

之前的代码设置了内容的最小和最大缩放比例。值为2表示内容将以两倍大小显示,而值为0.5表示内容将以一半大小显示。

对于实际的缩放操作,我们需要设置ViewForZoomingInScrollView属性,如下面的代码所示:

this.scrollView.ViewForZoomingInScrollView = delegate(UIScrollView scroll) {
  return this.imgView;
};

ViewForZoomingInScrollView属性接受一个UIScrollViewGetZoomView类型的delegate变量,并返回UIView。在这里,返回的是我们创建的图像视图,但也可以使用更高分辨率的另一个图像视图来提供在缩放时更好的图像质量。在将delegate变量分配后,使用以下代码设置初始缩放比例:

this.scrollView.ZoomScale = 1f;

最后,设置滚动视图的指示器样式,如下面的代码所示:

this.scrollView.IndicatorStyle = UIScrollViewIndicatorStyle.White;

指示器是在滚动或缩放时出现的两条线:滚动视图右侧的一条垂直线和底部的一条水平线。这些线告知用户内容的位置。

还有更多...

为了为用户提供更令人愉悦的滚动和缩放效果,UIScrollView公开了Bounce属性。默认情况下,它设置为true,但我们可以选择将其设置为false来禁用它。内容在水平和垂直方向上达到边界时,弹跳会给用户立即反馈。此外,可以单独设置AlwaysBounceHorizontalAlwaysBounceVertical属性。设置其中一个或两个这些属性将使滚动视图在相应的方向上始终弹跳内容,即使内容大小等于或小于滚动视图的边界。因此,实际上不需要滚动。

UIScrollView事件

UIScrollView类公开了一些以下非常有用的事件:

  • Scrolled:当内容正在滚动时发生

  • DecelerationStarted:当用户开始滚动内容时发生

  • DecelerationEnded:当用户完成滚动,内容停止移动时发生

注意

如果已将处理程序分配给 Scrolled 事件,则每当设置 ContentOffset 属性时,它都会被触发。

参见

  • 显示图像 菜谱

  • 显示和编辑文本 菜谱

  • 在分页的内容之间导航 菜谱

在分页的内容之间导航

在这个菜谱中,我们将学习如何使用 UIPageControl 类来提供页面导航。

准备工作

UIPageControl 提供了 iOS 应用程序中多个页面或屏幕的简单视觉表示,这由点表示。以下截图显示了页面控制器的示例,表明内容被分为三个页面:

准备工作

对应当前页面的点被突出显示。它通常与 UIScrollView 结合使用。在 Xamarin Studio 中创建一个新的 iPhone Single View Application 项目,并将其命名为 PageNavApp。在项目中添加三个图像文件,并将它们的 Build Action 设置为 BundleResource

如何操作...

以下创建此项目的步骤:

  1. 在 Interface Builder 中打开 PageNavAppViewController.xib 文件。

  2. UIPageControl 添加到视图的底部,并将 UIScrollView 添加到其上方。调整滚动视图的大小,使其占据视图剩余的所有空间,并保存文档。

  3. 回到 Xamarin Studio,在 PageNavAppViewController 类中输入以下代码:

    UIImageView page1;
    UIImageView page2;
    UIImageView page3;
    public override void ViewDidLoad()
    {
      base.ViewDidLoad();
      this.scrollView.DecelerationEnded += this.ScrollView_DecelerationEnded;
      this.pageControl.ValueChanged += this.PageControl_ValueChanged;
      this.scrollView.Scrolled += delegate {
        Console.WriteLine ("Scrolled!");
      } ;
    
      this.scrollView.PagingEnabled = true;
    
      RectangleF pageFrame = this.scrollView.Frame;
      this.scrollView.ContentSize = new SizeF (pageFrame.Width * 3, pageFrame.Height);
    
      this.page1 = new UIImageView (pageFrame);
     this.page1.ContentMode = UIViewContentMode.ScaleAspectFit;
      this.page1.Image = UIImage.FromFile ("Parga01.jpg");
    
      pageFrame.X += this.scrollView.Frame.Width;
      this.page2 = new UIImageView (pageFrame);
      this.page2.ContentMode = UIViewContentMode.ScaleAspectFit;
      this.page2.Image = UIImage.FromFile ("Parga02.jpg");
    
      pageFrame.X += this.scrollView.Frame.Width;
      this.page3 = new UIImageView (pageFrame);
      this.page3.ContentMode = UIViewContentMode.ScaleAspectFit;
      this.page3.Image = UIImage.FromFile ("Parga03.jpg");
    
      this.scrollView.AddSubview (this.page1);
      this.scrollView.AddSubview (this.page2);
      this.scrollView.AddSubview (this.page3);
    
    }
    
  4. 在类中添加以下方法:

    private void scrollView_DecelerationEnded (object sender, EventArgs e)
    {
      float x1 = this.page1.Frame.X;
      float x2 = this.page2.Frame.X;
    
      float x = this.scrollView.ContentOffset.X;
    
      if (x == x1)
      {
        this.pageControl.CurrentPage = 0;
      }  else if (x == x2)
      {
        this.pageControl.CurrentPage = 1;
      }  else
      {
        this.pageControl.CurrentPage = 2;
    
      }
    
    }
    
    private void pageControl_ValueChanged (object sender, EventArgs e)
    {
    
      PointF contentOffset = this.scrollView.ContentOffset;
    
      switch (this.pageControl.CurrentPage)
      {
    
        case 0:
        contentOffset.X = this.page1.Frame.X;
        this.scrollView.SetContentOffset (contentOffset, true);
        break;
    
        case 1:
        contentOffset.X = this.page2.Frame.X;
        this.scrollView.SetContentOffset (contentOffset, true);
        break;
    
        case 2:
        contentOffset.X = this.page3.Frame.X;
        this.scrollView.SetContentOffset (contentOffset, true);
        break;
    
        default:
        // do nothing
        break;
      }
    
    }
    
  5. 在模拟器上编译并运行应用程序。在滚动视图中水平滚动以更改页面。同样,在页面控制器上点击或滚动以更改页面。

它是如何工作的...

我们需要做的第一件事是将 UIScrollView.PagingEnabled 属性设置为 true,如下面的代码所示:

this.scrollView.PagingEnabled = true;

此属性指示滚动视图在滚动视图边界的倍数处停止滚动,从而提供分页功能。之后,准备将在不同页面上显示的图像视图。在这里,我们注意调整每个图像视图的框架,以便它们彼此相邻,如下面的代码所示:

this.page1 = new UIImageView (pageFrame);

// Frame for 2nd page
pageFrame.X += this.scrollView.Frame.Width;

// Frame for 3rd page
pageFrame.X += this.scrollView.Frame.Width;

我们为两个事件附加了处理程序。第一个是 UIScrollView.DecelerationEnded 事件,当用户滚动滚动视图时,它将调整页面控制器的当前页面。当前页面由滚动视图的 ContentOffset 属性确定,如下面的代码所示:

float x = this.scrollView.ContentOffset.X;
if (x == x1) {
  // First page
  this.pageControl.CurrentPage = 0;
// etc.

我们附加处理器的第二个事件是 UIPageControl.ValueChanged 事件。在这个处理程序中,我们确保当用户在页面控件上点击或拖动时内容会滚动。滚动操作是通过使用 UIScrollView.SetContentOffset(PointF, bool) 方法将 ContentOffset 属性设置为所需图像视图的 Frame.X 属性来执行的,如下面的代码所示:

case 0:
  // Scroll to first page
  contentOffset.X = this.page1.Frame.X;
    this.scrollView.SetContentOffset (contentOffset, true);
  break;
// etc.

SetContentOffset 方法的第二个参数指示滚动视图在滚动时进行动画。

更多...

在这个示例中,使用了不同的 UIImageView 对象。可以根据我们想要显示的内容类型使用任何类型的 UIView 对象。

正确使用 UIPageControl

用户期望在点击或拖动页面控件时滚动到其他页面。仅用于显示页面索引并不是一个好的做法。

参见

  • 显示图像 示例

  • 显示比屏幕大的内容 示例

显示警告

UIAlertView 类为我们提供了向用户显示警告消息的能力。在这个示例中,我们将讨论如何使用这个类并响应用户输入。

准备工作

对于这个示例,在 Xamarin Studio 中创建一个名为 AlertViewApp 的 iPhone 单视图应用程序 项目。在 Xcode 中打开 AlertViewAppViewController.xib 文件,并在其视图中添加一个按钮。别忘了将其连接到一个出口。

如何操作…

执行以下步骤以在应用程序中实现 UIAlertView

  1. 在 Xamarin Studio 中,打开 AlertViewAppViewController.cs 文件并添加以下方法:

    private void ShowAlert(string title, string message)
    {
      // Create the alert
      UIAlertView alertView = new UIAlertView();
      alertView.Title = title;
      alertView.Message = message;
      // Add buttons
      alertView.AddButton("OK");
      alertView.AddButton("Cancel");
      // Add event handler
      alertView.Dismissed += (sender, e) => {
        if (e.ButtonIndex == 0)
        {
          this.btnShowAlert.SetTitle("OK!", UIControlState.Normal);
        }  else
        {  
          this.btnShowAlert.SetTitle("Cancelled!", UIControlState.Normal);
        }//end if else
      };
      // Display it
      alertView.Show();
    }//end void ShowAlert
    
  2. ViewDidLoad 方法中,添加以下行代码:

    this.btnShowAlert.TouchUpInside += (sender, e) => this.ShowAlert("Alert Message", "Tap OK or Cancel");
    
  3. 在模拟器中编译并运行应用程序。

  4. 在视图中点击按钮。应该会显示警告,如下面的截图所示:如何操作…

  5. 点击确定取消显示警告按钮的标题将根据点击的警告按钮而改变。

它是如何工作的...

UIAlertView 是一个模态控件。这意味着一旦它被显示,用户必须采取某种行动才能使其消失。在创建实例后,我们通过 TitleMessage 属性分别设置要显示的标题和消息,如下面的代码所示:

alertView.Title = title;
alertView.Message = message;

我们通过 AddButton 方法添加我们想要显示的按钮,该方法接受一个用于按钮标题的 string 参数,如下面的代码所示:

// Add buttons
alertView.AddButton("OK");
alertView.AddButton("Cancel");

我们实际上可以添加尽可能多的按钮;然而,最好避免添加超过三个或四个按钮。如果需要更多选项,最好向用户显示一个包含这些选项的新视图,而不是使用警告视图。

添加按钮后,我们需要一个事件处理器(如下面的代码所示),它会通知我们用户在警告视图上的操作:

// Add event handler
alertView.Dismissed += (sender, e) => {
  if (e.ButtonIndex == 0)
  {
    this.btnShowAlert.SetTitle("OK!", UIControlState.Normal);
  }  else
  {  
    this.btnShowAlert.SetTitle("Cancelled!", UIControlState.Normal);
  }
};

对于此功能,我们使用 Dismissed 事件,该事件在警报视图被隐藏时触发。这发生在任何按钮被点击时。在事件处理程序中,我们可以通过 UIButtonEventArgs 传递的 ButtonIndex 属性确定哪个按钮被点击。哪个索引对应哪个按钮是很清晰的。我们添加的第一个按钮将具有索引 0,第二个按钮将具有索引 1,依此类推。

最后,为了显示警报视图,我们使用以下代码调用其 Show 方法:

// Display it
alertView.Show();

更多内容...

UIAlertView 也支持文本输入。我们可以在显示之前通过设置其 AlertViewStyle 属性来实现。AlertViewStyle 属性接受以下值:

  • UIAlertViewStyle.Default: 此警报视图将不包含文本输入

  • UIAlertViewStyle.SecureTextInput: 此警报视图将包含一个用于密码输入的文本字段,它会隐藏输入的文本

  • UIAlertViewStyle.PlainTextInput: 在此,仅包含一个简单的文本字段

  • UIAlertViewStyle.LoginAndPasswordInput: 使用此属性,将显示两个文本字段,一个普通和一个安全,用于输入登录凭据

要访问任何提到的文本字段,我们调用 GetTextField 方法,传递适当的索引,如下面的代码所示:

// Get the text that was entered in the second text field
string password = alertView.GetTextField(1).Text;

当然,我们也可以修改文本字段本身。例如,如果我们想禁用隐藏密码文本字段的字符,我们可以添加以下代码行:

alertView.GetTextField(1).SecureTextEntry = false;

参见

  • 使用按钮接收用户输入 菜单

  • 显示和编辑文本 菜单

创建自定义视图

在本菜谱中,我们将学习如何覆盖 UIView 类及其派生类以创建自定义视图。

准备工作

到目前为止,我们已经讨论了许多可用于创建 iOS 应用程序的视图。然而,在许多情况下,我们将需要实现自己的自定义视图。在本菜谱中,我们将了解如何创建自定义视图并使用它。

注意

创建自定义视图非常有用,当我们想要捕获触摸或实现其他自定义行为,如绘图时。

在 Xamarin Studio 中创建一个新的 iPhone Single View Application 项目,并将其命名为 CustomViewApp

如何操作...

完成此菜谱的以下步骤:

  1. 在项目中添加一个新的 C# 类文件,并将其命名为 MyView

  2. 使用以下代码实现:

    using System;
    using MonoTouch.UIKit;
    using MonoTouch.Foundation;
    using System.Drawing;
    
    namespace CustomViewApp
    {
      [Register("MyView")]
      public class MyView : UIView
      {
    
        private UILabel labelStatus;
    
        public MyView (IntPtr handle) : base(handle)
        {
          this.Initialize ();
        }
    
        public MyView (RectangleF frame) : base(frame)
        {
          this.Initialize ();
        }
    
        private void Initialize ()
        {
    
          this.BackgroundColor = UIColor.LightGray;
    
          this.labelStatus = new UILabel (new RectangleF (0f, 400f, this.Frame.Width, 60f));
          this.labelStatus.TextAlignment = UITextAlignment.Center;
          this.labelStatus.BackgroundColor = UIColor.DarkGray;
          this.AddSubview (this.labelStatus);
    
        }
    
        public override void TouchesMoved (NSSet touches, UIEventevtUIEvent evt)
        {
          base.TouchesMoved (touches, evt);
    
          UITouch touch = (UITouch)touches.AnyObject;
    
          PointF touchLocation = touch.LocationInView (this);
    
          this.labelStatus.Text = String.Format ("X: {0} - Y: {1}", touchLocation.X, touchLocation.Y);
    
        }
      }
    }
    
  3. 在 Interface Builder 中打开 CustomViewAppViewController.xib 文件,并在主视图中添加一个 UIView 对象。

  4. Identity Inspector 中的 Class 字段设置为 MyView

  5. 保存文档。

  6. 在模拟器上编译并运行应用程序。点击并拖动视图,观察触摸坐标在屏幕底部的标签中显示。

工作原理...

创建自定义视图时要注意的第一件事是从 UIView 类派生它们,并使用 RegisterAttribute 装饰它们,如下面的代码所示:

[Register("MyView")]
public class MyView : UIView

RegisterAttribute 基本上使我们的类暴露给 Objective-C 世界。请注意,我们传递给它的参数名称必须与我们在 Identity InspectorClass 字段中输入的名称相同。创建以下构造函数很重要:

public MyView (IntPtr handle) : base(handle) {}

此构造函数覆盖了基类的 UIView(IntPtr)。当通过原生代码初始化视图时,始终调用此构造函数。如果我们不覆盖它,初始化对象时将发生异常。此示例中使用的另一个构造函数仅作为指导,说明如果视图是通过编程初始化的,可能会使用什么:

public MyView (RectangleF frame) : base(frame) {}

这两个构造函数都调用了 Initialize() 方法,该方法执行所需的初始化,例如创建将要使用的标签并设置背景颜色。

然后,重写了 TouchesMoved 方法。这是当用户在视图中拖动手指时执行的方法。在方法内部,我们使用以下代码从方法参数的 NSSet 中检索 UITouch 对象:

UITouch touch = (UITouch)touches.AnyObject;

注意

NSSet 对象是一组没有特定顺序的数据集合。它类似于数组。它的 AnyObject 参数从集合中返回一个对象。

UITouch 对象包含有关用户触摸的信息。我们使用以下代码从 UITouch 对象中检索触摸的当前位置:

PointF touchLocation = touch.LocationInView (this);

UITouch 对象的 LocationInView 方法接受一个 UIView 类型的参数,它声明了位置将在哪个视图的坐标系中计算。在这种情况下,我们感兴趣的是 MyView 的坐标。

还有更多...

如果我们想通过编程初始化我们创建的自定义视图,我们将输入以下代码:

MyView myView = new MyView(new RectangleF(0f, 0f, 320f, 480f));

参见

  • 添加和自定义视图 菜谱

  • 在 第三章 的 加载视图与视图控制器 菜谱中,用户界面 – 视图控制器

视图样式化

iOS 通过 UIAppearance 协议提供了一套 API,允许我们一次性调整视图的外观,而无需显式修改每个视图实例的样式属性。如果我们想使特定视图在整个应用程序中具有相同的外观,这尤其有用。

除了全局设置视图的样式属性外,我们还可以定义在某些情况下此视图的外观不同。继续阅读以了解如何实现这一点。

准备工作

我们将在前一个菜谱中创建的现有 CustomViewApp 项目上进行工作。在 Xamarin Studio 中打开该项目。

注意

可下载的代码包含一个用于此菜谱的单独项目。它被命名为 CustomViewApp2

如何操作…

执行以下步骤以完成此菜谱:

  1. 在 Xcode 中打开 CustomViewAppViewController.xib 文件。

  2. 调整我们之前创建的 MyView 对象的大小,以便在顶部留出一些空间。

  3. MyView 对象上方添加一个 UILabel。确保标签是在主视图中添加的,而不是在 MyView 中。

  4. 将两个对象连接到它们各自的出口。

  5. 在 Xamarin Studio 中,将以下代码添加到 CustomViewAppViewControllerViewDidLoad 方法中:

    UILabel.Appearance.BackgroundColor = UIColor.Blue;
    var labelStyle = UILabel.AppearanceWhenContainedIn(typeof(MyView));
    labelStyle.BackgroundColor = UIColor.Green;
    
  6. 在模拟器上编译并运行应用程序。输出应类似于以下截图所示:如何操作…

它是如何工作的…

UIAppearance 类基本上是每个控件特定属性的代理。在 Xamarin.iOS 中,我们可以通过每个控件的静态 Appearance 属性来访问每个控件的代理。我们对这个对象属性所做的更改将反映在整个应用程序的对象实例上。在这种情况下,我们使用以下代码将所有 UILabel 实例的 BackgroundColor 属性设置为蓝色:

UILabel.Appearance.BackgroundColor = UIColor.Blue;

然而,我们可以为 UILabel 的特定实例提供不同的行为。例如,我们希望包含在 MyView 对象中的标签具有绿色背景。我们通过调用静态 AppearanceWhenContainedIn 方法来实现这一点,如下面的代码所示:

var labelStyle = UILabel.AppearanceWhenContainedIn(typeof(MyView));

我们传递我们想要设置特定样式的对象类型。在这种情况下,传递 typeof(MyView) 指示外观代理确保我们引用的是仅包含在 MyView 对象中的对象。然后我们将我们想要的值设置为我们从这个方法返回的对象,如下面的代码所示:

labelStyle.BackgroundColor = UIColor.Green;

更多内容…

通过 AppearanceWhenContainedIn 方法,我们可以针对更具体的样式集。例如,考虑以下代码行:

var labelStyle = UILabel.AppearanceWhenContainedIn(typeof(AnotherView), typeof(MyView));

这将返回一个样式对象,该对象充当 UILabel 所有实例的代理,这些实例是 MyView 的一部分,仅当 MyView 包含在 AnotherView 对象中时。

UIAppearance 的局限性

UIAppearance 协议有一些局限性,如下所示:

  • 只能设置特定的属性。例如,我们无法全局设置视图的 Frame。对于每个控件可以更改的属性集,都可以通过其外观代理访问。如果控件属性不在外观代理中,我们无法为该特定控件的所有实例修改它。

  • 对于修改自定义视图(在这种情况下,MyView)的外观,使用以下代码行将产生一个不希望的结果:

    MyView.Appearance.BackgroundColor = UIColor.Yellow;
    

即,所有 UIView 实例都将具有黄色背景。这是因为 C# 无法覆盖派生类中的静态方法。为了克服这个问题,我们使用派生类上的 GetAppearance<T> 静态方法,如下面的代码所示:

MyView.GetAppearance<MyView>().BackgroundColor = UIColor.Yellow;
// We can also call GetAppearance on the base class:
//UIView.GetAppearance<MyView>().BackgroundColor = UIColor.Yellow;

参见

  • 创建自定义视图菜谱

  • 第三章中的 创建自定义视图控制器 菜谱,用户界面 – 视图控制器

第三章. 用户界面 – 视图控制器

在本章中,我们将涵盖以下食谱:

  • 使用视图控制器加载视图

  • 在不同的视图控制器之间导航

  • 在标签页中提供控制器

  • 模态视图控制器

  • 创建自定义视图控制器

  • 高效使用视图控制器

  • iPad 视图控制器

  • 使用故事板进行 UI 流程设计

  • 在故事板中回滚

简介

到目前为止,我们已经讨论了视图及其使用方法。在大多数现实世界的应用程序场景中,仅使用视图是不够的。Apple 提供了一个另一个基类,即UIViewController类,它负责管理视图。视图控制器可以响应设备通知,例如当设备旋转时,或者可以提供不同的方式来显示和关闭多个视图,甚至其他视图控制器。有许多视图控制器可供我们使用。在本章中,我们将讨论其中最重要的。

这些视图控制器如下:

  • UIViewController:这是所有视图控制器的基类。

  • UINavigationController:这是一个提供多种在不同视图控制器之间导航方式的视图控制器。

  • UITabBarController:这是一个在类似标签的界面中显示多个视图控制器的视图控制器。

  • 针对 iPad 的视图控制器:这些是仅适用于 iPad 设备的视图控制器。

此外,我们将学习如何创建我们自己的自定义视图控制器,并且我们将创建一个用户界面使用故事板文件创建的应用程序。

使用视图控制器加载视图

在这个食谱中,我们将学习如何使用UIViewController类来管理视图。

准备工作

在 Xamarin Studio 中创建一个新的 iPhone 空项目,命名为ViewControllerApp

如何操作...

执行以下步骤以使用视图控制器加载视图:

  1. 向项目中添加一个新文件。

  2. 右键单击解决方案面板中的项目,然后转到添加 | 新建文件…

  3. 在出现的对话框中,从iOS部分选择iPhone 视图控制器。命名为MainViewController并点击新建按钮。Xamarin Studio 将创建一个新的 XIB 文件,并自动打开MainViewController.cs源文件。此文件包含一个覆盖UIViewController类的类,我们可以在其中实现与我们的视图控制器相关的任何代码。

  4. 在 Interface Builder 中打开MainViewController.xib文件。

  5. 在视图中添加UILabel

  6. MainViewController类中为其创建并连接一个输出,命名为myLabel

  7. 在标签中输入文本View in controller!

  8. 保存 XIB 文档。

  9. 在 Xamarin Studio 中,在AppDelegate类的FinishedLaunching方法中,在窗口初始化行之后输入以下代码:

    MainViewController mainController = new MainViewController ();
    window.RootViewController = mainController;
    
  10. 在模拟器上编译并运行应用程序。

如何工作...

当我们在项目中添加一个新的 iPhone 视图控制器 文件时,在这种情况下是 MainViewController,Xamarin Studio 基本上会创建并添加以下三个文件:

  • MainViewController.xib: 这是一个包含控制器的 XIB 文件。

  • MainViewController.cs: 这是一个实现我们控制器类的 C# 源文件。

  • MainViewController.designer.cs: 这是一个自动生成的源文件,它反映了我们在 Interface Builder 中对控制器所做的更改。

注意,我们不需要为视图添加出口,因为这是由 Xamarin Studio 处理的。我们通过其类初始化控制器,如下所示:

MainViewController mainController = new MainViewController ();

然后,我们将控制器分配给 window.RootViewController 属性,如下所示:

window.RootViewController = mainController.

我们的观点控制器现在是应用程序窗口的根视图控制器,并且是应用程序启动时首先显示的一个。

更多内容...

我们刚刚创建的项目仅展示了我们如何添加具有视图的控制器。注意,我们在 MainViewController 类内部创建了标签的出口,该类在 XIB 文件中充当文件的所有者对象。为了为 MainViewController 类提供一些功能,在 MainViewController.cs 文件中的 MainViewController 类中添加以下方法:

public override void ViewDidLoad ()
{
  this.myLabel.Text = "View loaded!";
}

此方法重写了 UIViewController.ViewDidLoad() 方法,该方法在控制器加载其视图之后执行。

需要重写的 UIViewController 方法

UIViewController 类包含许多方法,允许我们管理视图控制器的生命周期。这些方法由系统在视图控制器上调用,我们可以重写它们以添加自己的实现。以下是一些这些方法:

  • ViewWillAppear: 当控制器的视图即将出现时,将调用此方法。

  • ViewDidAppear: 当控制器的视图已被显示时,将调用此方法。

  • ViewWillDisappear: 当控制器的视图即将消失时,将调用此方法,例如,当另一个控制器将被显示时。

  • ViewDidDisappear: 当视图消失时,将调用此方法。

相关内容

  • 在不同的视图控制器之间导航 的配方

  • 从 第一章 的 使用 Xamarin Studio 创建 iOS 项目使用出口访问 UI 配方,开发工具

在不同的视图控制器之间导航

在这个配方中,我们将学习如何使用 UINavigationController 类在多个视图控制器之间进行导航。

准备工作

UINavigationController 类是一个提供具有多个视图控制器的分层导航功能的控制器。在 Xamarin Studio 中创建一个新的 iPhone 空项目,并将其命名为 NavigationControllerApp

如何操作...

执行以下步骤以在多个视图控制器之间创建导航:

  1. 在项目中添加三个新的 iPhone 视图控制器,并分别命名为 MainControllerViewController1ViewController2

  2. 打开 AppDelegate.cs 文件,并在 FinishedLaunching 方法中添加以下代码:

    MainController mainController = new MainController();
    mainController.Title = "Main View";
    UINavigationController navController = new UINavigationController(mainController);
    window.RootViewController = navController;
    
  3. 在 Interface Builder 中打开 MainController.xib 并添加两个按钮及其相应的出口。分别设置它们的标题为 First ViewSecond View

  4. MainController 类的 ViewDidLoad 方法中添加以下代码:

    this.buttonFirstView.TouchUpInside += (sender, e) => {
    
            ViewController1 v1 = new ViewController1();
            v1.Title = "First View";
            this.NavigationController.PushViewController(v1, true);
    
          } ;
          this.buttonSecondView.TouchUpInside += (sender, e) => {
    
            ViewController2 v2 = new ViewController2();v2.Title = "Second View";
            this.NavigationController.PushViewController(v2, true);
    
            };
    
  5. 在 Interface Builder 中为 ViewController1ViewController2 控制器各添加一个标题为 Pop to root 的按钮。然后,在这两个控制器的 ViewDidLoad 方法中添加以下代码:

    this.buttonPop.TouchUpInside += (sender, e) => {
      this.NavigationController.PopToRootViewController(true);
    };
    
  6. 在模拟器上运行应用程序。

  7. 点击按钮并查看用户界面是如何从一个控制器导航到另一个控制器的。

它是如何工作的...

UINavigationController 类保留了一个控制器栈。UIViewController 类有一个名为 NavigationController 的属性。在正常情况下,这个属性返回 null。然而,如果控制器被推入导航控制器栈中,它将返回导航控制器的实例。在这种情况下,我们所有控制器的 NavigationController 属性都将返回我们的导航控制器实例。因此,这样,在任何控制器层次结构中的任何一点,都可以访问导航控制器。要向导航栈中推入一个视图控制器,我们调用 UINavigationController.PushViewController(UIViewController, bool) 方法,使用以下代码行:

this.NavigationController.PushViewController (v1, true);

注意,MainController 类是导航栈中最顶层或根控制器。导航控制器必须至少有一个视图控制器作为其根控制器。我们可以在初始化导航控制器时提供它,如下所示:

UINavigationController navController = new UINavigationController(mainController);

要返回根控制器,我们可以在当前控制器中调用 PopToRootViewController(bool) 方法,如下所示:

this.NavigationController.PopToRootViewController (true);

两个方法中的布尔参数都用于在视图控制器之间动画过渡。将其设置为 false 将导致控制器瞬间固定在屏幕上,这在大多数情况下并不提供很好的用户体验。

更多内容...

在这个简单的示例中,我们通过按钮提供了返回根控制器的向后导航。注意,顶部栏中有一个箭头形状的按钮,如下面的截图所示:

更多内容...

这个顶部栏被称为导航栏,它是 UINavigationBar 类型。箭头形状的按钮被称为后退按钮,它是 UIBarButtonItem 类型。后退按钮,当它存在时,总是导航到导航栈中的上一个控制器。如果栈中的上一个控制器设置了 Title 属性,后退按钮将显示该标题。如果没有标题,后退按钮将显示为 Back

管理导航栏按钮

要更改、添加和隐藏导航栏按钮,我们可以使用我们当前显示的视图控制器的 NavigationItem 属性的以下方法:

  • SetLeftBarButtonItem: 此方法在导航栏的左侧添加一个自定义按钮,替换默认的返回按钮。

  • SetRightBarButtonItem: 此方法在导航栏的右侧添加一个自定义按钮。

  • SetHidesBackButton: 此方法设置默认返回按钮的可见性。

要删除或隐藏导航栏左侧或右侧的自定义按钮,请调用适当的方法并传递 null 而不是 UIBarButtonItem 对象。

参见

  • 模式视图控制器高效使用视图控制器 的食谱

  • 在 第十一章 的 动画视图 食谱中,图形和动画

在标签中提供控制器

在本食谱中,我们将学习如何在标签界面中显示多个视图控制器。

准备工作

UITabBarController 类提供了一种在相同层次结构级别上显示不同视图控制器的方法,这些控制器被划分为类似标签的界面。在 Xamarin Studio 中创建一个新的 iPhone 空项目 并将其命名为 TabControllerApp

如何做到这一点…

执行以下步骤以在标签中提供控制器:

  1. 将两个 iPhone 视图控制器添加到项目中。将它们命名为 MainControllerSettingsController

  2. 将以下代码添加到 MainControllerViewDidLoad 方法中:

    this.View.BackgroundColor = UIColor.Blue;
    
  3. 将以下代码添加到 SettingsControllerViewDidLoad 方法中:

    this.View.BackgroundColor = UIColor.Yellow;
    
  4. 将以下代码添加到 AppDelegate 类的 FinishedLaunching 方法中:

    MainController mainController = new MainController();
    SettingsController settingsController = new SettingsController();
    UITabBarController tabController = new UITabBarController();
    tabController.SetViewControllers(new UIViewController[] {
      mainController,
      settingsController
    }, true);
    tabController.TabBar.Items[0].Title = "Main";
    tabController.TabBar.Items[1].Title = "Settings";
    window.RootViewController = tabController;
    
  5. 在模拟器上运行应用程序。单击底部的每个标签。当选择 MainController 时,界面应类似于以下截图:如何做到这一点…

它是如何工作的...

UITabBarController 类为它管理的每个控制器显示一个标签。该标签是 UITabBarItem 类型,可以接受文本和图像。我们通过其 SetViewControllers 属性设置它将显示的控制器,如下所示:

tabController.SetViewControllers(new UIViewController[] {
  mainController,
  settingsController
}, true);

在我们添加了控制器之后,我们可以通过 TabBar 属性访问其标签栏项。在这种情况下,我们设置了标签的 Title 属性:

tabController.TabBar.Items[0].Title = "Main";

每个 UIViewController 都包含一个 TabController 属性。类似于 NavigationController 属性,当控制器是标签控制器的一部分时,该属性将返回该标签控制器的实例。

更多内容...

控制器可以接受我们想要的任意数量的控制器,但如果添加六个或更多,则四个将带有标签显示,而第五个预定义的More标签将代表所有剩余的控制器。这是为了通过保持标签到特定大小,适合人类手指的大小,来保持界面易于用户访问。当我们向标签栏控制器界面添加超过六个控制器时,默认情况下,对象在More标签的顶部提供一个Edit按钮,允许用户重新排列控制器的顺序。如果我们想排除某些控制器从这个功能中,我们必须从CustomizableViewControllers数组中移除它。

有用的UITabBarController属性

UITabBarController类的其他一些有用属性如下:

  • ViewControllers:这是一个包含标签控制器所持有所有控制器的数组。

  • SelectedIndex:这是选中标签的零基索引。通过程序设置此属性以选择相应的控制器。

  • SelectedViewController:这是当前选中的控制器。

确定标签选择

要确定用户是否在标签控制器上选择了一个标签,我们可以订阅其ViewControllerSelected事件:

tabController.ViewControllerSelected += (sender, e) => {
  // Do something with e.ViewController.
};

相关内容

  • 高效使用视图控制器的配方

模态视图控制器

在本配方中,我们将讨论如何以模态方式显示视图控制器。

准备工作

模态视图控制器是指呈现在其他视图或控制器之上的任何控制器。这个概念类似于显示 Windows 表单作为对话框,它控制界面并不允许访问应用程序的其他窗口,除非它被关闭。在 Xamarin Studio 中创建一个新的 iPhone Empty Project,并将其命名为ModalControllerApp

如何操作…

执行以下步骤:

  1. 将两个视图控制器添加到项目中,并分别命名为MainControllerModalController

  2. 在 Interface Builder 中打开MainController.xib文件,并在其视图中添加一个标题为Present的按钮。创建并连接按钮的适当出口。

  3. MainController类中,在ViewDidLoad方法中添加以下代码:

    this.buttonPresent.TouchUpInside += async (s, e) => {
      ModalController modalController = new ModalController();
      await this.PresentViewControllerAsync(modalController, true);
    };
    
  4. 打开ModalController.xib文件。在其视图中添加一个标题为Dismiss的按钮,并为它创建适当的出口。

  5. 将其视图背景颜色设置为除白色以外的颜色。保存文档,并在ModalControllerViewDidLoad方法中输入以下代码:

    
      this.buttonDismiss.TouchUpInside += async (s, e) => {
        await this.DismissViewControllerAsync (true);
      };
    
  6. 最后,在FinishedLaunching方法中添加代码以显示主控制器:

    MainController mainController = new MainController();
    window.RootViewController = mainController;
    
  7. 在模拟器上编译并运行应用程序。点击Present按钮,观察模态控制器如何在主控制器之上呈现。点击Dismiss按钮以隐藏它。

它是如何工作的...

每个控制器对象都有两个处理以模态方式呈现和消失控制器的方法。在我们的示例中,我们调用 PresentViewControllerAsync(UIViewController, bool) 方法来呈现控制器,如下所示:

this.buttonPresent.TouchUpInside += async (s, e) => {
  ModalController modal = new ModalController ();
  await this.PresentViewControllerAsync (modal, true);
};

其第一个参数表示我们想要以模态方式显示的控制器,第二个参数确定我们是否希望呈现具有动画效果。要消失控制器,我们调用其 DismissViewControllerAsync(bool) 方法,如下所示:

await this.DismissViewControllerAsync (true);

它只接受一个参数,用于切换消失动画。

在此示例中,我们使用 async/await 和具有 Async 后缀的方法来以模态方式呈现和消失控制器。这些方法包含在 Xamarin.iOS 中以方便使用。我们还可以使用 PresentViewControllerDismissViewController;两者都接受另一个参数,该参数为 NSAction 类型,表示完成回调。然而,我们真的需要陷入所有这些 "麻烦" 吗?

更多内容...

我们可以使用控制器的 ModalTransitionStyle 属性定义模态视图控制器呈现的过渡样式。在呈现模态控制器之前,输入以下代码行:

modalController.ModalTransitionStyle = UIModalTransitionStyle.FlipHorizontal;

主要控制器将翻转以呈现模态控制器,给人一种它附在其后面的印象。

访问模态控制器

每个以模态方式呈现另一个控制器的控制器都通过 ModalController 属性提供对其 "子" 控制器的访问。如果您需要通过此属性访问模态控制器,请确保在调用 DismissViewControllerAsync 方法之前进行操作。

有多少个模态控制器?

理论上,我们可以呈现无限数量的模态控制器。当然,对此有两个限制,如下所述:

  • 内存不是无限的:视图控制器会消耗内存,因此我们呈现的视图控制器越多,性能越差。

  • 糟糕的用户体验:连续以模态方式呈现多个控制器可能会让用户感到困惑。

通常建议不要连续以模态方式呈现超过一个控制器。

相关内容

  • 在不同视图控制器之间导航在标签中提供控制器 的食谱

创建自定义视图控制器

在本教程中,我们将学习如何创建 UIViewController 的子类,并使用它来派生在 Interface Builder 中创建的视图控制器。

准备中

在本教程中,我们将创建一个自定义视图控制器,它将充当基控制器,为其继承者提供共同的功能。在 Xamarin Studio 中创建一个新的 iPhone 空项目,并将其命名为 CustomControllerApp

如何操作...

执行以下步骤:

  1. 右键单击 解决方案 面板中的项目,然后转到 添加 | 新建文件…

  2. 在出现的对话框中,导航到 常规 | 空类。将文件命名为 BaseController 并点击 新建 按钮。

  3. 打开刚刚创建的 BaseController.cs 文件,并将其修改为以下代码:

    using System;
    using MonoTouch.UIKit;
    using MonoTouch.Foundation;
    using System.Drawing;
    
    namespace CustomControllerApp {
    public class BaseController : UIViewController {
    
      //Constructor
      public BaseController (string nibName, NSBundle bundle) : base(nibName, bundle) {}
    
      public override void TouchesMoved (NSSet touches, UIEventevt)
      {
        base.TouchesMoved (touches, evt);
        // Capture the position of touches
        UITouch touch = touches.AnyObject as UITouch;
        if (null != touch) {
          PointF locationInView = touch.LocationInView (this.View);
          Console.WriteLine ("Touch coordinates: {0}", locationInView);
      }
    }
    
  4. 现在,向项目中添加一个 iPhone 视图控制器,并将其命名为 DerivedController。在其类定义中将继承自 UIViewController 的类更改为 BaseControllerpublic partial class DerivedController : BaseController

  5. 将派生控制器设置为主窗口的根视图控制器(在 AppDelegate.cs 中):

    DerivedController derivedController = new DerivedController();
    window.RootViewController = derivedController;
    
  6. 在模拟器上编译并运行应用。点击并拖动鼠标指针在白色表面上,观察 Xamarin Studio 的应用输出垫显示模拟器屏幕上指针的当前位置。

它是如何工作的...

我们在这里所做的是创建了一个可以用于多个 Xamarin.iOS 项目的基控制器类。我们添加到这个控制器中的功能是响应用户触摸。任何继承它的控制器都将继承相同的功能。我们添加到创建 BaseController 类中的代码相当简单。为了使它工作,我们在类中添加了以下构造函数:

public BaseController (string nibName, NSBundle bundle) : base(nibName, bundle) {}

这是当使用新关键字通过派生对象的 DerivedController() 构造函数初始化 DerivedController 类时将被调用的基构造函数,this.derivedController = new DerivedController();。所以,这实际上意味着我们可以通常使用继承与从 XIB 文件加载的控制器。

还有更多...

我们还可以从 XIB 文件中创建基控制器。然而,如果 XIB 文件包含出口,我们需要确保在我们的派生类中填充这些出口;否则,它们将不会在我们的派生控制器中可用。例如,如果我们有一个名为 btnStart 的按钮出口在基 XIB 文件中,我们将在我们的派生类中创建以下属性:

[Outlet("btnStart")]
public UIButton BtnStart {
  get { return base.btnStart; }
  set { base.btnStart = value; }
}

Outlet 属性告诉运行时特定属性是一个出口。不仅如此,它还有助于在我们在 XIB 中使用派生类时创建 Xcode 项目。

参见

  • 使用视图控制器加载视图高效使用视图控制器使用故事板进行 UI 流程设计 菜单

  • 在 第二章 的 添加和自定义视图 菜单,用户界面 – 视图

高效使用视图控制器

iOS 对内存使用非常严格。如果一个应用使用了过多的内存,iOS 将会发出内存警告。如果我们不相应地通过释放不需要的资源来应对这些内存警告,iOS 很可能会终止该应用。

准备工作

让我们看看我们可以做些什么来避免这种情况。在 Xamarin Studio 中创建一个新的项目,并将其命名为 EfficientControllerApp

如何做到这一点...

执行以下步骤以完成此菜谱:

  1. 向项目中添加一个视图控制器,并将其命名为 MainController

  2. MainController 类的 DidReceiveMemoryWarning 方法中输入以下代码:

    Console.WriteLine("Main controller received memory warning!");
    
  3. AppDelegate.cs 中将控制器设置为应用的根视图控制器,如下所示:

    MainController mainController = new MainController();
    window.RootViewController = mainController;
    
  4. 在模拟器上编译并运行应用。

  5. 当 iOS 模拟器窗口处于活动状态时,在菜单栏上导航到 硬件 | 模拟内存警告,如图所示:如何操作…

  6. 检查 Xamarin Studio 中的 应用程序输出 选项卡。你应该会看到以下类似的输出:

    2013-12-04 08:09:47.695 EfficientControllerApp[1383:80b] Received memory warning.
    2013-12-04 08:09:47.709 EfficientControllerApp[1383:80b] Main controller received memory warning!
    

它是如何工作的...

此项目不提供任何有用的功能。其主要目的是展示如何通知 iOS 发出的内存警告。

当发出内存警告时,DidReceiveMemoryWarning 方法将在当前内存中所有实例化的视图控制器上被调用。当此方法被调用时,我们应该确保释放当前不需要的资源。这样,我们就可以为系统提供更多的内存。

iOS 模拟器提供了模拟内存警告的选项,这样我们就可以测试当内存不足时我们的应用会如何表现。在真实设备上,我们无法强制系统按需发出内存警告。请注意,尽管我们实际上可以在模拟器上模拟无限数量的内存警告,但应用永远不会被终止。另一方面,在设备上,应用将在两个或三个内存警告之后(实际数量根据内存使用情况而变化)被终止,因此我们需要考虑这一点。

还有更多...

视图控制器并不是唯一可以接收内存警告的对象。我们可以在 AppDelegate 类内部通过重写 UIApplicationDelegate.ReceiveMemoryWarning(UIApplication) 方法来捕获内存警告通知,如下所示:

public override void ReceiveMemoryWarning(UIApplication application)
{  //...    }

相关内容

  • 创建自定义视图控制器 菜谱

  • 第一章中的 Interface Builder 菜谱,开发工具

iPad 视图控制器

我们迄今为止所使用的所有控制器都可以用于 iPhone 和 iPad 应用。然而,有两个控制器仅适用于 iPad。这些是 UISplitViewControllerUIPopoverController 类。在此菜谱中,我们将创建一个使用 UISplitViewController 类的 iPad 项目。

准备工作

创建一个新的 iPad 空项目,并将其命名为 SplitControllerApp

如何操作…

执行以下步骤来完成此菜谱:

  1. 向项目中添加两个 iPad 视图控制器,并分别命名为 FirstControllerSecondController。将它们视图的背景颜色设置为不同的颜色,例如,将 FirstController 的背景颜色设置为蓝色,将 SecondController 的背景颜色设置为黄色。

  2. 在 Interface Builder 中打开 SecondController.xib 文件,并将 UIToolbar 添加到视图的顶部附近。将工具栏连接到名为 myToolbar 的出口。

  3. 默认情况下,Xamarin Studio 创建的表示出口的属性是私有的。在 SecondController 类中添加以下属性以公开工具栏出口:

    public UIToolbar MyToolbar {
      get { return this.myToolbar; }
    }
    
  4. 将以下类添加到项目中:

    public class SplitControllerDelegate : UISplitViewControllerDelegate
      {
        public SplitControllerDelegate (SecondController controller)
        {  
          this.secondController = controller;
        }
        private SecondController secondController;
        public override void WillHideViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem barButtonItem, UIPopoverController pc)
        {
          barButtonItem.Title = "First";
          this.secondController.MyToolbar.SetItems (new UIBarButtonItem[] { barButtonItem }, true);
        }
        public override void WillShowViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem button)
        {
          this.secondController.MyToolbar.SetItems (new UIBarButtonItem[0], true);
        }
      }
    
  5. AppDelegate 类的 FinishedLaunching 方法中添加以下代码:

    FirstController firstController = new FirstController();
    SecondController secondController = new SecondController();
    UISplitViewController splitController = new UISplitViewController();
    splitController.ViewControllers = new UIViewController[] {
        firstController,
        secondController
      };
      splitController.Delegate = new SplitControllerDelegate(secondController);
      window.RootViewController = splitController;
    
  6. 在模拟器上编译并运行应用程序。

  7. 点击工具栏上的 第一个 按钮。FirstController 应该从侧面滑入。结果类似于以下截图:如何操作…

它是如何工作的...

UISplitViewController 类有助于充分利用 iPad 更大的屏幕。它提供了一种在同一屏幕区域内同时显示两个不同视图的方法。它是通过在纵向模式下全屏显示一个控制器,并在需要时以较小的尺寸显示一个次要控制器来实现的。

为了让用户能够访问我们项目中的两个控制器,我们实现了一个继承自 UISplitViewControllerDelegate 的类,并在 FinishedLaunching 方法中将它分配给我们的分割控制器。我们创建的 Delegate 对象覆盖了两个方法。在第一个方法中,我们按照以下方式将一个按钮分配给工具栏:

public override void WillHideViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem barButtonItem, UIPopoverController pc)
{
  barButtonItem.Title = "First";
  this.secondController.MyToolbar.SetItems (new UIBarButtonItem[] { barButtonItem }, true);
}

UISplitViewController 从横向模式变为纵向模式,并且其较小的控制器即将被隐藏时,WillHideViewController 方法会被执行。因此,为了显示它,我们在全屏控制器的工具栏上提供了一个按钮。当我们点击该按钮时,另一个控制器将从侧面滑入。当方向从纵向变为横向时,较小的控制器会自动出现在较大的控制器旁边。因此,我们不再需要在工具栏上放置按钮;因此,我们覆盖了 WillShowViewController 方法来移除它。我们通过分配一个空的 UIBarButtonItem[] 数组来完成此操作,如下所示:

public override void WillShowViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem button)
{
  this.secondController.Toolbar.SetItems (new UIBarButtonItem[0], true);
}

更多...

要将模拟器旋转到(并从)横向模式,按住 Cmd 键和左箭头键(或右箭头键),同时让 iOS 模拟器上的应用运行。以下截图显示了已旋转到横向模式的 iOS 模拟器。没有采取其他任何操作来同时显示两个控制器,因为分割控制器为我们处理了这一点:

更多...

iPad 特定的控制器使用

尽管所有其他控制器都对 iPhone 和 iPad 可用,但如果在运行在 iPhone 上的应用程序中使用 UISplitViewController 方法,将会发生异常。

参见

  • 在 第九章 的 调整 UI 方向 菜谱中,与设备硬件交互

使用 storyboards 进行 UI 流程设计

当 iOS 5 发布时,苹果引入了 storyboards。Storyboard 是一种新的用户界面文件类型,它接受多个视图控制器,但它还包含有关所有这些控制器如何在应用程序的层次结构中相互关联的信息。Storyboard 在设计应用程序屏幕时非常有帮助,因为它们比从单个 XIB 文件加载不同的控制器更有效率;它们还把一组视图控制器保存在一个文件中。

准备工作

在 Xamarin Studio 中创建一个新的 iPhone 空项目,并将其命名为 StoryboardApp

注意

Xamarin Studio 包含许多用于故事板应用程序的项目模板。然而,我们将使用一个空 iPhone 项目,因为它将帮助我们更好地理解故事板的工作原理。

如何操作...

完成此菜谱的步骤如下:

  1. 向项目中添加两个新的 C# 类(不是视图控制器),并将它们命名为 FirstControllerSecondController

  2. UIViewController 派生这两个类,并用 Register 属性进行装饰。确保在属性中为每个控制器传递不同的名称,如下所示:

    • FirstController 类:

      [Register("FirstController")]
      public class FirstController : UIViewController
      { //..
      
    • SecondController 类:

      [Register("SecondController")]
      public class SecondController : UIViewController
      { //..
      
  3. 在两个类中添加 UIViewControllerIntPtr 构造函数:

    public FirstController(IntPtr handle) : base(handle)
    {} 
    
  4. 向项目中添加一个 Empty iPhone Storyboard 文件,并将其命名为 MainStoryboard

  5. 打开在 Interface Builder 中创建的 MainStoryboard.storyboard 文件。就像打开 XIBs 一样,在 Xamarin Studio 中双击该文件。

  6. 在空画布上拖动 UINavigationController。默认情况下,Xcode 在添加导航控制器时会添加一个表格视图。选择它并通过按 Backspace 键删除它;我们只需要导航控制器。

  7. 在画布上添加两个 UIViewController 对象。通过单独选择每个视图控制器,在 Identity Inspector 窗口中设置它们的 Class 字段为我们之前步骤中创建的类。以下截图显示了第一个控制器的 Class 字段设置为 FirstController如何操作...

  8. 我们现在需要连接这两个控制器。就像添加一个出口一样,按 Ctrl 并从导航控制器拖动到 FirstController。在你释放按钮时出现的上下文菜单中,选择 根视图

  9. FirstController 上添加 UIButton。按 Ctrl 并从按钮拖动到 SecondController。在出现的上下文菜单中选择 推送。不需要将按钮连接到出口。

  10. 在 Xamarin Studio 中,回到 FinishedLaunching 方法中添加以下代码:

    UIStoryboard storyboard = UIStoryboard.FromName("MainStoryboard", NSBundle.MainBundle);
    UINavigationController navController = (UINavigationController)storyboard.InstantiateInitialViewController();
    window.RootViewController = navController;
    
  11. 在模拟器上编译并运行应用程序。点击按钮会将第二个控制器推入显示。

它是如何工作的...

如您所见,我们成功地用最少的代码创建了应用程序的用户界面。在 storyboard 文件中,我们需要根据我们想要的屏幕层次结构来连接每个元素。我们首先将FirstController设置为导航控制器的根视图控制器。然后,我们将SecondController类分配给按钮的关系。因此,当按钮被点击时,SecondController类将被推入导航控制器的堆栈中,就像我们调用UINavigationController.PushViewController方法一样。这种关系称为segue。在 storyboard 文件中,我们可以单独选择 segues 并设置它们的属性。例如,我们可以设置一个标识符字符串或将行为从push更改为modal

FinishedLaunching方法中,我们首先通过静态UIStoryboard.FromName方法实例化一个UIStoryboard实例,如下所示:

UIStoryboard storyboard = UIStoryboard.FromName("MainStoryboard", NSBundle.MainBundle);

我们随后调用InstantiateInitialViewController方法来获取 storyboard 的初始控制器,如下所示。在这种情况下,初始控制器是UINavigationController

UINavigationController navController = (UINavigationController)storyboard.InstantiateInitialViewController();

注意,我们需要将返回值转换为正确的控制器类型,因为它的返回类型是NSObject

更多内容...

我们也可以通过编程方式启动 segues。为此,我们首先需要在 Xcode 中选择 segue 并为其在属性检查器标签中设置一个标识符。然后,我们可以通过调用属于它的UIViewController实例的PerformSegue方法来通过代码触发它,如下所示:

this.PerformSegue("MyPushSegue", null);

传递数据

使用 storyboards,系统正在实例化我们需要的视图控制器。我们可以通过覆盖拥有或源 segue 的控制器上的PrepareForSegue方法来访问将通过 segue 显示的视图控制器,如下所示:

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

    if (segue.Identifier == "MyPushSegue")
    {
      SecondController secondController = 
        (SecondController)segue.DestinationViewController;
      // Create a public method or property in SecondController 
      // for passing data to it.
    }//end if
}

如您所见,还有一个UIStoryboardSegue类为我们提供了必要的信息。

PrepareForSegue方法在涉及视图控制器实例化之后和 segue 开始之前被调用。因此,通过通过Identifier属性确定哪个 segue 触发了准备方法,我们可以通过DestinationViewController属性检索 segue 将要显示的控制器。

注意

无论 segue 是通过PerformSegue方法编程触发还是仅设置在 storyboard 文件中的按钮,PrepareForSegue方法都会被调用。

参见

  • 第一章中的Interface Builder配方,开发工具

在 storyboard 中回退

Storyboards 的另一个非常实用的功能是解包。解包是一个类似于转场的过程,但它不是显示下一个视图控制器,而是将故事板中的视图控制器反转到之前的一个。它的好处是它允许我们回到任何视图控制器,而不仅仅是当前控制器之前的一个。本食谱将展示如何使用解包。

准备工作

对于这个食谱,我们需要在之前的食谱中创建的StoryboardApp项目。在 Xamarin Studio 中打开它。

如何做到这一点...

执行以下步骤以实现解包:

  1. 在项目中添加一个新类,并将其命名为ModalController

  2. 将类设为自定义视图控制器,类似于项目中FirstControllerSecondController,如下所示:

    [Register("ModalController")]
    public class ModalController : UIViewController
    {
      public ModalController (IntPtr handle) : base(handle)
      {
      }
    }
    
  3. FirstController类中添加以下方法:

    [Action("unwindFromModalController:")]
    public void UnwindFromModalController(UIStoryboardSegue segue)
    {
    }
    
  4. 在 Xcode 中打开MainStoryboard.storyboard文件并添加另一个UIViewController。将此控制器的Class设置为ModalController

  5. SecondController中添加UIButton并将其标题设置为Show modal

  6. Ctrl键并从按钮拖动到ModalController。将此转场设置为模态

  7. ModalController上添加另一个按钮。将其标题设置为Unwind to first

  8. Ctrl键并从按钮拖动到控制器工具栏上的退出项,如图下所示:如何做到这一点…

  9. 在出现的上下文菜单中选择unwindFromModalController

  10. 在模拟器上编译并运行应用程序。浏览屏幕,直到到达模态控制器并点击解包到第一个按钮。用户界面将返回到第一个控制器。

它是如何工作的…

通过使用解包unwind segues,我们可以回到层次结构中的任何控制器。基本要求是在你想要解包到的控制器中添加一个带有Action属性的装饰方法,如下所示:

[Action("unwindFromModalController:")]
public void UnwindFromModalController(UIStoryboardSegue segue) {}

该属性基本上会将方法暴露给 Xcode 作为动作,这样当打开 storyboard 文件时,我们就可以添加解包转场。这就是我们在拖动到退出项时出现unwindFromModalController动作的原因。动作是否在另一个类中无关紧要,Xcode 足够智能,可以搜索 storyboard 中的所有类。

注意

每个视图控制器工具栏中的退出项负责创建解包转场。它代表视图控制器的退出,这取决于控制器是如何显示的。

还有更多...

我们可以通过传递给解包动作的UIStoryboardSegue对象来访问启动解包转场的控制器,如下所示:

//..
ModalControllermodalController = (ModalController)segue.SourceViewController;

故事板中的解包转场在哪里?

当我们创建解包转场时,Xcode 的外观没有明显变化,也没有创建某些内容的指示,就像我们创建转场时一样。创建解包后,我们可以通过展开文档大纲来找到它,如图下所示:

故事板中撤销转场在哪里?

参见

  • 导航不同视图控制器模态视图控制器 的食谱

第四章:数据管理

在本章中,我们将涵盖以下主题:

  • 创建文件

  • 使用 SQLite 数据库

  • 准备支持 iCloud

  • iCloud 键/值数据存储

简介

几乎每个应用程序都需要在文件系统中拥有永久的数据存储。在本章中,我们将讨论不同的数据存储方式。我们将看到如何创建一个SQLite 数据库,并在 iPhone 应用程序中使用它来管理数据。此外,我们还将学习如何在项目中使用现有的数据库。

注意

SQLite (www.sqlite.org) 是一个自包含的事务型数据库系统。每个数据库都保存在一个独立的文件中,没有数据库服务器。在 iOS 中,SQLite 支持是原生的。

在介绍 SQLite 之后,我们将探讨iCloud 存储以及如何在我们的应用程序中集成它。

创建文件

在这个菜谱中,我们将学习如何在 iOS 设备的文件系统中创建文件。

准备工作

在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序,并将其命名为FileCreationApp

如何操作…

按照以下步骤完成此菜谱:

  1. 在 Interface Builder 中打开FileCreationAppViewController.xib文件。

  2. 在其视图中添加一个按钮和一个标签。

  3. 在 Xamarin Studio 中,在控制器类的ViewDidLoad方法中输入以下代码:

    string filePath = Path.Combine (Environment.GetFolderPath(Environment.SpecialFolder.Personal), "MyFile.txt");
    using (StreamWriter sw = new StreamWriter (filePath))
    {
      sw.WriteLine ("Some text in file!");
    }
    this.btnShow.TouchUpInside += (s, e) => {
      using (StreamReader sr = new StreamReader (filePath))
      {
        this.labelStatus.Text = sr.ReadToEnd ();
      }
    };
    
  4. 在模拟器上编译并运行应用程序。点击按钮以将文件内容填充到标签中。

它是如何工作的...

如前述代码所示,我们可以使用来自System.IO 命名空间的标准类,就像在桌面应用程序中一样。我们将为要保存的文件设置一个路径。我们将在以下代码行中这样做:

string filePath = Path.Combine (Environment.GetFolderPath(Environment.SpecialFolder.Personal), "MyFile.txt");

在 iOS 中,我们无法访问整个文件系统,甚至在应用程序包内部也不行。如果我们尝试在无法访问的文件夹中写入,将会发生异常。因此,我们使用static Environment.GetFolderPath(SpecialFolder)静态方法来检索与我们的应用程序的Documents文件夹相对应的Personal特殊文件夹。注意Path.Combine(string, string)的使用,它将两个字符串组合并返回一个路径。之后,我们按照以下方式创建StreamWriter类的新实例:

using (StreamWriter sw = new StreamWriter (filePath))
{
  sw.WriteLine ("Some text in file!");
}

我们使用文件的WriteLine(string)方法在文件中写入一些文本。要从文件中检索文本,我们创建StreamReader类的新实例,并使用以下代码使用其ReadLine方法读取文本:

using (StreamReader sr = new StreamReader (filePath))
{
  this.labelMessage.Text = sr.ReadToEnd ();
}

更多内容...

实际上,System.IO命名空间中几乎每个可用的类在 Xamarin.iOS 上都会工作,只要我们有访问目标文件夹的权限。

文档文件夹

应用程序包的Documents文件夹仅与该应用程序相关。如果从设备中卸载应用程序,其内容也将被删除。在此文件夹中创建的文件将自动备份到 iCloud,除非我们明确请求排除特定文件。这可以通过跳过文件的备份属性来完成。例如,如果我们想排除我们创建的MyFile.txt文件,我们就必须添加以下代码:

NSError error = NSFileManager.SetSkipBackupAttribute(filePath, true);
if (null == error) {
  // Success
}

如果在调用此方法时文件不存在,error对象将包含适当的错误信息。

如果我们想将文件包含到 iCloud 备份中,我们只需调用前面的方法,将false作为第二个参数传递。

注意

考虑跳过大型文件的备份属性是很重要的。苹果将拒绝包含要备份到 iCloud 的大型文件的应用程序。

iCloud 备份是 iOS 备份功能的自动程序。它主要用于从 iCloud 恢复设备。除了从 iCloud 备份中排除或包含文件之外,我们没有其他访问权限。它也与我们可以访问的 iCloud 存储不同,我们将在本章后面讨论。

Caches 文件夹

Caches文件夹(Library/Caches/)可以用于存储应用程序特定的数据,这些数据可以由应用程序轻松重建。此文件夹中的文件不会备份到 iCloud,并且如果需要更多空间,系统可以将其删除。

我们可以通过Environment.SpecialFolder.InternetCache值在 iOS 应用程序中获取Caches文件夹的完整路径,如下面的代码所示:

string cachesFolder = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);

参见

  • iCloud 键/值数据存储教程

使用 SQLite 数据库

在本教程中,我们将学习如何创建 SQLite 数据库文件。我们将创建一个表,向其中插入一些数据,然后查询表以在屏幕上显示数据。

准备工作

在 Xamarin Studio 中创建一个新的 iPhone单视图应用程序,并将其命名为SQLiteApp。在视图控制器上添加三个按钮和一个标签。不要忘记将它们连接到输出端口。

如何操作...

执行以下步骤:

  1. 添加对Mono.Data.SqliteSystem.Data程序集的引用。以下截图显示了如何向项目中添加引用:如何操作...

  2. 为了创建数据库和表,请在SQLiteAppViewController类中输入以下方法:

    private void CreateSQLiteDatabase (string databaseFile)
    {
      try
      {
        if (!File.Exists (databaseFile))
        {
          SqliteConnection.CreateFile (databaseFile);
          using (SqliteConnection sqlCon = new SqliteConnection (String.Format ("Data Source = {0};", databaseFile)))
          {
            sqlCon.Open ();
            using (SqliteCommand sqlCom = new SqliteCommand (sqlCon))
            {
              sqlCom.CommandText = "CREATE TABLE Customers (ID INTEGER PRIMARY KEY, FirstName VARCHAR(20), LastName VARCHAR(20))";
              sqlCom.ExecuteNonQuery ();
            }
            sqlCon.Close ();
          }
          this.lblStatus.Text = "Database created!";
        } else {
          this.lblStatus.Text = "Database already exists!";
        }
      } catch (Exception ex) {
        this.lblStatus.Text = String.Format ("Sqlite error: {0}", ex.Message);
      }
    }
    
  3. 添加以下方法以向数据库中插入数据:

    private void InsertData(string databaseFile) {
      try {
        if (File.Exists(databaseFile)) {
          using (SqliteConnection sqlCon = new SqliteConnection(String.Format("Data Source = { 0};", databaseFile))) {
            sqlCon.Open();
            using (SqliteCommand sqlCom = new SqliteCommand(sqlCon)) {
              sqlCom.CommandText = "INSERT INTO Customers (FirstName, LastName) VALUES ('Dimitris', 'Tavlikos')";
              sqlCom.ExecuteNonQuery();
              }
            sqlCon.Close();
          }
          this.lblStatus.Text = "Inserted 1 row.";
        }  else {
          this.lblStatus.Text = "Database file does not exist!";
        }
      }  catch (Exception ex) {
        this.lblStatus.Text = String.Format("Sqlite error: { 0}", ex.Message);
      }
    }
    
  4. 添加以下方法以从数据库查询数据:

    private void QueryData(string databaseFile) {
      try {
        if (!File.Exists(databaseFile)) {
          using (SqliteConnection sqlCon = new SqliteConnection(String.Format("Data Source = { 0};", databaseFile))) {
              sqlCon.Open();
            using (SqliteCommand sqlCom = new SqliteCommand(sqlCon)) {
              sqlCom.CommandText = "SELECT * FROM Customers WHERE FirstName='Dimitris'";
              using (SqliteDataReader dbReader = sqlCom.ExecuteReader()) {
                while (dbReader.Read()) {
                  this.lblStatus.Text = String.Format("First name: { 0}\ nLast name: { 1}", dbReader["FirstName"], dbReader["LastName"]);
                }
              }
            }
          }
        }  else {
          this.lblStatus.Text = "Database file does not exist!";
        }
      }  catch (Exception ex) {
        this.lblStatus.Text = String.Format("Sqlite error: { 0}", ex.Message);
      }
    }
    
  5. ViewDidLoad方法中添加以下代码:

    string sqlitePath = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.Personal), "MyDB.db3");
    this.btnCreate.TouchUpInside += (s, e) => this.CreateSQLiteDatabase (sqlitePath);
    this.btnInsert.TouchUpInside += (s, e) => this.InsertData(sqlitePath);
    this.btnQuery.TouchUpInside += (s, e) => this.QueryData(sqlitePath);
    
  6. 在模拟器上编译并运行应用程序。按顺序点击每个按钮,以创建、插入和从数据库查询数据。

工作原理...

iOS 为 SQLite 数据库提供了原生支持。我们可以通过 Mono 的Mono.Data.Sqlite命名空间访问 SQLite 数据库:

using Mono.Data.Sqlite;

CreateSQLiteDatabase 方法内部,我们首先使用以下代码检查文件是否已存在,以避免破坏任何数据:

if (!File.Exists (databaseFile))

然后,我们可以继续创建数据库。我们首先使用以下代码使用 SqliteConnection.CreateFile(string) 静态方法创建文件:

SqliteConnection.CreateFile (databaseFile);

我们通过初始化一个 SqliteConnection 对象并调用其 Open() 方法来连接新创建的文件。SQLite 数据库的连接字符串是 Data Source = 后跟数据库文件名,如下面的代码所示:

using (SqliteConnection sqlCon = new SqliteConnection (String.Format ("Data Source = {0};", databaseFile)))
  sqlCon.Open();

要在数据库中创建表,初始化一个 SqliteCommand 对象。我们将一个标准的 SQL 字符串传递给其 CommandText 属性,并调用 ExecuteNonQuery() 方法来执行 SQL,如下面的代码所示:

sqlCom.CommandText = "CREATE TABLE Customers (ID INTEGER PRIMARY KEY, FirstName VARCHAR(20), LastName VARCHAR(20))";
sqlCom.ExecuteNonQuery ();

要向数据库中插入数据,我们在 InsertData 方法中使用以下代码:

sqlCom.CommandText = "INSERT INTO Customers (FirstName, LastName) VALUES ('Dimitris', 'Tavlikos')";
sqlCom.ExecuteNonQuery();

最后,我们通过 SELECT 语句查询数据,并通过 SqliteDataReader 获取数据,如下面的代码所示:

sqlCom.CommandText = "SELECT * FROM Customers WHERE FirstName='Dimitris'";
using (SqliteDataReader dbReader = sqlCom.ExecuteReader()) {
  while (dbReader.Read()) {
    this.lblStatus.Text = String.Format("First name: {0}\nLast name: {1}", dbReader["FirstName"], dbReader["LastName"]);
  }
}

还有更多...

注意使用 try-catch 块。它用于在创建数据库时出现错误时向用户显示消息。

SQL 表创建

在此菜谱中,我们为我们的数据库创建了一个名为 Customers 的简单表。它包含三个字段。FirstNameLastName 参数的类型为 VARCHAR(20),而 ID 的类型为 INTEGER,也是表的 PRIMARY KEY

除了使用 SQL 命令创建表之外,我们还可以使用各种商业或免费 GUI 工具创建 SQLite 数据库。在互联网上简单搜索即可得到各种结果。

参见

  • 创建文件 菜单

  • 在第五章 显示数据表格Displaying data in a table 菜单中,Displaying Data

准备 iCloud 支持

随着 iOS 5 的发布,Apple 引入了 iCloud。iCloud 是一项为 iOS 用户提供云存储的服务,具有各种配置。对于应用程序开发,我们可以使用 iCloud 存储来保存可以在同一用户账户下运行在不同设备上的应用程序的不同实例之间共享的信息。在此菜谱中,我们将学习如何准备应用程序以提供 iCloud 存储支持。

准备工作

在 Xamarin Studio 中创建一个新的 iPhone 单视图应用程序 并命名为 KeyValueApp。为此菜谱,开发账户中需要存在一个启用了 iCloud 的 App ID。有关如何创建 App ID 的更多信息,请参阅第十四章 部署Deploying

如何操作...

执行以下步骤:

  1. 双击 Entitlements.plist 文件以在 Xamarin Studio 中打开它。

  2. 选择 启用 iCloud 复选框。

  3. 如果它要求您选择一个账户,请从列表中选择您的 Apple 开发者账户。

  4. 启用 iCloud 后,应出现使用键值存储复选框。启用它。Entitlements.plist文件设置现在应类似于以下截图:如何操作...

  5. 在项目选项中,在iPhone Bundle Signing下,为自定义权限字段选择Entitlements.plist。执行此步骤很重要,尽管该字段可能已经设置。

它是如何工作的...

启用 iCloud 支持只是设置我们项目适当设置的问题。通过勾选启用 iCloud使用键值存储复选框,Xamarin Studio 在Entitlements.plist文件中添加必要的键,这将允许应用程序使用 iCloud 存储。

更多...

虽然我们可以在模拟器上运行启用 iCloud 的应用程序,但 iCloud 功能将无法工作。

参见

  • 第十四章中的创建配置文件菜谱,部署

iCloud 键值存储

在本菜谱中,我们将学习如何保存和检索适合存储应用程序设置或在不同设备间共享的有用数据的小量数据。

注意

应用程序只能使用键值存储存储最多 1 MB 的数据,最多 1024 个键。因此,它不能用于备份文件或类似功能。

准备工作

我们需要两个处于同一 iCloud 账户下的设备才能真正看到 iCloud 存储的实际效果。在一个设备上我们将保存一些数据,在第二个设备上我们将加载这些数据。如果只有一个设备可用,这不会成问题,因为它将完美工作,因为数据将从本地存储加载,而不是从 iCloud。

如何操作...

完成此菜谱的步骤如下:

  1. 在 Xamarin Studio 中创建一个新的 iPhone Single View Application 并启用它以支持 iCloud,如前一个菜谱所示。将项目命名为KeyValueApp

  2. 在视图控制器上添加两个按钮和一个标签。

  3. 在视图控制器的ViewDidLoad方法中添加以下代码:

    this.btnSave.TouchUpInside += (s, e) => {
      NSUbiquitousKeyValueStore kvStore = NSUbiquitousKeyValueStore.DefaultStore;
      kvStore.SetString("LastSavedSearch", "How to implement iCloud");
      kvStore.Synchronize();
      this.lblStatus.Text = "Saved!";
    };
    this.btnLoad.TouchUpInside += (s, e) => {
      NSUbiquitousKeyValueStore kvStore = NSUbiquitousKeyValueStore.DefaultStore;
      this.lblStatus.Text = string.Format("Last saved search is: {0}", kvStore.GetString("LastSavedSearch");
    };
    
  4. 在设备上编译并运行应用程序。点击保存按钮将键和值保存到 iCloud。

  5. 点击加载按钮以在标签上显示数据。

  6. 如果有第二个设备,请在上面运行应用程序并点击加载按钮。数据将从 iCloud 中检索并在第二个设备的屏幕上显示。

它是如何工作的...

要将键值对保存到 iCloud,我们使用NSUbiquitousKeyValueStore类,该类负责处理数据。我们通过DefaultStore静态属性检索默认键值存储并调用其Save方法,如下面的代码所示:

NSUbiquitousKeyValueStore kvStore = NSUbiquitousKeyValueStore.DefaultStore;
kvStore.Save("LastSavedSearch", "How to implement iCloud ");

调用Save方法会将数据推送到队列以进行本地保存,然后上传到 iCloud。Synchronize方法同步键/值存储,基本上可以用来加快数据同步的过程。然而,调用该方法并不意味着数据会立即同步。iOS 负责决定何时同步数据,我们无法控制它。然而,iCloud 旨在提供无缝的同步体验,因此延迟通常不易察觉:

kvStore.Synchronize();

要从 iCloud 加载数据,我们只需调用GetString方法,传递要检索数据的键,如下所示:

kvStore.GetString("LastSavesSearch");

还有更多...

iCloud 键/值存储只接受一组特定的值,这些值类型如下:

  • double

  • bool

  • long

  • NSObject[]

  • NSDictionary

  • NSData

  • string

在键/值存储中接收更改通知

我们还可以在另一个设备上更改键/值对或一组对时收到通知。为此,我们需要添加一个通知观察者,如下所示:

NSObject coudObserver = NSUbiquitousKeyValueStore.Notifications.ObserveDidChangeExternally((s, e) => {
  if (e.ChangeReason == NSUbiquitousKeyValueStoreChangeReason.
    ServerChange) {
    e.ChangeKeys.Foreach(k => Console.WriteLine("Key changed: {0}", k));
  }
};

NSUbiquitousKeyValueStoreChangeReason枚举包含以下值:

  • ServerChange:它显示是否在其他设备上更改了值。

  • QuotaViolationChange:配额限制已达到。一些键/值对需要被移除。

  • InitialSyncChange:由于设备上的初始 iCloud 设置尚未完成,因此丢弃了一个键/值对。

  • AccountChange:用户已更改设备上的 iCloud 账户。整个键/值存储被新的 iCloud 账户中的存储所替换。

参见

  • 为 iCloud 准备配方

  • 第十四章中的创建配置文件配方,部署

第五章。显示数据

在本章中,我们将涵盖以下主题:

  • 提供列表

  • 以表格形式显示数据

  • 自定义行

  • 编辑表格

  • 表格索引

  • 搜索数据

  • 创建一个简单的网页浏览器

  • 以网格形式显示数据

  • 自定义网格

简介

在上一章中,我们讨论了在 iOS 应用中数据管理的可用选项。在本章中,我们将讨论向用户显示数据的各种方式。

特别是,我们将关注如何使用以下控件:

  • UIPickerView:此控件提供与列表框类似的功能。

  • UITableView:这是一个用于显示数据的非常可定制的视图。它是 iOS 应用中最常用的控件之一。

  • UISearchBarUISearchDisplayController:这是一个提供易于使用界面进行数据搜索的控件组合。

  • UIWebView:这为应用带来了网页浏览器的功能。

  • UICollectionView:这以可定制的网格显示数据。

此外,我们还将学习如何在表格中提供索引,以便用户可以轻松访问大量数据。

提供列表

在本食谱中,我们将学习如何使用 UIPickerView 类。

准备工作

UIPickerView 类为我们提供了一个与列表框功能类似的控件。它专门为触摸屏幕的人手设计。它与普通列表框的主要区别在于,每一列都可以有自己的行数。要开始,创建一个新的 iPhone 单视图应用程序项目,并将其命名为 PickerViewApp

如何做...

执行以下步骤:

  1. 在 Interface Builder 中打开 PickerViewAppViewController.xib 文件。在主视图中添加 UILabelUIPickerView 并保存文档。

  2. 回到 Xamarin Studio,在 PickerViewAppViewController 类中创建一个嵌套类,该类继承自 UIPickerViewModel,并使用以下代码:

    private class PickerModel : UIPickerViewModel
    
  3. 在嵌套类中添加以下构造函数和字段:

    public PickerModel (PickerViewAppViewController controller) {
      this.parentController = controller;
      this.transportList = new List<string>() { "On foot", "Bicycle", "Motorcycle", "Car", "Bus" };
      this.distanceList = new List<string>() { "0.5", 	"1", "5", "10", "100" };
      this.unitList = new List<string>() { "mi", "km" };
      this.transportSelected = this.transportList[0];
      this.distanceSelected = this.distanceList[0];
      this.unitSelected = this.unitList[0];
    }
    private PickerViewAppViewController parentController;
    private List<string> transportList;
    private List<string> distanceList;
    private List<string> unitList;
    string transportSelected;
    string distanceSelected;
    string unitSelected;
    
  4. 您现在需要重写 UIPickerViewModel 类中的四个方法,如下面的代码所示:

    public override int GetComponentCount (UIPickerView picker) {
      return 3;
    }
    public override int GetRowsInComponent(UIPickerView picker, int component) {
      switch (component) {
      case 0: 
        return this.transportList.Count;
      case 1:
        return this.distanceList.Count;
      default:
        return this.unitList.Count;
      }
    }
    public override string GetTitle (UIPickerView picker, int row, int component) {
      switch (component) {
      case 0:
        return this.transportList[row];
      case 1:
        return this.distanceList[row];
      default:
        return this.unitList[row];
      }
    }
    public override void Selected (UIPickerView picker, int row, int component) {
      switch (component) {
        case 0:
          this.transportSelected = this.transportList[row];
        break;
        case 1:
          this.distanceSelected = this.distanceList[row];
        break;
        default:
          this.unitSelected = this.unitList[row];
        break;
      }
      this.parentController.lblStatus.Text = String.Format("Transport: {0}\nDistance: {1}{2}", this.transportSelected, this.distanceSelected, this.unitSelected);
    } 
    
  5. 最后,在控制器中的 ViewDidLoad 方法内将我们创建的模型对象设置为选择视图的 Model 属性,如下面的代码所示:

    this.pickerView.Model = new PickerModel (this);
    
  6. 在模拟器上编译并运行应用。拖动选择视图中的项目,并观察标签内容根据您的选择而更改。以下截图显示了它应该看起来像什么:如何做...

它是如何工作的...

UIPickerViewModel 类在 Objective-C 中不存在。Xamarin.iOS 提供了这个类,作为 UIPickerViewDataSourceUIPickerViewDelegate 原生协议的包装器,并包含这两个类的方法以供我们重写。这非常有帮助,因为我们只需要实现和分配一个类而不是两个类来为我们的选择视图。这两个协议在 Xamarin.iOS 中同时作为 C# 类可用。

在构造函数内部,我们初始化将用于在选择器中显示数据的列表。以下四个需要重写的方法负责显示数据:

  • int GetComponentCount (UIPickerView picker): 这个方法返回我们希望选择器视图显示的列数

  • int GetRowsInComponent (UIPickerView picker, int component): 这个方法返回每个组件将显示的行数

  • string GetTitle (UIPickerView picker, int row, int component): 这是每行的文本

  • void Selected (UIPickerView picker, int row, int component): 当用户在选择器视图的任何组件/行组合中选择一个项时,将执行此操作

我们使用在构造函数中分配的列表来显示数据。例如,GetTitle 方法实现如下所示:

switch (component)
{
case 0:
  return this.transportList[row];
case 1:
  return this.distanceList[row];
default:
  return this.unitList[row];
}

还有更多...

我们可以通过调用 Select (int, int, bool) 方法来程序化选择选择器的初始选择。前两个参数分别反映行和组件索引,而 bool 参数切换选择动画。使用此方法时需要注意的唯一一点是,在分配选择器的 Model 属性之后调用它。否则将发生异常。

更多 UIPickerView 定制

除了之前提供的选项外,我们还可以设置每个组件的宽度。为此,我们重写 GetComponentWidth (UIPickerView, int) 方法,该方法返回一个表示每个组件宽度的浮点数。

我们还可以将自定义视图作为选择器视图中的项,而不是纯文本。这可以通过重写 GetView(UIPickerView, int, int, UIView) 方法并返回我们希望在 UIPickerView 控制器的每个位置显示的视图来实现。

日期和时间选择

有一个名为 UIDatePicker 的控件,它与 UIPickerView 类似,专门定制用于显示和选择日期和时间值。请注意,尽管其用户界面与选择器视图相同,但它并不继承 UIPickerView 类。它只是使用它的一个实例作为子视图。

参见

  • 在表格中显示数据 食谱

在表格中显示数据

在本食谱中,我们将学习如何使用 UITableView 类来显示数据。这个类,连同 UITableViewCell 对象一起,提供了一个在屏幕上以多行但单列形式显示数据的接口。

准备工作

要开始,在 Xamarin Studio 中创建一个新的项目,并将其命名为 TableViewApp。在本食谱中,我们不会使用 XIB 文件。我们将通过代码创建用户界面。

如何做到...

执行以下步骤:

  1. 向项目中添加一个新的类,并将其命名为 TableController。使用以下代码从 UITableViewController 派生该类:

    public class TableController : UITableViewController
    
  2. TableController 类内部创建以下嵌套类:

    private class TableSource : UITableViewSource
    {
      public TableSource ()
      {
        this.cellID = "cellIdentifier";
        this.tableData = new Dictionary<int, string> () {
          {0, "Music"},
          {1, "Videos"},
          {2, "Images"}
        };
      }
      private readonly string cellID;
      private Dictionary<int, string> tableData;
      public override int RowsInSection (UITableView tableview, int section)
      {
        return this.tableData.Count;
      }
      public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
      {
        int rowIndex = indexPath.Row;
        UITableViewCell cell = tableView.DequeueReusableCell (this.cellID);
        if (null == cell)
        {
          cell = new UITableViewCell (UITableViewCellStyle.Default, this.cellID);
        }
        cell.TextLabel.Text = this.tableData[rowIndex];
        return cell;
      }
    }
    
  3. 重写控制器的方法 ViewDidLoad 并添加以下代码行:

    this.TableView.Source = new TableSource ();
    
  4. FinishedLaunching方法中添加以下代码以显示表格控制器:

    TableController tableController = new TableController();
    UINavigationController navController = new UINavigationController(tableController);
    window.RootViewController = navController;
    
  5. 在模拟器上编译并运行应用程序。结果应该类似于以下截图所示:如何操作...

它是如何工作的...

我们创建的嵌套类作为UITableView的数据源。在以下代码中创建的类继承自 Xamarin.iOS 的UITableViewSource类:

private class TableSource : UITableViewSource

注意

UIPickerView一样,在前面讨论的示例中,UITableViewSource类在 Objective-C 中不存在。它仅仅是 Xamarin.iOS 围绕UITableViewDelegateUITableViewSource协议提供的包装对象。

在其构造函数中,我们初始化了两个变量(如下面的代码所示):string,它将作为单元格的标识符,以及一个通用的Dictionary变量作为我们的数据源:

this.cellID = "cellIdentifier";
this.tableData = new Dictionary<int, string> () {
  {0, "Music"},
  {1, "Videos"},
  {2, "Images"}
};

要使TableSource类工作,我们需要重写两个方法。第一个方法名为RowsInSection,它返回表格应显示的行数。在以下代码中,我们返回数据源对象中的项目数:

return this.tableData.Count;

第二个方法GetCell返回将在表中显示的UITableViewCell对象。

注意

UITableViewCell类代表一行,并在UITableView中管理其内容。

为了更高效,表格视图在需要时创建其单元格对象。因此,我们需要通过其DequeueReusableCell方法从表格中获取UITableViewCell(之前使用过),如下面的代码所示:

UITableViewCell cell = tableView.DequeueReusableCell (this.cellID);

如果没有为特定的单元格标识符存在单元格,该方法将返回null。因此,我们使用以下代码创建将要使用的单元格:

cell = new UITableViewCell (UITableViewCellStyle.Default, this.cellID);

然后,我们分配特定单元格将显示的文本并使用以下代码返回:

cell.TextLabel.Text = this.tableData[rowIndex];
return cell;

默认情况下,UITableViewCell类包含两个标签,可以用来显示文本。主标签可以通过TextLabel属性访问,次要标签可以通过DetailTextLabel属性访问。请注意,当使用具有Default样式的单元格时,DetailTextLabel属性不能使用,并将返回null

更多...

为了在用户选择特定行时提供功能,我们需要在充当UITableViewSource的类中重写RowSelected属性。默认情况下,当用户点击一行时,单元格将以浅灰色突出显示,以指示选择。要取消选择行,我们使用UITableView.DeselectRow(NSIndexPath, bool)方法,如下面的代码所示:

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
  tableView.DeselectRow (indexPath, true);
}

UITableView 样式

UITableView可以使用两种不同的样式创建。默认样式是Plain。另一种可以使用的样式是Grouped样式。这种样式在许多 iOS 原生应用程序中正在使用,例如设置应用程序。

此外,UITableView 支持显示分为不同部分的数据。如果我们想使用不同的部分,我们必须在 RowsInSection 覆盖中显式返回每个部分将有的行数。

UITableViewCell 样式

表格单元格可以有四种不同的单元格样式,这些样式由 UITableViewCellStyle 枚举表示。其值如下所示:

  • 默认:这是默认单元格样式。只能使用 TextLabel 属性来显示文本。

  • 副标题:这是一种提供 DetailTextLabel 作为 TextLabel 副标题的样式。

  • 值 1:这是一种显示 TextLabelDetailTextLabel 文本大小相同、颜色不同,并居中对齐单元格两侧的样式。

  • 值 2:这是一种显示 TextLabel 文本比 DetailTextLabel 文本小的样式。这种样式用于原生 联系人 应用中的联系人详情屏幕。

参见

  • 提供列表 菜谱

  • 自定义行 菜谱

  • 第三章,用户界面 – 视图控制器 中的 在不同视图控制器间导航 菜谱

自定义行

在这个菜谱中,我们将创建一个使用我们自己的自定义 UITableViewCell 子类来显示数据的表格视图。

准备工作

以与先前菜谱中创建项目相同的方式在 Xamarin Studio 中创建一个新的项目。将其命名为 CustomRowsApp

如何操作...

执行以下步骤:

  1. 向项目中添加一个新的类,并将其命名为 CustomCell

  2. 使用以下代码实现该类:

    [Register("CustomCell")]
    public partial class CustomCell : UITableViewCell {
      public const string CELLID = "CustomCell";
      public CustomCell (IntPtr handle) : base(handle) {}
      [Outlet("lblTitle")]
      public UILabel LabelTitle { get; private set; }
      [Outlet("imgView")]
      public UIImageView ImgView { get; private set; }
    }
    
  3. 向项目中添加一个新的 Empty iPhone Interface Definition,并将其命名为 CustomCell。不用担心名称与之前创建的类冲突,因为这个是一个 XIB 文件。在 Interface Builder 中打开该文件。

  4. 在画布上添加 UITableViewCell。以下截图显示了 Xcode 对象浏览器中选中的 UITableViewCell如何操作...

  5. Identity 检查器中将表格单元格的 Class 字段设置为 CustomCell

  6. Attributes 检查器中将单元格的 Identifier 字段设置为 CustomCell

  7. 在单元格上添加一个 UIImageView 和一个 UILabel,并将它们连接到它们的出口。保存文档。

  8. 将我们在先前的菜谱中创建的 TableController.cs 文件添加到项目中。将其命名空间从 TableViewApp 更改为 CustomRowsApp

  9. TableSource 类的 GetCell 方法更改为以下代码:

    public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath) {
      int rowIndex = indexPath.Row;
      CustomCell cell = (CustomCell)tableView.DequeueReusableCell(CustomCell.CELLID);
      cell.LabelTitle.Text = this.tableData[rowIndex];
      return cell;
    }
    
  10. TableControllerViewDidLoad 方法中添加以下代码:

    this.TableView.RegisterNibForCellReuse (UINib.FromName("CustomCell", NSBundle.MainBundle), CustomCell.CellID);
    
  11. 最后,确保在 FinishedLaunching 方法中呈现 TableController,如下所示:

    TableController tableController = new TableController();
    UINavigationController navController = new UINavigationController();
    window.RootViewController = navController;
    

它是如何工作的...

就像创建自定义视图一样,我们能够使用UITableView创建我们自己的自定义单元格来显示数据。主要区别在于,表格重用其单元格的实例,这样当我们想要显示多行时,它会更有效率。

为了使我们的表格视图“意识到”我们的自定义单元格,我们使用以下代码调用RegisterNibForCellReuse方法:

this.TableView.RegisterNibForCellReuse (UINib.FromName("CustomCell", NSBundle.MainBundle), CustomCell.CellID);

这样,当我们调用GetCell中的DequeueReusable单元格方法时,系统将自动为我们创建一个单元格实例或获取之前创建的一个。因此,我们不需要检查单元格是否为 null:

CustomCell cell = (CustomCell)tableView.DequeueReusableCell(CustomCell.CELLID);

你注意到前面讨论的两个方法调用中的共同点了吗?它们都需要一个单元格的标识符字符串。CustomCell.CELLID常量具有我们在 Xcode 中单元格的标识符字段中输入的相同值:CustomCell。在这种情况下,它与我们的单元格类名相同,如果我们有多个自定义单元格要显示,这将使事情保持整洁。然而,基本上,单元格的标识符可以是任何我们想要的。

还有更多...

我们可以创建我们需要的任何数量的自定义单元格。如前所述,我们需要确保为我们将要使用的每个单元格类设置一个唯一的标识符。此外,如果我们创建的自定义单元格具有不同的高度,我们需要确保我们在UITableViewSource实现中使用以下代码覆盖GetHeightForRow方法:

public override float GetHeightForRow (UITableView tableView, NSIndexPath indexPath) {
  return 44f; // Or whatever height we want the particular row to have.
}

为了提高效率,最好事先计算出行高,而不是在GetHeightForRow方法中计算它们。

UITableViewCell 类的有用属性

除了在默认标签中添加文本外,UITableViewCell还包含一些其他属性,我们可以设置它们的值以在单元格中添加更多默认项。这些属性如下:

  • ImageView:接受一个UIImageView参数。我们可以用它来在单元格的左侧显示图像。

  • AccessoryView:接受任何UIView实例。其默认位置在单元格的右侧,位于单元格的 accessory 位置,该位置位于单元格的右侧。

  • Accessory:接受UITableViewCellAccessory类型的值。它为单元格的 accessory 提供预定义的视图,例如DetailDisclosureButtonCheckmark

UINib 类

UINib类负责在运行时加载 NIB 文件。我们通过其FromName静态方法实例化一个UINib实例,传递我们想要加载的 NIB 文件名(不带扩展名),如下面的代码所示:

UINib nib = UINib.FromName("CustomCell", NSBundle.MainBundle);

以编程方式添加内容

我们可以以编程方式向单元格添加视图。然而,我们不应直接将其添加到单元格中,而应将其添加到其ContentView中,使用以下代码:

// Inside our custom cell class:
this.ContentView.AddSubview(myView);

参见

  • 在表格中显示数据配方

  • 编辑表格配方

  • 在第二章中,用户界面 – 视图创建自定义视图配方

编辑表格

在本食谱中,我们将讨论如何在运行时从 UITableView 中插入和删除行,为用户提供适当的用户界面交互。

准备工作

打开我们在上一食谱中创建的 CustomRowsApp 项目,自定义行

如何做...

执行以下步骤:

  1. TableSource 类中删除 tableData 字段,并用以下属性替换它:

    public List<string> TableData { get; private set; }
    
  2. 在构造函数中使用以下代码初始化列表:

    this.TableData = new List<string>() { "Music", "Videos", "Images" };
    
  3. TableSource 类中,重写 CommitEditingStyle 方法并使用以下代码实现它:

    public override void CommitEditingStyle (UITableView tableView, UITableViewCellEditingStyle editingStyle, NSIndexPath indexPath) {
      if (editingStyle == UITableViewCellEditingStyle.Delete) {
        this.tableData.RemoveAt(indexPath.Row);
        tableView.DeleteRows(new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Automatic);
      }
    }
    
  4. TableController 类中,使用以下代码添加一个 UIBarButtonItem

    UIBarButtonItem btnAdd;
    public override ViewDidLoad() {
      // … existing code here.
      this.btnAdd = new UIBarButtonItem(UIBarButtonSystemItem.Add, (s, e) => {
        TableSource tableSource = (TableSource)this.TableView.Source;
        int itemCount = tableSource.TableData.Count;
        tableSource.TableData.Add(string.Format("Inserted item: {0}", itemCount));
        this.TableView.InsertRows(new NSIndexPath[] { 
          NSIndexPath.FromRowSection(itemCount, 0)
        }, UITableViewRowAnimation.Automatic);
      };
      this.NavigationItem.SetRightBarButtonItem(this.btnAdd, false);
    }
    
  5. 在模拟器上编译并运行应用程序。点击加号按钮向表格添加新行,从右向左滑动一个项以删除项。以下截图显示了添加了一个项并滑动另一个项后的表格:如何做...

    注意

    要在模拟器上滑动一个项,请点击并拖动光标向侧面。

它是如何工作的...

当即将进行编辑操作时,会调用 CommitEditingStyleForRow 方法。在我们的实现中,我们检查编辑操作是否是删除项,如果是,我们就删除该行。为此,我们首先从我们的数据源中删除相应的项,然后调用表格视图的 DeleteRows 方法:

this.tableData.RemoveAt(indexPath.Row);
tableView.DeleteRows(new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Automatic);

同样,当我们想要向表格中添加一行时,我们首先将我们想要的项添加到我们的数据源中,然后调用以下 InsertRows 方法:

tableSource.TableData.Add(string.Format("Inserted item: {0}", itemCount));
this.TableView.InsertRows(new NSIndexPath[] { 
  NSIndexPath.FromRowSection(itemCount, 0)
}, UITableViewRowAnimation.Automatic);

更多内容...

UITableView 也支持编辑模式。我们可以通过调用 SetEditing 方法来激活/停用表格视图的编辑模式,分别传递 truefalse,以下代码是一个示例:

this.TableView.SetEditing(true, true);

第二个参数确定我们是否希望表格视图通过动画过渡到/从编辑模式。

当表格视图处于编辑模式时,每一行的左侧都有一个红色减号。如果用户点击该符号,行右侧将出现 删除 按钮,就像他们滑动行时出现的那样。

为单个行启用编辑模式

我们还可以为单个行启用特定的编辑模式,甚至禁用它。为此,我们需要在我们的 UITableViewSource 子类中重写 EditingStyleForRow 方法,如下面的代码所示:

public override UITableViewCellEditingStyle EditingStyleForRow(UITableView tableView, NSIndexPath indexPath) {
  // To disable the editing style of a row:
  // return UITableViewCellEditingStyle.None;
  return UITableViewCellEditingStyle.Delete;
}

相关内容

  • 在表格中显示数据 食谱

表格索引

在本食谱中,我们将学习如何在表格中提供一个索引,使用户能够快速浏览 UITableView 的行。

准备工作

在 Xamarin Studio 中创建一个新的项目,并将其命名为 TableIndexApp。添加一个 UITableViewController,如本章前面的任务所示,并实现 TableSource 类。

如何做...

执行以下步骤:

  1. 在表格源类中,重写并实现以下方法:

    public override int NumberOfSections (UITableView tableView)
    {
      return this.tableData.Count;
    }
    public override string TitleForHeader (UITableView tableView, int section)
    {
      return Convert.ToString (this.tableData[section][0]);
    }
    public override string[] SectionIndexTitles (UITableView tableView)
    {
      return this.tableData.Select (s => Convert.ToString (s[0])).Distinct ().ToArray ();
    }
    

它是如何工作的...

在本食谱中创建的表格源包含许多不同的部分。为了简化,每个部分包含一行。NumberOfSections 方法返回表格将显示的总部分数。

要为每个部分设置标题,我们必须重写 TitleForHeader 方法,如下面的代码所示:

public override string TitleForHeader (UITableView tableView, int section)
{
  return Convert.ToString (this.tableData[section][0]);
}

此实现返回数据源中每个字符串的第一个字母。为了提供索引,我们重写 SectionIndexTitles 方法,如下面的代码所示:

public override string[] SectionIndexTitles (UITableView tableView)
{
  return this.tableData.Select (s => Convert.ToString (s[0])).Distinct ().ToArray ();
}

在这里,它返回数据源中每个项目的第一个字母。本项目的结果将类似于以下屏幕截图所示:

工作原理...

当用户在索引的任何地方触摸屏幕时,表格视图将滚动到该特定部分。

更多内容...

应将索引应用于具有 普通 样式的表格。不建议在设置了 分组 样式的表格上应用索引,因为索引将不易区分。

一个在表格上有索引的本地 iOS 应用程序的例子可以在本地的 Contacts 应用中找到。

参见

  • 在表格中显示数据 的食谱

搜索数据

在本食谱中,我们将学习如何为表格视图中的内容提供搜索功能。

准备工作

在 Xamarin Studio 中创建一个新的项目,并将其命名为 SearchTableApp。添加 UIViewController 并命名为 SearchController

如何做...

执行以下步骤:

  1. 在 Interface Builder 中打开 SearchController.xib 文件。在 UITableView 中添加 搜索栏和搜索显示控制器。以下屏幕截图显示了在对象浏览器中选中的 UISearchDisplayController 对象:如何做...

    注意

    注意,在此操作之后,将自动创建并连接一些出口。我们需要大多数它们,所以我们保留它们原样。

  2. 添加 UITableView 并将其连接到一个出口。保存文档。

  3. 回到 Xamarin Studio,创建一个 UITableViewSource 子类,它将作为表格视图的数据源。参考本章中的 在表格中显示数据 食谱以获取如何执行此操作的信息。这次,确保将用于存储数据的 List<string> 变量是 SearchController 类的成员。

  4. SearchController 类中使用以下代码添加另一个 List<string> 变量:

    private List<string> filterDataList;
    
  5. 实现一个子类,它将作为搜索显示控制器的委托对象,如下面的代码所示:

    private class SearchDelegate : UISearchDisplayDelegate
    {
      public SearchDelegate (TableController controller)
      {
        this.parentController = controller;
      }
      private TableController parentController;
      public override bool ShouldReloadForSearchString (UISearchDisplayController controller, string forSearchString)
      {
        this.parentController.filterDataList = this.parentController.tableData
          .Where (s => s.ToLower ().Contains (forSearchString.ToLower ()))
            .ToList ();
        this.parentController.filterDataList.Sort (delegate(string firstStr, string secondStr) {
            return firstStr.CompareTo (secondStr);
          });
        return true;
      }
    }
    
  6. ViewDidLoad 方法中添加以下代码,并在其中分配源和委托对象:

    this.TableView.Source = new TableSource (this);
    this.SearchDisplayController.SearchResultsSource = new TableSource(this);
    this.SearchDisplayController.Delegate = new SearchDelegate(this);
    
  7. 在模拟器上编译并运行应用程序。点击搜索栏并开始输入。它将自动搜索表格并显示结果。

    你可以在 SearchTableApp 项目中找到完整的代码。结果将是类似于以下屏幕截图所示的表格上方的常见 iOS 搜索栏:

    如何做...

如何工作...

UISearchDisplayController类提供了一个方便的方式来搜索数据。它包含UISearchBar,它接受用户的输入,以及UITableView,它用于显示结果。在我们向视图控制器添加搜索控制器后,我们可以通过该控制器的SearchDisplayController属性访问它。要触发结果表格,我们必须实现UISearchDisplayDelegate并覆盖其ShouldReloadForSearchString(返回一个布尔值)方法,如下所示:

private class SearchDelegate : UISearchDisplayDelegate

ShouldReloadForSearchString方法覆盖中,我们搜索我们的数据源,根据其forSearchString参数将过滤后的结果保存到新的数据源中:

this.parentController.filterDataList = this.parentController.tableData
  .Where (s => s.ToLower ().Contains (forSearchString.ToLower ()))
  .ToList ();

然后,我们按字母顺序排序结果并返回true,以便搜索控制器的表格重新加载数据,如下所示:

this.parentController.filterDataList.Sort (delegate(string firstStr, string secondStr) {
  return firstStr.CompareTo (secondStr);
});
return true;

搜索控制器的表格视图也需要一个源对象。在这个例子中,我们将其设置为为我们的表格创建的相同对象,如下所示:

this.TableView.Source = new TableSource (this);
this.SearchDisplayController.SearchResultsSource = new TableSource(this);

由于我们使用的是相同对象的实例,我们需要对其中的某些内容进行修改,以便根据哪个表格调用它来显示数据。例如,RowsInSection方法如下所示:

public override int RowsInSection (UITableView tableview, int section)
{
  if (tableview.Equals (this.parentController.TableView))
  {
    return this.parentController.tableData.Count;
  } else
  {
    return this.parentController.filterDataList.Count;
  }
}

以这种方式,我们根据哪个表格调用该方法来返回行数。同样,我们还需要在GetCell方法中设置每个单元格的文本标签,如下所示:

if (tableView.Equals (this.parentController.TableView))
{
  cell.TextLabel.Text = this.parentController.tableData[rowIndex];
} else
{
  cell.TextLabel.Text = this.parentController.filterDataList[rowIndex];
}

更多...

当用户点击搜索栏时,键盘出现,使搜索控制器变得活跃。要使其不活跃,我们可以挂钩到搜索栏的SearchButtonClicked事件。当用户点击键盘上的搜索按钮时,此事件将被触发:

this.SearchDisplayController.SearchBar.SearchButtonClicked += (s, e) => {
  this.SearchDisplayController.SetActive(false, true);
};

SetActive方法是我们用来启用或禁用搜索控制器的方法。

参见

  • 在表格中显示数据的食谱

  • 表格索引的食谱

创建简单的网页浏览器

在这个食谱中,我们将讨论使用UIWebView类显示在线内容。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用项目,并将其命名为WebBrowserApp

如何做到...

执行以下步骤:

  1. 在 Interface Builder 中打开WebBrowserAppViewController.xib文件,并在主视图中添加一个UIWebView对象。为其创建并连接一个名为webView的出口。保存文档。

  2. 如下代码所示,在WebBrowserAppViewController类中覆盖ViewDidAppear方法:

    public override void ViewDidAppear (bool animated)
    {
      NSUrl url = new NSUrl ("http://software.tavlikos.com");
      NSUrlRequest urlRequest = new NSUrlRequest (url);
      this.webView.LoadRequest (urlRequest);
    }
    
  3. 在模拟器上编译并运行应用。观察屏幕上网站加载的情况!

它是如何工作的...

UIWebView类是 iOS SDK 的网页浏览器控件。要加载网页内容,我们只需调用其LoadRequest方法,该方法接受一个NSUrlRequest类型的参数。NSUrlRequest对象包含我们想要加载的 URL,如下所示:

NSUrl url = new NSUrl ("http://software.tavlikos.com");

更多...

UIWebView类包含一些非常有用的事件,如下所示:

  • LoadStarted:当控件开始加载内容时触发

  • LoadFinished:当内容成功加载完成后触发

  • LoadError:当内容加载失败时触发

缩放内容

UIWebView 的另一个重要功能是内容的自动缩放。可以通过将其 ScalePageToFit 属性设置为 true 来激活它。

UIWebView 支持的文件

除了网页之外,UIWebView 控件还可以使用以下类型的文件来显示本地内容:

  • 电子表格(.xls

  • 演示文稿(.key.zip

  • 数字(.numbers.zip

  • 页面(.pages.zip

  • PDF(.pdf

  • 幻灯片(.ppt

  • 文档(.doc

  • 富文本格式(.rtf

  • 富文本格式目录(.rtfd.zip

  • 演示文稿(.key

  • 数字(.numbers

  • 页面(.pages

以网格形式显示数据

在这个菜谱中,我们将讨论使用 UICollectionView 对象以网格布局显示数据。UICollectionView 类是在 iOS 6 中引入的,这是一个被 iOS 开发人员忽视的非常有用的控件。在 UICollectionView 之前,显示网格数据的唯一方法是通过创建自定义控件,这并不是一个很容易的任务。

准备工作

在 Xamarin Studio 中创建一个新的项目,并将其命名为 CollectionViewApp。我们还需要一些内容来显示,所以将一张图片添加到项目中。

如何操作...

执行以下步骤:

  1. 在 Interface Builder 中打开 CollectionViewAppViewController.xib 文件,并在其主视图中添加一个 UICollectionView。以下截图显示了对象浏览器中的对象:如何操作...

  2. 回到 Xamarin Studio,添加以下类:

    public class ImageCell : UICollectionViewCell {
      public const string CELLID = "ImageCell";
      public ImageCell(IntPtr handle) : base(handle) {
        this.Initialize();
      }
      public UIImageView ImageView { get; private set; }
      private void Initialize() {
        this.ImageView = new UIImageView(this.ContentView.Bounds);
        this.ContentView.AddSubview(this.ImageView);
      }
    }
    
  3. 在控制器中添加以下嵌套类:

    private class CollectionSource : UICollectionViewSource {
      public CollectionSource(CollectionViewAppViewController parentController) {
        this.parentController = parentController;
      }
      private CollectionViewAppViewController parentController;
      public override int GetItemsCount(UICollectionView collectionView, int section) {
        return this.parentController.collectionData.Count;
      }
      public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) {
        ImageCell cell = (ImageCell)collectionView.DeqeueReusableCell((NSString)ImageCell.Cell, indexPath);
        cell.ImageView.Image = this.parentController.collectionData[indexPath.Row];
        return cell;
      }
    }
    
  4. 在控制器中添加以下代码:

    private List<UIImage> collectionData;
    public override ViewDidLoad() {
      base.ViewDidLoad();
      this.collectionData = new List<UIImage>();
      for (int i = 0; i < 30; i++) {
        this.collectionData.Add(UIImage.FromBundle("shapes"));
      }
      this.collectionView.RegisterClassForCell(typeof(ImageCell), (NSString)ImageCell.CELLID);
      this.collectionView.Source = new CollectionSource(this);
    }
    
  5. 在模拟器上编译并运行应用程序。结果应类似于以下截图所示:如何操作...

它是如何工作的...

UICollectionView 类的使用方式与 UITableView 类似。主要区别在于它不是在单列中显示数据,而是在网格排列中显示。UICollectionViewSource 类被重写以提供集合视图的数据源,如下面的代码所示:

private class CollectionSource : UICollectionViewSource {

就像在 UITableViewSource 中的表格视图一样,我们需要提供网格中的项目数量以及单个项目的对象,在这种情况下是 UICollectionViewCell,如下面的代码所示:

public override int GetItemsCount(UICollectionView collectionView, int section) {
  return this.parentController.collectionData.Count;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath) {
  ImageCell cell = (ImageCell)collectionView.DeqeueReusableCell((NSString)ImageCell.Cell, indexPath);
  cell.ImageView.Image = this.parentController.collectionData[indexPath.Row];
  return cell;
}

注意,与 UITableViewCell 不同,UICollectionViewCell 类为我们提供的功能不多。因此,我们必须重写它来为集合创建自己的单元格,如下面的代码所示:

public class ImageCell : UICollectionViewCell

由于我们使用的是自定义单元格,我们必须通过 RegisterClassForCell 方法让集合视图知道它,如下面的代码所示:

this.collectionView.RegisterClassForCell(typeof(ImageCell), (NSString)ImageCell.CELLID);

还有更多...

除了使用UICollectionViewCell类显示单个项目外,UICollectionView还支持显示UICollectionReusableView类型的辅助视图。这些视图基本上代表集合视图中各部分的头部或尾部。

要提供辅助视图,我们需要创建自己的子类,使用以下代码:

public class CollectionHeader : UICollectionReusableView

然后我们需要在集合源中重写以下方法(此方法将返回我们想要的辅助视图):

public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath) {
  CollectionHeader header = (CollectionHeader)collectionView.DequeueReusableSupplementaryView(UICollectionElementKindSection.Header, viewIdentifier, indexPath);
  return header;
}

elementKind参数是UICollectionElementKindSection枚举的NSString表示,它包含两个值:FooterHeader

最后,我们需要调用RegisterClassForSupplementaryView方法来将我们的自定义类注册到集合视图中,使用以下代码:

this.collectionView.RegisterClassForSupplementaryView(typeof(CollectionHeader), UICollectionElementKindSection.Header, viewIdentifier);

更多关于 UICollectionView 的信息

在 Xamarin 网站上可以找到一个关于UICollectionView的优秀教程:docs.xamarin.com/guides/ios/user_interface/introduction_to_collection_views/

相关阅读

  • 在表格中显示数据的食谱

  • 自定义行的食谱

自定义网格

在这个食谱中,我们将学习如何自定义集合视图的显示。

准备工作

在这个食谱中,我们将处理我们在在网格中显示数据食谱中创建的CollectionViewApp项目。在 Xamarin Studio 中打开项目。

如何操作...

执行以下步骤:

  1. 在控制器的ViewDidLoad方法中,添加以下代码:

    UICollectionViewFlowLayout flowLayout = new UICollectionViewFlowLayout();
    flowLayout.MinimumLineSpacing = 20f;
    flowLayout.MinimumInteritemSpacing = 4f;
    flowLayout.SectionInset = new UIEdgeInset(4f, 4f, 4f, 4f);
    flowLayout.ItemSize = new SizeF(20f, 20f);
    this.collectionView.CollectionViewLayout = flowLayout;
    
  2. 在模拟器上编译并运行应用。结果应该与以下截图所示类似:如何操作...

它是如何工作的...

可以通过UICollectionViewLayout类自定义集合视图的布局。UICollectionViewFlowLayout是这个类的子类,提供了一个简单的布局,我们可以使用它。

通过设置特定的属性,我们定义了集合视图将如何排列单元格。以下列表描述了我们在项目中设置的属性:

  • MinimumLineSpacing:这是网格中行之间的最小距离

  • MinimumInteritemSpacing:这是网格中单个项目之间的最小距离

  • SectionInset:这是集合视图中每个部分周围应该留空的区域

  • ItemSize:这是集合视图中每个项目的尺寸

以下图像显示了每个属性在集合视图中的对应关系:

它是如何工作的...

更多内容...

设置集合布局对象的ItemSize参数将调整集合视图中所有项目的尺寸。我们可以在CollectionSource子类中提供以下方法来为每个单元格单独设置尺寸:

[Export("collectionView:layout:sizeForItemAtIndexPath:")]
public SizeF GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath) {
  if (indexPath.Item > 11 && indexPath.Item < 19) {
    return new SizeF(40f, 40f);
  } else {
    return new SizeF(20f, 20f);
  }
}

将前面的方法添加到我们自己的CollectionSource子类中,将得到以下截图所示的结果:

还有更多...

参见

  • 在网格中显示数据 的配方

第六章。网络服务

在本章中,我们将涵盖以下主题:

  • 消费网络服务

  • 消费 REST 服务

  • 与原生 API 通信

  • 使用 WCF 服务

简介

向用户提供在线信息是移动开发的关键部分。在本章中,我们将讨论开发与网络服务通信的应用程序以提供信息。我们将了解如何基于 SOAP 消费和调用网络服务。我们还将讨论如何使用 REST 网络服务以及如何从网络服务器解析流行的 JSON 数据格式。最后但同样重要的是,我们将探讨如何使用原生 iOS API 进行通信以及如何使用 WCF 服务。

本章中的所有示例都使用XSP,这是一个与 Mono 框架一起提供的轻量级网络服务器;因此,无需在线或本地运行一个实时网络服务即可使用提供的代码。

消费网络服务

在本菜谱中,我们将学习如何在 Xamarin.iOS 项目中使用 SOAP 网络服务。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序项目,并将其命名为WebServiceApp。本章的代码包含一个名为MTWebService的网络服务项目。这就是我们将要使用的网络服务。

要使用MTWebService网络服务,我们需要一个网络服务器。Mono 框架为我们提供了一个用于测试目的的 XSP 轻量级网络服务器。打开一个终端,并键入以下命令以进入网络服务的目录,将<code_directory>替换为下载的代码所在的路径:

cd <code_directory>/CH06_code/MTWebService/MTWebService

在提示符中键入xsp4以运行 XSP 网络服务器。您将看到类似于以下输出的内容:

xsp4
Listening on address: 0.0.0.0
Root directory: /Users/dtavlikos/projects/CH06_code/MTWebService/MTWebService
Listening on port: 8080 (non-secure)
Hit Return to stop the server.

网络服务器现在正在运行。

如何操作...

完成此菜谱的以下步骤:

  1. 我们需要在项目中添加对网络服务的引用。在解决方案面板中的项目上右键单击,然后导航到添加 | 添加 Web 引用。在随后显示的对话框中,在Web 服务 URL字段中添加http://localhost:8080/MTTestWebService.asmx?wsdl,然后单击转到按钮。

  2. 框架组合框中选择.NET 2.0 Web Services

  3. 引用字段设置为mtWebService

  4. 在所有设置正确输入后,对话框应类似于以下截图。单击确定按钮以添加 Web 引用:如何操作...

  5. WebServiceAppViewController添加一个按钮和标签。

  6. 在控制器的ViewDidLoad方法中,添加以下代码:

    this.btnFetch.TouchUpInside += (s, e) => {
      using (MTTestWebService webService = new MTTestWebService())
      {
        this.lblOutput.Text = webService.GetMessage ("Hello Web Service!");
      }
    };
    
  7. 将以下using指令添加到WebServiceAppViewController.cs文件中:

    using WebServiceApp.mtWebService;
    
  8. 在模拟器上编译并运行应用程序。单击获取按钮,输出应显示在屏幕上。

它是如何工作的...

Xamarin.iOS 应用可以像 .NET 桌面应用程序一样消费 Web 服务。XSP 轻量级 Web 服务器在默认情况下通过安装 Mono 框架安装,这是 Xamarin 安装的要求。在终端中运行 xsp4 命令且不带任何参数时,它默认将其基本目录设置为当前目录并开始监听 8080 端口。如果 Web 服务器已启动,可以在浏览器中输入 http://localhost:8080/MTTestWebService.asmx 来查看 Web 服务描述。

Xamarin Studio 从提供的 URL 读取 WSDL 信息并创建必要的代理,这将允许我们在项目中使用 Web 服务。

然后,我们将 Framework 值设置为 .NET 2.0 Web Services 并提供一个 Reference 名称,这将反映 Web 引用的命名空间。为了在我们的代码中使用 Web 服务,我们实例化它,然后只需调用我们感兴趣的方法:

this.lblOutput.Text = webService.GetMessage ("Hello Web Service!");

更多内容...

除了使用本地托管 Web 服务外,互联网上还有许多示例 Web 服务。简单的搜索会产生许多结果。

异步调用 Web 服务方法

创建的代理还包含基于 Begin/End 和用于调用 Web 服务异步的事件的方法。以下示例展示了如何使用基于事件的方法:

MTTestWebService webService = new MTTestWebService();
webService.GetMessageCompleted += (sender, args) => this.InvokeOnMainThread(() => this.lblOutput.Text = args.Result);
webService.GetMessageAsync("Hello Web Service!");

注意在事件处理程序内部调用的 InvokeOnMainThread;它是在一个单独的线程上被调用的。因此,如果我们想在其中访问主线程,我们需要用 InvokeOnMainThread 来包装我们的调用。

XSP 关闭

要关闭 XSP Web 服务器,只需在执行它的终端中点击 Return 键。

参见

  • 与原生 API 通信 配方

消费 REST 服务

在这个配方中,我们将讨论如何正确使用和消费 REST 服务与 Xamarin.iOS。

准备工作

在 Xamarin Studio 中创建一个新的 Single View Application 并命名为 ForecastApp。在这个配方中,我们将使用 Open Meteo Foundation REST API。使用此 API 受此页面中声明的使用条款约束:openmeteofoundation.org/terms-of-use

如何操作...

执行以下步骤以完成此配方:

  1. ForecastAppViewController 上添加一个标签和一个按钮。确保标签的 Lines 属性设置为至少三行。

  2. 在控制器的 ViewDidLoad 方法中添加以下代码:

    this.btnForecast.TouchUpInside += async (sender, e) => {
    
      HttpClient client = new HttpClient();
      string jsonResponse = await client.GetStringAsync("http://api.ometfn.net/0.1/forecast/eu12/46.5,6.32/now.json");
      JsonValue jsonObj = JsonValue.Parse(jsonResponse);
      JsonArray tempArray = (JsonArray)jsonObj["temp"];
      double temp = (double)tempArray[0];
      JsonArray windSpeedArray = (JsonArray)jsonObj["wind_10m_ground_speed"];
      double windSpeed = (double)windSpeedArray[0];
      this.lblOutput.Text = string.Format("Temperature: {0}\nWind speed: {1}", temp, windSpeed);
    };
    
  3. System.Net.HttpSystem.Json 引用添加到项目中。别忘了将相应的 using 指令包含到 ForecastAppViewcontroller.cs 文件中。

  4. 在模拟器上编译并运行应用程序。点击 获取预报 按钮以在屏幕上显示当前温度和风速。

它是如何工作的...

在这个菜谱中,我们创建了一个使用 REST API 获取位置当前预报的应用程序。我们使用async/await模式,允许我们异步连接和检索数据。这有助于确保我们的应用程序在连接到检索数据时不会冻结。

由于所有操作都是在点击按钮时发生的,因此其TouchUpInside处理方法需要标记为async,如下所示:

this.btnForecast.TouchUpInside += async (sender, e) => {
//..

然后,我们使用HttpClient类,它是System.Net.Http命名空间的一部分,为我们提供了连接到端点的异步方法,如下所示:

HttpClient client = new HttpClient();
  string jsonResponse = await client.GetStringAsync("http://api.ometfn.net/0.1/forecast/eu12/46.5,6.32/now.json");

我们将端点传递给GetStringAsync方法,并返回一个 JSON 响应字符串。

在获取响应后,我们需要解析它以从中提取所需的信息。我们通过使用System.Json命名空间来完成此操作。该命名空间包含一组简单的类,允许我们解析 JSON 字符串,如下所示:

JsonValue jsonObj = JsonValue.Parse(jsonResponse);

温度包含在 JSON 响应的temp键下,该键包含一个数组,尽管只有一个条目,如下所示:

JsonArray tempArray = (JsonArray)jsonObj["temp"];
double temp = (double)tempArray[0];

在我们从 JSON 响应中读取所需的信息后,我们按以下方式显示它:

this.lblOutput.Text = string.Format("Temperature: {0}\nWind speed: {1}", temp, windSpeed);

更多内容...

System.Json命名空间对于解析简单的 JSON 字符串非常有帮助。然而,如果我们必须解析大型且更复杂的对象,事情可能会变得非常复杂。我们可以下载并使用在 Xamarin.iOS 项目中的一些开源库。以下是最受欢迎的几个:

参见

  • 消费 Web 服务菜谱

与原生 API 通信

在这个菜谱中,我们将讨论使用原生 iOS API 连接和消费 REST 服务。

准备工作

对于这个菜谱,我们将基于之前菜谱中创建的ForecastApp,即消费 REST 服务,进行工作。在 Xamarin Studio 中打开项目。

如何做到这一点...

执行以下步骤:

  1. ViewDidLoad方法中注释掉代码并添加以下代码:

    this.btnForecast.TouchUpInside += (sender, e) => {
      NSUrlRequest request = new NSUrlRequest(new NSUrl("http://api.ometfn.net/0.1/forecast/eu12/46.5,6.32/now.json"));
      NSUrlConnection connection = new NSUrlConnection(request, new ConnectionDelegate((response) => {
        JsonValue jsonObj = JsonValue.Parse(response);
        JsonArray tempArray = (JsonArray)jsonObj["temp"];
        double temp = (double)tempArray[0];
        JsonArray windSpeedArray = (JsonArray)jsonObj["wind_10m_ground_speed"];
        double windSpeed = (double)windSpeedArray[0];
        this.lblOutput.Text = string.Format("Temperature: { 0}\ nWind speed: { 1}", temp, windSpeed);
        }));
      connection.Start();
    };
    
  2. 将以下类添加到项目中:

    public class ConnectionDelegate : NSURLConnectionDelegate {
      private Action<string> finishedCallback;
      private StringBuilder responseData;
      public ConnectionDelegate(Action<string> callback) {
        this.finishedCallback = callback;
        this.responseData = new StringBuilder();
      }
      public override void ReceivedData(NSUrlConnection connection, NSData data) {
        if (null != data) {
          this.responseData.Append(data.ToString());
        }
      }
      public override FinishedLoading(NSUrlConnection connection) {
        if (null != this.finishedCallback) {
          this.finishedCallback(this.responseData.ToString());
        }
        this.responseData.Clear();
      }
    }
    
  3. 在模拟器上编译并运行应用程序。点击获取预报按钮以获取并显示预报数据。

它是如何工作的…

NSUrlConnection类是提供基本连接功能的原生 iOS 类。我们通过传递NSUrlRequest和一个代理对象来初始化它,如下所示:

NSUrlRequest request = new NSUrlRequest(new NSUrl("http://api.ometfn.net/0.1/forecast/eu12/46.5,6.32/now.json"));
  NSUrlConnection connection = new NSUrlConnection(request, new ConnectionDelegate((response) => {

我们创建的ConnectionDelegate类充当我们的NSUrlConnection的代理对象。在类内部,我们需要重写ReceivedData方法以用从服务接收到的数据填充我们的内部缓冲区,如下所示:

public override void ReceivedData(NSUrlConnection connection, NSData data) { 

类似于从HttpWebResponseStream对象中读取数据,一旦有新数据可用,就会调用ReceivedData方法。在ReceivedData内部,我们确保将数据追加到我们的缓冲区。当所有数据都已接收且响应完成时,将调用FinishedLoading方法,如下所示:

public override void FinishedLoading(NSUrlConnection connection) {

在初始化NSUrlConnection实例后,我们调用其Start方法来启动连接,如下所示:

connection.Start();

更多内容...

如果连接出现错误,NSUrlConnectionDelegateFailedWithError方法将被调用。我们可以重写它以获取发生的错误信息,如下所示:

public override void FailedWithError(NSUrlConnection connection, NSError error) {
  if (null != error) {
    Console.WriteLine("Connection error: {0}", error.LocalizedDescription);
  }
}

同步 NSUrlConnection

使用具有委托对象的NSUrlConnection类意味着连接将在启动它的线程上异步进行。如果我们想启动一个同步连接,可以使用SendSynchronousRequest静态方法,如下所示:

NSUrlResponse response;
NSError error;
NSData data = NSUrlConnection.SendSyncrhonousRequest(request, out response, out error);
// do something with data

用户体验

如您可能已经注意到的,使用NSUrlConnection比使用普通的 Mono BCL 类要复杂一些。通常,在大多数场景中使用 BCL 类是最佳实践,因为它有助于我们维护一个多平台代码库,等等。

然而,在某些情况下,原生 API 非常有用,并且是唯一可用的解决方案。例如,iOS 支持在后台运行某些连接功能,这些功能只能通过原生 API 实现。

参见

  • 消费 REST 服务菜谱

  • 在第十二章的在后台更新数据菜谱中,多任务

使用 WCF 服务

在这个菜谱中,我们将学习如何使用 Xamarin.iOS 消费 WCF 服务。

注意

WCF 服务支持仅适用于 Xamarin 的商业和企业许可证。

准备工作

对于这个项目,我们需要一个正在运行的 WCF 服务。WCF 服务可以在本章的代码下载中找到。要启动服务,打开终端并转到项目的目录。通过运行start_wcfservice.shshell 脚本启动服务,如下所示:

cd <code_directory>/CH06_code/WcfService/WcfService
./start_wcfservice.sh

服务启动后,在 Xamarin Studio 中创建一个新的单视图应用程序,命名为WcfServiceApp。还需要一台运行 Windows 的机器。

如何操作...

执行以下步骤:

  1. 在项目中将System.Runtime.SerializationSystem.ServiceModel命名空间及其对应的using指令添加到WcfServiceAppViewController.cs文件中。

  2. Xamarin.iOS 不完全支持 WCF 服务。为了生成客户端代理,我们需要在 Windows 机器上使用slsvcutil工具。在 Windows 的命令提示符下运行以下命令:

    "c:\Program Files\Microsoft SDKs\Silverlight\v3.0\Tools\slsvcutil /noconfig http://192.168.0.113:8080/WcfService.svc?wsdl"
    
    

    此命令将生成一个名为service.cs的 C#源文件。将此文件添加到 Xamarin.iOS 项目中。将以下突出显示代码中的 IP 地址替换为您自己的以使其正确工作。

  3. WcfServiceAppViewController的视图中添加一个标签和一个按钮。在ViewDidLoad方法中添加以下代码:

    this.btnFetchData.TouchUpInside += (sender, e) => {
     WcfTestServiceClient client = new WcfTestServiceClient (new BasicHttpBinding (), new EndpointAddress ("http://192.168.0.113:8080/WcfTestService.svc"));
        client.GetBookInfoCompleted += WcfTestServiceClient_GetBookInfoCompleted;
        client.GetBookInfoAsync ();
        UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
    };
    
  4. 最后,添加以下方法:

    private void WcfTestServiceClient_GetBookInfoCompleted (object sender, GetBookInfoCompletedEventArgs e)
    {
      this.InvokeOnMainThread (delegate {
        UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
        this.lblResponse.Text = String.Format ("Book title: {0}\nAuthor: {1}", e.Result.Title, e.Result.Name);
      } );
    }
    
  5. 在模拟器上编译并运行应用程序。点击按钮,观察从服务返回的数据填充到标签中。

它是如何工作的...

Xamarin.iOS 依赖于 Mono Framework 对 WCF 服务的支持,但这并不完整。尽管如此,WCF 服务可以在 iOS 应用程序中使用,这使得 Xamarin.iOS 对.NET 开发者更具吸引力。

然而,没有工具可以在 Mac 上创建客户端代理,Xamarin Studio 也无法创建合适的代理;因此,我们需要访问一台 Windows 机器来使用 Silverlight 服务模型代理生成工具(SLsvcUtil.exe)来完成这项工作。该工具生成的源文件允许我们在项目中使用 WCF 服务。

使用 Silverlight 版本 3.0 的slsvcutil创建客户端代理非常重要。

除了 Mono Framework 对 WCF 服务的支持外,还有一个限制,那就是 iOS 上不允许动态代码生成。这使得任何依赖于System.Reflection.Emit命名空间中的代码都无法使用。实际上,在 Xamarin.iOS 中,System.Reflection.Emit命名空间根本不可用。

在 Mac 上复制生成的文件后,我们将其添加到项目中,我们就准备好使用 WCF 服务了。前面高亮显示的代码显示了如何实例化服务对象。请注意,服务对象的默认构造函数不能使用,因为 Xamarin.iOS 不支持System.Configuration命名空间。

注意

实际的通信是通过在设置相应完成事件的处理器后调用方法的异步实现来进行的。请注意,在这种情况下,没有使用同步调用或BeginInvokeEndInvoke模式的替代方案:

client.GetBookInfoCompleted += WcfTestServiceClient_GetBookInfoCompleted;
client.GetBookInfoAsync ();

可以通过指定的EventArgs派生类的Result属性检索服务返回的结果:

this.labelResult.Text = String.Format ("Book title: {0}\nAuthor: {1}", e.Result.Title, e.Result.Name);

更多...

当调试一个消费 WCF 服务的项目时,请记住设置服务运行在的机器的地址,而不是 localhost 或 127.0.0.1。这是因为当我们运行应用程序在设备上时,应用程序将无法连接到服务。

关于 Xamarin Studio 的 WCF 支持的更多信息

在“消费 Web 服务”配方中显示的“添加 Web 引用”窗口中,可以通过 Xamarin Studio 添加一个 WCF Web 引用。然而,它尚未完成,并且它生成的代理将无法工作。

WCF 服务创建

WcfService服务返回的对象以及实际的服务本身完全是在 Mac 上作为一个 Xamarin Studio 项目创建的。由于没有 WCF 项目模板,使用了空项目模板。

参见

  • 消费 Web 服务配方

第七章. 多媒体资源

在本章中,我们将涵盖以下主题:

  • 选择图像和视频

  • 使用相机捕获媒体

  • 播放视频

  • 播放音乐和声音

  • 使用麦克风录制

  • 直接管理相册项目

简介

今天智能手机和平板电脑最重要的功能之一是它们捕获和管理多媒体资源的能力。无论是照片、视频还是音频,针对这些设备且能有效处理多媒体的应用程序非常重要。

在本章中,我们将了解如何管理设备上存储的媒体。我们还将学习如何使用设备的多媒体捕获设备(相机和麦克风)来捕获内容并创建一个将为用户提供丰富体验的应用程序。

更具体地说,我们将讨论以下主题:

  • UIImagePickerController:这是一个控制器,它通过用户界面提供对设备上保存的照片和视频的访问权限,同时也提供了一个相机界面,可以通过设备的相机硬件捕获照片。

  • MPMoviePlayerController:这是一个控制器,允许我们播放和流式传输视频文件。

  • MPMediaPickerController:这是访问由原生 iPod 应用程序管理的保存内容的默认用户界面。

  • MPMusicPlayerController:这是一个负责播放 iPod 内容的对象。

  • AVAudioPlayer:这是一个类,允许我们播放声音文件。

  • AVAudioRecorder:这是一个类,允许我们使用麦克风录制音频。

  • ALAssetsLibrary:这是一个类,提供对设备上可用资产及其元数据的访问权限。

选择图像和视频

在本食谱中,我们将学习如何为用户提供从设备相册导入图像和视频的能力。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ImagePickerApp。对于本食谱,我们需要一些图像存储在模拟器的相册中。

向模拟器添加图像的一个简单方法是通过 Safari 导航到一个网页。在 Safari 中长按(点击并按住)任何图像将显示一个包含保存选项的操作表。点击该选项将图像保存到相册中。

如何操作...

执行以下步骤:

  1. 在 Interface Builder 中打开ImagePickerAppViewController.xib文件,并向其中添加UIImageViewUIButton

  2. ViewDidLoad方法中输入以下代码:

    this.imagePicker = new UIImagePickerController();
    this.imagePicker.FinishedPickingMedia += this.ImagePicker_FinishedPickingMedia;
    this.imagePicker.Canceled += this.ImagePicker_Cancelled;
    this.imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary;
    this.btnSelect.TouchUpInside += async (s, e) => {
      await this.PresentViewControllerAsync(this.imagePicker, true);
    };
    
  3. 实现以下代码中的FinishedPickingMediaCanceled事件处理程序:

    private async void ImagePicker_FinishedPickingMedia (object sender, UIImagePickerMediaPickedEventArgs e)
    {
      UIImage pickedImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
      this.imageView.Image = pickedImage;
      await this.imagePicker.DismissViewControllerAsync(true);
    }
    private async void ImagePicker_Cancelled (object sender, EventArgs e)
    {
      await this.imagePicker.DismissViewControllerAsync(true);
    }
    
  4. 在模拟器上编译并运行应用程序。点击在初始步骤中添加的按钮以显示图片选择器,并通过点击其缩略图来选择一个图像。图像将在图像视图中显示。UIImagePickerController在以下屏幕截图中显示:如何操作...

注意

在应用程序中第一次显示 UIImagePickerController 之前,iOS 将显示一个警报,请求用户允许访问照片库。处理这种情况将在本章后面的 直接管理相册项目 食谱中描述。

它是如何工作的...

UIImagePickerController 是 iOS 提供的一个特殊视图控制器,用于选择保存在设备相册中的图像和视频,甚至可以从相机捕获新的媒体。

在初始化图像选择器对象后,我们需要订阅其 FinishedPickingMedia 事件,该事件为我们提供了用户所选的媒体。在分配给它的处理程序中,我们获取所选的图像:

UIImage pickedImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;

Info 属性返回一个包含有关所选媒体各种信息的 NSDictionary 对象。我们通过传递 UIImagePickerController.OriginalImage 常量作为键来检索图像。由于字典的值是 NSObject 类型,我们将返回值转换为 UIImage。在我们将图像分配给要显示的 UIImageView 之后,我们使用以下代码关闭控制器:

await this.imagePicker.DismissViewControllerAsync(true);

当用户点击控制器的 取消 按钮时,会触发 Canceled 事件。我们必须订阅它以关闭控制器,因为它在用户点击 取消 按钮时不会自动关闭。

还有更多...

我们可以通过图像选择器的 SourceType 属性定义它将从中读取的图像/视频的来源。在这个例子中,我们使用 UIImagePickerController.PhotoLibrary,因为模拟器不支持相机硬件。

选择视频

UIImagePickerController 默认只显示图像。要支持视频,其 MediaType 属性必须被设置。它接受一个 string[] 参数,其中包含指定的媒体名称,如下面的代码所示:

this.imagePicker.MediaTypes = new string[] { "public.image", "public.movie" };

要确定用户选择的媒体类型,我们在 FinishedPickingMedia 处理器中的字典中检查 MediaType 键。如果是视频,我们使用 MediaUrl 键获取其 URL,如下面的代码所示:

if (e.Info[UIImagePickerController.MediaType].ToString() == "public.movie")
{
  NSUrl mediaUrl = e.Info[UIImagePickerController.MediaURL] as NSUrl;
  // Do something useful with the media url.
}

参见

  • 使用相机捕捉媒体 的食谱

  • 直接管理相册项目 的食谱

使用相机捕捉媒体

在本食谱中,我们将学习如何使用设备相机来捕捉媒体。

准备工作

打开我们在上一个食谱中创建的 ImagePickerApp 项目。

注意

相机功能在 iOS 模拟器上不可用。此示例只能在设备上运行。有关更多信息,请参阅第十四章,部署

如何操作...

执行以下步骤:

  1. 在控制器类的 ViewDidLoad 方法中,将 this.imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary; 替换为以下代码块:

    if (UIImagePickerController.IsSourceTypeAvailable(UIImagePickerControllerSourceType.Camera))
    {
      this.imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;
    }  else
    {
      this.imagePicker.SourceType = UIImagePickerControllerSourceType.PhotoLibrary;
    }
    
  2. FinishedPickingMedia 处理程序中,在关闭图像选择器之前添加以下代码:

    pickedImage.SaveToPhotosAlbum((s, error) => {
      if (null != error)
      {
        Console.WriteLine("Image not saved! Message: {0}", error.LocalizedDescription);
      }
    } );
    
  3. 在设备上编译并运行应用。点击按钮打开相机并拍照。照片将被保存到设备相册中。

它是如何工作的...

在展示相机取景器之前,我们必须确保应用运行在具有适当硬件的设备上。我们通过调用 UIImagePickerController 类的静态 IsSourceTypeAvailable 方法来完成此操作,如下所示:

if (UIImagePickerController.IsSourceTypeAvailable(UIImagePickerControllerSourceType.Camera))

如果此返回 true,我们使用以下代码将源类型设置为 Camera

this.imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;

这将导致图像选择器控制器启动相机设备而不是加载设备相册。

当用户通过我们的应用程序拍照(或录像)时,它不会自动保存在设备上。要保存它,我们使用 UIImage 类的 SaveToPhotosAlbum 方法。此方法接受 UIImage.SaveStatus 类型的委托,如果发生错误将报告错误:

if (null != error)
{
  Console.WriteLine("Image not saved! Message: {0}", error.LocalizedDescription);
}

还有更多...

相机视图也可以进行自定义。要禁用默认的相机控件,将 ShowsCameraControls 属性设置为 false。然后,将您想要的控件的自定义视图传递给 CameraOverlayView 属性。要触发相机的快门,请调用 TakePicture 方法。

图像编辑

在捕获图像后,相机支持简单的编辑功能。此编辑功能允许用户选择图像的特定部分,甚至可以放大到特定区域。要显示编辑控件,将 AllowsEditing 属性设置为 true。编辑后的图像可以从 FinishedPickingMedia 处理器中的字典中检索,传递 UIImagePickerController.EditedImage 键。编辑界面如下截图所示:

图像编辑

相关内容

  • 选择图像和视频 菜谱

播放视频

在本菜谱中,我们将学习如何显示视频播放器界面并播放视频文件。

准备工作

在 Xamarin Studio 中创建一个新的 Single View Application,并将其命名为 PlayVideoApp

如何操作...

执行以下步骤:

  1. 在控制器的主视图中添加一个按钮。

  2. 将视频文件添加到项目中,并将其 Build Action 设置为 Content

  3. 将以下代码添加到控制器类中的 ViewDidLoad 方法:

    this.moviePlayer = new MPMoviePlayerController(NSUrl.FromFilename("video.mov"));
    this.moviePlayer.View.Frame = new RectangleF(0f, 20f, this.View.Frame.Width, 320f);
    this.View.AddSubview(this.moviePlayer.View);
    this.playbackStateChanged = MPMoviePlaybackController.Notifications.ObservePlaybackStateDidChange(this.MoviePlayer_PlaybackStateChanged);
    this.finishedPlaying = MPMoviePlaybackController.Notifications.ObservePlaybackDidFinish(this.MoviePlayer_FinishedPlayback);
    this.btnPlayVideo.TouchUpInside += delegate {
      this.moviePlayer.Play();
    } ;
    
  4. MainController 类中输入以下方法:

    private void MoviePlayer_PlaybackStateChanged(object sender, NSNotificationEventArgs e)
    {
      Console.WriteLine("Movie player load state changed: {0}", this.moviePlayer.PlaybackState);
    }
    private void MoviePlayer_FinishedPlayback(object sender, 
      NSNotificationEventArgs e)
    {
      Console.WriteLine("Movie player finished playing.");
    }
    
  5. 在模拟器上编译并运行应用。点击按钮,视频将加载并开始播放。在 Xamarin Studio 的 Application Output 面板中查看显示的消息。以下截图显示了在模拟器上播放的视频:如何操作...

它是如何工作的...

MPMoviePlayerController 控制器播放存储在本地的或从网络流式传输的视频文件。我们使用接受 NSUrl 参数的构造函数初始化控制器,如下面的代码所示:

this.moviePlayer = new MPMoviePlayerController(NSUrl.FromFilename("video.mov"));

NSUrl 类是标准的 iOS URL 类。

在创建实例后,我们使用以下代码为其视图定义一个框架并将其添加到我们的视图中:

this.moviePlayer.View.Frame = new RectangleF(0f, 20f, this.View.Frame.Width, 320f);
this.View.AddSubview(this.moviePlayer.View);

在上一节中突出显示的代码向默认通知中心添加了观察者,以便在播放状态改变或结束时通知我们。然后,我们调用它的 Play 方法,并显示 MPMoviePlayerController 控制器的视图,视频开始播放。

MoviePlayer_PlaybackStateChanged 方法中,我们使用以下代码输出 PlaybackState 属性:

Console.WriteLine("Movie player load state changed: {0}", this.moviePlayer.PlaybackState);

此属性告诉我们播放状态,例如,PausedPlayingSeekingForwardSeekingBackward

更多内容...

除了本例中使用的那些之外,我们还可以为 MPMoviePlayerController 控制器的更多通知添加观察者,其中一些如下:

  • DidEnterFullscreenNotification:它通知我们用户已点击全屏控制,控制器已进入全屏模式

  • DidExitFullscreenNotification:它通知控制器已离开全屏模式

  • DurationAvailableNotification:这通知我们控制器已收到关于视频持续时间的详细信息

  • LoadStateDidChangeNotification:这在网络播放中很有用;当控制器完成缓冲区中媒体的预加载时,它会触发。

  • NaturalSizeAvailableNotification:当电影框架的尺寸可用时,它会触发。大小可以通过播放器的 NaturalSize 属性检索

  • NowPlayingMovieDidChangeNotification:当播放器的视频内容发生变化时,它会触发。当前内容可以通过其 ContentUrl 属性获取

无线流媒体

从 iOS 版本 4.3 开始,MPMoviePlayerController 可以用于将视频流式传输到 Apple 的 AirPlay 兼容设备。要启用无线流媒体,将 MPMoviePlayerController 实例的 AllowsAirPlay 属性设置为 true。当 controller 显示时,它将提供一个界面,允许用户选择它检测到的设备。

相关内容

  • 播放音乐和声音 菜单

播放音乐和声音

在本菜谱中,我们将学习如何播放简单的音频文件和存储在设备上的歌曲。

准备中

在 Xamarin Studio 中创建一个新的 单视图应用程序 并将其命名为 PlayMusicApp

注意

此示例在模拟器上无法工作。您还需要在设备上的 iTunes 库中至少存储一首歌曲。

如何操作...

执行以下步骤:

  1. 向控制器的视图中添加三个按钮。

  2. PlayMusicAppViewController.cs 文件中添加以下 using 指令:

    using MonoTouch.MediaPlayer;
    
  3. 在类中添加以下两个字段:

    private MPMusicPlayerController musicPlayer;
    private MPMediaPickerController mediaPicker;
    
  4. ViewDidLoad 方法中添加以下代码:

    this.mediaPicker = new MPMediaPickerController(MPMediaType.Music);
    this.mediaPicker.ItemsPicked += MediaPicker_ItemsPicked;
    this.mediaPicker.DidCancel += MediaPicker_DidCancel;
    this.musicPlayer = 
     MPMusicPlayerController.ApplicationMusicPlayer;
    this.btnSelect.TouchUpInside += async (s, e) => {
      await this.PresentViewControllerAsync(this.mediaPicker, true);
    } ;
    this.btnPlay.TouchUpInside += (s, e) => {
      this.musicPlayer.Play();
    } ;
    this.btnStop.TouchUpInside += (s, e) => {
      this.musicPlayer.Stop();
    } ;
    
  5. 在类中添加以下方法:

    private async void MediaPicker_ItemsPicked (object sender, ItemsPickedEventArgs e)
    {
      this.musicPlayer.SetQueue(e.MediaItemCollection);
      await this.DismissViewControllerAsync(true);
    }
    private async void MediaPicker_DidCancel (object sender, EventArgs e)
    {
      await this.mediaPicker.DismissViewControllerAsync(true);
    }
    
  6. 在设备上编译并运行应用程序。点击 选择歌曲 按钮并选择一首或多首歌曲。

它是如何工作的...

MPMediaPickerController控制器提供了与原生音乐应用相同的用户界面,用于选择歌曲。MPMusicPlayerController控制器负责播放设备上存储的歌曲。

我们首先初始化媒体选择器,通过在构造函数中使用以下代码通过其构造函数传递我们想要它查找的媒体类型:

this.mediaPicker = new MPMediaPickerController(MPMediaType.Music);

之后,我们订阅其ItemsPickedDidCancel事件,以便我们可以通过以下代码捕获用户的反馈:

this.mediaPicker.ItemsPicked += MediaPicker_ItemsPicked;
this.mediaPicker.DidCancel += MediaPicker_DidCancel;

上一节中突出显示的代码显示了如何初始化音乐播放器对象。这里演示的选项MPMusicPlayerController.ApplicationMusicPlayer创建了一个仅适用于应用的实例。另一个可用的选项MPMusicPlayerController.iPodMusicPlayer创建了一个实例,允许在应用处于后台时播放媒体,类似于原生的音乐应用。

MediaPicker_ItemsPicked处理程序中,我们通过其SetQueue方法将用户选择的歌曲设置到音乐播放器中,如下所示:

this.musicPlayer.SetQueue(e.MediaItemCollection);

之后,我们关闭模态媒体选择器控制器。播放和停止歌曲分别通过MPMusicPlayerControllerPlay()Stop()方法实现。

还有更多...

MPMusicPlayerController包含正在播放的项目信息。这些信息可以通过其NowPlayingItem属性访问。它属于MPMediaItem类型,并包含正在播放的媒体的各种类型信息。以下示例输出正在播放的歌曲的标题:

Console.WriteLine(this.musicPlayerController.NowPlayingItem.ValueForProperty(MPMediaItem.TitleProperty));

播放声音文件

MPMusicPlayerController控制器是一个专门设计来管理和播放设备音乐库中存储的项目和播放列表的对象。

要播放简单的声音文件,Xamarin.iOS 提供了对 iOS 的AVAudioPlayer类的另一个包装器。以下代码是其最简单使用的示例:

using MonoTouch.AVFoundation;
//...
AVAudioPlayer audioPlayer = AVAudioPlayer.FromUrl(new NSUrl("path/to/sound file"));
audioPlayer.Play();

参见

  • 播放视频菜谱

使用麦克风录制

在本菜谱中,我们将学习如何使用设备的麦克风来录制声音。

准备工作

在 Xamarin Studio 中创建一个新的项目,并将其命名为RecordSoundApp

注意

此示例在模拟器上无法工作。

如何操作...

执行以下步骤:

  1. 将两个按钮和一个标签添加到控制器的视图中。

  2. RecordSoundAppViewController.cs文件中输入以下using指令:

    using System.IO;
    using MonoTouch.AVFoundation;
    using MonoTouch.AudioToolbox;
    
  3. 重写ViewDidLoad方法并向其中添加以下代码:

    NSUrl soundFileUrl = null;
    NSError error = null;
    AVAudioSession session = AVAudioSession.SharedInstance();
    session.SetCategory(AVAudioSession.CategoryPlayAndRecord, out error);
    session.SetActive(true, out error);
    bool grantedPermission = false;
    session.RequestRecordPermission((granted) => {
      if (granted) {
        grantedPermission = true;
        string soundFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "sound.wav");
        soundFileUrl = new NSUrl(soundFile);
        NSDictionary recordingSettings = NSDictionary.FromObjectAndKey(AVAudioSettings.AVFormatIDKey, NSNumber.FromInt32((int)AudioFileType.Wave));
        this.audioRecorder = AVAudioRecorder.ToUrl(soundFileUrl, recordingSettings, out error);
      } else {
        this.lblStatus.Text = "Permission to microphone refused";
      }
    });
    
    this.btnStart.TouchUpInside += (s, e) => {
      if (grantedPermission) {
        this.audioRecorder.Record();
        this.lblStatus.Text = "Recording…";
      }
    };
    this.btnStop.TouchUpInside += (s, e) => {
      if (grantedPermission) {
        this.audioRecorder.Stop();
       this.lblStatus.Text = "Idle";
        AVAudioPlayer player = AVAudioPlayer.FromUrl(soundFileUrl);
        player.Play();
      }
    };
    
  4. 在设备上编译并运行应用程序。点击开始录音按钮开始录音,例如,说出一些话以录制你的声音。点击停止****录音按钮停止录音并播放。

工作原理...

AVAudioRecorder类提供了录音功能。它是通过将捕获的音频直接流式传输到文件系统来实现的。在开始实际录音之前,我们需要使用以下代码准备共享音频会话:

NSError error = null;
AVAudioSession session = AVAudioSession.SharedInstance();
session.SetCategory(AVAudioSession.CategoryPlayAndRecord, out error);
session.SetActive(true, out error);

我们需要根据应用程序的需求调整音频会话,以便系统知道如何处理来自其他源的声音。通过将类别设置为AVAudioSession.CategoryPlayAndRecord,我们声明我们的应用程序在录音的同时能够播放音频。

第一次将共享音频会话的类别设置为任何需要使用麦克风的值时,iOS 会自动提示用户允许应用程序使用麦克风。通过调用RequestRecordPermission方法,我们可以确定用户是否已经允许我们的应用程序访问麦克风,如下面的代码所示:

session.RequestRecordPermission((granted) => {
  if (granted) {
    grantedPermission = true;
    //..

现在我们已经准备好了共享音频会话,是时候通过以下代码初始化一个AVAudioRecorder实例了:

this.audioRecorder = AVAudioRecorder.ToUrl(soundFileUrl, recordingSettings, out error);

如果与NSUrl变量对应的文件已经存在,它将被覆盖。

recordingSettings变量是NSDictionary类型,包含输出声音文件的设置。在初始化AVAudioRecorder时,我们必须提供至少一些最小设置。在这里,我们通过以下代码将声音格式设置为纯 WAV:

NSDictionary recordingSettings = NSDictionary.FromObjectAndKey(AVAudioSettings.AVFormatIDKey, NSNumber.FromInt32((int)AudioFileType.WAVE));

要指示录音器开始录音,我们只需通过以下代码行调用其Record()方法:

this.audioRecorder.Record();

当用户点击停止录音按钮时,录音停止,并使用AVAudioPlayer播放保存的声音:

this.audioRecorder.Stop();
AVAudioPlayer player = AVAudioPlayer.FromUrl(soundFileUrl);
player.Play();

更多内容...

AVAudioRecorder类还提供了声音计权选项。要启用声音计权,将它的MeteringEnabled属性设置为true。然后我们可以输出特定通道的峰值功率(分贝)。为了对录音的第一个通道执行此操作,在Record()方法调用后立即添加以下代码:

ThreadPool.QueueUserWorkItem(delegate {
  while (this.audioRecorder.Recording)
  {
    this.audioRecorder.UpdateMeters();
    Console.WriteLine(this.audioRecorder.PeakPower(0));
  }
} );

PeakPower方法接受通道的零基索引并返回该通道的峰值(分贝)。在调用PeakPower方法之前调用UpdateMeters()以获取最新的读数。

注意,在录音器上启用计权需要使用 CPU 资源。如果你不打算使用计权值,请不要启用它。

记录预定义的时间长度

要记录预定义时间长度的音频,而无需用户停止录音,请调用RecordFor(double)方法。它的参数是我们想要记录的秒数。

参见

  • 播放音乐和声音食谱

直接管理专辑项目

在本食谱中,我们将讨论如何通过编程方式访问设备的照片库。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ManageAlbumApp

注意

此示例在模拟器上工作。模拟器的照片库中必须至少存在一个图像。

如何操作...

执行以下步骤:

  1. 在控制器的主视图中添加一个按钮。

  2. MainController.cs 文件中输入以下 using 指令:

    using MonoTouch.AssetsLibrary;
    
  3. ViewDidLoad 方法中添加以下代码:

    this.btnEnumerate.TouchUpInside += (s, e) => {
      if (ALAssetsLibrary.AuthorizationStatus == ALAuthorizationStatus.Authorized ||
        ALAssetsLibrary.AuthorizationStatus == ALAuthorizationStatus.NotDetermined) {
        this.assetsLibrary = new ALAssetsLibrary();
        this.assetsLibrary.Enumerate(ALAssetsGroupType.All, this.GroupsEnumeration, this.GroupsEnumerationFailure);
      }
    } ;
    
  4. 在类中添加以下方法:

    private void GroupsEnumeration(ALAssetsGroup assetGroup, ref bool stop)
    {
      if (null != assetGroup)
      {
        stop = false;
        assetGroup.SetAssetsFilter(ALAssetsFilter.AllPhotos);
        assetGroup.Enumerate(this.AssetEnumeration);
      }
    }
    private void AssetEnumeration(ALAsset asset, int index, ref bool stop)
    {
      if (null != asset)
      {
        stop = false;
        Console.WriteLine("Asset url: {0}", asset.DefaultRepresentation.Url.AbsoluteString);
      }
    }
    private void GroupsEnumerationFailure(NSError error)
    {
      if (null != error)
      {
        Console.WriteLine("Error enumerating asset groups! Message: {0}", error.LocalizedDescription);
      }
    }
    
  5. 编译并运行应用。点击 Enumerate 按钮,观察保存照片的 URL 在 Application Output 面板中显示。

它是如何工作的...

ALAssetsLibrary 类提供了对设备相册项的访问。这些项由 ALAsset 类表示,并按组划分,由 ALAssetGroup 类表示。

我们需要做的第一件事是枚举资产组。为此,使用以下代码调用 Enumerate 方法:

this.assetsLibrary.Enumerate(ALAssetsGroupType.All, this.GroupsEnumeration, this.GroupsEnumerationFailure);

第一个参数是 ALAssetGroupTypes 类型,它指示要枚举的资产组。传递 ALAssetGroupTypes.All 表示我们想要枚举所有资产组。其他两个参数是委托类型。GroupsEnumeration 方法是我们读取组数据的地方,而 GroupsEnumerationFailure 方法将在发生错误时被触发。当第一次调用 Enumerate 方法时,用户会被要求授权应用访问设备的资产。如果用户拒绝访问,将触发失败方法。下次调用 Enumerate 方法时,访问消息将再次出现。

GroupsEnumeration 方法的签名如下:

private void GroupsEnumeration(ALAssetsGroup assetGroup, ref bool stop)

assetGroup 参数包含组的信息。

注意 stop 参数,它被声明为一个 ref 参数。当枚举发生时,该方法被触发一次以返回第一个组,并且无论存在多少更多组,都不会再次被调用。为了强制它继续被调用以枚举所有组,我们必须将 stop 变量设置为 false。当所有组都已枚举后,该方法最后一次被调用,此时 assetGroup 变量设置为 null。因此,我们需要检查这一点。要将所有这些放入代码中,请查看以下示例:

if (null != assetGroup)
{
  // Continue enumerating
  stop = false;
  // Determine what assets to enumerate
  assetGroup.SetAssetsFilter(ALAssetsFilter.AllPhotos);
  // Enumerate assets
  assetGroup.Enumerate(this.AssetEnumeration);
}

在对 ALAssetGroup 类的实例调用 SetAssetsFilter 方法之后,我们指示它过滤我们希望它查找的资产类型。之后,这个过程与组枚举类似。ALAssetGroup 类还包含一个 Enumerate 方法。它接受一个委托类型参数,在此处由 AssetsEnumeration 方法表示。其实现与 GroupsEnumeration 方法类似,如下所示:

if (null != asset)
{
  // Continue enumerating assets
  stop = false;
  // Output the asset url
  Console.WriteLine("Asset url: {0}", asset.DefaultRepresentation.Url.AbsoluteString);
}

ALAsset 类包含各种信息和属性。大部分信息存储在其 DefaultRepresentation 属性中,该属性为 ALAssetRepresentation 类型。

还有更多...

如果我们感兴趣的资产是一张图片,我们可以通过以下代码通过 DefaultRepresentation 属性获取实际图片:

CGImage image = asset.DefaultRepresentation.GetImage();

读取 EXIF 数据

我们可以通过 ALAssetRepresentationMetadata 属性读取照片的 可交换图像文件EXIF)格式元数据,该属性是 NSDictionary 类型,如下面的代码所示:

NSDictionary metaData = asset.DefaultRepresentation.Metadata;
if (null != metaData)
{
  NSDictionary exifData = (NSDictionary)metaData[new NSString("{Exif}")];
}

获取单个资源

如果我们知道资源的 URL,我们也可以通过 ALAssetLibraryAssetForUrl 方法检索单个资源。

检查权限

我们可以通过 ALAssetsLibrary.AuthorizationStatus 静态属性检查用户是否已授予对资源库的访问权限。ALAuthorizationStatus 枚举的可能值如下:

  • Authorized:这意味着用户已授权我们的应用程序。

  • Denied:这意味着用户已拒绝访问相册。

  • NotDetermined:这意味着我们的应用程序从未请求访问相册。

  • Restricted:这意味着应用程序未经授权访问相册,并且用户无法授予访问权限,可能由于家长控制限制。

注意,访问 AuthorizationStatus 属性不会提示用户请求权限。当我们实际上尝试通过调用 Enumerate 方法访问库时,iOS 才会提示用户请求权限。

参见

  • 选择图片和视频 菜单

第八章。集成 iOS 功能

在本章中,我们将涵盖以下内容:

  • 开始电话通话

  • 发送短信和电子邮件

  • 在我们的应用程序中使用短信

  • 在我们的应用程序中使用电子邮件消息

  • 管理地址簿

  • 显示联系人

  • 管理日历

简介

移动设备为用户提供了许多功能。创建一个与应用这些功能交互以向用户提供完整体验的应用程序可以肯定被视为一种优势。

在本章中,我们将讨论 iOS 的一些最常见功能以及如何将它们的一些或全部功能集成到我们的应用程序中。我们将看到如何使用原生平台应用或通过在我们的项目中集成原生用户界面来提供用户拨打电话、发送短信和电子邮件的能力。此外,我们还将讨论以下组件:

  • MFMessageComposeViewController:此控制器适用于发送文本(短信)消息

  • MFMailComposeViewController:此控制器用于发送带或不带附件的电子邮件

  • ABAddressBook:此类为我们提供了访问地址簿数据库的权限

  • ABPersonViewController:此控制器显示和/或编辑地址簿中的联系人信息

  • EKEventStore:此类负责管理日历事件

此外,我们还将学习如何读取和保存联系人信息,如何显示联系人详细信息,以及与设备的日历交互。

注意,本章中的一些示例可能需要设备。例如,模拟器不包含消息应用。要将模拟器部署到设备上,您需要通过苹果开发者门户注册为 iOS 开发者。

开始电话通话

在这个菜谱中,我们将学习如何调用原生的电话应用,允许用户拨打电话。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为PhoneCallApp

注意

原生的电话应用在模拟器上不可用。它仅在 iPhone 设备上可用。

如何操作...

执行以下步骤以允许用户拨打电话:

  1. PhoneCallAppViewController的视图中添加一个按钮。

  2. ViewDidLoad方法中添加以下代码:

    this.btnCall.TouchUpInside += (s, e) => {
      NSUrlurl = new NSUrl("tel:+123456789012");
      if (UIApplication.SharedApplication.CanOpenUrl(url))
      {
        UIApplication.SharedApplication.OpenUrl(url);
      }  else
      {
        Console.WriteLine("Cannot open url: {0}", url.AbsoluteString);
      }
    } ;
    
  3. 在设备上编译并运行应用。点击呼叫电话号码按钮开始通话。以下截图显示了电话应用正在拨打电话:如何操作...

它是如何工作的...

通过UIApplication.SharedApplication静态属性,我们可以访问应用的UIApplication对象。我们可以使用其OpenUrl方法,该方法接受一个NSUrl变量,以下是一行代码来启动通话:

UIApplication.SharedApplication.OpenUrl(url);

由于并非所有 iOS 设备都支持原生的电话应用,因此首先检查其可用性将很有用。您可以使用以下代码来完成此操作:

if (UIApplication.SharedApplication.CanOpenUrl(url))

当调用OpenUrl方法时,原生的Phone应用将被执行并立即开始拨打电话。请注意,需要tel:前缀来发起通话。

还有更多...

Xamarin.iOS 还通过MonoTouch.CoreTelephony命名空间支持 CoreTelephony 框架。这是一个简单的框架,它提供了有关呼叫状态、连接、运营商信息等方面的信息。请注意,当电话开始时,原生的Phone应用进入前台,导致应用挂起。CoreTelephony 框架的简单用法如下:

CTCallCenter callCenter = new CTCallCenter();
callCenter.CallEventHandler = delegate(CTCall call) {
  Console.WriteLine(call.CallState);
} ;

注意,处理程序是用等号(=)而不是常见的加等于(+=)组合分配的。这是因为CallEventHandler是一个属性,而不是一个事件。当应用进入后台时,事件不会分配给它。然而,当应用返回前台时,最后发生的事件将被分配。

关于 OpenUrl 的更多信息

OpenUrl方法可以用来打开各种原生和非原生应用程序。例如,要在 Safari 中打开网页,只需创建一个包含链接的NSUrl对象,如下所示:

NSUrl url = new NSUrl("http://www.packtpub.com");

参见

  • 发送短信和电子邮件菜谱

发送短信和电子邮件

在这个菜谱中,我们将学习如何在我们的应用程序中调用原生的MailMessaging应用。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为SendTextApp

如何操作...

执行以下步骤以调用应用程序:

  1. SendTextAppViewController的视图中添加两个按钮。

  2. ViewDidLoad方法中添加以下代码:

    this.btnSendText.TouchUpInside += (s, e) => {
      NSUrl textUrl = new NSUrl("sms:+123456789");
      if (UIApplication.SharedApplication.CanOpenUrl(textUrl))
      {
        UIApplication.SharedApplication.OpenUrl(textUrl);
      }  else
      {
        Console.WriteLine("Cannot send text message!");
      }
    } ;
    this.btnSendEmail.TouchUpInside += (s, e) => {
      NSUrl emailUrl = new NSUrl("mailto:mail@example.com");
      if (UIApplication.SharedApplication.CanOpenUrl(emailUrl))
      {
        UIApplication.SharedApplication.OpenUrl(emailUrl);
      }  else
      {
        Console.WriteLine("Cannot send email message!");
      }
    } ;
    
  3. 在设备上编译并运行应用程序。点击其中一个按钮以打开相应的应用程序。

如何工作...

再次使用OpenUrl方法,我们可以发送文本或电子邮件消息。只需使用前面示例代码中的sms:前缀即可打开原生的文本消息应用,如下所示:

UIApplication.SharedApplication.OpenUrl(new NSUrl("sms:+123456789012"));

sms:前缀后添加手机号码将打开原生的Messaging应用,如下面的屏幕截图所示:

如何工作...

对于打开原生的电子邮件应用,过程类似。传递mailto:前缀,如下所示:

UIApplication.SharedApplication.OpenUrl(new NSUrl("mailto:mail@example.com"));

这将打开编辑邮件控制器,如下面的屏幕截图所示:

如何工作...

mailto: URL 方案支持各种参数以自定义电子邮件消息。这些参数允许我们输入发件人地址、主题和消息,如下所示:

UIApplication.SharedApplication.OpenUrl("mailto:recipient@example.com?subject=Email%20with%20Xamarin.iOS!&body=This%20is%20the%20message%20body!");

还有更多...

虽然 iOS 提供了打开原生Messaging应用的访问权限,但在电子邮件的情况下,在应用内部预定义消息内容是控制停止的地方。实际上,没有通过代码发送消息的方法。是否发送消息将由用户决定。

关于打开外部应用的更多信息

OpenUrl方法提供了一个打开外部应用程序的接口。打开外部应用程序有一个缺点,那就是调用OpenUrl方法的 app 会切换到后台。在 iOS 版本 3.*之前,这是通过应用程序进行消息传递的唯一方式。自 iOS 版本 4.0 以来,苹果向 SDK 提供了消息控制器。以下食谱讨论了它们的用法。

参见

  • 开始电话通话在我们的应用程序中使用短信的食谱

在我们的应用程序中使用短信

在这个食谱中,我们将学习如何在我们的应用程序中显示短信控制器。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为TextMessageApp

如何操作...

执行以下步骤以在我们的应用程序中显示短信控制器:

  1. 在控制器的视图中添加一个按钮。

  2. TextMessageAppViewController文件中输入以下using指令:

    using MonoTouch.MessageUI;
    
  3. 使用以下代码实现ViewDidLoad方法,根据您的意愿更改收件人号码和/或消息正文:

    private MFMessageComposeViewController messageController;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.btnSendMessage.TouchUpInside += async (s, e) => {
     if (MFMessageComposeViewController.CanSendText)
        {
          this.messageController = new MFMessageComposeViewController();
          this.messageController.Recipients = new string[] { "+123456789012" };
          this.messageController.Body = "Text from Xamarin.iOS";
          this.messageController.Finished += MessageController_Finished;
          await this.PresentViewControllerAsync(this.messageController, true);
        }  else
        {
          Console.WriteLine("Cannot send text message!");
        }
      } ;
    }
    
  4. 添加以下方法:

    private async void MessageController_Finished(object sender, MFMessageComposeResultEventArgs e) {
      switch (e.Result) {
      case MessageComposeResult.Sent:
        Console.WriteLine("Message sent!");
        break;
      case MessageComposeResult.Cancelled:
        Console.WriteLine("Message cancelled!");
        break;
      default:
        Console.WriteLine("Message failed!");
        break;
      }
      e.Controller.Finished -= MessageController_Finished;
      await e.Controller.DismissViewControllerAsync(true);
    }
    
  5. 在设备上编译并运行应用程序。点击发送消息按钮以打开消息控制器。点击发送按钮发送消息,或点击取消按钮返回应用程序。

它是如何工作的...

MonoTouch.MessageUI命名空间包含实现 iOS 应用程序中消息传递所需的 UI 元素。对于短信(SMS),我们需要MFMessageComposeViewController类。

我们需要检查短信可用性,因为并非所有设备都能发送短信。MFMessageComposeViewController类包含一个名为CanSendText的静态方法,它返回一个布尔值,表示我们是否可以使用此功能。在这种情况下,重要的是在初始化控制器之前检查发送短信功能是否可用。这是因为当你尝试在不支持短信或模拟器上的设备上初始化控制器时,你会得到一个异常。

为了确定用户在消息 UI 中何时采取了行动,我们订阅了Finished事件,如下所示:

this.messageController.Finished += MessageController_Finished;

Finished方法内部,我们可以根据MessageComposeResult参数提供相应的功能。它可能有以下三个值之一:

  • 已发送:这意味着消息已成功发送。

  • 已取消:这意味着用户点击了取消按钮。消息将不会发送。

  • 失败:这意味着消息发送失败。

最后一件要做的事情是取消订阅事件并关闭消息控制器,如下所示:

e.Controller.Finished -= MessageController_Finished;
await e.controller.DismissViewControllerAsync(true);

初始化控制器之后,我们可以将收件人和正文消息设置为适当的属性,如下所示:

this.messageController.Recipients = new string[] { "+123456789012" };
this.messageController.Body = "Text from Xamarin.iOS";

Recipients属性接受一个string数组,允许多个收件人号码。

还有更多...

SDK 允许用户界面发送文本消息的事实并不意味着它是可定制的。就像调用原生Messaging应用一样,是否发送消息或丢弃消息的决定权在用户手中。实际上,在控制器显示在屏幕上之后,任何尝试更改实际对象或其任何属性的操作都将失败。此外,用户可以更改或删除收件人和消息正文。真正的优势在于,消息用户界面是在我们的应用内部显示,而不是单独运行。

附件

从 iOS 7 开始,MFMessageComposeViewController类支持附件。我们可以通过AddAttachment方法将文件附加到消息中,如下所示:

this.messageController.AddAttachment(new NSUrl("path/to/file"), "A wonderful image");

第二个参数是将在 UI 中显示的字符串,作为文件名。如果传递 null,则将显示实际文件名。

在我们的应用中使用电子邮件消息

在本食谱中,我们将学习如何在应用中使用电子邮件消息界面。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为EmailMessageApp

如何做...

执行以下步骤:

  1. EmailMessageAppViewController的视图中添加一个按钮,并在EmailMessageAppViewController.cs文件中添加MonoTouch.MessageUI命名空间。

  2. ViewDidLoad方法中输入以下代码:

    this.btnSendEmail.TouchUpInside += async (s, e) => {
      if (MFMailComposeViewController.CanSendMail)
      {
      this.mailController = new MFMailComposeViewController();
      this.mailController.SetToRecipients(new string[] { "recipient@example.com" });
      this.mailController.SetSubject("Email from Xamarin.iOS!");
      this.mailController.SetMessageBody("This is the message body!", false);
      this.mailController.Finished += this.MailController_Finished;
        await this.PresentViewControllerAsync(this.mailController, true);
      }  else
      {
        Console.WriteLine("Cannot send email!");
      }
    } ;
    
  3. 添加以下方法:

    private async void MailController_Finished (object sender, MFComposeResultEventArgs e)
    {
      switch (e.Result)
      {
      case MFMailComposeResult.Sent:
        Console.WriteLine("Email sent!");
      break;
      case MFMailComposeResult.Saved:
        Console.WriteLine("Email saved!");
      break;
      case MFMailComposeResult.Cancelled:
        Console.WriteLine("Email sending cancelled!");
      break;
      case MFMailComposeResult.Failed:
        Console.WriteLine("Email sending failed!");
        if (null != e.Error)
        {
          Console.WriteLine("Error message: {0}", e.Error.LocalizedDescription);
        }
      break;
      }
      e.Controller.Finished -= MailController_Finished;
      await e.Controller.DismissViewControllerAsync(true);
    }
    
  4. 在模拟器或设备上编译并运行应用。点击发送电子邮件按钮以显示邮件用户界面。

发送或取消消息。该应用将在模拟器上运行,并且与设备上的原生Mail应用行为相同,只是消息实际上不会发送或保存。

如何工作...

MFMailComposeViewController类提供了原生邮件编写界面。为了确定设备是否能够发送电子邮件,我们首先检查其CanSendMail属性,如下所示:

if (MFMailComposeViewController.CanSendMail)

就像我们对MFMessageComposeViewController类所做的那样,我们订阅了MFMailComposeViewController类的Finished事件。我们使用此事件来响应用户操作,而无需实现Delegate对象。我们根据MFComposeResultEventArgs.Result属性在MailController_Finished方法中这样做,该属性是MFMailComposeResult类型。MFMailComposeResult枚举的可能值将如下所示:

  • 已发送:这意味着电子邮件消息已排队待发送。

  • 已保存:这意味着用户点击了取消按钮,操作表的保存草稿选项自动出现。以下截图显示了用户点击取消按钮时出现的操作表:如何工作...

  • 已取消:这意味着用户在控制器上点击了取消按钮,并在操作表中选择了删除草稿选项。

  • 失败:这意味着电子邮件消息发送失败。

在初始化对象之后,我们可以通过相应的 Set 前缀方法集分配收件人列表、主题和消息正文,如下所示:

this.mailController.SetToRecipients(new string[] { "recipient@example.com" });
this.mailController.SetSubject("Email from MonoTouch!");
this.mailController.SetMessageBody("This is the message body!", false);

如果 SetMessageBody 消息的第二个参数设置为 true,它通知控制器该消息应被视为 HTML。

还有更多...

除了简单的或 HTML 格式的文本之外,我们还可以发送附件。我们可以使用 AddAttachmentData 方法通过以下代码行来实现:

this.mailController.AddAttachmentData(UIImage.FromFile("image.jpg").AsJPEG(), "image/jpg", "image.jpg");

第一个参数是 NSData 类型,是附件的实际内容。在这种情况下,我们通过 UIImage.AsJPEG() 方法附加一个图片,该方法返回一个包含在 NSData 对象中的图片内容。第二个参数代表附件的 Multipurpose Internet Mail Extensions (MIME) 类型,第三个参数代表将显示给用户的文件名。

参见

  • 在我们的应用程序中使用文本消息 菜谱

管理地址簿

在这个菜谱中,我们将讨论如何访问和管理设备地址簿中存储的用户联系人。

准备工作

在 Xamarin Studio 中创建一个新的 Single View Application 并将其命名为 AddressBookApp

如何做...

执行以下步骤:

  1. 在控制器的视图中添加一个按钮。

  2. AddressBookAppViewController.cs 文件中输入以下 using 指令:

    using MonoTouch.AddressBook;
    
  3. 重写 ViewDidLoad 方法:

    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.btnReadContacts.TouchUpInside += (s, e) => {
      ABAuthorizationStatus abStatus = ABAddressBook.GetAuthorizationStatus();
      NSError error;
      ABAddressBook addressBook = ABAddressBook.Create(out error);
      if (abStatus == ABAuthorizationStatus.NotDetermined)
      {
        addressBook.RequestAccess((g, err) => {
          if (!g)
          {
            Console.WriteLine("User denied address book access!");
          } else
          {
            this.InvokeOnMainThread(() =>this.ReadContacts(addressBook));
          }
        });
      } else if (abStatus == ABAuthorizationStatus.Authorized)
      {
        this.ReadContacts(addressBook);
      } else
      {
        Console.WriteLine("App does not have access to the address book!");
      }
      };
    }
    
  4. 添加以下方法:

    private void ReadContacts(ABAddressBook addressBook)
    {
      ABPerson[] contacts = addressBook.GetPeople();
      foreach (ABPerson eachPerson in contacts)
      {
        Console.WriteLine("{0} {1}", eachPerson.LastName, eachPerson.FirstName);
      }
    }
    
  5. 在模拟器上编译并运行应用程序。点击 获取联系人 按钮并接受或拒绝访问地址簿。以下截图显示了当我们请求访问地址簿时出现的警报:如何做...

注意

模拟器的地址簿包含一些我们可以使用的虚假联系人。

它是如何工作的...

MonoTouch.AddressBook 命名空间包含所有允许我们管理设备地址簿的类。要访问地址簿,我们首先需要检查用户是否以前已授予我们的应用程序地址簿访问权限,并实例化一个 ABAddressBook 实例,如下所示:

ABAuthorizationStatus abStatus = ABAddressBook.GetAuthorizationStatus();
NSError error;
ABAddressBook addressBook = ABAddressBook.Create(out error);

如果状态是 NotDermined,我们调用 RequestAccess 方法,该方法接受一个 Action<bool, NSError> 委托,如下所示:

addressBook.RequestAccess((g, err) => {
      if (!g)
      {
        Console.WriteLine("User denied address book access!");
      } else
      {
 this.InvokeOnMainThread(() =>this.ReadContacts(addressBook));
      }
    });

bool 参数告诉我们用户是否已授予访问权限。如果它是 true,我们就调用 ReadContacts 方法,以便我们继续读取我们想要的地址簿信息。注意,我们用 InvokeOnMainThread 调用包装了 ReadContacts 方法的调用,尽管它没有访问 UI。这是因为 RequestAccess 方法是在一个单独的线程上被调用的。

注意

我们可以从除了主线程之外的其他线程访问地址簿;然而,每个 ABAddressBook 实例都需要在同一个线程上使用。

ReadContacts 方法内部,我们通过 GetPeople 方法枚举单个联系人,如下所示:

ABPerson[] contacts = addressBook.GetPeople();
foreach (ABPerson eachPerson in contacts)
{
  Console.WriteLine("{0} {1}", eachPerson.LastName, eachPerson.FirstName);
}

ABPerson 类包含我们想要的联系人信息。

还有更多...

要获取联系人的存储电话号码(s),请调用GetPhones()方法,如下所示:

ABMultiValue<string> phones = eachPerson.GetPhones();
Console.WriteLine(phones[0].Value);

它返回一个ABMultiValue<string>类型的对象。ABMultiValue<T>是一个泛型集合,特别设计用于多个地址簿值。

将电话号码添加到联系人

要将电话号码添加到联系人中,我们可以使用ABPerson类的SetPhones方法。它接受一个ABMultiValue<string>对象作为其参数,但我们不能向ABMultiValue对象中添加新值。然而,我们可以将值写入一个ABMutableMultiValue<T>对象,如下所示:

ABMutableMultiValue<string> newPhones = phones.ToMutableMultiValue();

这行代码创建了一个新的ABMutableMultiValue<string>对象实例,然后我们使用它来添加我们想要的电话号码(s),如下所示:

newPhones.Add("+120987654321", ABPersonPhoneLabel.iPhone);
eachPerson.SetPhones(newPhones);
addressBook.Save();

Add方法的第二个参数是电话号码在保存到联系人时将拥有的标签。调用ABAddressBook.Save()方法非常重要,否则,更改将不会保存。

显示联系人

在本食谱中,我们将学习如何使用原生地址簿用户界面来显示联系人信息。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为DisplayContactApp

如何操作...

执行以下步骤:

  1. 在控制器上添加一个按钮。

  2. AppDelegate.cs文件中,将DisplayContactAppViewController添加到导航控制器中,如下所示:

    window.RootViewController = new UINavigationController(viewController);
    
  3. DisplayContactAppViewController.cs文件中添加以下命名空间:

    using MonoTouch.AddressBook;
    using MonoTouch.AddressBookUI;
    
  4. ViewDidLoad方法中添加以下代码:

    this.btnDisplayContact.TouchUpInside += (sender, e) => {
      ABAuthorizationStatus status = ABAddressBook.GetAuthorizationStatus();
      NSError error;
      ABAddressBook addressBook = ABAddressBook.Create(out error);
      if (status == ABAuthorizationStatus.NotDetermined)
      {
        addressBook.RequestAccess((g, err) => {
          if (g)
          {
            this.InvokeOnMainThread(() =>this.DisplayContactCard(addressBook));
          } else
          {
            Console.WriteLine("User denied access to the address book!");
          }
        });
      } else if (status == ABAuthorizationStatus.Authorized)
      {
        this.DisplayContactCard(addressBook);
      } else
      {
        Console.WriteLine("App does not have access to the address book!");
      }
    };
    
  5. 添加以下方法:

    private void DisplayContactCard(ABAddressBookaddressBook)
    {
      ABPerson[] contacts = addressBook.GetPeople();
      ABPersonViewController personController = new ABPersonViewController();
      personController.DisplayedPerson = contacts[0];
      this.NavigationController.PushViewController(personController, true);
    }
    
  6. 在模拟器上编译并运行应用程序。点击按钮以显示联系人卡片屏幕。结果应类似于以下截图:如何操作...

工作原理...

MonoTouch.AddressBookUI命名空间包含原生联系人应用程序使用的控制器,允许用户显示和管理联系人。每个联系人的详细信息都可以使用ABPersonViewController查看。此控制器必须推送到UINavigationController,否则它将无法正确显示。

初始化后,我们将要显示的ABPerson对象设置为它的DisplayedPerson属性,如下所示:

ABPersonViewController personController = new ABPersonViewController();
personController.DisplayedPerson = contacts[0];

然后,我们使用以下代码将其推送到导航控制器的堆栈中:

this.NavigationController.PushViewController(personController, true);

更多内容...

ABPersonViewController类也可以用于编辑。为此,将AllowsEditing属性设置为true,如下所示:

personController.AllowsEditing = true;

注意,更改通过ABPersonViewController类正常保存。

其他地址簿控制器

MonoTouch.AddressBookUI命名空间包含我们创建自定义联系人应用程序所需的所有控制器,如下所示:

  • ABPeoplePickerNavigationController:这是一个显示已保存联系人的导航控制器。用户可以从列表中选择一个联系人。

  • ABPersonViewController:这在本食谱给出的示例中有描述。

  • ABNewPersonViewController:这是创建新联系人的控制器。

  • ABUnknownPersonViewController:这是用于创建新联系人的部分数据显示的控制器。这与我们在设备上最近通话列表中点击未知号码时显示的控制器类似。

相关内容

  • 管理通讯录 菜谱

管理日历

在这个菜谱中,我们将学习如何创建一个事件并将其保存到设备的日历数据库中。

准备工作

在 Xamarin Studio 中创建一个新的 Single View Application 并将其命名为 CalendarEventsApp

我们将要创建的应用程序将输出未来 30 天的日历事件。请确保在该时间段内有日历事件。

如何实现...

让我们通过以下步骤创建一个事件并将其保存到设备的日历数据库中:

  1. 在控制器的主视图中添加一个按钮。

  2. CalendarEventAppViewController.cs 文件中添加 MonoTouch.EventKit 命名空间。

  3. ViewDidLoad 方法中输入以下代码:

    this.btnDisplayEvents.TouchUpInside += async (sender, e) => {
      EKAuthorizationStatus status = EKEventStore.GetAuthorizationStatus(EKEntityType.Event);
      EKEventStore evStore = new EKEventStore();
      if (status == EKAuthorizationStatus.NotDetermined)
      {
        if (await evStore.RequestAccessAsync(EKEntityType.Event))
        {
          this.DisplayEvents(evStore);
        } else
        {
          Console.WriteLine("User denied access to the calendar!");
        }
      } else if (status == EKAuthorizationStatus.Authorized)
      {
        this.DisplayEvents(evStore);
      } else
      {
        Console.WriteLine("App does not have access to the calendar!");
      }
    };
    
  4. 添加以下方法:

    private void DisplayEvents (EKEventStoreevStore)
    {
      NSPredicate evPredicate = 
        evStore.PredicateForEvents(DateTime.Now, DateTime.Now.AddDays(30), evStore.GetCalendars(EKEntityType.Event));
      evStore.EnumerateEvents(evPredicate, delegate(EKEventcalEvent, ref bool stop) {
        if (null != calEvent) {
          stop = false;
          Console.WriteLine("Event title: {0}\nEvent start date: {1}", calEvent.Title, calEvent.StartDate);
        }
      });
    }
    
  5. 在设备上编译并运行应用程序。点击 Display events 按钮以在 Application Output 面板中输出未来 30 天的日历事件。

它是如何工作的...

MonoTouch.EventKit 命名空间负责管理日历事件。为了读取存储的事件,我们首先检查我们是否有访问日历的权限,并初始化一个 EKEventStore 对象,如下所示:

EKAuthorizationStatus status = EKEventStore.GetAuthorizationStatus(EKEntityType.Event);
  EKEventStore evStore = new EKEventStore();

如果授权状态是 NotDetermined,我们调用 RequestAccessAsync 方法,以便用户被提示进行访问,如下所示:

if (await evStore.RequestAccessAsync(EKEntityType.Event))

如果结果是 true,这意味着用户已授予我们的应用程序日历访问权限。现在,我们调用 DisplayEvents 方法来读取和输出事件。

EKEventStore 类为我们提供了访问已存储事件的方法。要检索日历事件,我们需要一个 NSPredicate 类型的谓词。我们可以通过 EKEventStore 类的 PredicateForEvents 方法创建一个实例,如下面的代码片段所示:

NSPredicate evPredicate = 
    evStore.PredicateForEvents(DateTime.Now, DateTime.Now.AddDays(30), evStore.GetCalendars(EKEntityType.Event));

前两个参数是 NSDate 类型(可以隐式转换为 DateTime),代表要搜索事件的开始和结束日期。第三个参数是 EKCalendar[] 类型,代表要搜索的日历数组。要搜索所有可用的日历,我们传递 GetCalendars 方法的返回值。

最后,我们使用以下代码行调用 EnumerateEvents 方法:

evStore.EnumerateEvents(evPredicate, delegate(EKEventcalEvent, ref bool stop) {
//...

我们将之前创建的谓词传递给第一个参数。第二个参数是 EKEventSearchCallback 类型的委托。为了读取每个事件的数据,我们使用其 EKEvent 对象。请注意,枚举日历事件的过程与上一章中讨论的从资源库枚举资源的过程类似。这意味着如果 EKEvent 对象不为空,我们必须显式地将 stop 参数设置为 false,以便 EKEventStore 类继续枚举日历事件。

还有更多...

除了枚举事件之外,EKEventStore 类还允许我们创建新事件。以下示例创建并保存了一个新的日历事件:

EKEvent newEvent = EKEvent.FromStore(evStore);
newEvent.StartDate = DateTime.Now.AddDays(1);
newEvent.EndDate = DateTime.Now.AddDays(1.1);
newEvent.Title = "Xamarin event!";
newEvent.Calendar = evStore.DefaultCalendarForNewEvents;
NSError error = null;
evStore.SaveEvent(newEvent, EKSpan.ThisEvent, out error);

为了创建一个新的 EKEvent 实例,我们使用 EKEvent.FromStore 静态方法。然后我们设置开始和结束日期、标题以及事件将要存储的日历。在这里,我们使用默认的日历,我们可以通过 EKEventStoreDefaultCalendarForNewEvents 属性来获取。当我们设置好一切后,我们调用 SaveEvent 方法来保存它。

提醒事项

你可能已经注意到了 EKEntityType 枚举的使用。这定义了我们想要访问的实体类型。除了 Event,它指的是日历事件之外,我们还可以使用 Reminder 值,这样我们就可以与用户在 提醒事项 应用中的任务一起工作了。

即使用户已经授予了对日历的访问权限,我们也需要明确请求 提醒事项 权限。

参见

  • 在第七章 多媒体资源 中,关于 直接管理相册项目 的配方,第七章

第九章。与设备硬件交互

在本章中,我们将涵盖以下主题:

  • 检测设备方向

  • 调整 UI 方向

  • 距离传感器

  • 获取电池信息

  • 处理运动事件

  • 处理触摸事件

  • 识别手势

  • 自定义手势

  • 使用加速度计

  • 使用陀螺仪

简介

当前的移动设备配备了非常先进的硬件,无论是用于检测运动和方向的加速度计,还是距离传感器、GPS 模块,以及许多其他组件中的复杂多点触控屏幕。

在本章中,我们将重点关注如何在我们的应用程序中使用这项硬件,为用户提供一个扩展到 3D 世界的体验。具体来说,我们将讨论如何根据设备的位置调整用户界面方向,如何使用距离传感器,以及如何读取电池信息。在一系列四个任务中,我们将学习如何捕捉屏幕上的用户触摸并识别手势。

最后但同样重要的是,我们将创建高级应用程序,读取加速度计和陀螺仪传感器的原始数据,以详细和简单的指南检测设备运动和旋转。

检测设备方向

在本食谱中,我们将学习如何创建一个能够感知设备方向变化的程序。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为DeviceOrientationApp

如何做到这一点...

执行以下步骤:

  1. 向控制器添加一个标签。

  2. DeviceOrientationAppViewController类中,重写ViewWillAppear方法并使用以下代码实现:

    private NSObject orientationObserver;
    public override void ViewWillAppear (bool animated)
    {
      base.ViewWillAppear (animated);
      UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications();
      this.orientationObserver = UIDevice.Notifications.ObserveOrientationDidChange((s, e) => {
        this.lblOrientation.Text = UIDevice.CurrentDevice.Orientation.ToString();
      });
    }
    
  3. 使用以下代码重写ViewWillDisappear方法:

    public override void ViewWillDisappear (bool animated)
    {
      base.ViewWillDisappear (animated);
      NSNotificationCenter.DefaultCenter.RemoveObserver(this.orientationObserver);
        UIDevice.CurrentDevice.EndGeneratingDeviceOrientationNotifications();
    }
    
  4. 在模拟器上编译并运行应用程序。通过在 Mac 上按住Command键并按左右箭头键来旋转模拟器。

它是如何工作的...

尽管模拟器缺少加速度计硬件,但它支持方向变化的通知。

设备方向通知机制可以通过UIDevice.CurrentDevice静态属性访问。为了接收通知,我们首先需要指示运行时发出它们。我们使用以下方法来完成:

UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications();

此方法打开加速度计并开始生成方向通知。然后我们需要开始观察通知,以便对变化做出响应,如下面的代码所示:

this.orientationObserver = UIDevice.Notifications.ObserveOrientationDidChange((s, e) => {
  this.lblOrientation.Text = UIDevice.CurrentDevice.Orientation.ToString();
});

每当设备方向发生变化时,观察者都会触发匿名方法。在匿名方法中,我们将从Orientation属性获取的方向输出到标签。

ViewWillDisappear方法是在视图控制器即将隐藏时被调用的方法(例如,当我们在一个导航控制器上推送另一个视图控制器时)。在其内部,我们确保移除方向观察者,并使用以下代码指示运行时停止生成方向通知:

NSNotificationCenter.DefaultCenter.RemoveObserver(this.orientationObserver);
UIDevice.CurrentDevice.EndGeneratingDeviceOrientationNotifications();

更多内容...

UIDevice类的Orientation属性返回UIDeviceOrientation类型的枚举。其可能的值如下:

  • Unknown: 这表示设备方向未知

  • Portrait: 这表示设备处于正常的纵向方向,主页按钮在底部

  • PortraitUpsideDown: 这表示设备处于颠倒的纵向方向,主页按钮在顶部

  • LandscapeLeft: 这表示设备处于横屏方向,主页按钮在左侧

  • LandscapeRight: 这表示设备处于横屏方向,主页按钮在右侧

  • FaceUp: 这表示设备与地面平行,屏幕朝上

  • FaceDown: 这表示设备与地面平行,屏幕朝下

FaceUpFaceDown是模拟器上无法复制的两个值。

设备方向和用户界面方向

用户界面——在这种情况下,视图控制器——将默认旋转并调整到新的屏幕方向。然而,需要注意的是,设备方向和用户界面方向可能不同。例如,设备可以是横屏,UIDevice.CurrentDevice.Orientation返回LandscapeLeft,而视图控制器的外观没有任何变化。

参见

  • 调整 UI 方向配方

  • 使用加速度计配方

调整 UI 方向

在本章中,我们将学习如何根据屏幕方向旋转用户界面。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为UIOrientationApp

如何做到这一点...

执行以下步骤:

  1. 在视图控制器中添加一个标签。

  2. 覆盖ShouldAutoRotate方法:

    public override bool ShouldAutorotate ()
    {
      return true;
    }
    
  3. 覆盖GetSupportedInterfaceOrientations方法:

    public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations ()
    {
      return UIInterfaceOrientationMask.All;
    }
    
  4. 覆盖DidRotate方法:

    public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
    {
      base.DidRotate (fromInterfaceOrientation);
      this.lblOrientation.Text = this.InterfaceOrientation.ToString();
    }
    
  5. 在模拟器上编译并运行应用程序。通过按Command键和左箭头键或右箭头键旋转模拟器。当前的用户界面方向将在模拟器屏幕上显示。

    尝试旋转模拟器两次,将纵向方向颠倒。你会注意到用户界面不会旋转到这个方向,而将保持在横屏,如下面的截图所示:

    如何做到这一点...

它是如何工作的...

在加载的每个视图控制器上,系统会调用ShouldAutoRotate方法以确定是否应该旋转特定的控制器。如果该方法返回true,则系统会调用GetSupportedInterfaceOrientations方法以确定控制器可以旋转到哪些方向。以下代码显示了GetSupportedInterfaceOrientations方法的实现:

public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations ()
{
  return UIInterfaceOrientationMask.All;
}

然而,在Info.plist文件中有一个全局设置,它对所有视图控制器的方向设置具有优先权。这可以通过项目选项下的iOS 应用程序节点访问。默认设置如下截图所示:

如何工作...

这也解释了为什么当设备倒置时,尽管我们从GetSupportedInterfaceOrientations方法返回UIInterfaceOrientationMask.All,我们的用户界面仍然不会旋转。为了使视图控制器支持PortraitUpsideDown方向,我们必须通过方向设置启用倒置选项。

类似地,如果我们想让用户界面只保持特定的方向,比如横屏,我们只需从GetSupportedInterfaceOrientations方法返回UIInterfaceOrientationMask.Portrait,确保在项目设置中至少启用了横屏方向。

更多...

只要应用支持一个方向,视图控制器就会在运行时调整到它,如果我们想要它这样做的话。例如,如果我们以模态方式呈现第二个视图控制器,并且我们只想在横屏方向显示该视图控制器,我们就会实现它的GetSupportedInterfaceOrientations方法,如下面的代码所示:

public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations() {
  return UIInterfaceOrientationMask.LandscapeLeft | UIInterfaceOrientationMask.LandscapeRight;
}

子控制器用户界面方向

在我们在这里创建的项目中,如果UIOrientationAppViewController被呈现为一个子视图控制器(例如,通过UINavigationController),它的ShouldAutoRotateGetSupportedInterfaceOrientations方法将不会被调用,而是会调用UINavigationController的方法,并返回相应的默认值。

在这种情况下,为了确保用户界面会根据当前控制器旋转,我们不得不子类化UINavigationController并重写这两个方法,从导航堆栈中当前活动的视图控制器返回相应的值,如下面的代码所示:

// Inside our UINavigationController subclass:
public override ShouldAutoRotate() {
  return this.TopViewController.ShouldAutoRotate();
}
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations() {
  return this.TopViewController.GetSupportedInterfaceOrientations();
}

这适用于任何父子控制器关系,例如,如果我们的父控制器是UITabBarController等等。

参见

  • 检测设备方向菜谱

  • 使用加速度计菜谱

  • 在第三章的通过不同的视图控制器导航菜谱中,用户界面 – 视图控制器

接近传感器

在这个菜谱中,我们将讨论如何使用接近传感器来禁用设备屏幕。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ProximitySensorApp

注意

模拟器不支持接近传感器。

如何做到这一点...

执行以下步骤:

  1. 对于这个项目,在视图控制器上不需要任何控件。使用以下命令声明一个NSObject字段,该字段将用于保存通知观察者:

    private NSObject proximityObserver;
    
  2. 覆盖控制器中的ViewWillAppear方法并按照以下代码实现:

    public override void ViewWillAppear (bool animated)
    {
      base.ViewWillAppear (animated);
      UIDevice.CurrentDevice.ProximityMonitoringEnabled = true;
      if (UIDevice.CurrentDevice.ProximityMonitoringEnabled)
      {
        this.proximityObserver = UIDevice.Notifications.ObserveProximityStateDidChange((s, e) => {
          Console.WriteLine("Proximity state: {0}", UIDevice.CurrentDevice.ProximityState);
        });
      }
    }
    
  3. 在设备上编译并运行应用程序。将手指放在接近传感器上(在 iPhone 上,它位于扬声器旁边),并观察 Xamarin Studio 中的应用程序输出面板显示的传感器状态。

它是如何工作的...

尽管接近传感器的功能相当简单,但它提供了一个非常重要的特性。iOS 设备正面只有一个按钮,即主按钮。几乎所有的用户与设备的交互都是基于触摸屏。这在 iPhone 上造成了一个问题;除了其多种功能外,它还是一部电话。这意味着它很可能会在用户的脸部侧面花费一些时间来进行通话。

为了避免意外点击虚拟按钮,当手机应用运行时,接近传感器会被激活以禁用屏幕,当设备靠近用户的耳朵或传感器上方的任何物体时。

要启用接近传感器,将UIDevice.CurrentDevice.ProximityMonitoringEnabled属性的值设置为true

UIDevice.CurrentDevice.ProximityMonitoringEnabled = true;

如果设备不支持接近传感器,即使将其设置为true,此属性也会返回false。因此,在将其设置为true之后,我们可以通过以下代码检查设备是否支持传感器:

if (UIDevice.CurrentDevice.ProximityMonitoringEnabled)

在检查了接近传感器的支持后,我们可以添加一个观察者,通过以下代码来接收传感器状态的通知:

this.proximityObserver = UIDevice.Notifications.ObserveProximityStateDidChange((s, e) => {
  Console.WriteLine("Proximity state: {0}", UIDevice.CurrentDevice.ProximityState);
});

ProximityState属性返回true表示传感器已关闭屏幕,返回false表示它已重新打开。

还有更多...

接近传感器的使用不仅限于电话功能。例如,如果你正在开发一个在设备放在用户的口袋或钱包中时可以执行某些工作的应用程序,启用接近传感器可以帮助你确保不会意外点击控制按钮。你甚至可以通过关闭屏幕来节省电池电量。

相关内容

  • 获取电池信息配方

获取电池信息

在这个配方中,我们将学习如何读取设备的充电状态及其电池使用情况。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为BatteryInfoApp

如何实现...

执行以下步骤:

  1. 在控制器的视图中添加一个标签。

  2. 按照以下方式覆盖控制器类中的ViewWillAppear方法:

    private NSObject batteryStateChangeObserver;
    public override void ViewWillAppear (bool animated)
    {
      base.ViewWillAppear (animated);
      UIDevice.CurrentDevice.BatteryMonitoringEnabled = true;
     this.batteryStateChangeObserver = UIDevice.Notifications.ObserveBatteryStateDidChange((s, e) => {
     this.lblOutput.Text = string.Format("Battery level: {0}", UIDevice.CurrentDevice.BatteryLevel);
     Console.WriteLine("Battery state: {0}", UIDevice.CurrentDevice.BatteryState);
     });
    }
    
  3. 在设备上编译并运行应用程序。在应用程序加载后,断开并/或连接设备的 USB 线缆。电池电量将在标签上显示,当前状态将在应用程序输出面板中显示。

它是如何工作的...

我们可以通过UIDevice类来检索电池信息。我们必须做的第一件事是启用电池监控:

UIDevice.CurrentDevice.BatteryMonitoringEnabled = true;

在不支持电池监控的模拟器上,即使我们将它设置为true,此属性也会返回false

我们可以通过在前面章节中突出显示的代码中通过UIDevice.BatteryStateDidChangeNotification键添加一个观察者,来为电池状态变化通知添加观察者。电池电平可以通过BatteryLevel属性检索,该属性返回电池的充电百分比,范围在01之间(0表示完全放电,1表示 100%充电),如下面的代码所示:

this.lblOutput.Text = string.Format("Battery level: {0}", UIDevice.CurrentDevice.BatteryLevel);

同样,我们可以通过以下代码使用BatteryState属性检索电池的状态:

Console.WriteLine("Battery state: {0}", UIDevice.CurrentDevice.BatteryState);

BatteryState属性的可能的值如下:

  • Unknown: 这表示无法确定电池状态或电池监控已禁用

  • Unplugged: 这表示设备正在使用电池供电

  • Charging: 这表示设备电池正在充电,并且 USB 线已连接

  • Full: 这表示设备电池已满,并且 USB 线已连接

还有更多...

除了电池状态外,我们还可以获取其电量信息。为此,我们需要为UIDevice.BatteryLevelDidChangeNotification键添加一个观察者,如下面的代码所示:

private NSObject batteryLevelChangedObserver;
//...
this.batteryLevelChangedObserver = UIDevice.Notifications.ObserveBatteryLevelDidChange((s, e) => {..//

禁用电池监控

在不需要时始终禁用电池监控。实际的监控机制本身消耗了大量的电池电量。

参见

  • 接近传感器 菜谱

处理运动事件

在这个菜谱中,我们将学习如何拦截和响应摇动手势。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为MotionEventsApp

如何做到这一点...

执行以下步骤:

  1. 在控制器的视图中添加一个标签。

  2. MotionEventsAppViewController类中输入以下代码:

    public override bool CanBecomeFirstResponder
    {
      get {  return true; }
    }
    public override void ViewDidAppear (bool animated)
    {
      base.ViewDidAppear (animated);
      this.BecomeFirstResponder();
    }
    public override void MotionBegan (UIEventSubtype motion, UIEvent evt)
    {
      base.MotionBegan (motion, evt);
      this.lblOutput.Text = "Motion started!";
    }
    public override void MotionEnded (UIEventSubtype motion, UIEvent evt)
    {
      base.MotionEnded (motion, evt);
      this.lblOutput.Text = "Motion ended!";
    }
    public override void MotionCancelled (UIEventSubtype motion, UIEvent evt)
    {
      base.MotionCancelled (motion, evt);
      this.lblOutput.Text = "Motion cancelled!";
    }
    
  3. 在设备上编译并运行应用程序。摇动设备并观察标签上的输出。

注意

您也可以在模拟器上测试此应用程序。加载后,在菜单栏上导航到硬件 | 摇动手势

它是如何工作的...

通过重写UIViewController类的运动方法,我们可以拦截和响应系统发送的运动事件。然而,仅仅重写这些方法是不够的。为了控制器能够接收运动事件,它需要成为第一个响应者。为了确保这一点,我们首先重写CanBecomeFirstResponder属性,并从其中返回true,如下面的代码所示:

public override bool CanBecomeFirstResponder
{
  get {  return true; }
}

然后,我们确保当视图出现时,我们的控制器成为第一个响应者,通过在ViewDidAppear重写方法中调用BecomeFirstResponder方法,如下面的代码所示:

public override void ViewDidAppear (bool animated)
{
  base.ViewDidAppear (animated);
  this.BecomeFirstResponder();
}

ViewDidAppear方法在视图出现在屏幕上后被调用。

系统确定一个动作是否是摇动手势,并调用相应的方法。我们可以重写和捕获摇动手势的方法如下:

  • MotionBegan: 这表示摇动动作已经开始

  • MotionEnded:这意味着震动动作已经结束

  • MotionCancelled:这意味着震动动作已被取消

当设备开始移动时,会调用MotionBegan方法。如果运动持续大约一秒或更短,则调用MotionEnded方法。如果它持续更长,系统将其分类为不是震动手势,并调用MotionCancelled方法。当我们想在应用程序中实现震动手势时,建议重写所有这三个方法并相应地做出反应。

更多...

只有继承自UIResponder类的对象才会发送运动事件。这包括UIViewUIViewController类。

更多关于运动事件的信息

运动事件机制相当简单。它仅仅检测近瞬间的设备震动,而不提供有关其方向或速率的任何信息。为了根据不同的特性处理运动事件,可以将加速度计与组合使用。

参见

  • 使用加速度计菜谱

处理触摸事件

在这个菜谱中,我们将学习如何拦截和响应用户触摸。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为TouchEventsApp

如何做...

执行以下步骤:

  1. 将一个标签添加到控制器视图。

  2. TouchEventsAppViewController类中输入以下代码:

    public override void TouchesMoved (NSSet touches, UIEvent evt)
    {
      base.TouchesMoved (touches, evt);
      UITouch touch = touches.AnyObject as UITouch;
      UIColor currentColor = this.View.BackgroundColor;
      float red, green, blue, alpha;
      currentColor.GetRGBA(out red, out green, out blue, out alpha);
      PointF previousLocation = touch.PreviousLocationInView(this.View);
      PointF touchLocation = touch.LocationInView(this.View);
      if (previousLocation.X != touchLocation.X)
      {
        this.lblOutput.Text = "Changing background color...";
        float colorValue = touchLocation.X / this.View.Bounds.Width;
        this.View.BackgroundColor = UIColor.FromRGB(colorValue, colorValue, colorValue);
      }
    }
    
  3. 在模拟器上编译并运行应用程序。在模拟器的屏幕上点击并拖动光标,观察视图的背景颜色逐渐从白色变为黑色。请注意,在模拟器屏幕上用光标点击相当于用手指触摸设备屏幕。

它是如何工作的...

要响应用户触摸,充当触摸接收器的对象必须将其UserInteractionEnabled属性设置为true。默认情况下,几乎所有对象都启用了用户交互,除非它们的主要用途不是直接用于用户交互,例如UILabelUIImageView对象。如果我们想让这些对象对用户触摸敏感,我们需要明确地将UserInteractionEnabled设置为这些对象。除此之外,可以处理触摸事件的对象必须继承自UIResponder类。请注意,尽管UIViewController类继承自UIResponder并且可以捕获触摸事件,但它没有UserInteractionEnabled属性,而是它的主要属性UIView控制着触摸事件的传递。这意味着,如果你重写了UIViewController的触摸方法,但它的视图的UserInteractionEnabled属性设置为false,这些方法将不会响应用户触摸。

负责处理触摸事件的方法如下:

  • TouchesBegan:当用户触摸屏幕时被调用

  • TouchesMoved:当用户在屏幕上拖动手指时被调用

  • TouchesEnded:当用户从屏幕上抬起手指时调用

  • TouchesCancelled:当触摸事件被系统事件取消时调用,例如,当显示通知警报时

整个项目可以在可下载的源代码中找到。TouchesMoved方法实现已在下面解释。

每个触摸方法都有两个参数。第一个参数是NSSet类型,包含UITouch对象。NSSet类表示一组对象,而UITouch类保存每个用户触摸的信息。第二个参数是UIEvent类型,包含实际事件的信息。

我们可以通过使用以下代码通过NSSet.AnyObject返回值检索与实际触摸相关的UITouch对象:

UITouch touch = touches.AnyObject as UITouch;

它返回一个NSObject类型的对象,我们将它转换为UITouch。我们可以通过以下方法获取触摸的先前和当前位置:

PointF previousLocation = touch.PreviousLocationInView(this.View);
PointF touchLocation = touch.LocationInView(this.View);

前两种方法都返回一个PointF结构体,其中包含触摸在接收者坐标系中的位置。在接收到触摸位置后,我们相应地调整背景颜色。

更多...

本示例基于单个用户的触摸。为了使视图能够响应多个触摸,我们必须将其MultipleTouchEnabled属性设置为true。然后我们可以从数组中获取所有的UITouch对象:

UITouch[] allTouches = touches.ToArray<UITouch>();

获取点击次数

我们可以通过在ToucheEnded方法内部使用UITouch.TapCount属性来确定连续用户点击的次数。

参见

  • 处理运动事件食谱

  • 识别手势食谱

  • 自定义手势食谱

识别手势

在本食谱中,我们将讨论如何识别触摸手势并相应地做出反应。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为GestureApp

如何做...

执行以下步骤:

  1. 在控制器的视图中添加一个标签。

  2. GestureAppViewController类中添加以下方法:

    private void OnPinchGesture(UIPinchGestureRecognizer pinch)
    {
      switch (pinch.State)
      {
      case UIGestureRecognizerState.Began:
        this.lblOutput.Text = "Pinch began!";
        break;
      case UIGestureRecognizerState.Changed:
        this.lblOutput.Text = "Pinch changed!";
        break;
      case UIGestureRecognizerState.Ended:
        this.lblOutput.Text = "Pinch ended!";
        break;
      }
    }
    
  3. ViewDidLoad方法中添加以下代码:

    UIPinchGestureRecognizer pinchGesture = new UIPinchGestureRecognizer(this.OnPinchGesture);
    this.View.AddGestureRecognizer(pinchGesture);
    
  4. 在模拟器上编译并运行应用程序。按住选项键并点击拖动鼠标以执行模拟器屏幕上的捏合动作。

    在模拟器上按住选项键并拖动鼠标光标相当于用两只手指触摸设备的屏幕,如下面的截图所示:

    如何做...

它是如何工作的...

随着 iOS 3.2 版本的发布,iPad 一同推出,苹果公司引入了UIGestureRecognizer类及其派生类。手势识别器利用 iOS 设备上的多点触控屏幕。手势基本上是用于特定动作的触摸组合。

例如,在原生照片应用程序的全屏图像上缩放手势,将缩小图像。缩放手势是用户执行的手势,而手势识别器负责识别并将手势事件传递给接收器。

在这个例子中,我们创建了一个UIPinchGestureRecognizer实例,它将识别屏幕上执行的手势。其实例是通过以下代码创建的:

UIPinchGestureRecognizer pinchGesture = new UIPinchGestureRecognizer(this.OnPinchGesture);

初始化实例的构造函数接受一个参数,该参数为Action<UIPinchGestureRecognizer>类型,表示当识别器接收到手势时将被调用的方法。

在方法内部,我们读取手势识别器对象的State属性并根据情况进行响应,如下面的代码所示:

switch (pinch.State) { 
//…

还有更多...

每个手势识别器的状态由UIGestureRecognizerState类型的枚举表示。其可能的值如下所示:

  • Possible:这表示手势尚未被识别。这是默认值。

  • Began:这表示手势已开始。

  • Changed:这表示手势已改变。

  • Ended:这表示手势已结束。

  • Cancelled:这表示手势已被取消。

  • Failed:这表示无法识别手势。

  • Recognized:这表示手势已被识别。

手势识别器的优势

手势识别器的优势在于它们可以节省开发者创建自己的手势识别机制的时间,通过触摸事件实现。此外,它们基于用户在 iOS 设备上习惯使用的手势。

参见

  • 处理触摸事件配方

  • 自定义手势配方

自定义手势

在这个配方中,我们将学习如何创建自定义手势识别器以创建我们自己的手势响应机制。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为CustomGestureApp

如何做到...

执行以下步骤:

  1. 在控制器的视图中添加一个标签。

  2. CustomGestureAppViewController类中创建以下嵌套类:

    private class DragLowerLeftGesture : UIGestureRecognizer
    {
      private PointF startLocation;
      private RectangleF lowerLeftCornerRect;
      public override UIGestureRecognizerState State
      {
        get
        {
          return base.State;
        }  set
        {
          base.State = value;
        }
      }
      public override void TouchesBegan (NSSet touches, UIEvent evt)
      {
        base.TouchesBegan (touches, evt);
        UITouch touch = touches.AnyObject as UITouch;
        this.startLocation = touch.LocationInView(this.View);
        RectangleF viewBounds = this.View.Bounds;
        this.lowerLeftCornerRect = new RectangleF(0f, viewBounds.Height - 50f, 50f, 50f);
        if (this.lowerLeftCornerRect.Contains(this.startLocation))
        {
          this.State = UIGestureRecognizerState.Failed;
        }  else
        {
          this.State = UIGestureRecognizerState.Began;
        }
      }
      public override void TouchesMoved (NSSet touches, UIEvent evt)
      {
        base.TouchesMoved (touches, evt);
        this.State = UIGestureRecognizerState.Changed;
      }
      public override void TouchesEnded (NSSet touches, UIEvent evt)
      {
        base.TouchesEnded (touches, evt);
        UITouch touch = touches.AnyObject as UITouch;
        PointF touchLocation = touch.LocationInView(this.View);
        if (this.lowerLeftCornerRect.Contains(touchLocation))
        {
          this.State = UIGestureRecognizerState.Ended;
        }  else
        {
          this.State = UIGestureRecognizerState.Failed;
        }
      }
    }
    
  3. 将以下方法添加到类中:

    private void OnDragLowerLeft(NSObject gesture)
    {
      DragLowerLeftGesture drag = (DragLowerLeftGesture)gesture;
      switch (drag.State)
      {
      case UIGestureRecognizerState.Began:
        this.lblOutput.Text = "Drag began!";
        break;
      case UIGestureRecognizerState.Changed:
        this.lblOutput.Text = "Drag changed!";
        break;
      case UIGestureRecognizerState.Ended:
        this.lblOutput.Text = "Drag ended!";
        break;
      case UIGestureRecognizerState.Failed:
        this.lblOutput.Text = "Drag failed!";
        break;
      }
    }
    
  4. 如下所示,在ViewDidLoad方法中初始化并添加手势识别器:

    DragLowerLeftGesture dragGesture = new DragLowerLeftGesture();
    dragGesture.AddTarget(this.OnDragLowerLeft);
    this.View.AddGestureRecognizer(dragGesture);
    
  5. 在模拟器上编译并运行应用程序。在模拟器的屏幕上点击并拖动到左下角。

它是如何工作的...

要创建手势识别器,我们需要声明一个继承自UIGestureRecognizer类的类。在这个例子中,我们创建了一个手势,用户可以通过在屏幕上拖动手指到屏幕左下角的 50 x 50 点区域来识别这个手势。以下代码行显示了类声明:

private class DragLowerLeftGesture : UIGestureRecognizer

UIGestureRecognizer 类包含我们用来在视图中拦截触摸的相同触摸方法。我们还可以通过其 View 属性访问它所添加到的视图。在 TouchesBegan 方法中,我们确定初始触摸位置。如果它在视图的左下角外部,我们将 State 属性设置为 Began。如果它在左下角内部,我们将 State 属性设置为 Failed,这样回调就不会被调用。

TouchesEnded 方法中,如果触摸的位置在视图的左下角内部,我们考虑手势为 Ended。如果没有,手势识别被认为是 Failed

TouchesMoved 方法是设置 Changed 状态的地方。对于这个简单的手势识别器,不需要其他逻辑。

由于 UIGestureRecognizer 类没有接受手势处理程序的 Action<T> 对象的构造函数,我们使用默认构造函数初始化它,并使用以下代码通过 AddTarget 方法来达到这个目的:

dragGesture.AddTarget(this.OnDragLowerLeft);

在这种情况下,唯一的区别是参数是 Action<NSObject> 类型,我们可以将其转换为我们的自定义类型,如下面的代码行所示:

DragLowerLeftGesture drag = (DragLowerLeftGesture)gesture;

更多...

这是一个简单的手势识别器,它依赖于单个触摸。通过触摸方法提供的信息,我们可以创建更复杂的支持多个触摸的手势。

自定义手势识别器的另一种用途

有一些视图继承自 UIView 类,根据苹果开发者文档,这些类不应该被子类化。MKMapView 类代表这些用于显示地图的视图之一。如果我们想拦截这些视图的触摸事件,这会引发问题。虽然我们可以在其上使用另一个视图并拦截该视图的触摸事件,但这相当复杂(且容易出错)。一个更简单的方法是创建一个简单的自定义手势识别器并将其添加到我们无法子类化的视图中。这样,我们可以在不子类化的情况下拦截其触摸。

参见

  • 识别手势 菜单

  • 处理触摸事件 菜单

使用加速度计

在这个菜谱中,我们将学习如何接收加速度计事件以创建一个能够感知设备运动的 app。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序 并命名为 AccelerometerApp

注意

模拟器不支持加速度计硬件。本例中的项目将在设备上正确运行。

如何操作...

执行以下步骤:

  1. 在控制器的视图中添加两个按钮和一个标签。

  2. ViewDidLoad 方法中,添加以下代码:

    this.btnStop.Enabled = false;
    UIAccelerometer.SharedAccelerometer.UpdateInterval = 1 / 
      10;
    this.btnStart.TouchUpInside += delegate {
      this.btnStart.Enabled = false;
      UIAccelerometer.SharedAccelerometer.Acceleration += this.Acceleration_Received;
      this.btnStop.Enabled = true;
    } ;
    this.btnStop.TouchUpInside += delegate {
      this.btnStop.Enabled = false;
      UIAccelerometer.SharedAccelerometer.Acceleration -= this.Acceleration_Received;
      this.btnStart.Enabled = true;
    } ;
    
  3. 在类中添加以下方法:

    private void Acceleration_Received (object sender, UIAccelerometerEventArgs e)
    {
      this.lblOutput.Text = string.Format("X: {0}\nY: {1}\nZ: {2}", e.Acceleration.X, e.Acceleration.Y, e.Acceleration.Z);
    }
    
  4. 在设备上编译并运行 app。轻触 开始加速度计 按钮,并观察在移动或摇晃设备时值在标签上显示。

它是如何工作的...

UIAccelerometer类通过其SharedAccelerometer静态属性提供对加速度计硬件的访问。要激活它,我们只需要将其Acceleration事件分配给一个处理器,使用以下代码:

UIAccelerometer.SharedAccelerometer.Acceleration += this.Acceleration_Received;

在处理器内部,我们通过UIAccelerometerEventArgs.Acceleration属性接收加速度计的值。该属性返回一个UIAcceleration类型的对象,它包含三个属性:XYZ。这些属性代表x轴、y轴和z轴的运动。考虑以下截图:

如何工作...

这些值中的每一个都测量设备在每个轴上移动的 G 力的大小。例如,如果X的值为1,则设备在x轴上向右移动,加速度为 1G。如果X的值为-1,则设备在x轴上向左移动,加速度为 1G。当设备背对地面放置在桌子上且不移动时,加速度的正常值应接近或等于以下值:

  • X: 0

  • Y: 0

  • Z: -1

尽管设备没有移动,但 Z 将为-1,因为设备测量地球的重力。

我们可以通过设置加速度计发布加速度事件的时间间隔来设置,通过使用以下代码设置其UpdateInterval属性:

UIAccelerometer.SharedAccelerometer.UpdateInterval = 1 / 10;

该属性接受一个double类型的数字,它表示加速度计在秒内发布加速度事件的时间间隔。设置更新间隔时必须小心,因为加速度计在特定时间内需要发布的更多事件,它消耗的电量就越多。

要停止使用加速度计,我们只需要通过以下代码从Acceleration事件中取消绑定处理器:

UIAccelerometer.SharedAccelerometer.Acceleration -= this.Acceleration_Received;

还有更多...

UIAcceleration类还包含另一个有用的属性,名为Time。它是一个double,表示加速度事件发生的相对时间。它是相对于 CPU 时间的,不建议您使用此值来计算事件的精确时间戳。

使用加速度计时的注意事项

虽然 iPhone 的加速度计是一个非常精确和敏感的传感器,但不应该用于精确测量。此外,它产生的结果可能在不同 iOS 设备之间有所不同,即使它们是同一型号。

参见

  • 使用陀螺仪的食谱

使用陀螺仪

在这个食谱中,我们将学习如何使用设备的内置陀螺仪。

准备工作

在 Xamarin Studio 中创建一个新的项目,并将其命名为GyroscopeApp

注意

模拟器不支持陀螺仪硬件。此外,只有较新的设备才包含陀螺仪。如果此应用程序在没有陀螺仪的设备或模拟器上运行,则不会发生错误,但不会显示数据。

如何做到这一点...

执行以下步骤:

  1. 在控制器视图中添加两个按钮和一个标签。

  2. GyroscopeAppViewController.cs文件中添加MonoTouch.CoreMotion命名空间。

  3. 在类中输入以下私有字段:

    private CMMotionManager motionManager;
    
  4. 使用以下代码实现ViewDidLoad方法:

    this.motionManager = new CMMotionManager();
    this.motionManager.GyroUpdateInterval = 1 / 10;
    this.btnStart.TouchUpInside += delegate {
      this.motionManager.StartGyroUpdates(NSOperationQueue.MainQueue, this.GyroData_Received);
    } ;
    this.btnStop.TouchUpInside += delegate {
      this.motionManager.StopGyroUpdates();
    } ;
    

    添加以下方法:

    private void GyroData_Received(CMGyroData gyroData, NSError error)
    {
      Console.WriteLine("rotation rate x: {0}, y: {1}, z: {2}", gyroData.RotationRate.x, gyroData.RotationRate.y, gyroData.RotationRate.z);
    }
    
  5. 在设备上编译并运行应用程序。轻触开始陀螺仪按钮,并在所有轴上旋转设备。观察值在应用程序输出面板中显示。

它是如何工作的...

陀螺仪是一种测量方向的机制。较新的 iOS 设备支持陀螺仪硬件,以及加速度计,以提供更精确的设备运动测量。

MonoTouch.CoreMotion命名空间封装了原生 CoreMotion 框架中的对象。在代码中使用陀螺仪硬件的过程与用于加速度计的过程类似。第一个区别是UIApplication类中没有针对陀螺仪的单个对象。因此,我们需要创建CMMotionManager类的实例,如下面的代码所示:

private CMMotionManager motionManager;
//...
   this.motionManager = new CMMotionManager();

就像我们使用加速度计一样,我们可以通过以下代码设置我们将以秒为间隔接收陀螺仪事件:

this.motionManager.GyroUpdateInterval = 1 / 10;

要开始接收陀螺仪事件,我们调用对象的StartGyroUpdates方法,如下面的代码所示:

this.motionManager.StartGyroUpdates(NSOperationQueue.MainQueue, this.GyroData_Received);

此方法被重载;第一个重载是无参数的,当被调用时,陀螺仪测量值的GyroData属性被设置。使用这个重载非常简单且容易,但在这里不会触发任何事件,我们必须提供一个机制来从属性中读取测量值。

第二个重载,在此示例中使用,接受两个参数。第一个参数是更新将发生的NSOperationQueue参数,第二个参数是当发生更新时将被执行的处理器。

NSOperationQueue类代表 iOS 管理NSOperation对象执行的一种机制。我们通过静态NSOperationQueue.MainQueue属性访问运行时的主操作队列。基本上,这种方式,我们指示运行时以更有效的方式管理处理器的传递。

第二个参数是CMGyroHandler类型的委托。它的签名,由我们创建的方法表示,类似于以下代码:

private void GyroData_Received(CMGyroData gyroData, NSError error)

CMGyroData对象包含从陀螺仪通过其RotationRate属性接收到的实际测量值。以下代码输出属性中的数据:

Console.WriteLine("rotation rate x: {0}, y: {1}, z: {2}", gyroData.RotationRate.x, gyroData.RotationRate.y, gyroData.RotationRate.z);

旋转速率反映在xyz轴上,分别由相应的XYZ属性表示。每个值是该轴上每秒发生的旋转角度的数量,以弧度为单位。

虽然一开始可能看起来有点复杂,但实际上相当简单。例如,z 轴上的值为 0.5 表示设备以 0.5 弧度/秒的速度向左旋转。z-轴上的值为 -0.5 表示设备以 0.5 弧度/秒的速度向右旋转。确定旋转方向的模式基于右手定则。

更多内容...

如果您希望您的应用程序仅适用于支持陀螺仪的设备,请在项目的 Info.plist 文件中添加 UIRequiredDeviceCapabilities 键,并使用 gyroscope 值。如果您的应用程序的功能完全基于陀螺仪,添加此键是必不可少的,以避免应用程序被较老设备的用户下载,最终导致应用程序无法使用。

确定陀螺仪硬件的可用性

要确定运行应用程序的设备是否支持陀螺仪硬件,请检查 CMMotionManager 实例的 GyroAvailable 属性值。

将弧度转换为度数

弧度是角度的测量单位。要将角度测量值从弧度转换为度数,请考虑以下辅助方法:

public static double RadiansToDegrees (double radians)
{
  return (radians * 180 / Math.PI);
}

参见

  • 使用加速度计 菜单

第十章:位置服务和地图

在本章中,我们将涵盖以下内容:

  • 确定位置

  • 确定航向

  • 使用区域监控

  • 使用显著变化位置服务

  • 背景位置服务

  • 显示地图

  • 地理编码

  • 添加地图标注

  • 添加地图覆盖

简介

当今的智能手机和手持设备都配备了高精度的全球定位系统(GPS)硬件。GPS 硬件从卫星星座接收位置信息。除了卫星之外,iOS 设备还利用蜂窝和 Wi-Fi 网络向用户提供位置信息。

在本章中,我们将讨论如何使用适当的框架来利用设备的位置服务。此外,我们还将学习如何显示地图并对其进行标注。具体来说,我们将重点关注以下主题:

  • 位置服务:在此,我们将讨论设备上提供位置信息的服务。这些服务如下:

    • 标准位置服务:此位置服务完全依赖于设备的 GPS 模块,并提供最高精度的位置数据

    • 区域监控服务:此位置服务监控边界穿越

    • 显著变化位置服务:此服务监控设备位置的显著变化

  • CLLocationManager:此类允许我们使用位置服务

  • Compass:此类展示了如何使用内置罗盘

  • MKMapView:此视图用于显示地图

  • CLGeocoder:此类提供地理编码功能

  • MKAnnotation:此类允许我们在地图上添加标注

  • MKOverlay:此类允许我们在地图上添加覆盖

确定位置

我们现在将学习如何从内置的 GPS 硬件接收位置信息。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为LocationApp。在控制器的视图中添加两个按钮和一个标签。

如何操作...

执行以下步骤以接收设备的位置:

  1. 要从内置的 GPS 硬件检索位置信息,我们需要使用 Core Location 框架。它通过以下MonoTouch.CoreLocation命名空间公开:

    using MonoTouch.CoreLocation;
    
  2. LocationAppViewController类中添加以下代码:

    private CLLocationManager locationManager;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.locationManager = new CLLocationManager();
      this.locationManager.LocationsUpdated += LocationManager_LocationsUpdated;
      this.locationManager.Failed += this.LocationManager_Failed;
    
      this.btnStart.TouchUpInside += delegate {
        this.lblOutput.Text = "Determining location...";
        this.locationManager.StartUpdatingLocation();
      } ;
      this.btnStop.TouchUpInside += delegate {
        this.locationManager.StopUpdatingLocation();
        this.lblOutput.Text = "Location update stopped.";
      } ;
    }
    private void LocationManager_LocationsUpdated (object sender, CLLocationsUpdatedEventArgs e)
    {
      CLLocation location = e.Locations[0];
      double latitude = Math.Round(location.Coordinate.Latitude, 4);
      double longitude = Math.Round(location.Coordinate.Longitude, 4);
      double accuracy = Math.Round(location.HorizontalAccuracy, 0);
      this.lblOutput.Text = string.Format("Latitude: {0}\nLongitude: {1},\nAccuracy: {2}m", latitude, longitude, accuracy);
    }
    private void LocationManager_Failed (object sender, NSErrorEventArgs e)
    {
      this.lblOutput.Text = string.Format("Location update failed! Error message: {0}", e.Error.LocalizedDescription);
    }
    
  3. 在设备上编译并运行应用程序。点击开始按钮以在屏幕上查看您的位置坐标。

注意

使用 Core Location 框架确定设备当前位置的项目可以在模拟器上运行。通过导航到模拟器的调试 | 位置菜单,我们可以自定义设备将使用的位置。

工作原理...

通过CLLocationManager类可以访问 GPS 模块提供的位置数据。初始化该类的实例后,我们需要订阅其LocationsUpdated事件,如下所示:

this.locationManager = new CLLocationManager();
this.locationManager.LocationsUpdated += LocationManager_LocationsUpdated;

位置数据将通过此事件变得可用。同时,订阅Failed事件也是一个好的实践,如下所示:

  this.locationManager.Failed += this.LocationManager_Failed;

当位置管理器首次请求位置更新时,用户将通过系统特定的警报得到通知,这与以下截图所示类似:

如何工作...

此警报基本上是请求用户权限,允许应用程序检索位置数据。如果用户拒绝此请求,将触发带有适当信息的Failed事件。未来的位置请求将不会触发权限警报,用户必须通过设备的设置启用应用程序的位置服务,因此我们需要相应地处理这种情况。

在订阅适当的事件后,我们通过StartUpdatingLocation方法请求位置更新,如下所示:

this.locationManager.StartUpdatingLocation();

要停止接收位置更新,我们按照以下方式调用StopUpdatingLocation方法:

this.locationManager.StopUpdatingLocation();

还有更多...

LocationsUpdated事件接受EventHandler<CLLocationsUpdatedEventArgs>类型的委托。CLLocationsUpdatedEventArgs参数包含一个属性,该属性返回一个CLLocation对象的数组。数组中的最后一个项目包含从位置服务检索到的最新位置数据。数组将始终包含至少一个CLLocation项目。

坐标以double类型返回,并代表位置坐标的度数,如下所示:

CLLocation location = e.Locations[0];
double latitude = Math.Round(location.Coordinate.Latitude, 4);
double longitude = Math.Round(location.Coordinate.Longitude, 4);
double accuracy = Math.Round(location.HorizontalAccuracy, 0);

负纬度值表示南坐标,正值表示北坐标。负经度值表示西坐标,而正值表示东坐标。

HorizontalAccuracy属性返回 GPS 定位的精度(以米为单位)。例如,17 米的值表示位置被确定在一个直径为 17 米的圆内。较低的值表示更好的精度。

GPS 精度

位置数据始终存在一定的误差范围,这与 GPS 硬件无关,并且存在一些变量因素定义它,例如周围建筑和各种障碍物。您会注意到,当设备在户外时,HorizontalAccuracy属性将返回较低的值,而当我们在室内使用 GPS 或在有高楼的城市街道上时,将返回较高的值。

位置服务可用性

并非所有设备都配备了位置服务硬件。此外,即使设备配备了适当的硬件,用户也可能已禁用位置服务。

要确定设备上是否可用或启用了位置服务,我们在初始化位置管理器对象之前,读取CLLocationManager.LocationServicesEnabled静态属性的返回值,如下所示:

if (CLLocationManager.LocationServicesEnabled) {
  // Initialize the location manager
  //...
}

此外,我们还可以通过CLLocationManager.Status属性检查位置服务的授权状态,如下所示:

if (CLLocationManager.Status == CLAuthorizationStatus.Authorized) {
  //..
}

位置服务使用指示器

当使用任何类型的定位服务时,位置服务图标会出现在状态栏的右侧,紧挨着电池指示器,如下面的截图所示:

位置服务使用指示器

相关内容

  • 确定航向后台位置服务菜谱

确定航向

在这个菜谱中,我们将学习如何使用内置的指南针来确定设备的航向。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为HeadingApp。就像你在上一个菜谱中所做的那样,在控制器的视图中添加两个按钮和一个标签。

注意

在这个菜谱中的项目不能在模拟器上测试。需要一个带有指南针硬件(磁力计)的设备。

如何操作...

要确定设备的航向,请执行以下步骤:

  1. HeadingAppViewController类中添加以下代码:

    private CLLocationManager locationManager;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      // Perform any additional setup after loading the view, typically from a nib.
      this.locationManager = new CLLocationManager();
      this.locationManager.UpdatedHeading += LocationManager_UpdatedHeading;
      this.locationManager.Failed += (sender, e) => Console.WriteLine("Failed! {0}", e.Error.LocalizedDescription);
    
      this.btnStart.TouchUpInside += delegate {
        this.lblOutput.Text = "Starting updating heading...";
        this.locationManager.StartUpdatingHeading();
      } ;
      this.btnStop.TouchUpInside += delegate {
        this.locationManager.StopUpdatingHeading();
        this.lblOutput.Text = "Stopped updating heading.";
      };
    }
    private void LocationManager_UpdatedHeading (object sender, CLHeadingUpdatedEventArgs e)
    {
      this.lblOutput.Text = string.Format("Magnetic heading: {0}", Math.Round(e.NewHeading.MagneticHeading, 1));
    }
    
  2. 在设备上编译并运行应用程序。点击开始按钮并旋转设备以查看不同的航向值。

它是如何工作的...

要检索航向信息,我们首先需要订阅位置管理器的UpdatedHeading事件,如下所示:

this.locationManager.UpdatedHeading += this.LocationManager_UpdatedHeading;

要开始发送航向信息,我们调用StartUpdatingHeading方法,如下所示:

this.locationManager.StartUpdatingHeading();

UpdatedHeading事件处理程序中,我们通过事件参数的NewHeading属性,通过以下方式检索CLHeading对象的MagneticHeading属性来获取航向信息:

this.lblOutput.Text = string.Format("Magnetic heading: {0}", Math.Round(e.NewHeading.MagneticHeading, 1));

要停止检索航向更新,我们调用StopUpdatingHeading方法,如下所示:

this.locationManager.StopUpdatingHeading();

更多内容...

航向是以度数来测量的。在简单指南针上可以看到地平线的四个方向的值如下:

  • 0 或 360 度:当设备朝北时,磁力计将返回最多 359.99 度的值,然后返回 0。

  • 90 度:设备朝东

  • 180 度:设备朝南

  • 270 度:设备朝西

磁航向与真航向

磁航向是基于普通指南针显示的北方。真航向是基于地球北极实际位置的北方方向。两者之间有一个微小的差异,这个差异根据地球磁场的变化而变化,通常约为 2 度。

CLHeading类通过MagneticHeadingTrueHeading属性提供两种读数。这对开发者来说非常有帮助,因为计算两种读数之间的差异可能需要昂贵的设备或基于年份和其他因素的非常复杂的计算。

指南针可用性

磁力计,一个可以确定方向(以度为单位)并为设备提供指南针功能的功能模块,并非所有设备都可用。要检查设备是否可以提供方向信息,请从 CLLocationManager.HeadingAvailable 静态属性中检索值,如下所示:

if (CLLocationManager.HeadingAvailable) {
  // Start updating heading
  //...
}

参见

  • 确定位置后台位置服务 的食谱

使用区域监控

在本食谱中,我们将学习如何使用 GPS 来响应特定区域的位置变化。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用 并命名为 RegionApp。在控制器的视图中添加两个按钮和一个标签。

如何操作...

执行以下步骤:

  1. RegionAppViewController 类中创建两个字段,如下所示:

    private CLLocationManager locationManager;
    private CLCircularRegion region;
    
  2. ViewDidLoad 方法中,初始化 RegionAppViewController 类,并按以下方式订阅 LocationsUpdatedRegionEnteredRegionLeft 事件:

    this.locationManager.RegionEntered += this.LocationManager_RegionEntered;
    this.locationManager.RegionLeft += this.LocationManager_RegionLeft;
    this.locationManager.UpdatedLocation += this.LocationManager_UpdatedLocation;
    
  3. 在类中输入以下事件处理程序:

    private void LocationManager_LocationsUpdated (object sender, CLLocationUpdatedEventArgs e)
    {
      CLLocation location = e.Locations[0];
      if (location.HorizontalAccuracy < 100)
      {
        this.region = new CLCircularRegion(location.Coordinate, 100, "Home");
        this.locationManager.StartMonitoring(this.region);
        this.locationManager.StopUpdatingLocation();
      }
    }
    private void LocationManager_RegionLeft (object sender, CLRegionEventArgs e)
    {
      this.lblOutput.Text = string.Format("{0} region left.", e.Region.Identifier);
    }
    private void LocationManager_RegionEntered (object sender, CLRegionEventArgs e)
    {
      this.lblOutput.Text = string.Format("{0} region entered.", e.Region.Identifier);
    }
    
  4. 在启动按钮的 TouchUpInside 处理程序中,使用以下代码调用 StartUpdatingLocation 方法:

    this.locationManager.StartUpdatingLocation();
    
  5. 在停止按钮的 TouchUpInside 处理程序中,使用以下代码调用 StopMonitoring 方法:

    this.locationManager.StopMonitoring(this.region);
    
  6. 在模拟器上编译并运行应用。在模拟器的菜单中导航到 调试 | 位置 | 高速公路驾驶,然后点击 开始区域监控 按钮。

它是如何工作的...

区域监控是一个监控边界穿越的功能。当特定区域边界被穿越时,CLLocationManager 对象将发出适当的事件,如下所示:

this.locationManager.RegionEntered += this.LocationManager_RegionEntered;
this.locationManager.RegionLeft += this.LocationManager_RegionLeft;

在此示例中,我们根据当前位置定义区域;因此,我们也订阅了 LocationsUpdated 事件。

当应用开始接收位置更新时,它首先使用以下代码检查位置精度:

if (location.HorizontalAccuracy < 100)

如果达到所需的精度(<100 m,可自行修改),我们使用以下代码行初始化 CLCircularRegion 对象:

this.region = new CLRegion(e.NewLocation.Coordinate, 100, "Home");

CLCircularRegion 类用于定义圆形区域,并继承自 CLRegion 类。在此,我们根据当前位置创建要监控的区域。第二个参数声明了围绕坐标的半径(以米为单位),定义了区域的边界。第三个参数是我们想要分配给区域的字符串标识符。

要开始监控区域,我们使用以下代码行调用 StartMonitoring 方法:

this.locationManager.StartMonitoring(this.region);

当区域监控开始时,当设备进入或离开区域时,将触发适当的事件。

更多内容...

区域监控是一个非常实用的功能。例如,一个应用可以根据用户接近的不同区域提供特定的信息。此外,它还可以在应用处于后台时通知边界穿越。

区域监控可用性

要检查设备是否支持区域监控,调用CLLocationManager.IsMonitoringAvailable静态方法,并传递我们想要使用的CLRegion对象的类型,如下所示:

if (CLLocationManager.IsMonitoringAvailable(typeof(CLCircularRegion)) {
  // Start monitoring a region
  //...
}

相关内容

  • 使用显著变化位置服务后台位置服务菜谱

使用显著变化位置服务

在本章中,我们将学习如何使用显著位置变化监控功能。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为SLCApp。在控制器的视图中添加一个标签和两个按钮。

如何操作...

执行以下步骤:

  1. SLCAppViewController类中添加以下ViewDidLoad方法:

    private CLLocationManager locationManager;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
    
      // Perform any additional setup after loading the view, typically from a nib.
      this.locationManager = new CLLocationManager();
      this.locationManager.LocationsUpdated += LocationManager_LocationsUpdated;
      this.btnStart.TouchUpInside += (s, e) => {
        this.lblOutput.Text = "Starting monitoring significant location changes...";
        this.locationManager.StartMonitoringSignificantLocationChanges();
      } ;
      this.btnStop.TouchUpInside += (s, e) => {
        this.locationManager.StopMonitoringSignificantLocationChanges();
        this.lblOutput.Text = "Stopped monitoring significant location changes.";
      } ;
    }
    
  2. 添加以下方法:

    private void LocationManager_LocationsUpdated (object sender, CLLocationsUpdatedEventArgs e)
    {
      CLLocation location = e.Locations[0];
      double latitude = Math.Round(location.Coordinate.Latitude, 4);
      double longitude = Math.Round(location.Coordinate.Longitude, 4);
      double accuracy = Math.Round(location.HorizontalAccuracy, 0);
      this.lblOutput.Text = string.Format("Latitude: {0}\nLongitude: {1}\nAccuracy: {2}", latitude, longitude, accuracy);
    }
    
  3. 在 iOS 模拟器中,通过菜单导航到调试 | 位置 | 高速公路驾驶

  4. 在模拟器上编译并运行应用。点击开始监控按钮以开始监控显著位置变化。

工作原理...

显著变化位置服务监控显著位置变化,并在这些变化发生时提供位置信息。在功耗方面,它是要求较低的位置服务。它使用设备的蜂窝无线电收发器来确定用户的位置。只有配备了蜂窝无线电收发器的设备才能使用此服务。

使用显著变化位置服务的代码与标准位置服务的代码类似。唯一的不同之处在于启动和停止服务的方法。要启动服务,我们使用以下代码行调用StartMonitoringSignificantLocationChanges方法:

this.locationManager.StartMonitoringSignificantLocationChanges();

位置更新通过LocationsUpdated事件处理器发布,这与我们用于标准位置服务的相同事件,如下所示:

this.locationManager.LocationsUpdated += LocationManager_LocationsUpdated;
//...
private void LocationManager_LocationsUpdated (object sender, CLLocationUpdatedEventArgs e)
{
//...
}

更多内容...

显著变化位置服务可以在后台报告位置变化,唤醒应用。对于需要使用位置服务但精度低于标准位置服务的应用来说,它非常有用。

显著变化位置服务可用性

要确定设备是否能够使用显著变化位置服务,按照以下方式检索SignificantLocationChangeMonitoringAvailable静态属性的值:

if (CLLocationManager.SignificantLocationChangeMonitoringAvailable) {
  // Start monitoring for significant location changes.
  //...
}

相关内容

  • 使用区域监控后台位置服务菜谱

后台位置服务

在本菜谱中,我们将讨论如何在应用处于后台时使用位置服务。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为BackgroundLocationApp。就像我们在前面的菜谱中所做的那样,在控制器的视图中添加一个标签和两个按钮。

如何操作...

在应用处于后台时使用位置服务,请执行以下步骤:

  1. Solution 面板中,双击 Info.plist 文件以打开它。在 Source 选项卡下,通过单击加号(+)或通过右键单击并从上下文菜单中选择 New Key 来添加一个新键。

  2. 从下拉列表中选择 Required background modes,或者直接在字段中输入 UIBackgroundModes

  3. 展开键并右键单击其下方的空白项。在上下文菜单中单击 New Key。在其 Value 字段中,选择 App registers for location updates,或者输入单词 location。保存文档。完成后,你应该会有以下类似的截图:如何操作...

  4. BackgroundLocationAppViewController 类中,输入与本章中 Determining location 菜单中使用的相同代码。

  5. LocationManager_LocationsUpdated 方法的底部添加以下行:

    Console.WriteLine("{0}:\n\t{1} ", DateTime.Now, this.lblOutput.Text);
    
  6. 在模拟器上编译并运行应用。轻触 Start 按钮以开始接收位置更新。

  7. 当模拟器窗口处于活动状态时,按 Cmd + Shift + H。这个键组合模拟在设备上按主页按钮,并将应用移动到后台。观察 Xamarin Studio 的 Application Output 面板继续显示位置更新。

它是如何工作的...

要在应用后台运行时接收位置更新,我们需要在 Info.plist 文件中将 location 值设置为 UIBackgroundModes 键。这基本上确保了应用在后台运行时具有接收位置更新的适当权限,并且它不会进入挂起状态。

如果你在一个文本编辑器中打开 Info.plist 文件,这是添加的内容:

<key>UIBackgroundModes</key>
  <array>
    <string>location</string>
  </array>
</key>

为了确保应用正在接收位置更新,检查状态栏。即使应用在后台运行,位置服务图标也应显示。

还有更多...

为位置服务设置 UIBackgroundModes 键仅适用于标准位置服务。区域监控和重大变更位置服务默认支持在应用后台运行时提供位置更新。当其中一个位置服务开始更新位置数据时,应用甚至可以被终止。当接收到位置更新时,应用将被启动或从挂起状态唤醒,并给予有限的时间来执行代码。

要确定应用是否是由这两个位置服务之一启动的,请检查 AppDelegate 类中 FinishedLaunching 方法的 options 参数,如下所示:

if (null != options)
{
 if (options.ContainsKey (UIApplication.LaunchOptionsLocationKey))
  {
    Console.WriteLine ("Woken from location service!");
    CLLocationManager locationManager = new CLLocationManager();
    locationManager.UpdatedLocation += this.LocationUpdatedHandler;
    locationManager.StartMonitoringSignificantLocationChanges();
  }
}

options 参数的类型是 NSDictionary。如果这个字典包含 UIApplication.LaunchOptionsLocationKey,则应用是因为位置服务而被启动或从挂起状态唤醒的。在这种情况下,我们需要再次在 CLLocationManager 实例上调用 StartMonitoringSignificantLocationChanges 方法,以检索位置数据。

同样适用于区域监控位置服务。请注意,如果我们使用这两种位置服务之一,但我们的应用不支持位置事件的背景交付,我们必须确保在不再需要时停止监控位置更新。如果不这样做,位置服务将继续运行,导致电池消耗显著增加。

限制到支持的硬件

如果我们的应用功能完全依赖于位置服务,并且无法在不支持这些服务的设备上正确运行,我们必须在 Info.plist 文件中添加 UIRequiredDeviceCapabilities 键,并使用 location-services 值。

此外,当应用需要使用使用 GPS 硬件的常规位置服务时,我们需要将 gps 值添加到 UIRequiredDeviceCapabilities 键。这样,我们确保应用不会通过 App Store 供没有配备适当硬件的设备使用。

在后台进行 UI 更新

在本食谱中,我们故意在应用处于后台时设置标签的 Text 属性的值。然而,在应用处于后台时更新 UI 应该避免,因为 iOS 如果有太多的更新可能会终止我们的应用。此外,在后台发生的 UI 更新基本上是在应用返回前台时排队,并在这种情况下立即发生。这可能会导致我们应用中出现意外的行为。

参见

  • 确定位置 食谱

  • 在 第一章 开发工具使用 Xamarin Studio 创建 iOS 项目 食谱

显示地图

在本食谱中,我们将学习如何在我们的应用中显示地图。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序 并将其命名为 MapDisplayApp

如何操作...

执行以下步骤以在应用中显示地图:

  1. 在控制器上添加 MKMapView。以下截图显示了 Xcode 对象库中 MKMapView 的符号:如何操作...

  2. MapDisplayAppViewController.cs 文件中添加以下 using 指令:

    using MonoTouch.MapKit;
    using MonoTouch.CoreLocation;
    
  3. MapDisplayAppViewController 类中输入以下代码:

    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.mapView.ShowsUserLocation = true;
      this.mapView.RegionChanged += this.MapView_RegionChanged;
    }
    private void MapView_RegionChanged (object sender, MKMapViewChangeEventArgs e)
    {
      if (this.mapView.UserLocation.Location != null)
      {
        CLLocationCoordinate2D mapCoordinate = this.mapView.UserLocation.Location.Coordinate;
        Console.WriteLine("Current coordinates: LAT: {0}, LON: {1}", mapCoordinate.Latitude, mapCoordinate.Longitude);
      }
    }
    
  4. 在模拟器或设备上编译并运行应用。

  5. 通过在屏幕上捏合来缩放或平移地图(在模拟器上按 Option 并点击拖动)以在 应用程序输出 框中输出当前位置。(注:原文中的 Option 应为 Option 键,即键盘上的 Option 键

它是如何工作的...

MonoTouch.MapKit 命名空间包装了 MapKit 框架中包含的所有对象。MapKit 框架使用 Apple 地图来显示地图。

MKMapView 是默认的 iOS 视图,用于显示地图。它专门为此目的而设计,不应被子类化。

要在地图上显示用户的当前位置,我们使用以下代码行将其 ShowsUserLocation 属性设置为 true

this.mapView.ShowsUserLocation = true;

这将激活标准位置服务以开始接收位置更新并将它们内部传递给MKMapView对象。

注意

MKMapView首次在应用中显示时,系统将提示用户请求使用位置服务的权限,就像我们直接尝试使用位置服务一样。

要确定用户何时缩放或平移地图,我们使用以下代码订阅RegionChanged事件:

this.mapView.RegionChanged += this.MapView_RegionChanged;

在事件处理程序内部,我们通过UserLocation属性如下检索当前位置:

if (this.mapView.UserLocation.Location != null)
{
  CLLocationCoordinate2D mapCoordinate = this.mapView.UserLocation.Location.Coordinate;
  Console.WriteLine("Current coordinates: LAT: {0}, LON: {1}", mapCoordinate.Latitude, mapCoordinate.Longitude);
}

如果ShowsUserLocation属性设置为false,则位置服务将不会激活,并且UserLocation.Location属性将返回null。当应用第一次运行时,它也会返回null,因为它将要求用户授权使用位置服务。然而,只要设备或模拟器有活动的互联网连接,就会显示地图。

更多内容...

我们可以通过SetCenterCoordinate方法如下设置要显示的地图的中心坐标:

CLLocationCoordinate2D mapCoordinates = new CLLocationCoordinate2D(0, 0);
this.mapView.SetCenterCoordinate(mapCoordinates, true);

第一个参数是我们希望地图中心对齐的地图坐标,由CLLocationCoordinate2D类型的对象表示。第二个参数声明我们是否希望地图中心对齐是动画的。

除了定位地图外,我们还可以设置其缩放级别。我们通过SetRegion方法如下进行设置:

this.mapView.SetRegion(MKCoordinateRegion.FromDistance(mapCoordinates, 1000, 1000), true);

第一个参数是MKCoordinateRegion类型。在这里,使用其FromDistance静态方法来创建一个实例。它的第一个参数是区域中心的坐标,接下来的两个参数代表要显示的地图的水平跨度,单位为米。这基本上意味着由这个MKCoordinateRegion实例表示的区域将具有mapCoordinates为中心,地图的水平部分和垂直部分将分别代表地图上的 1000 米。

注意,MKMapView将实际区域设置为MKCoordinateRegion值的近似值。这是因为MKMapView的尺寸不一定总是与提供的水平和垂直跨度值匹配。例如,在这里,我们设置了一个 1000 x 1000 米的正方形区域,但我们的MKMapView布局并不是一个绝对的正方形,因为它基本上占据了整个屏幕。我们可以通过其Region属性检索MKMapView显示的实际地图区域。

相关内容

  • 地理编码添加地图标注添加地图覆盖菜谱

地理编码

在这个菜谱中,我们将学习如何根据位置坐标提供关于地址、城市或国家的信息。

准备中

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为GeocodingApp

如何实现...

执行以下步骤:

  1. MainController视图的上半部分添加一个MKMapView,一个标签,以及下半部分的一个按钮。

  2. GeocodingAppViewController.cs 文件中添加 MonoTouch.MapKitMonoTouch.CoreLocation 命名空间。

  3. 在类中输入以下 ViewDidLoad 方法:

    private CLGeocoder geocoder;
    public override void ViewDidLoad () {
      base.ViewDidLoad ();
      this.mapView.ShowsUserLocation = true;
      this.btnGeocode.TouchUpInside += async (sender, e) => {
        this.lblOutput.Text = "Reverse geocoding location...";
        this.btnGeocode.Enabled = false;
        CLLocation currentLocation = 
          this.mapView.UserLocation.Location;
        this.mapView.SetRegion(MKCoordinateRegion.FromDistance(currentLocation.Coordinate, 1000, 1000), true);
        this.geocoder = new CLGeocoder();
        try	{
          CLPlacemark[] placemarks = 
            await this.geocoder.ReverseGeocodeLocationAsync(currentLocation);
          if (null != placemarks)  {
            CLPlacemark placemark = placemarks[0];
            this.lblOutput.Text = 
              string.Format("Locality: {0}, Administrative area: {1}", 
                placemark.Locality,
                placemark.AdministrativeArea);
          }
        } catch (Exception ex) {
          Console.WriteLine("Error reverse geocoding location! {0}", ex.Message);
        } finally {
          this.btnGeocode.Enabled = true;
        }
      };
    }
    
  4. 确保模拟器的位置设置为固定位置。导航到 调试 | 位置 | 自定义(或 Apple)。

  5. 在模拟器或设备上编译和运行应用程序。结果应类似于以下截图:如何操作...

它是如何工作的...

地理编码是将地址信息与地理坐标匹配的过程。反向地理编码是相反的过程,即将地理坐标与地址信息匹配。在本食谱中,我们通过 CLGeocoder 类使用以下方式进行反向地理编码:

private CLGeocoder geocoder;

初始化地理编码器对象后,我们按照以下方式调用 ReverseGeocodeAsync 方法:

CLPlacemark[] placemarks = 
        await this.geocoder.ReverseGeocodeLocationAsync(currentLocation);

该方法接受一个 CLLocation 参数,它表示我们想要检索地理编码数据的地理位置。return 值是一个包含 CLPlacemark 对象的数组。CLPlacemark 类包含反向地理编码信息,例如国家、城市和坐标的地址,如下面的代码所示:

CLPlacemark placemark = placemarks[0];
this.lblOutput.Text = 
  string.Format("Locality: {0}, Administrative area: {1}", 
            placemark.Locality,
            placemark.AdministrativeArea);

当反向地理编码位置时,数组将始终包含一个项目。如果返回值为 null,则表示发生了错误。

还有更多...

我们还可以使用 CLGeocoder 类进行正向地理编码。例如,要获取 Apple 中央办公室的坐标,我们使用以下 GeocodeAddressAsync 方法:

CLPlacemark[] forward = 
  await this.geocoder.GeocodeAddressAsync("Infinite Loop, 1-5, Cupertino, CA, USA");

当我们向该方法传递所有拥有的信息时,该方法将给出更准确的结果。

注意

使用 GeocodeAddressAsync 方法进行正向地理编码可能会在结果 CLPlacemark[] 对象中返回多个项目。这是因为地理编码器可能无法通过传递的信息确定确切的位置,因此会返回一组可能的结果。

对于 CLGeocoder 需要注意的事项

Apple 提供了地理编码功能,并设置了速率限制。尽管确切的限制没有记录,但建议您每分钟不要进行超过一次地理编码请求。如果超过速率限制,地理编码器将失败并返回错误。

已废弃的 API

CLGeocoder 类基本上取代了 MKReverseGeocoder,后者在 iOS 5 之前仅提供反向地理编码功能。

参见

  • 显示地图添加地图注释添加地图覆盖食谱

添加地图注释

在本食谱中,我们将讨论如何注释地图以向用户提供各种信息。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,并将其命名为 MapAnnotateApp。在控制器的视图中添加 MKMapView 和一个按钮。

如何操作...

执行以下步骤以向地图添加注释:

  1. MapAnnotateAppViewController.cs 文件中添加 MonoTouch.MapKitMonoTouch.CoreLocation 命名空间。

  2. 使用以下代码将 IMKMapViewDelegate 接口添加到 MapAnnotateAppViewController 类声明中:

    public partial class MapAnnotateAppViewController : UIViewController, IMKMapViewDelegate
    
  3. ViewDidLoad 方法中添加以下代码:

    this.mapView.ShowsUserLocation = true;
    this.mapView.WeakDelegate = this;
    this.btnAddPin.TouchUpInside += (sender, e) => {
      CLLocationCoordinate2D mapCoordinate = 
        this.mapView.UserLocation.Coordinate;
      this.mapView.SetRegion(MKCoordinateRegion.FromDistance(mapCoordinate, 1000, 1000), true);
      MKPointAnnotation myAnnotation = new MKPointAnnotation();
      myAnnotation.Coordinate = mapCoordinate;
      myAnnotation.Title = "My Annotation";
      myAnnotation.Subtitle = "Standard pin with Xamarin";
      this.mapView.AddAnnotation(myAnnotation);
    };
    
  4. MapAnnotateAppViewController 类中添加以下方法:

    [Export ("mapView:viewForAnnotation:")]
    public MKAnnotationView GetViewForAnnotation (MKMapView mapView, NSObject annotation)
    {
      if (annotation is MKUserLocation)
      {
        return null;
      } else
      {
        string reuseID = "myAnnotation";
        MKPinAnnotationView pinView = 
          mapView.DequeueReusableAnnotation(reuseID) as MKPinAnnotationView;
        if (null == pinView)
        {
          pinView = new MKPinAnnotationView(annotation, reuseID);
          pinView.PinColor = MKPinAnnotationColor.Purple;
          pinView.AnimatesDrop = true;
          pinView.CanShowCallout = true;
    
        }
        return pinView;
      }
    }
    
  5. 编译并运行应用程序,无论是在模拟器上还是在设备上。点击按钮在地图上添加一个图钉。结果应该类似于以下截图:如何操作...

点击图钉会显示带有注释标题和副标题的呼出气泡。

它是如何工作的...

在地图上注释非常有用,可以提供与地图数据一起的各种信息。我们可以使用 MKPointAnnotation 类创建一个简单的注释,如下所示:

MKPointAnnotation myAnnotation = new MKPointAnnotation();
myAnnotation.Coordinate = mapCoordinate;
myAnnotation.Title = "MyAnnotation";
myAnnotation.Subtitle = "Standard annotation";
this.mapView.AddAnnotation(myAnnotation);

我们将要在地图坐标上出现的注释分配给,并且可选地,标题和副标题也可能出现。然后我们使用 AddAnnotation 方法将注释添加到地图视图中。

只在地图视图中添加注释对象是不够的。注释需要一个将显示其信息的视图。为了为注释提供视图,我们需要将代理对象分配给我们的地图视图。在这个菜谱中,我们使用我们的控制器类作为地图视图的代理对象,如下所示:

this.mapView.WeakDelegate = this;

我们可以将任何从 NSObject 派生的对象分配给 WeakDelegate 属性。我们只需要确保我们提供必要的方法。这就是 GetViewForAnnotation 方法出现的地方,如下所示:

[Export ("mapView:viewForAnnotation:")]
public MKAnnotationView GetViewForAnnotation (MKMapView mapView, NSObject annotation)

这个方法在 MKMapViewDelegate 类中,当系统需要为注释获取视图时被调用。我们实现中的主要区别是,我们不是子类化 MKMapViewDelegate,而是仅仅将我们的控制器作为地图视图的代理。

GetViewForAnnotation 方法内部,我们需要确保我们为我们的注释创建并返回一个视图。由于地图显示用户的位置,地图上有两个注释。我们首先需要使用以下代码检查注释对象是否为 MKUserLocation

  if (annotation is MKUserLocation)

在这种情况下,我们只需返回 null。如果注释参数是 MKPointAnnotation 类型,我们首先尝试以类似于 UITableView 创建其包含的单元格的方式检索其视图,如下所示:

MKPinAnnotationView pinView = mapView.DequeueReusableAnnotation(reuseIdentifier) as MKPinAnnotationView;

如果 DequeueReusableAnnotation 方法的返回结果是 null,我们使用以下代码初始化我们的注释视图的新实例:

pinView = new MKPinAnnotationView(annotation, reuseIdentifier);
pinView.PinColor = MKPinAnnotationColor.Purple;
pinView.AnimatesDrop = true;
pinView.CanShowCallout = true;

在这里为注释创建的视图类型是 MKPinAnnotationView。这是由地图上的图钉表示的标准视图。我们设置的属性相当直接,定义了其外观和行为。PinColor 属性定义了图钉的颜色,AnimatesDrop 属性定义了图钉是否将以动画形式显示在地图上,而 CanShowCallout 属性定义了注释视图是否将在呼出气泡中显示其底层注释的信息。

在我们为注释创建了视图之后,我们只需使用以下代码行从方法中返回它:

return pinView;

还有更多...

我们还可以创建自定义注释和注释视图。对于注释,我们必须重写 MKAnnotation 类,而对于注释视图,我们可以重写 MKAnnotationView 类。

注释的性能

理论上,我们可以向地图视图中添加任意数量的注释。虽然 MKMapView 可以有效地管理大量注释,但强烈建议考虑性能下降。一种克服此问题的方法是只显示所需的注释,而不是所有注释。为此,我们可以调用 ShowAnnotations 方法,使用以下代码将特定的注释对象作为数组传递:

this.mapView.ShowAnnotations(myAnnotationsArray, true);

参见

  • 显示地图添加地图覆盖层 的食谱

  • 在 第五章 的 在表中显示数据 食谱中,显示数据

添加地图覆盖层

在本食谱中,我们将学习如何在地图上的一个点上添加红色圆覆盖层。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,命名为 MapOverlayApp。在控制器中添加 MKMapView 和一个按钮。

如何操作...

执行以下步骤以在地图上添加覆盖层:

  1. MapOverlayAppViewController.cs 文件中添加 MonoTouch.MapKitMonoTouch.CoreLocation 命名空间。

  2. 使用以下代码将 IMKMapViewDelegate 接口添加到类声明中:

    public partial class MapOverlayAppViewController : UIViewController, IMKMapViewDelegate
    
  3. ViewDidLoad 方法中添加以下代码:

    this.mapView.ShowsUserLocation = true;
    this.mapView.WeakDelegate = this;
    this.btnAddOverlay.TouchUpInside += (sender, e) => {
      CLLocationCoordinate2D mapCoordinate = 
        this.mapView.UserLocation.Coordinate;
      this.mapView.SetRegion(MKCoordinateRegion.FromDistance(mapCoordinate, 1000, 1000), true);
      MKCircle circle = 
        MKCircle.Circle(mapCoordinate, 250);
      this.mapView.AddOverlay(circle, MKOverlayLevel.AboveRoads);
    };
    
  4. 将以下方法添加到类中:

    [Export ("mapView:rendererForOverlay:")]
    public MKOverlayRenderer OverlayRenderer (MKMapView mapView, IMKOverlay overlay)
    {
      MKCircle circle = overlay as MKCircle;
      if (null != circle)
      {
        MKCircleRenderer renderer = new MKCircleRenderer(circle);
        renderer.FillColor = UIColor.FromRGBA(1.0f, 0.5f, 0.5f, 0.5f);
        renderer.StrokeColor = UIColor.Red;
        renderer.LineWidth = 2f;
        return renderer;
      } else
      {
        return null;
      }
    }
    
  5. 在模拟器或设备上编译并运行应用程序。当你点击按钮时,结果应该类似于以下截图:如何操作...

如何工作...

虽然 MK MKAnnotation 代表地图上的一个点,但 MKOverlay 对象可以代表地图上的一个区域。在这个例子中,我们使用继承自 MKOverlayMKCircle 类来在地图上显示一个圆。

我们使用以下代码使用 Circle 静态方法初始化 MKCircle 实例:

MKCircle circle = MKCircle.Circle(mapCoordinate, 250);

第一个参数表示圆心的坐标,第二个参数表示圆的半径,单位为米。初始化后,我们使用以下 AddOverlay 方法将覆盖层添加到地图视图中:

this.mapView.AddOverlay(circle, MKOverlayLevel.AboveRoads);

AddOverlay 方法的第二个参数确定覆盖层相对于地图信息应该如何渲染。有两种可能的值,以下附图进行了说明:

  • MKOverlayLevel.AboveRoads: 此覆盖层将在地图的道路之上显示,但在地图标签之下,如下面的截图所示:如何工作...

  • MKOverlayLevel.AboveLabels: 此覆盖层将在地图上显示在道路和标签之上,但在注释和建筑物的 3D 投影之下,如下面的截图所示:如何工作...

与注释不同,覆盖层需要 MKOverlayRenderer 来显示其信息。

注意

在 iOS 7 之前,叠加是以 MKOverlayView 类型的视图显示的。这个类现在已弃用。

为了为我们的叠加提供渲染器,我们使用 OverlayRenderer 方法如下:

public override MKOverlayRenderer OverlayRenderer (MKMapView mapView, IMKOverlay overlay)

在此方法中,我们首先使用以下代码检查 overlay 参数是否为我们想要的类型(在这种情况下,一个 MKCircle):

MKCircle circleOverlay = overlay as MKCircle;
if (null != circleOverlay)

然后,我们创建一个 MKCircleView 类的实例,并如下返回它:

MKCircleRenderer renderer = new MKCircleRenderer(circle);
renderer.FillColor = UIColor.FromRGBA(1.0f, 0.5f, 0.5f, 0.5f);
renderer.StrokeColor = UIColor.Red;
renderer.LineWidth = 2f;
return renderer;

我们设置适当的属性,这些属性将定义我们的叠加外观。在这种情况下,我们设置了 FillColorStrokeColorLineWidth 属性。

还有更多...

地图视图高效地处理叠加。地图视图为我们处理的一个重要事情是,当我们缩放地图时,叠加会自动缩放到匹配每个缩放级别。这样,我们就不需要在代码中手动缩放叠加。

创建自定义叠加

我们可以创建自己的自定义叠加。为此,我们需要为叠加覆盖 MKOverlay 类,并为渲染器覆盖 MKOverlayRenderer 类。

标准叠加对象

除了 MKCircle 之外,其他标准叠加对象是 MKPolygon,用于创建多边形形状,以及 MKPolyline,用于创建折线,如在轨迹记录应用程序中。

参见

  • 显示地图添加地图标注 的食谱

第十一章. 图形和动画

在这一章中,我们将涵盖以下主题:

  • 动画视图

  • 变换视图

  • 动画图像

  • 动画层

  • 绘制线条和曲线

  • 绘制形状

  • 绘制文本

  • 一个简单的绘图应用

  • 创建图像上下文

简介

在这一章中,我们将讨论自定义绘制和动画。iOS SDK 包含两个用于这些任务的非常有用的框架:Core Graphics 和 Core Animation。

这两个框架简化了在 UI 元素上动画化和绘制 2D 图形的过程。有效使用这两个框架将在平淡无奇和令人惊叹的应用之间产生差异。毕竟,这两个框架在使 iOS 平台在其类别中独一无二方面发挥着非常重要的作用。

我们将学习如何为控件提供简单甚至更复杂的动画,以提供独特的用户体验。我们还将看到如何在屏幕上自定义绘制线条、曲线、形状和文本。最后,通过所有提供的示例,我们将创建两个绘图应用。

动画视图

在这个菜谱中,我们将学习如何利用 UIKit 动画在屏幕上移动 UILabel

准备工作

在 Xamarin Studio 中创建一个新的 Single View Application 并将其命名为 ViewAnimationApp。在控制器的视图中添加一个标签和按钮。

如何做...

执行以下步骤:

  1. ViewDidLoad 方法中输入以下代码:

    this.lblOutput.BackgroundColor = UIColor.Green;
    this.btnAnimate.TouchUpInside += (sender, e) => {
      RectangleF labelFrame = this.lblOutput.Frame;
      labelFrame.Y = 380f;
      UIView.Animate(1d, 0d, UIViewAnimationOptions.CurveEaseInOut, 
        () => this.lblOutput.Frame = labelFrame, 
        () => {
          this.lblOutput.Text = "Animation ended!";
          this.lblOutput.BackgroundColor = UIColor.Red;
        });
    };
    
  2. 在模拟器上编译并运行应用。点击 Animate! 按钮,观察标签过渡到视图的下半部分。

它是如何工作的...

UIView 类包含许多提供动画功能的静态方法。在这个例子中,我们通过动画简单地改变标签的位置。

要动画化视图的变化,我们调用静态的 UIView.Animate 方法,如下所示:

UIView.Animate(1d, 0d, UIViewAnimationOptions.CurveEaseInOut, 
    () => this.lblOutput.Frame = labelFrame, 
    () => {
      this.lblOutput.Text = "Animation ended!";
      this.lblOutput.BackgroundColor = UIColor.Red;
    });

以下列表解释了 UIView.Animate 方法的参数,分别如下:

  • 持续时间: 这指定了动画的持续时间(以秒为单位)。

  • 延迟: 这表示动画开始前的秒数。将其设置为零以使动画立即开始。

  • 选项: 这包括动画的各种选项。在这个例子中,我们传递 UIViewAnimationOptions.CurveEaseInOut,这将对动画应用一个缓动曲线。

  • 动画: 这是一个包含将要动画化更改的 NSAction 代理。在这个例子中,我们将修改后的框架设置为标签,如下所示:

    () => this.lblOutput.Frame = labelFrame,
    
  • 完成: 这是一个 NSAction 代理,动画完成后将被调用。

我们可以组合多个 UIViewAnimationOptions 值。例如,如果我们想使动画无限重复,我们将传递 UIViewAnimationOptions.CurveEaseInOut | UIViewAnimationOptions.Repeat

更多内容...

Xamarin.iOS 还为 UIView 动画提供了一个异步方法。该方法如下:

await UIView.AnimateAsync(1, () => this.lblOutput.Frame = labelFrame);

然而,异步方法中没有 delayoptions 参数。

可动画属性

UIKit 动画支持一组特定的 UIView 属性。这些属性被称为 可动画属性。以下是可以动画化的 UIView 属性列表:

  • Frame

  • Bounds

  • Center

  • Transform

  • Alpha

  • BackgroundColor

  • ContentStretch

转换视图

在这个菜谱中,我们将通过应用转换来旋转 UILabel。此外,旋转将是动画化的。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,并将其命名为 TransformViewApp。在控制器中添加一个标签和一个按钮。

如何实现...

执行以下步骤:

  1. TransformViewAppViewController.cs 文件中添加 MonoTouch.CoreGraphics 命名空间如下:

    using MonoTouch.CoreGraphics;
    
  2. TransformViewAppViewController 类中输入以下 ViewDidLoad 方法:

    private double rotationAngle;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.btnRotate.TouchUpInside += async (sender, e) => {
        this.rotationAngle += 90;
        CGAffineTransform rotation = 
        CGAffineTransform.MakeRotation((float)this.DegreesToRadians(this.rotationAngle));
        await UIView.AnimateAsync(0.5d, () => this.lblOutput.Transform = rotation);
        this.lblOutput.Text = string.Format("Rotated to {0} degrees.", this.rotationAngle);
        if (this.rotationAngle >= 360) {
          this.rotationAngle = 0;
          this.lblOutput.Transform = CGAffineTransform.MakeIdentity();
        }
      };
    }
    
  3. 添加以下方法:

    public double DegreesToRadians(double degrees)
    {
      return (degrees * Math.PI / 180);
    }
    
  4. 在模拟器上编译并运行应用程序。点击按钮并观察标签旋转。以下截图显示了标签旋转了 270 度:如何实现...

它是如何工作的...

MonoTouch.CoreGraphics 命名空间是 CoreGraphics 框架的包装器。这个框架是 iOS 的基本图形框架。

要旋转一个视图,我们需要一个转换对象,该对象将通过视图的 Transform 属性应用如下:

CGAffineTransform rotation = CGAffineTransform.MakeRotation((float)this.DegreesToRadians(this.rotationAngle));

转换对象是 CGAffineTransform 类的实例,并通过 MakeRotation 静态方法初始化。此方法接受一个浮点值,表示要应用的旋转角度,以弧度为单位。可以使用 DegreesToRadians 方法将度转换为弧度。创建转换对象后,我们将其分配给标签的 Transform 属性,如下所示:

await UIView.AnimateAsync(0.5d, () => this.lblOutput.Transform = rotation);

注意,每次按钮被按下时,我们需要增加旋转角度,因为我们应用的转换不会被自动增加。如果我们应用另一个具有相同角度的旋转转换对象,将没有效果,因为它基本上是相同的转换。

当标签被旋转到完整一圈(360 度)时,我们重置 rotationAngle 值和转换对象如下:

this.rotationAngle = 0;
this.lblOutput.Transform = CGAffineTransform.MakeIdentity();

MakeIdentity 静态方法在将转换对象应用于视图之前,创建一个身份转换对象,这是所有视图的默认转换。

还有更多...

CGAffineTransform 类包含用于创建转换对象的多种静态方法。这些方法如下:

  • CGAffineTransformInvert: 这将反转当前转换并返回结果

  • MakeIdentity: 这将创建一个身份转换

  • MakeRotation: 这将创建一个旋转转换

  • MakeScale: 这将创建一个缩放转换

  • MakeTranslation: 这将创建一个平移转换

  • Multiply: 这将两个转换相乘并返回结果

转换和框架

在对一个视图应用转换后,其 Frame 属性不应被考虑,因为其值将是未定义的。如果需要在应用转换后更改视图的大小或位置,请分别使用 BoundsCenter 属性。

参见

  • 动画视图动画图层 食谱

动画图片

在本食谱中,我们将使用 UIImageView 内置的动画功能创建一个简单的图片幻灯片。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,并将其命名为 ImageAnimationApp。在控制器上添加一个 UIImageView 和两个按钮。此任务的示例项目包含三张图片。将两张或更多图片添加到项目中,并确保它们的 构建操作 设置为 内容

如何操作...

执行以下步骤:

  1. ViewDidLoad 方法中输入以下代码:

    this.imgView.ContentMode = UIViewContentMode.ScaleAspectFit;
    this.imgView.AnimationImages = new UIImage[] {
      UIImage.FromFile("Kastoria.jpg"),
      UIImage.FromFile("Parga02.jpg"),
      UIImage.FromFile("Toroni.jpg")
    };
    this.imgView.AnimationDuration = 3;
    this.imgView.AnimationRepeatCount = 10;
    this.btnStart.TouchUpInside += (sender, e) => {
      if (!this.imgView.IsAnimating) {
        this.imgView.StartAnimating();
      }
    };
    this.btnStop.TouchUpInside += (sender, e) => {
      if (this.imgView.IsAnimating) {
        this.imgView.StopAnimating();
      }
    };
    
  2. 编译并在模拟器上运行应用程序。点击 开始动画 按钮以开始动画。

它是如何工作的...

UIImageView 类可以接受一个 UIImage 对象数组,并自动按顺序显示它们。

要加载视图将动画化的图片,按照以下方式将其图片数组分配给其 AnimationImages 属性:

this.imageView.AnimationImages = new UIImage[] {
  UIImage.FromFile("Kastoria.jpg"),
  UIImage.FromFile("Parga02.jpg"),
  UIImage.FromFile("Toroni.jpg")
};

将要显示的图片的顺序由它们在数组中的顺序定义。在设置将要动画化的图片后,我们设置动画的持续时间(以秒为单位)和它将发生的次数,如下所示:

this.imageView.AnimationDuration = 3;
this.imageView.AnimationRepeatCount = 10;

要开始或停止动画,分别调用 StartAnimatingStopAnimating 方法。

还有更多...

UIImageView 类的 AnimationImagesImage 属性之间没有关系。在动画进行时,设置到 UIImageView 类的 Image 属性的图片将不会显示。

检查动画

要确定是否正在进行动画,请检查 UIImageViewIsAnimating 属性。

参见

  • 动画视图 食谱

  • 第二章中的 显示图片 食谱,用户界面 – 视图

动画图层

在本食谱中,我们将学习如何使用 Core Animation 框架通过动画其图层来在屏幕上复制一个 UILabel

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,并将其命名为 LayerAnimation。在控制器上添加两个标签和一个按钮。为第一个标签设置文本和背景颜色,并为第二个标签设置不同的背景颜色。

如何操作...

执行以下步骤:

  1. LayerAnimationViewController.cs 文件中添加 MonoTouch.CoreAnimation 命名空间,如下所示:

    using MonoTouch.CoreAnimation;
    
  2. 在类中添加一个 CALayer 类型的字段,如下所示:

    private CALayer copyLayer;
    
  3. ViewDidLoad 方法中添加以下代码:

    this.btnCopy.TouchUpInside += (s, e) => {
      this.lblTarget.Text = string.Empty;
      this.lblTarget.BackgroundColor = UIColor.Blue;
      this.copyLayer = new CALayer();
      this.copyLayer.Frame = this.lblSource.Frame;
      this.copyLayer.Contents = this.lblSource.Layer.Contents;
      this.View.Layer.AddSublayer(this.copyLayer);
      CABasicAnimation positionAnimation = CABasicAnimation.FromKeyPath("position");
      positionAnimation.To = NSValue.FromPointF(this.lblTarget.Center);
      positionAnimation.Duration = 1;
      positionAnimation.RemovedOnCompletion = true;
      positionAnimation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseInEaseOut);
      positionAnimation.AnimationStopped += delegate {
        this.lblTarget.BackgroundColor = this.lblSource.BackgroundColor;
        this.lblTarget.Text = this.lblSource.Text;
        this.lblTarget.TextColor = this.lblSource.TextColor;
       this.copyLayer.RemoveFromSuperLayer();
      } ;
      CABasicAnimation sizeAnimation = CABasicAnimation.FromKeyPath("bounds");
      sizeAnimation.To = NSValue.FromRectangleF(new RectangleF(0f, 0f, this.lblSource.Bounds.Width * 2f, this.lblSource.Bounds.Height * 2));
      sizeAnimation.Duration = positionAnimation.Duration / 2;
      sizeAnimation.RemovedOnCompletion = true;
      sizeAnimation.AutoReverses = true;
      this.copyLayer.AddAnimation(positionAnimation, "PositionAnimation");
    this.copyLayer.AddAnimation(sizeAnimation, "SizeAnimation");
    } ;
    
  4. 在模拟器上编译并运行应用程序。点击复制标签按钮,以动画方式将第一个标签的内容复制到第二个标签。以下截图是在复制过程中捕获的:如何操作...

它是如何工作的...

MonoTouch.CoreAnimation命名空间是 Core Animation 框架的包装器。

每个视图都有一个Layer属性,它返回视图的CALayer对象。在这个任务中,我们正在创建一个动画,该动画以图形方式显示从标签复制到另一个标签的内容。

而不是创建另一个标签并通过UIView动画移动它,我们将创建一个图层并移动该图层。我们通过设置其FrameContents属性来创建图层;后者是从源标签的图层设置的。然后我们使用AddSublayer方法将图层添加到主视图的图层中。在此之后,主视图包含一个显示相同内容且位于源标签之上的图层。我们将借助以下代码完成所有这些操作:

  this.copyLayer = new CALayer();
  this.copyLayer.Frame = this.lblSource.Frame;
  this.copyLayer.Contents = this.lblSource.Layer.Contents;
  this.View.Layer.AddSublayer(this.copyLayer);

为了从源标签到目标标签的过渡动画,我们将使用CABasicAnimation类。在步骤 3 中代码的高亮部分显示了如何初始化和设置类的实例。FromKeyPath静态方法创建一个新的实例,接受图层属性的名称作为参数;这个名称将被动画化。To属性表示属性将被动画化的值。Duration属性表示动画的持续时间(以秒为单位),而RemovedOnCompletion属性声明当动画完成后应从图层中移除动画对象。TimingFunction属性设置动画的行为。当动画完成后,将触发AnimationStopped事件。在分配给它的处理程序内部,我们将源标签的内容设置为目标标签,从而完成复制。AutoReverses属性表示当To属性的值达到时,动画应该反转。正是这个属性使得标签在达到最终位置时先变大然后变小。

当动画被添加到图层时,动画开始:

this.copyLayer.AddAnimation(positionAnimation, "PositionAnimation");
this.copyLayer.AddAnimation(sizeAnimation, "SizeAnimation");

还有更多...

可以在developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html#//apple_ref/doc/uid/TP40004514-CH12-SW2找到FromKeyPath方法接受的字符串列表。

除了To属性外,CABasicAnimation类还有两个用于定义动画的属性:FromBy。它们都是NSObject类型,但实际分配给它们的值应该是NSValue类型。NSValue类包含各种静态方法用于创建其实例。

层是非常强大和高效的物体,可以用于绘制和动画。强烈建议使用层在视图中执行动画,而不是实际视图本身。

参见

  • 动画视图菜谱

绘制线条和曲线

在这个菜谱中,我们将实现自定义绘图,在UIView类上绘制两条线。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,命名为DrawLineApp

如何操作...

执行以下步骤:

  1. 向项目中添加一个新的类,命名为DrawingView,并从UIView派生,如下所示:

    public class DrawingView : UIView
    
  2. DrawingView.cs文件中添加以下using指令:

    using MonoTouch.CoreGraphics;
    using MonoTouch.UIKit;
    using System.Drawing;
    
  3. 向类中添加以下构造函数:

    public DrawingView(RectangleF frame) : base(frame) {}
    
  4. 重写UIViewDraw方法,并使用以下代码实现它:

    public override void Draw (RectangleF rect)
    {
      base.Draw (rect);
      Console.WriteLine("DrawingView draw!");
      CGContext context = UIGraphics.GetCurrentContext();
      context.SetLineWidth(5f);
      context.SetStrokeColorWithColor(UIColor.Green.CGColor);
      context.AddLines(new PointF[] { 
        new PointF(0f, this.Bounds.Height),
        new PointF(this.Bounds.Width, 0f)
      } );
      context.StrokePath();
      context.SetStrokeColorWithColor(UIColor.Red.CGColor);
      context.MoveTo(0, this.Bounds.Height);
      context.AddCurveToPoint(0f, this.Bounds.Height, 50f, this.Bounds.Height / 2f, this.Bounds.Width, 0f);
      context.StrokePath();
    }
    
  5. DrawLineAppViewControllerViewDidLoad重写中,初始化并添加视图,如下所示:

    DrawingView drawingView = new DrawingView(new RectangleF(0f, 20f, this.View.Bounds.Width, this.View.Bounds.Height));
    drawingView.BackgroundColor = UIColor.Gray;
    this.View.AddSubview(drawingView);
    
  6. 在模拟器上编译并运行应用程序。结果应该类似于以下截图所示:如何操作...

它是如何工作的...

MonoTouch.CoreGraphics命名空间是围绕本地 Core Graphics 框架的包装器。Core Graphics 框架包含在视图中进行自定义绘制所需的必要对象。

要在视图中绘制,我们必须重写其Draw(RectangleF)方法,如下所示:

public override void Draw (RectangleF rect)

Draw方法内部,我们需要当前图形上下文的实例,如下所示:

CGContext context = UIGraphics.GetCurrentContext();

图形上下文由CGContext类表示。UIGraphics.GetCurrentContext静态方法返回当前上下文的实例。

CGContext类包含各种方法,允许我们在视图中绘制。我们需要设置线宽、颜色,然后添加绘制类型,如下所示:

context.SetLineWidth(5f);
context.SetStrokeColorWithColor(UIColor.Green.CGColor);
context.AddLines(new PointF[] { 
  new PointF(0f, this.Bounds.Height),
  new PointF(this.Bounds.Width, 0f)
} );

要添加线条,我们使用接受PointF结构数组作为参数的AddLines方法,包含每条线的起点和终点。仅仅将线条添加到上下文中是不够的。为了在视图中呈现绘制,我们调用StrokePath方法,如下所示:

context.StrokePath();

要在绘制中添加另一个项目,我们相应地重复步骤。MoveTo方法将当前点移动,以便附加项目将有一个曲线的起点。

还有更多...

当运行时需要绘制视图的内容时,会调用Draw方法。我们只能在Draw方法内部获取当前图形上下文的实例。我们不应直接调用它,因为如果这样做,UIGraphics.GetCurrentContext方法将返回null。如果我们需要强制运行时调用Draw方法,我们需要调用SetNeedsDisplay()。在调用时应小心,因为绘制操作在 CPU 使用方面代价高昂。

当不需要重新绘制整个视图区域时,我们可以调用SetNeedsDisplayInRect方法,传递一个在视图区域坐标系中的RectangleF对象,以更新我们想要更新的区域。

UIImageView类上的图形上下文

UIImageView对象的当前图形上下文保留用于绘制图像的内容。在继承自UIImageView的自定义视图中调用SetNeedsDisplay与直接调用Draw方法具有相同的效果。如果我们需要在自定义图像视图中绘制,我们必须要么在其上方添加另一个视图并在该视图上绘制,要么在自定义图层上绘制并将其添加到视图的主图层中。

参考也

  • 绘制文本配方

  • 在第二章的创建自定义视图配方中,用户界面 – 视图

绘制形状

按照上一个配方中的示例,我们将在屏幕上绘制一个圆和一个正方形。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为DrawShapeApp。向项目中添加一个自定义视图,就像我们在上一个任务中所做的那样,并将其命名为DrawingView

如何操作...

执行以下步骤:

  1. 重写DrawingView类的Draw方法,并使用以下代码实现:

    CGContext context = UIGraphics.GetCurrentContext();
    context.SetFillColorWithColor(UIColor.Blue.CGColor);
    context.SetShadow(new SizeF(10f, 10f), 5f);
    context.AddEllipseInRect(new RectangleF(100f, 100f, 100f, 100f));
    context.FillPath();
    context.SetFillColorWithColor(UIColor.Red.CGColor);
    context.AddRect(new RectangleF(150f, 150f, 100f, 100f));
    context.FillPath();
    
  2. DrawShapeAppViewController类的ViewDidLoad方法中,使用以下代码初始化并显示视图:

    DrawingView drawView = new DrawingView(new RectangleF(0f, 20f, this.View.Bounds.Width, this.View.Bounds.Height));
    drawView.BackgroundColor = UIColor.DarkGray;
    this.View.AddSubview(drawView);
    
  3. 在模拟器上编译并运行应用程序。屏幕上的结果应该类似于以下截图所示:如何操作...

它是如何工作的...

要在视图中绘制形状,我们需要调用适当的方法。我们首先将CGContext实例的填充颜色设置为以下所示:

context.SetFillColorWithColor(UIColor.Blue.CGColor);

要绘制一个圆,我们调用AddEllipseInRect方法,传递一个包含圆的边界矩形的RectangleF对象,如下所示:

context.AddEllipseInRect(new RectangleF(100f, 100f, 100f, 100f));

形状将是一个椭圆还是一个绝对圆,是通过边界矩形的尺寸定义的。然后我们按照以下方式调用FillPath方法:

context.FillPath();

阴影效果由SetShadow方法定义如下:

context.SetShadow(new SizeF(10f, 10f), 5f);

第一个参数,其类型为SizeF,定义了阴影的偏移量,而第二个参数定义了模糊量,以点为单位。

还有更多...

当调用SetShadow方法时,所有添加到上下文中的对象都会显示阴影。要移除阴影,调用SetShadowWithColor方法,传递一个完全透明的颜色或为颜色参数传递 null。

透明颜色

要用透明颜色填充形状,创建一个具有适当值的CGColor实例,如下所示:

context.SetFillColorWithColor(new CGColor(1f, 0f, 0f, 0.5f));

这将创建一个红色,其 alpha 值设置为 50%。

参见

  • 绘制线条和曲线食谱

绘制文本

在本食谱中,我们将学习如何在视图中绘制带有轮廓的样式文本。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为DrawTextApp。向项目中添加一个自定义视图,类似于我们在前面的食谱中创建的,并将其命名为DrawingView

如何实现...

执行以下步骤:

  1. DrawingView类中实现以下Draw方法重写:

    CGContext context = UIGraphics.GetCurrentContext();
    PointF location = new PointF(10f, 100f);
    UIFont font = UIFont.FromName("Verdana-Bold", 28f);
    NSString drawText = new NSString("This text is drawn!");
    context.SetTextDrawingMode(CGTextDrawingMode.Stroke);
    context.SetStrokeColorWithColor(UIColor.Black.CGColor);
    context.SetLineWidth(4f);
    drawText.DrawString(location, font);
    context.SetTextDrawingMode(CGTextDrawingMode.Fill);
    context.SetFillColorWithColor(UIColor.Yellow.CGColor);
    drawText.DrawString(location, font);
    
  2. 在控制器的ViewDidLoad方法中,初始化并显示DrawingView方法如下:

    DrawingView drawView = new DrawingView(new RectangleF(0f, 20f, this.View.Bounds.Width, this.View.Bounds.Height));
    drawView.BackgroundColor = UIColor.DarkGray;
    this.View.AddSubview(drawView);
    
  3. 在模拟器上编译并运行应用程序。文本将显示在屏幕上。结果应类似于以下截图:如何实现...

工作原理...

NSString类包含非常实用的DrawString方法,它将包含的文本绘制到当前上下文中。为了提供轮廓效果,我们调用SetTextDrawingMode方法如下:

context.SetTextDrawingMode(CGTextDrawingMode.Stroke);

我们传递CGTextDrawingMode.Stroke值。然后我们将轮廓的颜色和宽度设置为图形上下文,并在屏幕上绘制文本,如下所示:

context.SetStrokeColorWithColor(UIColor.Black.CGColor);
context.SetLineWidth(4f);
drawText.DrawString(location, font);

SetStrokeColorWithColor方法设置描边的颜色,而SetLineWidth方法设置描边的宽度。调用NSStringDrawString方法可以在指定位置和指定字体下在图形上下文中绘制文本。

类似地,为了填充文本,我们将文本绘制模式设置为Fill,如下所示:

context.SetTextDrawingMode(CGTextDrawingMode.Fill);

对于填充,我们不需要关心线的宽度,所以我们只需要再次调用DrawString方法,如下所示:

drawText.DrawString(location, font);

DrawString方法有多个重载。我们在这里使用的是接受一个PointF结构,它表示字符串在视图坐标系中的位置,以及一个UIFont实例,该实例表示文本将在屏幕上通过该字体渲染。

更多内容...

使用DrawString方法在屏幕上绘制文本非常简单,也是最快的方法。对于更复杂的功能,例如自定义文本布局、外观等,我们需要使用CoreText 框架。这可以通过 Xamarin.iOS 中的MonoTouch.CoreText命名空间访问。

绘制文本的大小

NSString类的DrawString方法返回文本的边界矩形的大小。然而,我们可以通过StringSize方法在绘制文本之前获取文本的大小,如下所示:

Console.WriteLine("Text size: {0}", drawText.StringSize(UIFont.FromName("Verdana-Bold", 28f)));

参见

  • 绘制线条和曲线绘制形状食谱

一个简单的绘图应用程序

在本食谱中,我们将使用我们学到的技术来创建一个绘图应用程序。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序 并将其命名为 FingerDrawingApp。再一次,我们需要一个自定义视图。添加一个从 UIView 继承的类并命名为 CanvasView

如何做...

执行以下步骤:

  1. 使用以下代码实现 CanvasView 类:

    public class CanvasView : UIView
    {
      public CanvasView (RectangleF frame) : base(frame)
      {
        this.drawPath = new CGPath();
      }
      private PointF touchLocation;
      private PointF previousTouchLocation;
      private CGPath drawPath;
      private bool fingerDraw;
      public override void TouchesBegan (NSSet touches, UIEvent evt)
      {
        base.TouchesBegan (touches, evt);
        UITouch touch = touches.AnyObject as UITouch;
        this.fingerDraw = true;
        this.touchLocation = touch.LocationInView(this);
        this.previousTouchLocation = touch.PreviousLocationInView(this);
        this.SetNeedsDisplay();
      }
      public override void TouchesMoved (NSSet touches, UIEvent evt)
      {
        base.TouchesMoved (touches, evt);
        UITouch touch = touches.AnyObject as UITouch;
        this.touchLocation = touch.LocationInView(this);
        this.previousTouchLocation = touch.PreviousLocationInView(this);
        this.SetNeedsDisplay();
      }
      public override void Draw (RectangleF rect)
      {
        base.Draw (rect);
        if (this.fingerDraw)
        {
          using (CGContext context = UIGraphics.GetCurrentContext())
          {
            context.SetStrokeColorWithColor(UIColor.Blue.CGColor);
            context.SetLineWidth(5f);
            context.SetLineJoin(CGLineJoin.Round);
            context.SetLineCap(CGLineCap.Round);
            this.drawPath.MoveToPoint(this.previousTouchLocation);
            this.drawPath.AddLineToPoint(this.touchLocation);
            context.AddPath(this.drawPath);
            context.DrawPath(CGPathDrawingMode.Stroke);
          }
        }
      }
    }
    
  2. FingerDrawingAppViewController 类的 ViewDidLoad 方法中,初始化并显示画布如下:

    CanvasView canvasView = new CanvasView(new RectangleF(0f, 20f, this.View.Bounds.Width, this.View.Bounds.Height));
    canvasView.BackgroundColor = UIColor.Gray;
    this.View.AddSubview(canvasView);
    
  3. 在模拟器或设备上编译并运行应用程序。触摸并拖动手指(或使用光标点击并拖动)开始绘图。以下截图显示了在此应用程序中绘制的草图:如何做...

它是如何工作的...

在这个任务中,我们将触摸事件和自定义绘图结合起来创建一个简单的绘图应用程序。当用户触摸并移动屏幕上的手指时,我们保留触摸位置点的信息,并在 Draw 方法中使用这些信息来绘制线条。

在将触摸位置设置到类字段之后,我们调用 SetNeedsDisplay 以强制调用 Draw 方法。fingerDraw 变量用于确定 Draw 方法是由屏幕上的触摸触发的,而不是在视图首次加载时由运行时触发的。

每次我们调用一个方法将某些内容绘制到图形上下文中时,该上下文中的先前绘图都会被清除。为了避免这种行为,我们使用一个 CGPath 对象。我们可以在 CGPath 中添加各种绘图对象,并通过将其添加到图形上下文中来在屏幕上显示这些对象。因此,每当用户在屏幕上移动手指时,由触摸位置点定义的新线条就会被添加到路径中,并且路径会在当前上下文中绘制。

注意,我们需要保留当前触摸位置和上一个位置的信息。这是因为 AddLineToPoint 方法接受一个点,该点定义了线的终点,假设路径中已经有一个点。每条线的起点是通过调用 MoveToPoint 并传递上一个触摸位置点来定义的。

通过在屏幕上滑动手指绘制的路径基本上由一系列连续的直线组成。然而,结果是平滑的路径,它遵循手指的移动,因为每当屏幕上有单个手指移动时,TouchesMoved 方法就会被触发。

在将线条添加到路径之后,我们将它添加到上下文中并在图形上下文中绘制它,因此如下所示显示在屏幕上:

context.AddPath(this.drawPath);
context.DrawPath(CGPathDrawingMode.Stroke);

更多内容...

在这个任务中引入了两个新的 CGContext 方法:SetLineJoinSetLineCapSetLineJoin 方法设置每条线如何与前一条线连接,而 SetLineCap 方法设置线条端点的外观。

他们接受的值在以下两个表中解释:

SetLineJoin 描述
CGLineJoin.Miter 使用角度角落连接两条线
CGLineJoin.Round 使用圆角连接两条线
CGLineJoin.Bevel 使用方形端点连接两条线
SetLineCap 描述
--- ---
CGLineCap.Butt 线条将以端点上的方形边缘结束
CGLineCap.Round 线条将以一个扩展到端点的圆形边缘结束
CGLineCap.Square 线条将以一个扩展到端点的方形边缘结束

清除绘图

要清除绘图,我们只需将fingerDraw变量设置为false并调用SetNeedsDisplay。这样,Draw方法将调用而不需要我们的自定义绘图代码,清除当前上下文。

相关内容

  • 绘制线条和曲线绘制形状绘制文本菜谱

创建图像上下文

在这个菜谱中,我们将通过提供用户创建的绘图保存功能来扩展我们之前创建的手指绘图应用程序。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ImageContextApp。将我们在早期任务中创建的CanvasView类添加到项目中。别忘了将CanvasView.cs文件中的命名空间更改为与新项目对应的命名空间。

如何做到这一点...

执行以下步骤:

  1. CanvasView类中添加以下方法:

    public UIImage GetDrawingImage()
    {
      UIImage toReturn = null;
      UIGraphics.BeginImageContext(this.Bounds.Size);
      using (CGContext context = UIGraphics.GetCurrentContext())
      {
        context.SetStrokeColorWithColor(UIColor.Blue.CGColor);
        context.SetLineWidth(10f);
        context.SetLineJoin(CGLineJoin.Round);
        context.SetLineCap(CGLineCap.Round);
        context.AddPath(this.drawPath);
        context.DrawPath(CGPathDrawingMode.Stroke);
        toReturn = UIGraphics.GetImageFromCurrentImageContext();
      }
      UIGraphics.EndImageContext();
      return toReturn;
    }
    public void ClearDrawing()
    {
      this.fingerDraw = false;
      this.drawPath.Dispose();
      this.drawPath = new CGPath();
      this.SetNeedsDisplay();
    }
    
  2. 在控制器的视图中添加两个按钮。一个按钮将用于保存绘图,另一个按钮用于清除画布。

  3. ImageContextAppViewController类的ViewDidLoad方法中添加以下代码:

    CanvasView canvasView = new CanvasView(new RectangleF(0f, 0f, this.btnSave.Frame.Top - 10f, this.View.Bounds.Width));
    canvasView.BackgroundColor = UIColor.Gray;
    this.View.AddSubview(canvasView);
    this.btnSave.TouchUpInside += (sender, e) => {
      UIImage drawingImage = canvasView.GetDrawingImage();
      drawingImage.SaveToPhotosAlbum((img, err) => {
        if (null != err)
        {
          Console.WriteLine("Error saving image! {0}", err.LocalizedDescription);
        }
      });
    };
    this.btnClear.TouchUpInside += (sender, e) => canvasView.ClearDrawing ();
    
  4. 在模拟器上编译并运行应用程序。在画布上绘制一些东西,然后点击保存绘图按钮来保存你的绘图。点击清除画布按钮来清除画布。然后你可以在模拟器的照片库中检查你的绘图。

它是如何工作的...

使用UIGraphics类,我们可以创建一个图像上下文,通过它可以检索我们的绘图,并将其存储在UIImage对象中。

要创建图像上下文,我们在GetDrawingImage方法内部调用BeginImageContext静态方法,并传递我们想要图像上下文具有的大小,如下所示:

UIGraphics.BeginImageContext(this.Bounds.Size);

当前上下文现在是使用BeginImageContext调用创建的图像上下文。然后我们重复Draw方法中的代码;只是这次,我们不需要向路径中添加新线条。我们只需将我们已有的路径添加到上下文中并绘制它。

添加路径后,我们可以通过调用以下GetImageFromCurrentContext方法来获取上下文图像:

toReturn = UIGraphics.GetImageFromCurrentImageContext();

最后,我们必须结束图像上下文块并返回UIImage对象,如下所示:

UIGraphics.EndImageContext();
return toReturn;

要清除屏幕上的绘图,我们只需将fingerDraw变量设置为false,并在ClearDrawing方法中处理并准备我们的CGPath对象以供重用,如下所示:

this.fingerDraw = false;
this.drawPath.Dispose();
this.drawPath = new CGPath();

要立即反映屏幕上的清除,我们调用以下SetNeedsDisplay方法:

this.SetNeedsDisplay();

还有更多...

我们无法在Draw方法内部创建图像上下文。这是因为当我们调用BeginImageContext方法时,实际上会创建一个上下文,但视图的默认上下文仍然保持为当前上下文。因此,GetImageFromCurrentImageContext方法会返回null

UIImageView上绘图

这里讨论的技术可以用来在自定义的UIImageView对象上绘图。为了在手指在屏幕上滑动时显示绘图,我们只需将其Image属性设置为从图像上下文获取的图像。

保存绘图的背景信息

你会注意到,尽管我们将CanvasView的背景设置为灰色,但保存的绘图却是白色的背景。这是因为视图的背景颜色不包括在绘图内。要包括它,我们只需在图形上下文中绘制一个矩形。这个矩形应该与背景颜色相同。

参见

  • 绘制线条和曲线绘制形状绘制文本一个简单的绘图应用的食谱

第十二章。多任务处理

在本章中,我们将涵盖以下主题:

  • 检测应用状态

  • 接收应用状态的通知

  • 在后台运行代码

  • 在后台播放音频

  • 在后台更新数据

简介

当 iOS 平台在 2007 年推出时,它为用户带来了许多令人兴奋的新功能,并极大地改变了移动设备的概念。

尽管它当时取得了巨大的成功,但它缺少一些被认为是基本的功能。其中之一就是多任务处理,即同时运行多个进程的支持。实际上,该平台在内部支持系统进程的多任务处理,但这对开发者来说并不可用。从 iOS 4 开始,苹果提供了对多任务处理的支持,尽管它与大多数开发者习惯的还相当不同。

在本章中,我们将讨论如何利用平台的多任务功能。我们将了解在什么情况下可以使用这些功能,以及我们如何通过多任务为我们的应用用户提供功能。具体来说,我们将了解应用的状态及其运行时生命周期。通过一系列详细的示例项目,我们能够在应用处于后台时执行代码,支持音频播放,并接收数据更新。

检测应用状态

在这个菜谱中,我们将讨论如何检测应用的状态,并在应用从活动状态过渡到非活动状态以及相反时相应地做出反应。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用,并将其命名为 AppStateApp

如何做到这一点...

执行以下步骤:

  1. 将以下方法覆盖添加到 AppDelegate 类中:

    public override void OnActivated (UIApplication application)
    {
      Console.WriteLine("Activated, application state: {0}", application.ApplicationState);	
    }
    public override void OnResignActivation (UIApplication application)
    {
      Console.WriteLine("Resign activation, application state: {0}", application.ApplicationState);
    }
    public override void DidEnterBackground (UIApplication application)
    {
      Console.WriteLine("Entered background, application state: {0}", application.ApplicationState);
    }
    public override void WillEnterForeground (UIApplication application)
    {
      Console.WriteLine("Will enter foreground, application state: {0}", application.ApplicationState);
    }
    
  2. 要编译和运行应用,可以在模拟器或设备上操作。按下主页按钮(或在模拟器上按 Shift + Command + H 键)以挂起应用,并观察 Xamarin Studio 中的应用输出面板。

它是如何工作的...

UIApplicationDelegate 类包含由运行时发出的特定通知触发的方法。这些方法如下:

  • OnActivated:当应用变为活动状态时,此方法被调用,例如,当解锁屏幕时。

  • OnResignActivation:当应用即将变为非活动状态时,此方法被调用,例如,当屏幕锁定或发生来电时。

  • DidEnterBackground:当应用进入后台时,此方法被调用,例如,当按下主页按钮时。此时,应用处于挂起状态。

  • WillEnterForeground:当应用即将返回前台时,此方法被调用。

注意,当应用移动到后台时,OnResignActivationDidEnterBackground 方法都会被调用。同样,当应用移动到前台时,WillEnterForegroundOnActivated 方法都会被调用。

所有这些方法都包含一个参数,该参数包含应用程序的UIApplication实例。UIApplication类包含ApplicationState属性,该属性返回应用程序的状态,作为UIApplicationState属性的值。这些值如下:

  • 活跃状态:这表示应用程序处于活跃状态

  • 非活跃状态:这表示应用程序处于非活跃状态,例如,当显示通知警报时

  • 后台状态:这表示应用程序处于后台状态

更多内容...

有时候 iOS 会终止你的应用程序,例如,当发出内存警告而你的应用程序没有释放资源时。在这些情况下,将调用WillTerminate方法。

正确使用

前面的方法非常有用,因为它们允许我们在应用程序状态改变时保存向用户展示的当前数据。当应用程序过渡到非活跃或后台状态时,每个方法都有一定的时间来执行,因此我们应该确保它不会执行长时间运行的操作,否则 iOS 会终止应用程序。

接收应用程序状态的通知

在这个菜谱中,我们将讨论当应用程序的状态在UIApplicationDelegate实现的作用域之外发生变化时,如何接收通知。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为NotifyStatesApp

如何实现...

执行以下步骤:

  1. NotifyStatesAppViewController类中输入以下字段:

    private NSObject appDidEnterBackgroundObserver, appWillEnterForegroundObserver;
    
  2. 创建以下方法:

    private void AddNotificationObservers()
    {
      this.appDidEnterBackgroundObserver = UIApplication.Notifications.ObserveDidEnterBackground((s, e) => Console.WriteLine("App did enter background! App state: {0}", UIApplication.SharedApplication.ApplicationState));
      this.appWillEnterForegroundObserver = UIApplication.Notifications.ObserveWillEnterForeground((s, e) => Console.WriteLine("App will enter foreground! App state: {0}", UIApplication.SharedApplication.ApplicationState));
    }
    private void RemoveNotificationObservers()
    {
      NSNotificationCenter.DefaultCenter.RemoveObservers(new [] {
        this.appDidEnterBackgroundObserver,
        this.appWillEnterForegroundObserver
      });
    }
    
  3. ViewWillAppear的覆盖方法中,按照以下方式调用AddNotificationObservers方法:

    public override void ViewWillAppear(bool animated) {
      base.ViewWillAppear(animated);
      this.AddNotificationObservers();
    }
    
  4. ViewWillDisappear的覆盖方法中,按照以下方式调用RemoveNotificationObservers方法:

    public override void ViewWillDisappear(bool animated) {
      base.ViewWillDisappear(animated);
      this.RemoveNotificationObservers();
    }
    
  5. 在模拟器上编译并运行应用程序。按住主页按钮(或按Shift + Command + H),并在应用程序输出面板中查看输出。

它是如何工作的...

除了调用应用程序状态的UIApplicationDelegate对象的方法之外,iOS 还会发出我们可以接收的通知。这非常有用,因为在大多数情况下,我们需要在应用程序状态在AppDelegate类的作用域之外改变时收到通知。

为了完成这个任务,我们通过UIApplication.Notifications类使用NSNotificationCenter方法,如下所示:

this.appDidEnterBackgroundObserver = UIApplication.Notifications.ObserveDidEnterBackground((s, e) => Console.WriteLine("App did enter background! App state: {0}", UIApplication.SharedApplication.ApplicationState));

此示例仅添加了在后台和前台之间转换的通知观察者。我们可以通过其他可用的Observe*方法添加更多通知观察者。

结果与之前菜谱中使用的示例类似,但在这个情况下,我们在视图控制器的作用域内收到通知。

更多内容...

要在应用程序激活或放弃激活时添加通知观察者,我们分别使用UIApplication.Notifications.ObserveDidBecomeActiveUIApplication.Notifications.ObserveWillResignActive方法。

移除通知观察者

在这个例子中,我们在 ViewWillAppear 方法内部调用 RemoveNotificationObservers。然而,当应用过渡到后台时,该方法不会被调用,而只有在显示另一个视图控制器时才会被调用。

相关内容

  • 检测应用程序状态 的配方

在后台运行代码

在这个配方中,我们将学习如何在后台执行代码,充分利用 iOS 的多任务功能。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用 并将其命名为 BackgroundCodeApp

如何做到这一点...

执行以下步骤:

  1. AppDelegate 类中输入以下代码:

    private int taskID;
    public override void DidEnterBackground (UIApplication application)
    {
      if (this.taskID == 0)
      {
        this.taskID = application.BeginBackgroundTask(() => {
          application.EndBackgroundTask(this.taskID);
          this.taskID = 0;
        });
        ThreadPool.QueueUserWorkItem(delegate {
          for (int i = 0; i < 60; i++)
          {
            Console.WriteLine("Task {0} - Current time {1}", this.taskID, DateTime.Now);
            Thread.Sleep(1000);
          }
          application.EndBackgroundTask(this.taskID);
          this.taskID = 0;
        });
      }
    }
    public override void WillEnterForeground (UIApplication application)
    {
      if (this.taskID != 0)
      {
        Console.WriteLine("Background task is running!");
      } else
      {
        Console.WriteLine("Background task completed!");
      }
    }
    
  2. 在模拟器上编译并运行应用。按住主页按钮(Command + Shift + H)使应用进入后台,并观察 应用程序输出 面板。在后台任务完成之前(1 分钟),可以通过在多任务栏或主屏幕上的图标上轻触来将应用带到前台。

它是如何工作的...

在前面的任务中,我们学习了如何得知应用从前台过渡到后台以及相反的情况。

iOS 的多任务处理并不完全像我们在其他平台上所习惯的那样。iOS 平台帮助我们确保前台应用将拥有所有可用的资源(以及用户的)。为了实现这一点,当应用进入后台时,它会被操作系统挂起。当它被挂起时,它不会执行任何代码。

如果我们想要防止用户按下主页按钮时应用被挂起,我们可以请求后台时间。我们请求的时间限制为 600 秒(10 分钟),这对于我们可能在后台执行的大多数任务来说已经足够了(例如,保存 UI 状态,完成文件下载/上传,关闭任何打开的连接,等等)。

要请求后台时间,我们按照以下方式调用我们的 UIApplication 实例的 BeginBackgroundTask 方法:

this.taskID = application.BeginBackgroundTask(() => {
  application.EndBackgroundTask(taskID);
  this.taskID = 0;
} );

该方法接受一个 NSAction 类型的参数并返回一个整数,该整数对应于任务 ID。NSAction 参数表示将在后台时间耗尽前执行的代码块。在该代码块内部,我们必须调用 EndBackgroundTask 方法,传递已启动任务的 ID,这将通知运行时我们不再需要后台时间。每次调用 BeginBackgroundTask 都应紧随 EndBackgroundTask 的调用。如果我们不调用此方法并且后台时间耗尽,应用将被终止。

在调用BeginBackgroundTask方法后,我们可以执行我们想要的代码。为了允许DidEnterBackground方法完成并避免阻塞主线程,我们只需将我们的代码封装到异步调用或单独的线程中。在这个例子中,我们使用ThreadPool中的一个线程。由于这个特定任务将在我们有时间之前完成,我们调用EndBackgroundTask方法来让系统知道工作已完成。我们传递给BeginBackgroundTask方法的代码块将不会执行,因为我们已经结束了任务。

然而,也存在用户在后台任务仍在运行时将应用带到前台的情况。为了覆盖这种场景,我们需要重写WillEnterForeground方法,并适当地处理它。我们可以停止后台任务(通过调用EndBackgroundTask),或者向用户提供某种反馈,告知任务仍在运行。这种场景也使得异步调用我们的代码成为最佳实践。如果我们的后台任务代码是同步的,那么当用户将应用带到前台且任务仍在运行时,应用将会冻结,直到任务完成。

还有更多...

要知道执行后台任务剩余多少时间,我们可以检查BackgroundTimeRemaining属性的值如下:

Console.WriteLine("Remaining time: {0}", application.BackgroundTimeRemaining);

后台代码的重要考虑因素

当您处理后台代码时,以下是需要考虑的重要点:

  • 当应用处于后台时,不要更新 UI。这样做可能会导致您的应用终止或崩溃。在应用处于后台时发生的任何 UI 元素更新都会排队,以便在它返回前台时执行。这肯定会使得应用无响应。

  • 不要通知用户将您的应用带到前台以给任务更多时间。这样做肯定会使得您的应用在应用商店的审批过程中被拒绝。如果后台任务正在进行中,并且用户将应用带到前台,再次将应用移回后台基本上会继续剩余的后台时间。

  • 在后台执行轻量级操作,以避免运行时杀死您的应用。

  • 避免使用外部资源(例如,来自资源库的资源)。

参见

  • 检测应用程序状态配方

在后台播放音频

在这个配方中,我们将学习如何防止应用被挂起,以便允许音频播放。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用,并将其命名为BackgroundAudioApp。在控制器的视图中添加一个按钮。您还需要一个音频文件。在这个例子中,使用了一个时长为 21 秒的 M4A 文件。

如何操作...

执行以下步骤:

  1. 双击 Info.plist 文件以打开它。在底部选择 Source 选项卡,并添加 UIBackgroundModes 键(必需的后台模式)并设置字符串值为 audio。以下截图显示了在设置后如何在编辑器中显示键和值:如何操作...

  2. BackgroundAudioAppViewController.cs 文件中添加 MonoTouch.AVFoundation 命名空间。

  3. 在类中输入以下 ViewDidLoad 方法:

    private AVAudioPlayer audioPlayer;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      NSError error = null;
      AVAudioSession.SharedInstance().SetCategory(AVAudioSession.CategoryPlayback, out error);
      if (error != null)
      {
        Console.WriteLine("Error setting audio session category: {0}", error.LocalizedDescription);
      }
      this.audioPlayer = AVAudioPlayer.FromUrl(NSUrl.FromFilename("sound.m4a"));
      this.btnPlay.TouchUpInside += (sender, e) => this.audioPlayer.Play();
    }
    
  4. 将声音文件添加到项目中,并将其 Build Action 设置为 Content

  5. 在设备上编译并运行应用。轻触 播放声音 按钮并按住主页按钮,使应用进入后台。注意,声音继续播放。

它是如何工作的...

为了确保我们的应用在后台时能够播放音频,我们必须在 Info.plist 文件中的 UIBackgroundModes 键中设置音频项。

在这个例子中,我们使用了 AVAudioPlayer 类来播放声音文件。仅仅创建类的实例并调用其 Play 方法是不够的。我们必须为音频会话类别设置一个特定的类型。我们将通过以下代码来完成这项工作:

NSError error = null;
AVAudioSession.SharedInstance ().SetCategory (AVAudioSession.CategoryPlayback, out error);

静态方法 AVAudioSession.SharedInstance 返回当前的音频会话对象。音频会话类别设置为 AVAudioSession.CategoryPlayback,允许 AVAudioPlayer 类在应用处于后台时播放声音。这个要求仅适用于 MonoTouch.AVFoundation 命名空间中的对象。

还有更多...

以下是可以用的音频会话类别:

  • CategoryAmbient: 在这个类别中,当设备屏幕锁定或设备的静音开关打开时,声音会被静音。外部资源的声音(如 iPod 应用)会与这个类别混合。

  • CategorySoloAmbient: 这是默认类别。使用此类别,外部资源的声音会被静音。当屏幕锁定或设备的静音开关打开时,声音会被静音。

  • CategoryPlayback: 在这个类别中,当屏幕锁定或静音开关打开时,声音不会被静音。外部资源的声音会被静音,但如果将 MonoTouch.AudioToolbox.AudioSession.OverrideCategoryMixWithOthers 属性设置为 true,则可以混合。

  • CategoryRecord: 这个类别用于录音。所有音频播放都被静音。即使在屏幕锁定的情况下,录音也会继续。

  • CategoryPlayAndRecord: 这个类别用于需要录音和播放音频的应用。外部资源的声音会被静音,但如果将 MonoTouch.AudioToolbox.AudioSession.OverrideCategoryMixWithOthers 属性设置为 true,则可以混合。当屏幕锁定或静音开关打开时,声音会继续播放。

  • CategoryAudioProcessing: 这个类别专门用于处理音频。在此类别中,声音播放和录音被禁用。

音频的后台状态

即使应用程序通过Info.plist文件配置为支持后台音频播放,播放完成后应用程序也会被挂起。

参见

  • 在第十章中,位置服务和地图后台位置服务菜谱

在后台更新数据

在这个菜谱中,我们将学习如何利用 iOS 7 的后台获取功能。这个功能会在系统管理的间隔自动唤醒应用程序,给它一定的时间来检索数据和更新 UI。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为BackgroundFetchApp。向控制器中添加一个标签。

如何做...

执行以下步骤:

  1. 我们需要从BackgroundFetchAppViewController类的范围外访问标签,因此创建一个公共属性,如下所示:

    public UILabel LabelStatus {
      get { return this.lblStatus; }
    }
    
  2. 打开Info.plist文件,在选项卡下,添加UIBackgroundModes键(必需的后台模式)并设置字符串值为fetch。以下截图显示了设置后的编辑器:如何做...

  3. AppDelegate类的FinishedLaunching方法中,输入以下行:

    UIApplication.SharedApplication.SetMinimumBackgroundFetchInterval(UIApplication.BackgroundFetchIntervalMinimum);
    
  4. 再次在AppDelegate类中输入以下代码:

    private int updateCount;
    public override void PerformFetch (UIApplication application, Action<UIBackgroundFetchResult> completionHandler)
    {
      try {
        HttpWebRequest request = WebRequest.Create("http://software.tavlikos.com") as HttpWebRequest;
        using (StreamReader sr = new StreamReader(request.GetResponse().GetResponseStream())) {
          Console.WriteLine("Received response: {0}", sr.ReadToEnd());
        }
        this.viewController.LabelStatus.Text = string.Format("Update count: {0}/n{1}", ++updateCount, DateTime.Now);
        completionHandler(UIBackgroundFetchResult.NewData);
    
      } catch {
        this.viewController.LabelStatus.Text = string.Format("Update {0} failed at {1}!", ++updateCount, DateTime.Now);
        completionHandler(UIBackgroundFetchResult.Failed);
      }
    }
    
  5. 在模拟器或设备上编译并运行应用程序。按下主页按钮(或Command + Shift + H)将应用程序移至后台并等待输出。但这可能需要一段时间。

它是如何工作的...

带有fetch值的UIBackgroundModes键启用了我们应用程序的后台获取功能。如果不设置它,应用程序将不会在后台唤醒。

Info.plist中设置键后,我们覆盖AppDelegate类中的PerformFetch方法,如下所示:

public override void PerformFetch (UIApplication application, Action<UIBackgroundFetchResult> completionHandler)

当系统唤醒应用程序时,会调用此方法。在这个方法内部,我们可以连接到服务器并检索所需的数据。在这里需要注意的一个重要事项是,我们不需要使用 iOS 特定的 API 来连接到服务器。在这个例子中,使用了简单的HttpWebRequest方法来获取这个博客的内容:software.tavlikos.com

在我们收到所需的数据后,我们必须调用传递给方法的事件处理程序,如下所示:

completionHandler(UIBackgroundFetchResult.NewData);

我们还需要传递获取的结果。在这个例子中,如果更新成功,我们传递UIBackgroundFetchResult.NewData;如果发生异常,我们传递UIBackgroundFetchResult.Failed

如果我们没有在指定的时间内调用回调,应用程序将被终止。此外,它可能在未来获取数据的次数更少。

最后,为了确保一切正常工作,我们必须设置应用程序将被唤醒的间隔,如下所示:

UIApplication.SharedApplication.SetMinimumBackgroundFetchInterval(UIApplication.BackgroundFetchIntervalMinimum);

默认间隔是UIApplication.BackgroundFetchIntervalNever,所以如果我们不设置间隔,后台获取永远不会被触发。

更多内容

除了我们在本项目添加的功能外,后台获取完全由系统管理。我们设置的间隔只是一个指示,我们唯一能保证的是它不会早于间隔被触发。一般来说,系统会监控所有应用程序的使用情况,并确保根据应用程序的使用频率来触发后台获取。

注意

除了预定义的值之外,我们可以传递我们想要的任何秒数值。

UI 更新

我们可以在PerformFetch方法中更新 UI。iOS 允许这样做,以便在应用程序处于后台时更新应用程序的截图。然而,请注意,我们需要将 UI 更新保持在绝对最小化。

参见

  • 背景运行代码的配方

第十三章。本地化

在本章中,我们将涵盖以下主题:

  • 为不同语言创建应用程序

  • 可本地化资源

  • 区域格式化

简介

随着 iOS 平台和 App Store 这种全球软件市场的推出,苹果公司使得开发者更容易在全球范围内分发应用程序。

然而,全球用户甚至不会费心下载和使用发布在他们不理解的语言中的应用程序。为了扩大应用程序的用户基础,开发者必须对其进行本地化。本地化是将文本翻译成多种语言、提供针对多个区域的具体资源,从而创建面向不同文化受众的应用程序的过程。

在本章中,我们将讨论提供符合每个用户区域首选项的翻译文本的最佳实践。我们还将看到如何根据这些首选项提供资源(图像和视频)。最后,我们将使用常见的.NET 实践来格式化日期、货币和数字。

为不同语言创建应用程序

在本食谱中,我们将创建一个支持两种不同语言的程序。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为MultipleLanguageApp

如何操作...

执行以下步骤:

  1. MultipleLanguageAppViewController的视图中添加两个标签。

  2. 将两个文件夹添加到项目中。分别命名为en.lprojes.lproj

  3. en.lproj文件夹中添加一个纯文本文件,并将其命名为Localizable.strings。在文件中输入以下内容并保存:

    // Localized output on MultipleLanguageAppViewController
    "Have a nice day!" = "Have a nice day!";
    
  4. es.lproj文件夹中添加另一个纯文本文件,并将其命名为之前相同的名称:Localizable.strings。在文件中输入以下内容并保存:

    // Localized output on MultipleLanguageAppViewController
    "Have a nice day!" = "¡Qué tenga un buen día!";
    
  5. MultipleLanguageAppViewController类中输入以下代码:

    public override void ViewWillAppear (bool animated)
    {
      base.ViewWillAppear (animated);
      this.lblLocale.Text = string.Format("Locale: {0} - Language: {1}", NSLocale.CurrentLocale.LocaleIdentifier, NSLocale.PreferredLanguages[0]);
      string resourcePath = NSBundle.MainBundle.PathForResource(NSLocale.PreferredLanguages[0], "lproj");
      NSBundle localeBundle = NSBundle.FromPath(resourcePath);
      this.lblLocalizedOutput.Text = localeBundle.LocalizedString("Have a nice day!", "Localized output on MultipleLanguageAppViewController");
    }
    
  6. 通过模拟器的设置应用程序,将语言设置为英语(如果尚未设置)并运行应用程序。消息将以英语显示。尝试将模拟器的语言设置为西班牙语(Español)并再次运行应用程序。消息将以西班牙语显示。

它是如何工作的...

为了使开发者更容易在应用程序中提供对多种语言的支持,iOS 从相应的语言文件夹中读取不同语言中的文本。在本应用程序中,我们支持英语和西班牙语。它们对应的文件夹分别是en.lprojes.lproj。当我们调用LocalizedString方法时,它会查找并解析Localizable.strings文件,以返回适当的文本。

字符串文件的内容由一组用 C 风格语法定义的引号键值对组成,每个集合以分号结尾,如下面的代码所示:

// Localized output on MultipleLanguageAppViewController
"Have a nice day!" = "¡Qué tenga un buen día!";

如您所见,我们还可以提供注释以协助翻译文本的人的工作,即使我们自己翻译。

NSLocale.PreferredLanguages静态属性返回一个包含用户首选语言标识符的字符串数组。该数组中的第一个项目是当前选定的语言。如果选定的语言是英语,它将返回en;如果是西班牙语,它将返回es,依此类推。

注意

这两种语言代码都基于 ISO 639-1 标准。也支持三字母的 ISO 639-2 标准。所有可用的语言代码列表可以在www.loc.gov/standards/iso639-2/php/code_list.php找到。

NSBundle.PathForResource方法返回我们传递给它的参数的应用程序包路径。我们使用此路径根据选定的语言获取适当的NSBundle实例,如下所示:

string resourcePath = NSBundle.MainBundle.PathForResource(NSLocale.PreferredLanguages[0], "lproj");
NSBundle localeBundle = NSBundle.FromPath(resourcePath);

然后,我们调用LocalizedString方法来显示适当的文本,如下所示:

this.lblLocalizedOutput.Text = localeBundle.LocalizedString("Have a nice day!", "Localized output on MultipleLanguageAppViewController");

第一个参数的目的是双重的。它既是查找以返回翻译文本的键,也是在找不到指定的本地化路径时将显示的文本。第二个参数是我们想要给翻译者提供的注释或任何指令。它不会显示并且基本上不会被使用。我们可以传递 null 到这个参数,并且不会发生错误。然而,始终包含注释或指令是明智的,因为它将有助于在翻译多个字符串时避免混淆。

还有更多...

建议始终提供可以作为在英语中显示的回退文本的键,以防用户选定的语言未包含在我们的应用程序中。

然而,LocalizedString方法有重载。第二个重载接受三个参数。考虑以下示例:

this.lblLocalizedOutput.Text = localeBundle.LocalizedString("Have a nice day!", "Have a nice day!", "Localizable");

第一个参数是查找键。第二个参数是在找不到指定的本地化路径时的回退值。第三个参数是不带.strings扩展名的字符串文件名。这种重载更有帮助,我们可以为我们的字符串使用不同的键,这有助于我们识别特定字符串在代码中的使用位置。例如,在这种情况下,我们可以在字符串文件中将键设置为MultipleLanguageAppViewController.lblLocalizedOutput

// Localized output on MainController
"MultipleLanguageAppViewController.lblLocalizedOutput" = "Have a nice day!";

然后,在代码中使用它如下:

this.lblLocalizedOutput.Text = localeBundle.LocalizedString("MultipleLanguageAppViewController.lblLocalizedOutput", "Have a nice day!", "Localizable");

这种重载还帮助我们将字符串分离到多个.strings文件中,通过传递相应的文件名作为第三个参数。

最后一个重载包含四个参数。前三个与第二个重载相同。第四个参数是我们希望特定字符串具有的注释。

在现实世界的应用程序场景中的本地化

在这个例子中,我们使用 PathForResource 方法来获取当前区域包的实例。这是因为 LocalizedString 方法返回的值会被缓存。在现实世界的应用程序场景中,如果应用程序以特定语言下载,并且用户不太可能更改设备的语言来使用它,只需调用 NSBundle.MainBundle.LocalizedString 就足够了。

可本地化的字符串编码

Localizable.strings 文件的编码始终应该是 UTF-8 或 UTF-16。

可本地化资源

可本地化资源是特定于区域的内容,例如图片和声音文件。在本例中,我们将学习如何根据用户的本地化首选项加载和显示资源。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,命名为 LocalizableResourcesApp。在 LocalizableResourcesAppViewController 的视图中添加一个标签和一个 UIImageView。还需要两个不同的图片,每个区域一个。本例中使用的是美国和西班牙的图片。

如何操作...

执行以下步骤:

  1. 将两个文件夹(en.lprojes.lproj)添加到项目中,用于英语和西班牙语区域。

  2. 每个文件夹中添加一个图片。确保图片的文件名在两个文件夹中都是相同的。

  3. LocalizableResourcesAppViewController 类中输入以下代码:

    public override void ViewWillAppear (bool animated)
    {
      base.ViewWillAppear (animated);
      this.lblLocale.Text = NSLocale.PreferredLanguages[0];
      this.imageView.Image = UIImage.FromFile(NSBundle.MainBundle.PathForResource("flag", "jpg"));
    }
    
  4. 编译并在模拟器上运行应用程序,在 设置 应用中选择英语作为语言。结果应该类似于以下截图:如何操作...

  5. 现在,将模拟器的语言设置为西班牙语,然后再次运行应用程序。应该显示西班牙国旗,如下面的截图所示:如何操作...

它是如何工作的...

PathForResource 方法会自动搜索适当的语言文件夹,并通过其参数加载指定的资源。在本例中,我们将方法的结果传递给 UIImage.FromFile 方法,以便加载图片并将其分配给图像视图的 Image 属性。

还有更多...

除了图片之外,我们还可以使用 PathForResource 方法来加载视频、PDF 文件和任何其他需要的可本地化资源。

关于可本地化资源的更多信息

我们需要确保特定语言文件夹的资源存在。如果不存在,将会发生异常。避免这种情况的一种方法是,在项目中添加一个通用的图片文件,并在每个语言文件夹内使用一个 Localizable.strings 文件,其中包含资源的路径,如下面的代码所示:

// US flag image
"flag_path"="en.lproj/flag.jpg";

要加载适当的国旗,我们使用 LocalizedString 方法加载图片,如下所示:

this.Image = UIImage.FromFile(NSBundle.MainBundle.LocalizedString("flag_path", "path/to/universal/image.jpg", "Localizable");

这样,如果找不到相应的语言文件夹,将会加载 image.jpg 图片。

相关内容

  • 为不同语言创建应用程序 的配方

区域格式化

区域格式化是指根据世界各地的不同区域,以不同的方式显示各种信息,例如货币、日期和时间。在本教程中,我们将讨论如何根据用户的区域格式化设置显示格式化的数字和日期。

准备工作

在 Xamarin Studio 中创建一个新的 单视图应用程序,并将其命名为 RegionalFormattingApp

如何操作...

执行以下步骤:

  1. RegionalFormattingAppViewController 的视图中添加五个标签。

  2. RegionalFormattingAppViewController 类中输入以下代码:

    public override void ViewDidAppear (bool animated)
    {
      base.ViewDidAppear (animated);
      this.lblLocale.Text = string.Format("Locale: {0}", NSLocale.CurrentLocale.LocaleIdentifier);
      this.lblDate.Text = string.Format("Date: {0}", DateTime.Now.ToLongDateString());
      this.lblTime.Text = string.Format("Time: {0}", DateTime.Now.ToLongTimeString());
      this.lblCurrency.Text = string.Format("Currency: {0:c}", 250);
      this.lblNumber.Text = string.Format("Number: {0:n}", 1350);
    }
    
  3. 在模拟器上编译并运行应用程序,将区域格式设置为 美国西班牙 | 西班牙,在 设置 | 通用 | 国际 | 区域格式 下。美国区域格式的输出将类似于以下屏幕截图:如何操作...

    西班牙区域格式的输出将类似于以下屏幕截图所示:

    如何操作...

它是如何工作的...

要格式化日期、货币和数字,我们使用标准的 .NET 代码。对于日期和时间,DateTime.ToLongDateStringDateTime.ToLongTimeString 方法分别返回根据区域设置确定的值。

对于货币和数字,我们使用 C# 数值字符串,如下所示:

this.lblCurrency.Text = string.Format("Currency: {0:c}", 250);
this.lblNumber.Text = string.Format("Number: {0:n}", 1350);

还有更多...

System.Globalization 命名空间在 Xamarin.iOS 中受支持。要显示当前区域设置,请考虑以下代码行:

Console.WriteLine(CultureInfo.CurrentCulture.Name);

注意,前面的代码与 NSLocale.CurrentLocale.LocaleIdentifier 之间有一个区别。前者使用破折号(-),而后者在区域名称中使用下划线(_)。

第十四章:部署

在本章中,我们将涵盖以下主题:

  • 创建配置文件

  • 创建一个 ad hoc 分发包

  • 为 App Store 准备应用

  • 将应用提交到 App Store

简介

在本章中,我们将详细介绍在开发计算机上准备和安装适当证书的所有必要步骤。我们还将学习如何创建允许我们将应用部署到设备(无论是自己的还是他人的)或发送给测试人员进行设备安装的配置文件。

最后,我们将了解如何为 App Store 提交应用以及其最终发布到 App Store 的过程。

创建配置文件

在本食谱中,我们将逐步指导创建和安装部署应用到设备所需的适当证书和配置文件。

如何操作...

以下步骤将指导您完成创建开发者证书和为应用创建适当的配置文件的过程。

我们将从开发者证书开始,如下所示:

  1. 登录到iOS 开发者网站

  2. 前往 iOS 配置文件门户

  3. 从右侧菜单转到证书、标识符和配置文件,然后在下一页点击证书

  4. 点击右侧的加号(+)按钮添加新证书。以下截图显示了新证书的设置页面:如何操作...

  5. 选择iOS 应用开发并点击继续

  6. 下一页提供了在您的 Mac 上创建证书签名请求CSR)的信息。按照说明创建 CSR 并点击继续

  7. 在下一页,上传 CSR 文件并点击生成

  8. 证书生成后,您将可以选择下载它。下载并双击.cer文件进行安装。Keychain Access 将打开,显示机器上安装的证书。

现在证书已安装,我们需要设置 Xamarin Studio 以便使用它。如果您尚未这样做,请关闭 Keychain Access,因为我们不需要它。执行以下步骤:

  1. 打开 Xamarin Studio。

  2. 打开首选项Cmd + ,)。

  3. 开发者账户选项下,点击加号按钮添加您的 Apple 开发者账户。

  4. 输入您的 Apple 开发者账户凭据并点击确定

以下截图显示了 Xamarin Studio 的首选项窗口,其中已添加了开发者账户:

如何操作...

现在我们已经颁发并安装了我们的开发者证书,我们需要通过以下步骤注册我们将用于调试的设备:

  1. 返回到 Apple 开发者门户的证书、标识符和配置文件,点击页面左侧的设备

  2. 点击加号按钮(+)以添加新设备。

  3. 输入设备的名称。如果您在测试不属于您的多个设备,您只能通过此名称来区分它们,所以请确保它有助于您识别设备,例如,Mike 的 iPhone 5s

  4. 输入设备的唯一设备标识符UDID)。您可以通过在 Mac 上连接设备并打开 iTunes 来找到设备的 UDID。在设备的摘要选项卡下,点击序列号将显示 UDID。右键单击并点击复制。您现在可以将其粘贴到 UDID 字段中。

  5. 点击继续。设备现在已添加到您的 Apple 开发者账户中,并可用于调试和测试您的应用。

我们已创建开发配置文件,设置好 Xamarin Studio,并将至少一个设备添加到我们的 Apple 开发者账户中。下一步是创建 App ID 和配置文件,这将允许我们的应用在设备上安装。

按照以下步骤创建 App ID:

  1. 证书、标识符和配置文件页面,点击页面左侧的App IDs

  2. 点击右侧的加号按钮(+)以创建新的 App ID。

  3. App ID 描述部分,输入 App ID 的名称。

  4. 选择App ID 前缀。如果没有可用的前缀,请选择生成新

  5. App ID 后缀部分,输入包 ID。根据说明,使用反向域名风格字符串(例如,com.mycompany.myapp)作为包 ID 是一个好习惯。

  6. 点击继续,App ID 即被创建。

我们即将完成。现在,是时候创建配置文件了。执行以下步骤:

  1. 点击页面左侧的配置文件

  2. 点击右侧的加号(+)按钮以创建新的配置文件。

  3. 选择iOS 应用开发选项并点击继续

  4. 在下一页,选择将配置文件绑定到的 App ID 并点击继续

  5. 选择 App ID 后,我们需要选择开发者证书。选择我们之前创建的开发者证书后,点击继续

  6. 我们现在需要选择应用可以安装的设备。选择一个或多个设备并点击继续。请注意,应用不会安装到此处未包含的设备上。

  7. 最后一步是为配置文件命名。输入您首选的名称并点击生成按钮。

  8. 配置文件生成后,我们有下载它的选项。下载并双击 .mobileprovision 文件以在您的机器上安装它。Xcode 现在将打开,组织者窗口将显示机器上所有已安装的配置文件。

我们完成了。我们已经成功地为 iOS 开发准备了我们的机器,以及所有必要的配置文件,这将允许我们在设备上调试我们的应用!

它是如何工作的...

本食谱中描述的过程将允许您在连接到您的 Mac 的设备上部署和调试您的应用。它不会允许您将应用分发给测试人员或 App Store。

开发者证书是允许编译将部署到设备的应用的证书。它仅用于开发,并且一个开发者证书对应于一个 iOS 开发者计划的注册。

每个配置文件都包含有关它可以安装在其上的设备的信息。注册到 iOS 开发者计划的 Apple 开发者可以添加最多 100 台设备并将它们包含在配置文件中。

App ID 是您应用的标识符。为您的每个应用创建一个 App ID。

配置文件是允许您的应用部署到设备的电子签名。每个配置文件对应一个应用,并包含所有适当的权限,允许应用在其包含的设备(和 App ID 信息)上执行。它也是区分开发或发布应用的关键。配置文件带有有效期。在撰写本书时,配置文件的持续时间是 1 年。

还有更多...

要在设备上编译和调试应用,请在 Xamarin Studio 的项目选项中iPhone 包签名节点下选择开发者证书和配置文件,如图所示:

还有更多...

这必须为每个构建配置(调试发布等)执行。

iPhone 应用节点下,为您的应用设置显示名称包标识符包版本字段。如果您留空,Xamarin Studio 将使用默认值设置它们的值。具体来说,包标识符字段将设置为包含在 App ID 中的值。然而,如果您将包标识符字段设置为与 App ID 中声明的内容不同的值,则在编译时将发生错误。

配置文件的过期

当配置文件过期时,应用将无法在设备上运行。您可以续订现有的配置文件或创建一个新的配置文件并将其重新安装到设备上。

相关内容

  • 创建一个临时的发布包食谱

  • 第一章中的 编译 iOS 项目 食谱,开发工具

创建一个临时的发布包

在本食谱中,我们将学习如何创建我们的应用包。我们将能够将此包发送给测试人员,以便他们在他们的设备上测试它。

准备工作

要创建一个临时的发布包,请确保您已在 iOS 配置文件门户上为您的应用创建了一个 App ID。

如何操作...

创建临时配置文件的过程与创建开发分发配置文件的过程类似。以下步骤将指导您完成这个过程:

  1. 创建分发证书。对于分发到未连接到您的 Mac 的各种设备,以及提交到 App Store,您需要一个已安装的分发证书。遵循之前菜谱中描述的创建开发者证书的相同步骤。不过,这次在添加新证书时,在 生产 部分下选择 App Store 和临时。所有其他必需步骤都相同。

  2. 创建分发配置文件。遵循之前菜谱中描述的创建配置文件的相同步骤。不过,这次在 分发 部分选择 临时 选项,而不是 iOS 应用开发

  3. 下载配置文件,双击它以在您的机器上安装。

现在我们已经准备好了所有分发证书和配置文件,我们需要通过以下步骤创建我们的临时构建:

  1. 在 Xamarin Studio 中打开项目。在这个例子中,使用的是 RegionalFormattingApp 项目。

  2. 在屏幕左上角的解决方案配置组合框中选择 临时,如图所示:如何操作...

  3. 打开项目选项,在 iPhone 包签名 选项下,从列表中选择分发证书和配置文件。只需确保您选择的是 临时 配置的证书和配置文件。在下面的屏幕截图中,已经选择了分发证书:如何操作...

  4. iOS IPA 选项 下,确保 构建临时/企业包(IPA) 已勾选。

  5. 导航到菜单栏上的 构建 | 重新构建全部 来创建构建。

我们的应用的分发构建已经准备好了!现在是时候与我们的测试人员分享它了。以下步骤将指导您完成这个过程:

  1. 在 Mac 上,打开 Finder 并导航到您项目的 bin 文件夹。

  2. 打开 iPhone/Ad-Hoc 文件夹。

  3. 您现在可以将 *.ipa 文件以及配置文件(*.mobileprovision)发送给测试人员。

  4. 测试人员可以通过在 iTunes 中拖放这两个文件并将它们与设备同步来安装应用。

它是如何工作的...

对于分发应用,我们需要一个分发证书。就像开发者证书一样,分发证书一旦创建,就可以在需要时转移到另一台 Mac 上。

创建临时分发配置文件的创建过程与创建开发配置文件的过程相同。唯一的区别是我们有选择分发类型的选项,可以是 App Store临时*.ipa 文件是 iTunes 识别的文件。

还有更多...

还有一个第三方服务可以使分发过程变得非常简单。您可以创建团队并上传不同的构建版本,每当有新的构建版本时,通过电子邮件通知您的测试人员,最重要的是,跳过 iTunes 同步。您可以在www.testflightapp.com找到所有信息。请注意,TestFlight 仅用于分发。必要的证书和配置文件仍然需要创建。

使用 iTunes 同步 ad hoc 应用包

不同用户在他们的 iTunes 应用中设置了不同的设置。如果用户同步设备且无法在设备上找到应用,请确保在 iTunes 中选中设备的应用选项卡下的应用进行同步。

参见

  • 创建配置文件菜谱

为 App Store 准备应用

在这个菜谱中,我们将讨论为准备 App Store 应用所需的重要步骤。

准备工作

按照前一个菜谱中的步骤为您的应用创建 App Store 分发配置文件。

如何操作...

在准备 App Store 的过程中,有一个非常重要的步骤是关于应用中应包含的图片。

最重要的是应用图标。这是将在用户设备上代表您应用的图标。根据目标设备,图标的尺寸应不同。Xamarin Studio 通过为每个目标设备/平台提供特定的槽位来分配图标,使得这个过程变得简单。例如,iPhone 3s 的图标大小与 iPhone 4s 的大小不同,iPhone 5 或运行 iOS 7 的后续设备的图标也是如此。

以下步骤将指导您完成这个过程:

  1. 双击项目的Info.plist文件以在嵌入式编辑器中打开文件。以下截图显示了通用(iPhone 和 iPad)应用中所有可用的应用图标选项:如何操作...

  2. 点击每个相应的图标来分配图标。图标必须是 PNG 格式。

  3. 现在我们已经设置了应用图标,我们需要设置启动画面。

  4. 启动画面是应用启动时首先显示的内容。为 iPhone 和 iPod Touch 应用准备至少两个维度的启动画面:低分辨率版本为 320 x 480 像素,高分辨率版本为 640 x 960 像素,4 英寸设备(如 iPhone 5、iPhone 5s 等)为 640 x 1136 像素。

  5. Info.plist编辑器中点击相应的框来设置启动画面。

我们现在必须设置 iTunes 艺术作品图片。这是一个 512 x 512 和 1024 x 1024 像素的图片,将在我们的应用页面上显示在 iTunes 中。它可以是你想要的任何东西;然而,一个好的做法是让它与应用图标相同。只需点击Info.plist文件中的相应按钮来分配 iTunes 艺术作品图片。

工作原理...

应用图标非常重要。这是用户将在设备屏幕上看到并点击以启动你的应用的内容。尽管所有应用图标都显示为带圆角和光效的按钮,但你不应在图标中包含这些图形特征。这些图形特征在提交到 App Store 时会自动渲染。图标应该是完美的正方形。此外,始终为图标提供背景。不要使用透明度,因为图标上的任何透明度都将显示为黑色,可能会破坏你预期的图标外观。

启动图像是应用启动时首先显示的。当启动时屏幕变黑,这意味着没有启动图像。根据苹果的 iOS 人机界面指南 (developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/index.html),这个图像应该是应用完成启动过程并准备好接受输入时加载的第一个屏幕。它应仅包含第一个屏幕的静态内容,而不是可能改变的内容,如本地化文本。

还有更多...

并非必须为编辑器中每个可用的槽位创建你的应用图标。实际上,即使你只创建一个低分辨率的图标,也是可以接受的,你将能够上传你的应用到 App Store。不过,当安装在高分辨率设备(如 iPad Air)上时,其质量可能不会是顶级的。这对应用及其开发者的声誉来说是不利的。

4 英寸屏幕启动图像

正如你可能已经注意到的,Info.plist 编辑器已经为视网膜(4 英寸)屏幕设置了一个启动图像,背景是纯黑色。这是因为当 iPhone 5 首次发布时,它的屏幕高度与之前的型号不同。

苹果选择帮助开发者轻松支持更高屏幕的方法要求开发者包括一个与新屏幕尺寸相匹配的启动图像。这样,iOS 在启动时会自动调整视图控制器的大小以适应新的屏幕尺寸。通过默认包含这个空白启动图像,Xamarin Studio 让我们的工作变得更简单,这样我们所有的项目都能在 4 英寸设备上正确显示。

参见

  • 创建配置文件 菜谱

将应用提交到 App Store

在这个菜谱中,我们将介绍提交应用至 App Store 所需的步骤。

准备工作

对于这个菜谱,你需要准备好你的压缩版分发应用包。

如何操作...

提交你的应用到 App Store 的步骤如下:

  1. 准备最多五张展示你应用不同方面的截图。对于 iPhone/iPod Touch 应用,肖像模式的尺寸应为 640 x 1136 像素,横屏模式的尺寸应为 1136 x 640 像素。

  2. 准备最能描述您应用的文本。尽量包括最重要的功能。记住,描述是用户在下载应用之前会阅读的内容,所以越吸引人越好。

    注意

    准备有助于您的应用在搜索结果中排名靠前的关键词。应用描述和关键词都是必需的。

  3. iTunes Connect 是管理并提交应用(以及其他与 App Store 相关的内容)的开发商门户。使用您的 Apple 开发者 ID 登录到 iTunes Connect (itunesconnect.apple.com)。点击 管理您的应用 链接。然后,点击左上角的 添加新应用 按钮。按照门户上的步骤完成应用的准备。完成时,请确保应用的状态是 等待上传

  4. 在门户上创建新应用后,您可以使用 Application Loader 上传压缩的应用程序包。它默认安装在 Xcode 中,可以在 /Developer/Application/Utilities 下找到,或者通过 Spotlight 搜索。

    当您启动 Application Loader 时,它将要求您使用 Apple 开发者 ID 登录。登录后,您将看到一个以下窗口:

    如何操作...

  5. 点击 提交您的应用 按钮,它将连接到 iTunes Connect,找到您处于 等待上传 状态的应用,并将它们加载到列表框中。

  6. 您将看到一个关于您应用的概览视图。

  7. 点击 选择... 按钮,将出现一个对话框,允许您选择压缩的应用程序包。选择后,继续上传。

您已经准备好了!如果所有步骤都正确完成,应用将被上传,并将在 App Store 上进行发布审查。

它是如何工作的...

应用截图可以是 JPG、TIF 或 PNG 格式,RGB 颜色,分辨率至少为 72 DPI。

然而,图片只有在用户已经在 App Store 中查看您的应用时才重要。关键词和描述是允许您的应用在搜索结果中排名更高并使用户决定应用是否值得下载的参数。关于关键词,要明智地选择。不要包含尽可能多的关键词;反映应用关键方面的较少关键词总是更好的。

iTunes Connect 是管理应用、审查财务数据、应用下载的开发商门户,它还包括开发商需要签署的合同和协议。请确保您阅读并接受这些合同,否则您将无法继续进行应用的准备流程。在此过程中,您需要提供必要的信息,对于您的应用而言,包括如果是付费应用,其价格范围、将在哪些国家提供,以及如果您不希望它自动在通过 App Store 审查流程后立即发布,其发布日期。

当一切设置正确且应用状态为等待上传时,您就可以运行应用程序加载器来上传它。随着 iOS 和 iOS SDK 版本的定期发布,各种组件或流程都会发生变化。请始终确保您的 iOS SDK 版本是最新的。

还有更多...

在应用准备过程中某个阶段,您将需要输入一个库存单位SKU)编号。这个编号是每个产品或服务的唯一标识符。它可以是你想要的任何数字,但请保持一个特定的模式以跟踪标识符,例如,当你开发额外的应用时。

参见

  • 为 App Store 准备应用 的配方

第十五章。高级功能

在本章中,我们将涵盖以下主题:

  • 重新生成翻页效果

  • 集成内容共享

  • 实现自定义过渡效果

  • 在 UI 元素中使用物理效果

  • 实现文本到语音的功能

简介

在本章中,我们将探索 iOS 平台提供的众多高级功能中的一部分。

具体来说,我们将创建一个项目,显示内容分为页面,用户可以通过UIPageViewController类像在普通书中一样导航。

然后,我们将讨论集成内容共享功能,并使用UIActivityViewController在我们的应用中提供分享/发布功能。对于用户界面,我们将探讨UIKit Dynamics的一些基本方面,它允许丰富的动画,以提供更好的用户体验。我们还将学习如何提供视图控制器之间的自定义过渡。

在本章的最后一个小节中,我们将使用新的文本到语音功能,并创建一个应用,该应用使用AVSpeechSynthesizer类进行语音输出!

重新生成翻页效果

在本菜谱中,我们将创建一个应用,使用UIPageViewController类显示类似书籍的内容。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用,并将其命名为BookApp。向项目中添加另一个控制器,并将其命名为Page。根据您的喜好配置Page控制器的外观。在此菜谱的源代码中,包含一个UIImageView和一个UILabel

如何操作...

执行以下步骤:

  1. BookAppViewController类中输入以下代码:

    private UIPageViewController pageViewController;
    private int pageCount = 3;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      Page firstPage = new Page(0);
      this.pageViewController = new UIPageViewController(UIPageViewControllerTransitionStyle.PageCurl, UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation.Min);
      this.pageViewController.SetViewControllers(new UIViewController[] { firstPage }, UIPageViewControllerNavigationDirection.Forward, false, s => { });
      this.pageViewController.GetNextViewController = this.GetNextViewController;
      this.pageViewController.GetPreviousViewController = this.GetPreviousViewController;
      this.pageViewController.View.Frame = this.View.Bounds;
      this.View.AddSubview(this.pageViewController.View);
    }
    
    private UIViewController GetNextViewController(UIPageViewController pageController, UIViewController referenceViewController)
    {
    
      Page currentPageController = referenceViewController as Page;
    
      if (currentPageController.PageIndex >= (this.pageCount - 1))
      {
    
        return null;
    
      }  else
      {	
        int nextPageIndex = currentPageController.PageIndex + 1;
        return new Page(nextPageIndex);
    
      }
    }
    
    private UIViewController GetPreviousViewController(UIPageViewController pageController, UIViewController referenceViewController)
    {
    
      Page currentPageController = referenceViewController as Page;
      if (currentPageController.PageIndex <= 0)
      {
        return null;
      }  else
      {
    
        int previousPageIndex = currentPageController.PageIndex - 1;
    
        return new Page(previousPageIndex);
    
      }
    }
    
  2. Page类添加一个属性并更改其构造函数,如下面的代码所示:

    public Page (int pageIndex) : base ("Page", null)
    {
      this.PageIndex = pageIndex;
    }
    
    public int PageIndex
    {
      get;
      private set;
    }
    
  3. 最后,在ViewDidLoad方法中配置将在Page中显示的内容:

    this.imgView.Image = UIImage.FromFile(string.Format("images/{0}.jpg", this.PageIndex + 1));
      this.lblPageNumber.Text = string.Format("Page {0}", this.PageIndex + 1);
    
  4. 在模拟器上编译并运行应用。在模拟器的屏幕区域上点击并拖动光标以更改页面。结果应类似于以下截图:如何操作...

它是如何工作的...

iOS 5 中引入的UIPageViewController类是许多开发人员期望的组件。它允许我们通过类似于真实书籍的效果导航内容,就像在苹果的iBooks应用中一样。

我们使用以下行初始化它:

this.pageViewController = new UIPageViewController(UIPageViewControllerTransitionStyle.PageCurl, UIPageViewControllerNavigationOrientation.Horizontal, UIPageViewControllerSpineLocation.Min);

构造函数的第一个参数确定效果的类型。目前唯一可用的值是PageCurl。第二个参数确定效果的方向。Horizontal参数是类似于书的效应的值,而Vertical是类似于笔记本的效应的值,其中页面在顶部装订。第三个参数确定书的装订位置。Min参数表示装订在屏幕的一边(在这种情况下,在左侧)。

在初始化页面控制器之后,我们需要通过调用其SetViewControllers方法来设置其第一页,如下面的代码所示:

this.pageViewController.SetViewControllers(new UIViewController[] { firstPage }, UIPageViewControllerNavigationDirection.Forward, 
  false, s => { });

该方法的第一参数是一个UIViewController对象的数组。我们可以为该参数设置一个或两个控制器,具体取决于设备的方向。例如,如果应用支持横屏方向,我们可能希望同时显示两个页面。第二个参数基本上决定了包含页面的导航方向。Forward参数意味着如果我们从右向左在屏幕上滑动,将加载下一页,而Reverse意味着对于相同的滑动将加载上一页。最后一个参数是委托类型UICompletionHandler,表示在控制器被添加后要执行的处理器。在这个例子中,我们不需要它,所以我们只传递一个空的 lambda。

接下来,我们需要为我们的“书籍”的其余页面提供数据源。Xamarin 再次通过为我们提供两个非常有用的属性来简化事情:GetNextViewControllerGetPreviousViewController。这两个属性仅仅代表如果我们为页面控制器创建一个委托对象时必须重写的回调方法。除了它们的名称外,这两个方法的签名是相同的,如下面的代码所示:

UIViewController GetNextViewController(UIPageViewController pageController, UIViewController referenceViewController);
UIViewController GetPreviousViewController(UIPageViewController pageController, UIViewController referenceViewController);

第一个参数给我们页面控制器,而第二个参数给我们在方法调用时屏幕上当前显示的控制器。

在这些方法的实现中,我们只需返回在当前控制器之后或之前应该加载的控制器。如果我们不想激活效果,我们只需返回null

最后但同样重要的是,我们通过以下代码设置页面控制器的视图大小并将其添加到父视图中,以便它将被显示:

this.pageViewController.View.Frame = this.View.Bounds;
  this.View.AddSubview(this.pageViewController.View);

更多...

如果我们想让我们的应用支持横屏方向,我们首先必须在BookAppViewController类中实现ShouldAutoRotateGetSupportedInterfaceOrientations方法。其次,我们必须向UIPageViewController类的SetViewControllers方法提供两个视图控制器。

双面页面

正如你可能在前面的菜谱截图中所注意到的,当我们翻页时,其内容在页面的背面以相反的方式显示,就像我们通过真实书籍的一页看过去一样。我们有通过将UIPageViewController.DoubleSided属性设置为true来创建双面页面的选项。

集成内容共享

在这个菜谱中,我们将向应用中添加内容共享功能。应用将能够通过社交网络、电子邮件、短信或AirDrop来共享内容。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ContentShareApp。该应用将在模拟器上运行,但在实际设备上会有更多的共享目标。

如何做到...

执行以下步骤:

  1. 在控制器中添加一个按钮。

  2. ContentShareAppViewController类中添加以下代码:

    private UIActivityViewController shareController;
    public override void ViewDidLoad ()
    {
      base.ViewDidLoad ();
      this.btnShare.TouchUpInside += async (sender, e) => {
        NSString link = new NSString("http://software.tavlikos.com");
        this.shareController = new UIActivityViewController(new NSObject[] {
          link
        }, null);
        this.shareController.CompletionHandler = this.ActivityCompleted;
    
        await this.PresentViewControllerAsync(this.shareController, true);
      };
    }
    private void ActivityCompleted(NSString activityType, bool completed)
    {
      Console.WriteLine("Activity type: {0}", activityType);
      Console.WriteLine("Completion: {0}", completed);
    }
    
  3. 在设备上编译并运行应用。点击分享链接按钮,将出现类似于以下截图的屏幕:如何操作...

它是如何工作的...

UIActivityViewController类负责显示可用的分享选项,具体取决于每个设备上这些选项的可用性。

要初始化一个UIActivityViewController实例,我们需要传递以下两个参数:

this.shareController = new UIActivityViewController(new NSObject[] {
  link
}, null);

第一个参数是一个NSObject对象的数组。在这个例子中,我们只传递一个NSString类型的对象。此对象是一个 URL。第二个参数是一个UIActivity对象的数组。在这个例子中,我们传递nullUIActivity类旨在被继承,这样我们就可以向UIActivityViewController添加我们自己的自定义活动“提供者”,并具有我们自己的图标和功能。

然后,我们使用以下代码设置当用户完成操作时将被调用的回调:

this.shareController.CompletionHandler = this.ActivityCompleted;
//..
private void ActivityCompleted(NSString activityType, bool completed)
{
  Console.WriteLine("Activity type: {0}", activityType);
  Console.WriteLine("Completion: {0}", completed);
}

通过回调,我们获取活动类型的NSString表示和一个布尔值,表示用户是否实际上完成了(true)或取消了(false)活动。请注意,无论用户是否在UIActivityViewController上点击了取消按钮,还是后来通过相应的屏幕(例如,在邮件编写屏幕中点击取消按钮)实际上取消了操作,此参数都将为false

在我们设置了UIActivityViewController之后,我们使用以下代码以模态方式展示它:

await this.PresentViewControllerAsync(this.shareController, true);

无论用户是否完成了操作或取消了操作,控制器都将自动消失。

当我们点击一个可用的选项时,将出现相应的屏幕。以下截图显示了 Facebook 分享屏幕:

它是如何工作的...

还有更多...

我们可以通过ExcludeActivityTypes属性排除我们不想显示的活动。例如,要从选项中删除邮件活动,我们将以下NSString对象的数组设置到该属性:

this.shareController.ExcludeActivityTypes = new NSString[] { "com.apple.UIKit.activity.Mail" };

实现自定义过渡

在这个菜谱中,我们将创建一个应用,该应用以模态方式显示视图控制器,但具有我们自己的自定义动画过渡。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用,并将其命名为CustomTransitionApp。将另一个视图控制器添加到项目中,并将其命名为ModalController。最后,我们还需要在每个控制器上添加一个按钮。

如何操作...

执行以下步骤:

  1. 将以下类添加到项目中:

    public class MyTransitionAnimator : UIViewControllerAnimatedTransitioning
    {
      public bool IsPresenting { get; set; }
      public override double TransitionDuration (IUIViewControllerContextTransitioning transitionContext) {
        return 1;
      }
      public override void AnimateTransition (IUIViewControllerContextTransitioning transitionContext) {
        if (this.IsPresenting) {
          UIView containerView = transitionContext.ContainerView;
          UIViewController toViewController = transitionContext.GetViewControllerForKey(UITransitionContext.ToViewControllerKey);
          containerView.AddSubview(toViewController.View);
          RectangleF frame = toViewController.View.Frame;
          toViewController.View.Frame = RectangleF.Empty;
          UIView.Animate(this.TransitionDuration(transitionContext), () => toViewController.View.Frame = new RectangleF (20f, 20f, frame.Width - 40f, frame.Height - 40f), () => transitionContext.CompleteTransition (true));
        } else {
          UIViewController fromViewController = transitionContext.GetViewControllerForKey(UITransitionContext.FromViewControllerKey);
          RectangleF frame = fromViewController.View.Frame;
          frame = RectangleF.Empty;
          UIView.Animate(this.TransitionDuration(transitionContext), () => fromViewController.View.Frame = frame, () => transitionContext.CompleteTransition (true));
        }
      }
    }
    public class MyTransitionDelegate : UIViewControllerTransitioningDelegate
    {
      private MyTransitionAnimator animator;
      public override IUIViewControllerAnimatedTransitioning PresentingController (UIViewController presented, UIViewController presenting, UIViewController source)
      {
    
        this.animator = new MyTransitionAnimator();
        this.animator.IsPresenting = true;
        return this.animator;
      }
      public override IUIViewControllerAnimatedTransitioning GetAnimationControllerForDismissedController (UIViewController dismissed) {
        this.animator.IsPresenting = false;
        return this.animator;
      }
    }
    
  2. CustomTransitionAppViewControllerViewDidLoad方法中添加以下代码:

    this.btnPresent.TouchUpInside += async (sender, e) => {
      ModalController modalController = new ModalController();
      modalController.ModalPresentationStyle = UIModalPresentationStyle.Custom;
      modalController.TransitioningDelegate = new MyTransitionDelegate();
      await this.PresentViewControllerAsync(modalController, true);
    };
    
  3. 在模拟器上编译并运行应用。点击按钮,观察模态控制器从左上角平滑地展示。结果应该类似于以下截图:如何操作...

它是如何工作的...

要创建我们的自定义过渡,我们需要创建两个对象。

第一个对象是 UIViewControllerAnimatedTransitioning 的子类,如下面的代码行所示:

public class MyTransitionAnimator : UIViewControllerAnimatedTransitioning

这个类包含我们需要的两个方法:TransitionDuration,它指定了动画过渡的持续时间,以及 AnimateTransition,其中实际进行动画。

AnimateTransition 方法内部,我们得到一个 IUIViewControllerContextTransitioning 对象,该对象负责整个过程。动画将在对象为此目的创建的 UIView 上进行。这个 UIView 对象可以通过过渡上下文对象的 ContainerView 属性访问,如下面的代码行所示:

UIView containerView = transitionContext.ContainerView;

通过过渡上下文对象,我们还可以获取参与过渡的控制器。要获取目标控制器,我们调用 GetViewControllerForKey 方法,传递给它 UITransitionContext.ToViewControllerKey,如下面的代码所示:

UIViewController toViewController = transitionContext.GetViewControllerForKey(UITransitionContext.ToViewControllerKey);

在我们获取所需的对象后,我们将目标控制器的视图添加到过渡上下文的视图中,并使用 UIView.Animate 方法更改其框架。当所有动画执行完毕后,我们需要在过渡上下文中调用 CompleteTransition 方法,如下面的代码所示:

containerView.AddSubview(toViewController.View);
//..
UIView.Animate(this.TransitionDuration(transitionContext), () => toViewController.View.Frame = new RectangleF(20f, 20f, frame.Width, frame.Height), () => transitionContext.CompleteTransition(true));

第二个对象是 UIViewControllerTransitioningDelegate 的子类。类的声明如下面的代码行所示:

public class MyTransitionDelegate : UIViewControllerTransitioningDelegate

MyTransitionDelegate 子类内部,我们重写 PresentingController 方法并返回我们之前创建的 MyTransitionAnimator 实例,如下面的代码所示:

this.animator = new MyTransitionAnimator();
this.animator.IsPresenting = true;
return this.animator;

MyTransitionAnimatorIsPresenting 属性用作标志,以便动画器知道过渡是为了显示控制器还是取消显示。我们在 GetAnimationControllerForDismissedController 方法中将它设置为 false,并返回相同的 MyTransitionAnimator 实例,如下面的代码所示:

this.animator.IsPresenting = false;
return this.animator;

很明显,当控制器要被显示时,将调用一个方法,而当控制器要被取消显示时,将调用另一个方法。

最后,为了启用一切,我们将 ModalPresentationStyle 属性设置为 UIModalPresentationStyle.Custom,并将一个新的 MyTransitioningDelegate 实例设置到将要显示的控制器 TransitioningDelegate 属性中,如下面的代码所示:

modalController.ModalPresentationStyle = UIModalPresentationStyle.Custom;
modalController.TransitioningDelegate = new MyTransitionDelegate();

更多...

自定义过渡不仅限于模态控制器。我们可以使用自定义过渡将控制器推入导航控制器的导航堆栈,或者完全创建我们自己的导航堆栈。

在子控制器之间切换

UIViewController 类包含 Transition 方法,允许我们在父控制器内部从一个子控制器切换到另一个子控制器。

注意

子控制器是那些其视图是第三个控制器视图层次结构一部分的控制器。这个控制器是父控制器。

参见

  • 在第十一章的动画视图菜谱中,图形和动画,第十一章

在 UI 元素中使用物理

在这个菜谱中,我们将使用 UIKit Dynamics 为图像视图添加物理属性。图像视图将从其初始位置下落到屏幕底部,模拟物体掉落在地面的效果。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,并将其命名为ViewPhysicsApp。向控制器中添加UIImageView和两个按钮。我们还需要一个要在图像视图中显示的图像。

如何操作...

执行以下步骤:

  1. ViewPhysicsAppViewController类中添加以下字段:

    private RectangleF imageRect;
    private UIDynamicAnimator animator;
    
  2. ViewDidLoad方法中添加以下代码:

    this.View.InsertSubviewBelow(this.imgView, this.btnReset);
    this.imageRect = this.imgView.Frame;
    this.imgView.Image = UIImage.FromFile("1.jpg");
    this.animator = new UIDynamicAnimator(this.View);
    
  3. 接下来,再次在ViewDidLoad方法中添加以下按钮处理程序:

    this.btnDrop.TouchUpInside += (sender, e) => {
      UIGravityBehavior gravity = new UIGravityBehavior(this.imgView);
      UICollisionBehavior collision = new UICollisionBehavior(this.imgView);
      collision.TranslatesReferenceBoundsIntoBoundary = true;
      this.animator.AddBehaviors(gravity, collision);
    };
    this.btnReset.TouchUpInside += (sender, e) => {
      this.animator.RemoveAllBehaviors();
      this.imgView.Frame = this.imageRect;
    };
    
  4. 在模拟器上编译并运行应用程序。点击Drop!按钮,观察图像视图下落到屏幕底部。点击Reset按钮将其重置到原始位置。以下截图显示了应用程序的初始状态和点击Drop!按钮后的状态:如何操作...

它是如何工作的...

UIKit Dynamics 提供了一系列对象,允许我们向 UIKit 对象添加物理属性。

我们首先需要做的是初始化一个UIDynamicAnimator对象。这个类提供了所有物理动画将发生的上下文。我们传递控制器的视图,这会自动使其成为我们的 2D“物理世界”,如下面的代码行所示:

this.animator = new UIDynamicAnimator(this.View);

在我们创建了动态动画器之后,我们需要向其中添加一些行为。在btnDrop处理程序内部,我们首先确保图像视图会受到重力的影响,通过创建一个UIGravityBehavior实例,如下面的代码所示:

UIGravityBehavior gravity = new UIGravityBehavior(this.imgView);

如果我们保持原样,图像视图将直接下落到屏幕底部边界以下。因此,我们还需要一个碰撞行为,我们可以使用以下代码添加它:

UICollisionBehavior collision = new UICollisionBehavior(this.imgView);
collision.TranslatesReferenceBoundsIntoBoundary = true;

注意,碰撞也需要一个与之碰撞的边界,否则如果没有边界,它将产生相同的效果。在这种情况下,我们使用动画器对象的边界,如前面突出显示的代码所示。

现在我们已经设置了行为,我们将它们添加到我们的动画器中,使用以下代码来使一切动起来:

this.animator.AddBehaviors(gravity, collision);

还有更多...

我们还可以修改图像视图撞击地面时的弹跳效果。尝试在UICollisionBehavior初始化行下方添加以下代码:

UIDynamicItemBehavior dynBehavior = new UIDynamicItemBehavior(this.imgView);
dynBehavior.Density = 1f;
dynBehavior.Elasticity = 0.7f;
dynBehavior.Friction = 1f;

当然,别忘了将新的行为添加到动画器中,如下面的代码行所示:

this.animator.AddBehaviors(gravity, collision, dynBehavior);

如果你运行应用程序并点击Drop!按钮,图像在撞击地面时会弹跳得更高!

UIKit Dynamics 使用

UIKit Dynamics 被设计用来为UIView对象或实现 Objective-C UIDynamicItem协议(在 C#中为IUIDynamicItem接口)的每个对象提供简单的 2D 物理。它不是用来用UIView对象开发游戏的。为此,我们有SpriteKit 框架,它可以通过MonoTouch.SpriteKit命名空间访问。这超出了本书的范围。

实现文本到语音功能

在本食谱中,我们将学习如何使用AVSpeechSynthesizer类,该类为许多不同的语言提供了文本到语音(TTS)功能。

准备工作

在 Xamarin Studio 中创建一个新的单视图应用程序,命名为SpeechApp。将一个UITextField和一个按钮添加到控制器中。

如何做到这一点...

执行以下步骤:

  1. SpeechAppViewController.cs文件中添加MonoTouch.AVFoundation命名空间,使用以下代码:

    using MonoTouch.AVFoundation;
    
  2. ViewDidLoad方法中添加以下代码:

    this.txtEntry.ShouldReturn = (textField) => textField.ResignFirstResponder();
    this.btnSpeak.TouchUpInside += (sender, e) => {
      AVSpeechSynthesizer synth = new AVSpeechSynthesizer();
      AVSpeechUtterance utterance = new AVSpeechUtterance(this.txtEntry.Text);
      utterance.Rate = 0.3f;
      utterance.Voice = AVSpeechSynthesisVoice.FromLanguage("en-US");
      synth.SpeakUtterance(utterance);
    };
    
  3. 在模拟器上编译并运行应用程序。在文本字段中输入一些英文文本,然后点击说话按钮。当你的应用程序说话时,请听一下!

它是如何工作的…

AVSpeechSynthesizer类是在 iOS 7 中引入的。它提供了非常简单实用的文本到语音(TTS)功能。

在初始化类的实例之后,我们创建一个AVSpeechUtterance对象,将其传递给要处理的文本,如下面的代码所示:

AVSpeechSynthesizer synth = new AVSpeechSynthesizer();
AVSpeechUtterance utterance = new AVSpeechUtterance(this.txtEntry.Text);

然后,我们设置语音的速率并使用以下代码将一个语音分配给语音:

utterance.Rate = 0.3f;
utterance.Voice = AVSpeechSynthesisVoice.FromLanguage("en-US");

速率调整文本将被说话的速度。你可以测试各种速度以满足你的需求。

语音是一个AVSpeechSynthesisVoice的实例。要初始化它,我们调用FromLanguage静态方法,传递 BCP-47 语言代码。不幸的是,每种可用语言都只有一种语音类型,我们无法控制它。

最后,为了开始语音,我们调用合成器的SpeakUtterance方法,并使用以下代码将语音对象传递给它:

synth.SpeakUtterance(utterance);

注意

我们可以多次调用SpeakUtterance方法,每次传递一个不同的语音对象。语音合成器将排队每个语音并按顺序播放。

更多内容...

我们可以通过枚举AVSpeechSynthesisVoice.GetSpeechVoices()方法的返回值来枚举语音合成器支持的可用语言代码,如下面的代码所示:

foreach (AVSpeechSynthesisVoice eachVoice in AVSpeechSynthesisVoice.GetSpeechVoices()) {
  Console.WriteLine(eachVoice.Description);
}

调整语音

我们可以通过以下AVSpeechUtterance类的属性来进一步调整语音的执行方式:

  • PitchMultiplier:这是语音的音调。它是一个浮点数,其值在0.52之间。

  • PostUtteranceDelayPreUtteranceDelay:这是在每个语音被说出之后(post)和/或之前(pre)等待的时间量,以秒为单位。

  • Volume:这是语音的音频音量。它在0.0(静音)到1.0(最响)之间。

posted @ 2025-10-25 10:40  绝不原创的飞龙  阅读(28)  评论(0)    收藏  举报