zoukankan      html  css  js  c++  java
  • Apple Watch应用开发 1

    写了一个测试的demo 发现如果设置了next Page,在InterfaceController中的push操作是不成功的(点击第一个"助力"的row push到TowInterfaceControl) 但是present是好用的

    参考转载:http://www.swiftkiller.com/?p=613

    A.概览

    一.概览--开始为 Apple Watch 进行开发

    -1.第三方应用需要两个不同的可执行文件:在Apple Watch上运行的Watch应用(Watch应用只包含与应用程序的用户界面有关的storyboards和资源文件),

                         在用户iPhone上运行的WatchKit应用扩展(WatchKit应用扩展则包含了用于管理、监听应用程序的用户界面以及响应用户交互的代码)。

    Watch应用需要尽可能实现Apple Watch提供的所有交互动作。由于Watch应用目的在于扩展iOS应用的功能,因此Watch应用和WatchKit应用扩展将被捆绑在一起,并且都 会被打包进iOS应用包。如果用户有与iOS设备配对的Apple Watch,那么随着iOS应用的安装,系统将会提示用户安装相应的Watch应用。

    -2.创建

    1.创建Watch应用

    2.创建Glance界面

    3.自定义通知界面

     二.概览--配置Xcode项目
    -1.向iOS应用中添加Watch应用

    要向现有项目中添加Watch应用对象,请执行以下操作:

    1. 打开现有的iOS应用项目

    2. 选择 File > New > Target,然后选中Apple Watch

    3. 选择 Watch App

    4. 单击 Next

    5. 如果您想要使用glance或者自定义通知界面,请选择相应的选项

    我们建议您激活应用通知选项。选中之后就会创建一个新的文件来调试该通知界面。如果您没有选择这个选项,那么之后您只能手动创建这个文件了。

    6. 单击 Finish

    如果修改了bundleId,那么相对应的Watch应用和WatchKit应用扩展的bundleId
     
    三.概览--Watch应用的体系结构
    -1.Apple Watch应用程序包含两个部分:Watch应用和WatchKit应用扩展。Watch应用驻留在用户的Apple Watch中,只含有故事板和资源文件,要注意它并不包含任何代码。而WatchKit应用扩展驻留在用户的iPhone上(在关联的iOS应用当中), 含有相应的代码和管理Watch应用界面的资源文件
    -2.Watch应用的生命周期
    WatchKit应用扩展提供一个名为WKInterfaceController的子类来管理相应的界面.
    图3-2展示了Watch应用的启动序列。
    当应用启动时,WatchKit框架自行创建了相应的`WKInterfaceController`对象并调用initWithContext:方法。使用该方法来初始化界面控制器,然后加载所需的数据,最后设置所有界面对象的值。对主界面控制器来说,初始化方法紧接着willActivate方法运行,以让您知道界面已显示在屏幕上。

    图3-2 启动Watch应用

    launch_cycle_2x.png

     

    当用户在Apple Watch上与应用进行交互时,WatchKit应用扩展将保持运行。如果用户明确退出应用或者停止与Apple Watch进行交互,那么iOS将停用当前界面控制器,并暂停应用扩展的运行,如图3-3所示。与Apple Watch的互动是非常短暂的,因此这几个步骤都有可能在数秒之间发生。所以,界面控制器应当尽可能简单,并且不要运行长时任务。重点应当放在读取和显示 用户想要的信息上来。

    图3-3 界面控制器的生命周期

    032.png

    应用生命周期中各阶段需执行不同的任务

    在应用生命周期的不同阶段,iOS将会调用WKInterfaceController对象的相关方法来让您做出相应的操作。表3-1列出了大部分您应当在界面控制器中声明的主要方法。

    Table 3-1:WKInterfaceController`的主要方法

    方法 要执行的任务
    initWithContext: 这个方法用来准备显示界面。借助它来加载数据,以及更新标签、图像和其他在故事板场景上的界面对象。
    willActivate 这个方法可以让您知道该界面是否对用户可视。借助它来更新界面对象,以及完成相应的任务,完成任务只能在界面可视时使用。
    didDeactivate 使用didDeactivate方法来执行所有的清理任务。例如,使用此方法来废止计时器、停止动画或者停止视频流内容的传输。您不能在这个方法中设置界面控制器对象的值,在本方法被调用之后到willActivate方法再次被调用之前,任何更改界面对象的企图都是被忽略的。 

    除 了在表3-1中列出的方法,WatchKit同样也调用了界面控制器的自定义动作方法来响应用户操作。您可以基于用户界面来定义这些动作方法。例如,你可 能会使用动作方法来响应单击按钮、跟踪开关或滑条值的变化,或者响应表视图中单元格的选择。对于表视图来说,您同样也可以用table:didSelectRowAtIndex:而不是动作方法来跟踪单元格的选择。用好这些动作方法来执行任务并更新Watch应用的用户界面。

    提示:Glances不支持动作方法。单击应用glance始终会启动应用。

    与Containing iOS应用共享数据

    如 果您的iOS应用和WatchKit应用扩展都依赖于相同的数据,那么您可以使用共享程序组来存储数据。共享程序组是一个位于本地文件系统的区域,应用扩 展和应用都能够访问。由于两个程序在不同的沙箱环境中运行,它们一般情况下都不与对方共享文件和进行通信。共享程序组让共享数据成为可能。你可以使用这个 空间来存储共享的数据文件或者在两个程序间交换消息。

    您可以在iOS应用和WatchKit应用扩展中的Capabilities选项卡中 启动共享程序组。激活这项功能后,Xcode将会为每个对象添加授权文件(需要的话),并给那个文件添加 com.apple.security.application-groups授权。要共享数据,这两个对象必须选择相同的共享程序组。

    程序运行时,您可以通过在共享容器目录中读写文件以在两个程序间共享数据。要访问容器,请使用NSFileManager中的containerURLForSecurityApplicationGroupIdentifier:方法来接收该目录的根路径。使用方法返回的URL来枚举目录内容或者在目录中为文件创建新的URL。

    重要:请始终在共享容器目录中使用文件演示器和协调器来访问文件。文件演示器和协调器允许对文件和目录进行同步访问。没有它们,您的WatchKit应用扩展和iOS程序可能会相互冲突并毁坏共享的文件。有关如何使用文件演示器和协调器的信息,请参阅:File System Programming Guide

     

     四.概览--使用iOS技术

    · 避免使用需要用户权限的技术,比如Core Location。

    · 不要使用后台执行模式的技术。

    · 避免使用需要长时间运行的技术。

    要使用iOS技术,其中一种解决方法是让您的iOS应用来使用这些技术。例如,在您的iOS应用中使用位置服务,而不是在WatchKit中使用这个技术。iOS应用可以收集所需的数据然后通过共享程序组来让应用扩展稍后访问。

    当 使用iOS技术的时候,请始终确保使用这些技术能够给用户带来明显的好处,并且不要让这些技术成为您应用的核心功能。同样要记住某些技术需要您提供用法说 明,使用说明存放在`Info.plist`文件中,其描述了您打算如何使用相应的数据。如果在iOS应用中已经有了这些用法说明字符串,请考虑更新它们 以让用户明白,这些数据可能会结合Watch应用来使用。

    B.WatchKit Apps

    一.WatchKit Apps--App概要

    -1.在Xcode中创建你的界面时,尽可能地让对象自己调整尺寸来适应可用空间。App界面应该可以运行两种尺寸的Apple Watch。让系统调整对象尺寸来适应可用空间,这样可最小化为每款设备编写的代码量。

    -2.在运行时更新界面(Updating Your Interface at Runtime)

    在运行过程中,界面控制器能让对象在相应的storyboad场景中做如下改变:

    • 设置或修改数据变量

    • 支持改变对象的外观

    • 改变对象的尺寸

    • 改变对象的透明度

    • 显示或隐藏对象

    你不能添加新的对象到界面或者改变已经存在对象的次序。尽管你不能移除对象,但你可以通过隐藏对象来暂时从图层中移除。当控件被隐藏时,其他对象将会填充此前被占用的空间。 如果不想填满这些空间,可以把对象的alpha值设置为0。

    二.WatchKit Apps--界面导航

    对于内容超过一屏的WatchKit app来说,您必须在设计时选择一个导航类型。您所选择的导航类型定义了如何在应用程序中展示和管理界面控制器。WatchKit支持两种互斥的界面风格:

    • 分层风格

    • 分页风格

    这两种导航都可以模态形式展示一个或多个界面。当您想暂时打断用户的工作流程以请求输入或者展示信息时,适合使用模态形式。当模态地展示两个或多个界面控制器时,系统会以一系列的页面展示它们,类似于基于分页的导航。

    -1.实现分层界面(Implementing a Hierarchical Interface)

    分层界面主要针对分层的数据集,用户可在此选择某个条目以展示相关详细信息。分层界面通常始于单个的根界面控制器。在该界面控制器中,您可以通过调用当前界面控制器的pushControllerWithName:context:方法在屏幕上推出一个新的界面控制器。通常,当用户点击按钮或表格行时,您可以从动作方法中调用该方法。每个新的界面控制器可展示下一级分层中的信息。

    当 在屏幕上推出新的界面控制器时,会将数据对象传递给pushControllerWithName:context:方法的上下文参数。在新界面控制器展 示在屏幕上之前,这个上下文对象就是您与其通信的机会。您可以使用该对象来告知界面控制器展示什么样的数据或者传达什么样的状态信息。

    想要解除界面控制器,可调用其popController方法。用户也可以直接从屏幕的左边缘轻扫解除界面控制器。界面控制器解除后即失效,而先前的界面控制器将会重新展示。根界面控制器不能被解除。

    -2.实现基于分页的界面(Implementing a Page-Based Interface)

    基于分页的界面主要针对本质上没有分层的数据。分页界面包含两个或者多个独立的界面控制器,并且在指定时间内仅展示其中一个界面。在运行时,通过向左/右轻扫屏幕进行导航。屏幕底部的圆点指示器控件指示用户当前的位置。

    在设计过程中,您可以创建下一页的segue,以便在app的storyboard文件中配置分页界面。想要创建该segue:

    1.对您希望作为第一屏的界面控制器执行Control-click操作,并将其拖拽到不同的界面控制器中。

    应该强调第二个界面控制器,以指明segue是可用的。

    2.释放鼠标按钮。

    3.从relationship segue面板中选择next page。

    通常在storyboard文件中配置一组初始的页面集合。当app启动时,WatchKit会实例化和初始化您的初始界面控制器,然后是分页界面中的其他 界面控制器。如果您想改变一组界面控制器,请在初始界面控制器的init方法中调用 reloadRootControllersWithNames:contexts:方法。调用该方法会使得WatchKit在尝试展示界面中任何其他页 面之前加载新的界面控制器。您也可以在app运行时调用reloadRootControllersWithNames:contexts:方法来改变展 示中的页面集合。

    当系统加载您的WatchKit app界面时,它将实例化和初始化组成界面的所有界面控制器。当用户从一个界面控制器切换至下一个时,它将调用当前界面控制器的didDeactivate方法,以及即将展示的界面控制器的willActivate方法。willActivate方法可确保界面中的信息是最新的。

    -3.以模态形式展示界面控制器(Presenting Interface Controllers Modally)

    模态界面可临时中断当前导航流以提示用户或者展示信息。您可以使用分页界面或分层界面来组成一个模态界面。想要模态地展示界面控制器,请从当前可视界面中调用以下方法之一:

    模态界面的左上角包含一个文本标签,当点击时会解除界面。如果您没有为该标签指定一个准确的字符串,那么WatchKit会自动为其使用"Cancel"。 您可以根据需要更改字符串,以反映解除模态视图的真实用意。比如,当展示那些您不希望用户响应的内容时,您可能会将字符串更改为"Done"或 "Close"。如果您使用了"Cancel"字符串,那要考虑为接受模态界面中的任何更改添加一个或多个按钮。

    三.Interface Object--界面对象

    Apple Watch上界面对象与对应的视图之间的通信有单向性,即信息流是从WatchKit扩展传到Apple Watch的。换句话说,你可以对界面对象的某些属性值进行更改设置,但你无法获得其属性的当前值。在对设备状态进行更改的时候从Apple Watch上获取数据对性能和延迟均有影响。因此我们推荐你在自己的WatchKit扩展中保存你对界面的配置信息。

    -1.在设计阶段配置界面

    在设计阶段,可以使用Xcode配置Storyboard中视觉元素的外观。对于很多与布局相关的属性,design-time是您可以配置属性的唯一机会了。比如,你可以使用一个WKInterfaceLabel对象来更改标签的文本、颜色以及字体,不过却不能更改它的行数或者是行高。这些属性必须在Xcode中配置,如下图:

    formatted_text_attributes.png

    了解更多有关如何配置界面对象的信息,可参考WatchKit框架介绍中相关的类型描述。

    -2.在运行时更改界面

    为了提高性能和延长电池寿命,WatchKit框架优化了在app界面对象上设置数值的任何试图。只要在同一的Run Loop中,无论你何时为一个或者多个界面对象设置值,这些值都会被打包传给Apple Watch并作为单个批处理以提高效率。合并这些改变意味着对象的既有属性只有最后一次更改被发送给设备。更为重要的是,如果为同样的属性设置了相同的 值,那么会生一条日志信息,以便你跟踪重复调用。

    -3.响应用户交互

    请使用按钮、开关以及其他的交互控件来更改应用状态。当点击按钮或某一控件的值发生 变化时,WatchKit会调用界面控制器中相关的动作方法(action method)。每种界面对象针对其动作方法都有一个必须的格式,如下图所示,可以更改动作方法的名称以匹配您的应用程序:

    031.jpg

    当您的界面控制器初始化并显示出来之后,WatchKit就可以调用它的动作方法了,当然仅在用户与相应的控件交互时。如果您希望在没有用户交互的情况下更新用户界面,那必须配置一个NSTimer对象来安排这个任务。

    这 些任务可能会耗费一到两秒的时间,可以考虑将其交由父级应用(parent iOS app)来执行。像网络连接以及定位这些需要较长运行时间的任务最好由父级应用执行,然后通过一个共享的群组容器目录将信息传回WatchKit扩展。关 于将任务切换给parent app来执行的更多信息,请参看Communicating Directly with Your Containing iOS App.

     -4.隐藏界面对象

    通过隐藏对象你可以使用相同的界面控制器来显示不同类型的内容。 Storyboard文件中的每个场景都必须包含所有运行时需要展示其内容的界面对象。如果你要根据有效数据来自定义界面,那可以将不需要的对象隐藏起 来。隐藏一个对象可有效地将其从界面中移除。在设计布局时,隐藏起某些项目就可以当做是将其从布局中完全删除。如果要隐藏某个对象,只需要调用setHidden:方法并为其传递YES值就行了。

    四.Watch Apps--文本、标签以及图片

    -1.Using Custom Fonts 使用自定义字体

    除了标准字体风格,你可以定制经过格式化的字符串的字体。按照下面的方式来定制字体:

    · 在Watch app和WatchKit扩展包中都导入定制的字体文件。

    · 添加__UIAppFonts__键到你的Watch app中的Info.plist文件中,并用这个来指定添加到包中的字体。更多关于此键的信息,请查阅 Information Property List Key Reference

    注意:你必须在WatchKit扩展中导入字体,才能在运行时创建指定字体的字符串。当发送到Apple Watch的时候,字体的信息包含了属性字符串,在Watch app包中的字体拷贝将会渲染这个字体。

    使用自定义的字体作为标签的字符串

    // Configure an attributed string with custom font information.
         UIFont* menloFont = [UIFont fontWithName:@"Menlo" size:12.0];
         NSAttributedString *attrString = [[NSAttributedString alloc]
           initWithString:@"My Formatted Text"
           attributes:@{NSFontAttributeName: menloFont}];
      
         // Set the text on the label object.
         [self.myCustomFontLabel setAttributedText:attrString];

    -2.Customizing the System Font 自定义系统字体

    自定义系统字体,使用UIFontDescriptor对 象来指定基于系统的新字体。列表 8-2 的例子展示了怎样让定制只使用小写字符的标准的系统字体。在转换系统字体到一个字体描述符后, 添加__kLowerCaseType__和__kLowerCaseSmallCapsSelector__属性(在Core Text framework中定义)然后字体描述符的结果来创建新的字体对象。

    指定系统字体为小写

    CGFloat fontSize = 18.0;
         UIFont *afont = [UIFont systemFontOfSize:fontSize];
         UIFontDescriptor *fontDescriptor =
              [[afont fontDescriptor] fontDescriptorByAddingAttributes:@{
              UIFontDescriptorFeatureSettingsAttribute : @[
                 @{    UIFontFeatureTypeIdentifierKey : @(kLowerCaseType),
              UIFontFeatureSelectorIdentifierKey :      @(kLowerCaseSmallCapsSelector) },],
         }];
      
         UIFont *smallCapFont = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];

    -3.国际化您的文本代码

    Watch apps能使用iOS apps想通的国际化技术方法。

    · 使用基于Xcode国际化支持的storyboard和xib文件。基于国际化可以让你仅仅只用一个storyboad文件就能支持所有的本地化。本地化字符串分别存在特定的区域语言字符串中。

    · 使用NSLocalizedString族的宏定义来让程序自动检索本地化字符串。

    · 通过NSNumberFormatter类使用用户所在区域和本地的设置来格式化数值型的值。

    · 通过NSDateFormatter类使用用户所在区域和本地设置来格式化时间。

    当要国际化你的应用,你主要考虑的是如何调整界面,让标签列表(还有其他文本的控制)能够足够容纳得下。比如,比起水平的排列三个按钮,垂直排列更好能给每一个标签的文本提供更长的容纳空间。

    更多关于国际化的信息,请查阅Internationalization and Localization Guide

    -4.图像

    · WKInterfaceImage用于展示单个的图片或者一组图片作为单个图像展示的内容。   

    · WKInterfaceGroupWKInterfaceButtonWKInterfaceController类允许你指定一张图作为某些内容的背景图。

    指定你的图像资源

    下面是当你创建图像资源时要注意的准则:

    · 尽量只用PNG的格式的图。

    · 保证创建的图像的尺寸是符合你的界面的。对于你不好控制的图像,使用setWidth:setHeight:方法来做自适应。

    使用命名的图片来提高执行效率

    下面是几种更换界面对象现有图片的方式:

    ·  使用 setImageNamed:setBackgroundImageNamed:方法来分配一个已经存在于Watch app资源包里,或者是正在设备缓存中的图像。

    · 使用WatchKit的setImage:setImageData:setBackgroundImage:setBackgroundImageData:方法无线传输图像数据来扩展你的Watch app。

    用命名指定图像的好处是这些图不需要再从用户的iphone中无线传输到手表。当你指定了图片的命名,WatchKit只发送名称字符串到你的Watch app中,这样减少了时间和功耗。这个字符串用来检索Watch app包中的图像,或者从图片缓存中去取得。

    任何时候你在你的扩展中创建的__UIImage__对象,必须要先将存在于用户的iPhone上的图像对象发送到Apple Watch上才能使用。甚至使用__UIImage__的 imageNamed:方法来加载你的WatchKit扩展包资源,也不是从Watch app中得到的。

    在设备上缓存图像

    如果你频繁使用在你WatchKit扩展中创建的图像,可以把它们缓存到设备里,然后通过名称来引用。你必须在调用它们之前先缓存图片,使用__WKInterfaceDevice__的addCachedImage:name:addCachedImageWithData:name:方法。

    使用缓存图片需要注意以下两点:

    · 对于WKInterfaceImage对象,调用__setImageNamed: __方法,指定缓存图像的名称。

    · 对于WKInterfaceGroupWKInterfaceButton对象,调用__setBackgroundImageNamed:__方法,指定缓存中的名称

    Apple Watch图像的缓存是限制尺寸的,每个app大概可获得20MB的缓存空间。缓存是持久的并可以在启动的Watch app之间使用。当你达到最大缓存时,WachKit会丢弃比较老的图像,把空间让给新分配的图像。

     
    五.WatchKit Apps--表格(table)

    使用table展示内容可动态更改的列表数据。WatchKit 仅支持使用WKInterfaceTable类的单列表格。想要在table中展示数据,需要先为数据定义布局,并在运行时通过编码填写表数据。您需要在Xcode工程中做以下事情:

    在storyboard文件中:

    • 给界面控制器场景添加table对象,并在界面控制器中为table创建一个outlet。

    • 像Configuring Row Types描述的那样为table配置一个或多个row types。

    在代码中:

    您可以为每个table定义多个行类型,每种类型都有不同的外观。在运行时,您可以指定所需行类型以及它们在table中的排列顺序。对于如何配置table的详细信息,请参看WKInterfaceTable Class Reference.

    配置Row Types

    Row Type是一个在table中以单行形式展示数据的模板。每个table必须至少有一个row type,您可以根据需求定义额外的row type。您可以用row type区分table中的不同内容。比如您可能在内容行上、标题和注脚上使用不同的类型。当您为界面控制器场景添加table时,Xcode会自动创建 初始的row types供您配置。

    为table添加row  types:

    • 在storyboard文件中选中table对象

    • 打开Attributes inspector.

    • 使用Rows属性更改可用row type数量。

    每个row type最初都包含一个单个组元素。您可以为该组元素添加标签、图片以及其他所需对象。标签和图片的真实内容通常是无关紧要的。在运行时,作为配置工作的一部分,您可以替换界面对象的内容。

    想 要在运行时管理表格行的内容,您需要提供一个自定义的row controller 类。大部分row controller类包含少量代码,或者根本就不包含代码,它们主要用来访问那些包含界面对象的outlets。不过,如果您在表格行中添加了按钮或者 其他控件,那么其类也会包含一些动作方法,以响应用户与控件的交互。

    为row type定义一个row controller类:

    • 为WatchKit扩展添加一个新的Cocoa Touch类

    • 让新类继承NSObject

    • 为每个计划在运行时访问的标签、图片或者控件添加声明的属性。声明属性可使用以下格式,请更改类以匹配相应的界面对象:

    @property (weak, nonatomic) IBOutlet WKInterfaceLabel* label;

    Listing 10-1 展示了一个用于管理row type的示例类

    @interface MainRowType : NSObject
    @property (weak, nonatomic) IBOutlet WKInterfaceLabel* rowDescription;
    @property (weak, nonatomic) IBOutlet WKInterfaceImage* rowIcon;
    @end

    您可以在storyboard文件中完成row type配置。配置表格行要求设置其类,并将任何outlets连接到对应的界面对象上。您还必须提供一个方法,以便通过命名row type在运行时辨别表格行。

    在storyboard中配置row type:

    • 在storyboard文件中选中row controller对象。

    • 将row controller的Identifier属性设置为唯一值,随后您会在创建表格行时使用该标识符。

        在行类型中该值必须是唯一的,但是实际值还是由您来定夺。在Attributes inspector中设置该值。

    • 将row type的类设置为您的自定义类。您需要在Identity inspector中设置类信息。

        将row type元素和类中对应的outlets连接起来。

    • 将storyboard文件中的项目和outlets连接并绑定。当您在代码中配置table时,WatchKit会在运行时使用该信息实例化对应的类。

    Figure 10-1 在Xcode中配置一个命名为"Main Row Type"的row type,并设置为使用Listing 10-1中定义的MainRowType类。该类中的rowDescription 和rowIcon outlets被连接到行中的图片和标签。

    566.png

    运行时配置table内容

    在运行时,您可以为table添加表格行并以编程形式配置其内容。通常,添加并配置表格行是初始化界面控制器过程中的一部分。

    创建并配置列表行

    • 先决定你需要创建的行的数目和类型,这取决于你想要展示的数据类型。

    • 使用setRowTypes:或者setNumberOfRaws:withRowType:方法来创建。这两个方法都会在界面上创建新的列表行,并且会在WatchKit扩展中实例化对应的类。这些实例被保存在table中,且可通过rowControllerAtIndex:方法访问。

    • 使用rowControllerAtIndex:方法遍历表格中的每一行。

    • 使用row controller对象来配置每一行中的内容。

    setRowType: 和setNumberOfRaws:withRowType:方法实例化与storyboard文件中对应的行类型相关的类。在调用这些方法之后,紧接着 就可以直接获取最新创建的row controller对象了,当然还可以通过这些对象来设置内容。下面的Listing 10-2这段代码则是一个能创建新行并对其进行配置的简单示例。在这个例子里,使用的是在代码Listing 10-1中定义的MainRowType类,以及自定义的MyDataObject类,MyDataObject类用于提供table中用于行内容的数 据。在setNumberOfRaws:withRowType:方法中设置好数据对象之后,代码会遍历该对象,并使用它们配置表格中每一行的内容。

    Listing 10-2 创建并配置table中的行

    - (void)configureTableWithData:(NSArray*)dataObjects {
        [self.table setNumberOfRows:[dataObjects count] withRowType:@"mainRowType"];
        for (NSInteger i = 0; i < self.table.numberOfRows; i++) {
            MainRowType* theRow = [self.table rowControllerAtIndex:i];
            MyDataObject* dataObj = [dataObjects objectAtIndex:i];
      
            [theRow.rowDescription setText:dataObj.text];
            [theRow.rowIcon setImage:dataObj.image];
        }
    }

    表格行的选中处理

    界面控制器负责处理table中的行选中操作。当用户点击table中的某一行,WatchKit会选中该行并调用WatchKit扩展中正确的方法。您可以在以下地方处理表格行的选中操作:

    您可以使用以上两者之一来处理行选中事件。如果您的界面控制器有多个table,那推荐使用一个动作方法,因为您无需判定操作跟哪个table有关。如果您使用了一个动作方法,那需要遵循下面的语法:

    - (IBAction)myTableAction:(NSInteger)rowIndex

    通过选中表格行来执行app相关的任何操作,比如跳转新界面,或者更新行中显示的内容。如果您不希望用户选中表格行,可以在Storyboard中禁用相应的row controller的Selectable选项。

    六.Watch Apps--情景菜单

    Apple Watch上Retina屏的Force Touch特性提供了与内容进行交互的新途径。与点击屏幕上的项目不同,该特性并非轻触,而是需要一定的按压力度才能激活与当前界面控制器相关的情景菜单。

    情景菜单是可选的。使用情景菜单来展示当前屏幕相关的操作,图11-1展示不同数量操作下的情景菜单。

    context_menu_overview.png

    情景菜单可以展示4项操作。每个操作由一个标题和一张图片表示。点击屏幕上某张图片可执行对应的操作,点击屏幕上任何其他地方即可解除菜单。

    设计菜单项

    每个菜单项包含一个可点击区域和一个标题。可点击区域由纯色背景和您提供的图片组成。图片必须是模板图片,其alpha通道指定了绘制在纯色背景

    上的图形。模板图片的不透明部分在背景上显示为黑色,全部或者部分半透明部分可透露出背景的颜色。

    图11-2展示了菜单项目的图片和标题的布局。您提供的模板图片实际上应当比背景小一点。更多关于图片尺寸的信息和如何创建图片的指南,请参看Apple Watch Human Interface Guidelines.

    图11-2 菜单项目布局

    menu_image_template.png

    为界面控制器添加情景菜单

    你可以以编程方式或者在设计阶段配置情景菜单。您添加到storyboard中的菜单项是持久性的不能在运行时移除。您通过编程方式添加的菜单项可以被移除。使用WKInterfaceController类的方法来添加或者移除新的菜单项。

    重要:情景菜单中的菜单项目总数不能超过4个,不管是在storyboard文件中定义还是以编程方式添加,或者组合两种方式。

    向界面控制器中添加情景菜单需要:

    1.打开storyboard文件。

    2.从工具库中拖拽一个菜单项,并将其添加到界面控制器场景中。初始菜单包含一个单独的菜单项目。

    3.最多从库中拖拽3个菜单项到菜单中。你还可以使用菜单的Attributes检查器来设置菜单项数量,您添加的菜单项不能被移除。

    4.对于每个菜单项目,可使用Attributes检查器来指定菜单的标题和图片,这两者都是需要的。

    5.在界面控制器类中将每个菜单项和操作连接起来。菜单操作方法格式如下:

    - (IBAction)doMenuItemAction
    

    想要在运行时添加菜单项目,请调用界面控制器对象的addMenuItemWithImage:title:action:或者addMenuItemWithImageNamed:title:action:方法。您添加的菜单项被附加到storyboard文件中指定的对象上。以编程方式添加的菜单项目持续附属于菜单,直到您明确地移除它们或者您的界面控制器解除配置。

    当 用户点击菜单项目时,WatchKit则调用界面控制器中定义的关联动作方法。通过动作方法的实现来执行用户请求的操作。如果要求使用任何状态信息来执行 操作,那么您需要在界面控制器中保持所需状态。比如,如果一个动作依赖于表格中当前的选中行,那么您的界面控制器必须包含一个变量来跟踪当前选中的行。


    C.Glance

    一.Glance开发基础

    Glance是Apple Watch三种用户交互之一,就和它的字面意思一样,它将重要信息展示在一个视图里,让用户能在一瞥之间快速获取,是对一个完整的Watch app的有益补充。

    一个Glance是用户浏览Watch app中的重要信息的补充方式,它对于Watch app并不是必需的。Glance应该及时的提供直接相关的信息。比如,一个日历应用的glance可以展示用户的下一场会议,而一个航班应用的 glance能够展示你要搭乘班机的登机口信息。图片12-1显示了Lister示例应用的glance,它显示用户to-do列表的已完成和剩余项目的条数。

    图片12-1 Lister示例应用的Glance界面

    glance_lister_2x.png

    Glance是Watch app和WatchKit扩展的一部分,你的glance界面位于Watch app的storyboard文件当中,并且这个界面被自定义的WKInterfaceController对象管理。需要注意的是,这个glance界面控制器只负责设置glance中的内容,Glance不支持互动操作,触摸glance将会自动启动对应的Watch app。

    Glance的生命周期

    Glance界面控制器的生命周期和其他WatchKit界面控制器一样,不过glance的界面控制器的初始化要早于其它,以便于glance能快速的显示给用户。考虑到glance从初始化到显示可能会花费一些时间,可能让信息过时,你需要在willActivate方法中包含检查,以确保显示的信息是最新的。

    如需获取界面控制器的声明周期信息,阅读WatchKit Extension Life Cycle

    Glance界面指南

    Xcode提供几种固定的布局来安排glance中的内容,在选定适合你的一种布局后,遵循下面的指南来填充内容:

    · Glance的设计目的在于快速的传达信息。不要显示一堆文字。适当的使用图像、颜色和动画来快速传达信息。

    · 聚焦在最重要的数据上。Glance不是你的应用的替代。就像Watch app是对应的iOS app的缩水版,你也可以把glance看做Watch app的缩水版。

    · 不要在glance界面中包含交互控件。比如按钮、选择器、滑动器和菜单。

    · 避免使用表格和地图。尽管并没有禁止你这么做,手表上有限的空间让表格和地图不是那么有用。

    · 让显示的信息保持及时。使用所有可用的资源,包括时间和地理位置,来向用户提供有用的信息。并且注意更新你的glance,以避免因为glance初始化到显示花费的时间而让信息过时。

    一个app只允许有一个glance界面控制器,因此你需要在这一个控制器中显示所有你希望展示的内容。

    二.管理你的Glance界面

    当添加Watch app target到你的Xcode工程当中时,你能指定是否需要一个glance界面。如果你在一开始忘了添加,也能稍后向工程中加入glance。一个 glance界面控制器在storyboard中的显示有些不一样的地方,特别是,它有一个Glance入口点对象,就像图片 13-1显示的一样。

    图片 13-1 一个拥有glance入口点对象的界面控制器

    glance_interface

    Glance界面由自定义的WKInterfaceController子类驱动,此子类的实现方式和其他界面控制器类相同。

    实现一个Glance界面控制器

    Glance界面控制器的具体实现比较简单,因为它唯一的任务是设置glance中的标签和图像内容。所以你的glance基本上只需要实现两种方法:

    • 使用initWithContext:方法来初始化你的glance界面,并且设置标签和图像的初始值。

    • 基于内容的改变,使用willActivate来更新glance。

    当glance已经显示给用户后,如需更新,使用NSTimer对象,你也能使用WKInterfaceDateWKInterfaceTimer 类来显示日期和时间信息,以及倒计时或者显示特定时间。

    自定义从Glance启动应用

    当用户触摸glance,Apple Watch能够启动对应的Watch app。一般来说,启动app将显示它的主界面控制器。如果你想显示一个不同的界面控制器,调用你的glance界面控制器的updateUserActivity:userInfo:方法。

    调用updateUserActivity:userInfo:方法将告诉WatchKit来请求启动时需要显示的界面控制器。在启动进程当中,WatchKit调用主界面控制器的actionForUserActivity:context: 方法来找出到底需要显示哪个界面控制器。在你的Watch app的主界面控制器里实现这个方法,并让它返回你需要的界面控制器名称。通过这个方法,你也能提供一个上下文对象,并将它作为参数传递给指定的界面控制器的initWithContext: 方法。

    总结一下,为了在触摸glance时启动并显示一个不同的界面控制器,你需要按如下步骤操作:

    • 在glance界面控制器中:

      •   配置glance的initWithContext:willActivate ,这一步和其他情况是相同的。

      •   调用updateUserActivity:userInfo:方法,并且使用userinfo参数来传达你的应用glance的状态。

    • 在你的应用的主界面控制器:

      •   实现actionForUserActivity:context: 方法,使用提供的userinfo字典来确定显示哪个界面控制器,你还应该生成一个上下文对象并将它传递给指定的界面控制器。

    D:通知

    一.有关通知的一些要点

    如果您的 iOS 应用支持本地或远程通知,Apple Watch 也会同步显示这些通知。当某个应用的本地或远程通知抵达用户的 iPhone 后,iOS 将会自行判断显示该通知的设备(iPhone 还是 Apple Watch)。对于发送到 Apple Watch 的通知来说,系统首先会暗示用户该通知已经送达。如果用户选择查看这个通知,系统将会首先展示该通知的概览。如果用户选择继续浏览这个通知,系统将会显示 其具体信息。借助上述功能,用户可以选择忽略该通知,也可以选择单击某个可用的按钮(如果存在的话)来查看该通知或者启动您的应用。

    要支持通知的话,应用其实并不需要面面俱到。系统提供了一个默认通知界面以展示来自通知的提醒信息。然后,应用可以自定义这个通知界面,同时向里面添加自定义图形、内容,甚至商标。

    提示:要了解更多关于 iOS 应用是如何支持通知的信息,请参阅 Local and Remote Notification Programming Guide

    Short-Look 界面

    当 用户第一次看通知的时候,系统将会显示这个 Short-Look 界面,图14-1显示了一个大概的例子。Short-Look 界面是一个不可滚动的视图,并且不可以被定制。系统使用一个通用的模板来显示应用名称、图标,以及存储在本地或远程通知消息中的标题。如果用户一直在查看 这个通知,系统将会迅速将 Short-Look 界面转变为 Long-Look 界面。

    QQ截图20141127111840.jpg

    Long-Look 界面

    Long-Look 界面是一个可滚动的界面,它显示了通知的全部内容,以及所有的关联按钮。如果您没有提供自定义的通知界面,那么 Apple Watch将会显示默认界面,里面包括了应用图标、通知的标题,以及提示消息。而如果您提供了自定义的通知界面,那么 Apple Watch 将会显示它。

    Long-Look 通知界面分为三个区域:

    · 窗扇是一个包含有应用图标和应用名称的区域。窗扇的背景默认情况下是透明的,但是您可以在自定义通知界面中改变它的背景颜色。

    · 内容区域包含有即时通知的详细内容。对于自定义界面来说,您可以配置该界面的起始位置,是从窗扇顶部开始,还是在窗扇下面开始。要了解更多关于如何自定义这个区域的内容,请参阅 Custom Notification Interfaces

    · 按钮区域包含有一个取消按钮和其他您应用定义的按钮。取消按钮是系统提供的,并且不可以删除它。

    图14-2展示了一个包含有两个按钮的Long-Look通知界面示例。

    图14-2 Long-Look 通知界面

    notification_long_look.png

    单 击窗扇区和内容区的任意位置都将启动您的应用。单击任何一个由您应用定义的按钮都将启动 WatchKit 扩展或者 iOS 应用,然后向其传递动作信息。发送给 WatchKit 扩展的是前台动作,而发送给 iOS 应用的是则后台动作。单击取消按钮将会直接关闭这个通知。

    欲了解如何制作一个自定义 Long-Look 界面,请参阅 Custom Notification Interfaces

    为通知上添加按钮

    按钮通过为通知提供封装的动作响应来帮用户节省时间。Apple Watch使用iOS 应用注册的交互通知界面来显示按钮。在 iOS 8 以及更高版本中,应用都需要使用UIUserNotificationSettings 对象来注册它们显示的生成通知对话框。一旦对象创建,应用也同样可以指定自定义通知类别以及其对应的按钮动作。Apple Watch使用这个类别信息来添加相应的按钮到 Long-Look 界面上。

    代 码列14-1显示了一个方法的部分实现代码,其用来为一个示例应用注册设置及类别。这个方法在containing iOS app中声明,而不是在 WatchKit 扩展中声明,并且它在启动时被 iOS 应用委托调用。这个实现代码展示了“invitation”类别的创建和注册,其包含了接收或拒接会议邀请的按钮动作。

    代码列14-1 在iOS 容器应用(containing app)中注册按钮动作

    - (void)registerSettingsAndCategories {
        // Create a mutable set to store the category definitions.
        NSMutableSet* categories = [NSMutableSet set];
      
        // Define the actions for a meeting invite notification.
        UIMutableUserNotificationAction* acceptAction = [[UIMutableUserNotificationAction alloc] init];
        acceptAction.title = NSLocalizedString(@"Accept", @"Accept invitation");
        acceptAction.identifier = @"accept";
        acceptAction.activationMode = UIUserNotificationActivationModeBackground;
        acceptAction.authenticationRequired = NO;
      
        UIMutableUserNotificationAction* declineAction = [[UIMutableUserNotificationAction alloc] init];
        declineAction.title = NSLocalizedString(@"Decline", @"Decline invitation");
        declineAction.identifier = @"decline";
        declineAction.activationMode = UIUserNotificationActivationModeBackground;
        declineAction.authenticationRequired = NO;
      
        // Create the category object and add it to the set.
        UIMutableUserNotificationCategory* inviteCategory = [[UIMutableUserNotificationCategory alloc] init];
        [inviteCategory setActions:@[acceptAction, declineAction]
                        forContext:UIUserNotificationActionContextDefault];
        inviteCategory.identifier = @"invitation";
      
        [categories addObject:inviteCategory];
      
        // Configure other actions and categories and add them to the set...
      
        UIUserNotificationSettings* settings = [UIUserNotificationSettings settingsForTypes:
            (UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound)
            categories:categories];
      
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
    }

    要了解如何在 iOS 应用中配置类别和按钮动作的信息,请参阅 Local and Remote Notification Programming Guide

    让按钮响应单击动作

    当用户在通知界面里面单击按钮时,系统使用在注册过的 UIUserNotificationAction 对象里面的信息,来决定了如何处理这个操作。按钮动作可以在前台处理,也可以在后台处理,并且它可以附带或者不附带用户的认证信息。不过,前台处理按钮动作和后台处理是有一定区别的:

    · 前台按钮动作将启动您的 Watch 应用,然后将按下的按钮的 ID 传递给主界面控制器。

    · 后台按钮动作将在后台启动 containing iOS app,以让其来处理这个动作。被选择动作的相关信息将被传递给应用委托中的application:handleActionWithIdentifier:forRemoteNotification:completionHandler:application:handleActionWithIdentifier:forLocalNotification:completionHandler:方法。

    对于前台按钮动作来说,要记住WKUserNotificationInterfaceController子类并不处理这个按钮动作。选中一个前台按钮动作将启动您的应用,并为您应用的主入口点载入界面控制器。该初始界面控制器负责处理所有按钮动作。这个界面控制器必须实现 handleActionWithIdentifier:forRemoteNotification:方法(如果适用的话)来处理和本地或远程通知相关的按钮动作。

    二.自定义通知界面

    自定义通知界面含有两个独立的界面:一个静态界面,一个动态界面。动态界面显示 了完整的自定义通知内容样式,并且它可以包含由 WatchKit 扩展提供的定制内容和图形。静态界面是一个简单的界面,它仅仅只含有通知的提示信息,以及您在设计时配置的静态图像以及文本信息。

    当 您在 storyboard 文件中添加新的通知界面控制器的时候,Xcode 将会创建一个独立的 storyboard 场景,其中包含有一个动态界面和一个静态界面,如图15-1所示。静态界面是必需的,如果需要的话您可以删除动态界面。如果静态界面已经可以显示您想要的 全部信息,那么您就可以删除动态界面。动态界面和静态场景都和相同的通知类型相关联,而这个通知类型可以用来指定使用何种通知类别来关联静态界面。

    图15-1 静态界面和动态界面

    notification_static_dynamic.png

    当 合适类型的通知抵达时,WatchKit便尝试显示动态界面。如果您并未提供静态界面,或者出于某些原因您的动态界面不可用,那么Apple Watch 将会用静态界面来代替。Apple Watch 同样也会在您明白告知使用静态场景时显示静态界面。配置动态界面需要调用didReceiveRemoteNotification:withCompletion:didReceiveLocalNotification:withCompletion:方法。如果 这些方法使用WKUserNotificationInterfaceTypeDefault常量来调用完成处理程序的话,那么 Apple Watch将会显示静态场景。

    配置自定义界面的通知类型

    每个通知界面必须分配一个通知类型,以告知 Apple Watch 何时使用它。到来的通知可以在消息载体中包含类别值。Apple Watch 使用这些类别值来决定显示何种通知场景。如果到来的通知并未包含类别值,那么Apple Watch会展示一个类型被设定为`default`的通知界面。

    要给通知界面分配通知类型,请在 storyboard 中配置和场景对应的通知类别(Notification Catagory)对象。该对象的属性检查器含有一个Name 属性,如图15-2所示。在这个属性中输入通知类型的类别名称。您同样也可以使用该对象来指定窗扇的颜色。每个通知场景的Name 属性不能相同。

    图15-2 配置通知类型信息

    256.png

    当生成远程通知时,您的服务器要通过在其`aps`字典中包含category键的值来指定通知类型。`category`的键值要和您在 iOS 应用以及您在通知类别对象中的 Name 属性指定的一样。例如,在图15-2中,类别文本是`MeetingInvite`。

    提示:类别字符串同样定义了被添加到通知界面末端的操作按钮(如果存在的话)。欲了解关于支持自定义按钮动作的更多信息,请参阅Adding Action Buttons to Notifications

    配置静态通知界面

    使用静态通知界面来定义一个自定义通知界面的基本样式。使用静态界面的目的是在 WatchKit 扩展不能及时配置动态界面的时候,提供一个回退界面。创建静态界面的规则如下:

    · 所有的图像必须驻留在 Watch 应用包中

    · 界面不能包含控件、表格、地图,以及其他交互元素

    · 界面的`notificationAlertLabel`输出口必须与某个标签相关联。标签的文本设置为通知的警告信息。其他的标签文本不变,并且只能够在设计时设置。

    图15-3显示了在一个日历应用当中的静态和静态场景,其使用了自定义通知界面。通知箭头指向了静态场景,其中包含了自定义图标和两个标签。“Message”标签和`notificationAlertLabel`输出口连接,因此它将在运行时接收通知的警告信息。

    图15-3 单一通知类型的静态和动态场景

    155.png

    除了和`notificationAlertLabel`输出口相连的标签外,在静态通知场景中的标签和图像是不能够改变的。当您在设计界面时请记住这条准则,并确保每个标签的文本是合适的。

    配置动态通知界面

    动态通知界面给用户提供了一个更丰富的通知体验。借助动态界面,您可以显示很多的内容。您可以添加额外的信息,配置多个标签,动态地生成内容,等等。

    实现动态通知界面需要创建一个自定义的WKUserNotificationInterfaceController子类。该子类的实现代码负责在通知需要显示时配置界面。

    设计动态界面

    配置动态界面和配置其他界面控制器场景十分相似。

    您可以在子类中包含标签、图像以及场景中的其他对象,并使用这些输出口以在运行时配置场景的内容。单击通知界面将启动应用,因此通知界面不应当包含交互控制。

    · 可在绝大多数界面上使用标签、图像、组以及分隔符。

    · 您可能想要在需要的时候使用表格和地图。

    · 不要包含有按钮、开关,或者其他交互控件。

    在运行时配置动态界面

    界面初始化之后,WatchKit将负载数据传输到通知界面控制器的不同方法中。对于远程通知来说,WatchKit 调用didReceiveRemoteNotification:withCompletion:方法。对于本地通知来说,它调用didReceiveLocalNotification:withCompletion:方法。请使用接受到的数据来配置您的通知界面。界面配置完成后,您必须要调用所提供的完成处理代码块来让 WatchKit 知道您的界面已经准备就绪。图15-4显示了处理远程通知的初始化和配置进程。

    图15-4 显示自定义通知界面

    notification_event_cycle_2x.png

    始终选择使用 didReceiveRemoteNotification:withCompletion:didReceiveLocalNotification:withCompletion:方法来配置您的通知界面。当实现完这两个方法后,尽快运行所提供的完成处理方法来配置界面。然后尽快运行这个代码块。如果您等待了太长时间,那么Ale Watch将会使用静态界面。

    代 码表15-1显示了`didReceiveRemoteNotification:withCompletion:`方法的实现例程。这个方法被一个发送 远程通知的虚构日历应用所实现,用来提示某个新的会议邀请。这个方法从远程通知负载中获取数据,然后使用这个数据来设置通知界面的标签值。为了简便,这个 示例假定服务器发送的键值中都有合适的对应值,但是您自己的代码应该执行必要的错误检查,来保证复杂数据是由有效的。在配置完标签后,这个方法将调用完成 处理器,来让 WatchKit 知晓自定义界面已经就绪。

    代码表15-1 配置远程通知的自定义界面

    // Standard remote notification payload keys.
    NSString* apsKeyString = @"aps";
    NSString* titleKeyString = @"title";
     
    // Payload keys that are specific to the app.
    NSString* customDataKey = @"cal";
    NSString* invitationDateKey = @"date";
    NSString* invitationLocationKey = @"loc";
    NSString* invitationNotesKey = @"note";
     
    - (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void(^)(WKUserNotificationInterfaceType interface)) completionHandler {
        // Get the aps dictionary from the payload.
        NSDictionary* apsDict = [remoteNotification objectForKey:apsKeyString];
     
        // Retrieve the title of the invitation.
        NSString* titleString = [apsDict objectForKey:titleKeyString];
        [self.titleLabel setText:titleString];
     
        // Extract the date and time from the custom section of the payload.
        // The date/time information is stored as the number of seconds since 1970.
        NSDictionary* customDataDict = [remoteNotification objectForKey:customDataKey];
        NSNumber* dateValue = [customDataDict objectForKey:invitationDateKey];
        NSDate* inviteDate = [NSDate dateWithTimeIntervalSince1970:[dateValue doubleValue]];
     
        // Format the date and time strings.
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
     
        // Call a custom method to get the localized date format string for the user.
        // The default date format string is "EEE, MMM d".
        dateFormatter.dateFormat = [self dateFormatForCurrentUser];
        NSString *formattedDateString = [dateFormatter stringFromDate:inviteDate];
     
        // Call a custom method to get the localized time format string for the user.
        // The default time format string is "h:mm a".
        dateFormatter.dateFormat = [self timeFormatForCurrentUser];
        NSString *formattedTimeString = [dateFormatter stringFromDate:inviteDate];
     
        // Set the date and time in the corresponding labels.
        [self.dateLabel setText:formattedDateString];
        [self.timeLabel setText:formattedTimeString];
     
        // Set the location of the meeting.
        NSString* locationString = [customDataDict objectForKey:invitationLocationKey];
        [self.locationLabel setText:locationString];
     
        // Set the invitation's notes (if any).
        NSString* notesString = [customDataDict objectForKey:invitationNotesKey];
        [self.notesLabel setText:notesString];
     
        // Tell WatchKit to display the custom interface.
        completionHandler(WKUserNotificationInterfaceTypeCustom);
    }
    

    当调用完成处理代码块时,如果你想 WatchKit 要显示静态界面的话,请指定WKUserNotificationInterfaceTypeDefault的内容。

    测试您的自定义界面

    当您准备在模拟器测试您的自定义界面时,如果您还没有确定扩展已制作完成的话,请创建一个自定义编译方案来运行您的通知。使用 Xcode 模板提供的`RemoteNotificationPayload.json`文件来指定您的负载数据。要了解更多关于设置编译方案以及配置负载数据的内 容,请参阅The Build, Run, Debug Process

     
     
     
  • 相关阅读:
    软件测试之测试策略
    如何在需求不明确的情况下保证测试质量
    《Web 前端面试指南》1、JavaScript 闭包深入浅出
    Vue-Router 页面正在加载特效
    Webpack 配置摘要
    svn 常用命令总结
    Vue + Webpack + Vue-loader 系列教程(2)相关配置篇
    Vue + Webpack + Vue-loader 系列教程(1)功能介绍篇
    关于Vue.js 2.0 的 Vuex 2.0,你需要更新的知识库
    Vue.js 2.0 和 React、Augular等其他框架的全方位对比
  • 原文地址:https://www.cnblogs.com/lxllanou/p/4425741.html
Copyright © 2011-2022 走看看