WatchKit开发的博客

同步新浪微博:@WatchKit开发

watchOS2 Complication模板一览(22个)

CLKComplicationTemplateCircularSmallRingImage



CLKComplicationTemplateCircularSmallRingText



CLKComplicationTemplateCircularSmallSimpleImage



CLKComplicationTemplateCircularSmallSimpleText



CLKComplicationTemplateCircularSmallStackImage



CLKComplicationTemplateCircularSmallStackText



CLKComplicationTemplateModularLargeColumns



CLKComplicationTemplateModularLargeStandardBody



CLKComplicationTemplateModularLargeTable



CLKComplicationTemplateModularLargeTallBody



CLKComplicationTemplateModularSmallColumnsText



CLKComplicationTemplateModularSmallRingImage



CLKComplicationTemplateModularSmallRingText



CLKComplicationTemplateModularSmallSimpleImage



CLKComplicationTemplateModularSmallSimpleText



CLKComplicationTemplateModularSmallStackImage



CLKComplicationTemplateModularSmallStackText



CLKComplicationTemplateUtilitarianLargeFlat



CLKComplicationTemplateUtilitarianSmallFlat



CLKComplicationTemplateUtilitarianSmallRingImage



CLKComplicationTemplateUtilitarianSmallRingText



CLKComplicationTemplateUtilitarianSmallSquare



【译】构建AppleWatch应用(iOS App与WatchKit Extension的数据通信)

NSUserDefaults

NSUserDefaults是快速共享信息的途径。它适合存储各种快速访问和计算的小型数据,比如用户名与档案信息。如果希望使用UserDefaults,请用于静态数据这样用户不必考虑数值的变化。

你需要设定App Group来让设备通过共享容器来实现数据共享,确保手表扩展和ios target都已如此设置。基本上就是针对两个设备创建一个统一的App Group标识符。如果需要删除它,可以以类似的方法进行。

你可以通过之前创建的App Group名来使用defaults,基本上就是为特定的key键值设置对象。在iPhone上,用户输入了文本,保存,文本就存到了应用共享的UserDefaults里。在Watch上,你可以从AppGroup得到defaults然后进行手表显示内容的更新。

// on the iPhone app
letdefaults=NSUserDefaults(suiteName:"group.com.natashatherobot.userdefaults")
    letkey="userInput"
    
    overridefuncviewDidLoad(){
        super.viewDidLoad()

        textLabel.text=defaults?.stringForKey(key)??"Type Something..."
    }


    @IBActionfunconSaveTap(sender:AnyObject){
        
        letsharedText=textField.text
        
        textLabel.text=sharedText
        
        defaults?.setObject(sharedText,forKey:key)
        defaults?.synchronize()
    }

// WatchKit
classInterfaceController:WKInterfaceController{

    @IBOutletweakvartextLabel:WKInterfaceLabel!
    
    letdefaults=NSUserDefaults(suiteName:
        "group.com.natashatherobot.userdefaults")
    
    varuserInput:String?{
        defaults?.synchronize()
        returndefaults?.stringForKey("userInput")
    }

 

NSFileCoordinator

对更大型的数据来说,NSFileCoordinator是管理应用和watch扩展的共享空间里文件的方式之一。对于有限列表的内容它很合适,同时也适用于图像文件。

下面的例子是个简单的代办事项列表app,在手机上增加任务然后暑假传输到WatchKit扩展并在手表上显示。你的视图控制器需要遵循NSFilePresenter协议,除了实现两个必需方法,其它不是很关键。FilePresenter协议有一个item URL,就是填你的AppGroup标识符的地方。通过URL,你在对应目录建立一个文件。有必要的话你也可以通过操作队列来控制多线程访问。

另外,presentedItemDidChange这个代理方法,在FilePresenter里通知你是否一个对象发生了改变,来让你更新app数据而无需用户手动刷新。

然而这里还是有个关于NSFileCoordinator与NSFilePresenter 的bug而不方便在扩展里使用。具体可参见Natasha的网站。

在代办事项数组里利用FileCoordinator写入一个文件,可以通过读写文件以实现打包和解包事项的数据到事项数组,接下来可以依据文件里的事项数据计算生成表格。需要注意的是如果你设计了删除功能,而watch扩展和iPhone应用都能修改文件,会遇到线程同步的麻烦。

// iPhone app
    privatefuncsaveTodoItem(todoItem:String){
            
            // write item into the todo items array
            ifletpresentedItemURL=presentedItemURL{
                
                fileCoordinator.coordinateWritingItemAtURL(presentedItemURL,options:nil,error:nil)
                    {[unownedself](newURL)->Voidin
                        
                        self.todoItems.insert(todoItem,atIndex:0)
                        
                        letdataToSave=NSKeyedArchiver.archivedDataWithRootObject(self.todoItems)
                        letsuccess=dataToSave.writeToURL(newURL,atomically:true)
                }
            }
        }

// in the Watch
// MARK: Populate Table From File Coordinator
    
    privatefuncfetchTodoItems(){
        
        letfileCoordinator=NSFileCoordinator()
        
        ifletpresentedItemURL=presentedItemURL{
            
            fileCoordinator.coordinateReadingItemAtURL(presentedItemURL,options:nil,error:nil)
                {[unownedself](newURL)->Voidin
                    
                    ifletsavedData=NSData(contentsOfURL:newURL){
                        self.todoItems=NSKeyedUnarchiver.unarchiveObjectWithData(savedData)as[String]
                        self.populateTableWithTodoItems(self.todoItems)
                    }
            }
        }
    }

Frameworks

“If the code appears more than once, it probably belongs in a framework.(如果代码出现超过一次,应该考虑能否放到框架里)”
-WWDC 2014, Building Modern Frameworks

框架对于业务逻辑、CoreData、可重用UI组件来说很棒。就像WWDC里说的那样,你可以将重复代码放到框架里。在FileCoordinator的例子里,我们获取和读写文件的代码出现了两次,可以把它们提取到一个framework框架里。建立框架很简单:建立新target,选择Cocoa Touch framework,然后命名。它会在你的iOS应用里自动链接,因此也不要忘了在WatchKit扩展里进行链接。

关键的一点,特别是对于Swift语言来说,应该把框架认作一个API。它需要声明为公共的(public),因为这是iOS应用和watchkit扩展共用的接口。因此如果你在建立对象类,确保public关键字也加上了。这样在手机和手表应用里你导入了框架就可以访问任何公共内容。

importWatchKit
importMySharedDataLayer

classInterfaceController:WKInterfaceController{

    @IBOutletweakvarfavoriteThingsLabel:WKInterfaceLabel!
    
    overridefuncawakeWithContext(context:AnyObject?){
        super.awakeWithContext(context)
        
        letmyFavoriteThings=MySharedData().myFavoriteThings
        
        letfavoriteThingsString=", ".join(myFavoriteThings)
        favoriteThingsLabel.setText("My favorite things are \(favoriteThingsString)")
    }

}

Keychain Sharing

钥匙链共享是针对更高安全性要求的数据的。UserDefaults提供的安全性不满足时,你可以用钥匙链共享来保障信息安全及跨扩展的共享能力。

WatchKit目前的一个大问题是没有认证机制。苹果提供了KeychainIteamWrapper的示例,但API太老不支持ARC。我推荐使用这个版本https://gist.github.com/dhoerl/1170641,它基于ARC并有清晰的接口。

根据问题是如何通过access group初始化KeychainItemWrapper。与AppGroup的概念类似,设备之间有共享空间。你在iOS和WatchKit扩展中都需要钥匙链来访问用户数据。通过键值存储体系,你设定用户名和密码并用同一个标识符建立同一类型的keychain项。这个例子里仅展示了当用户填好用户名密码时WatchKit扩展展示数据的这一工作过程。

// iPhone app
@IBActionfunconSaveTap(sender:AnyObject){
        
        letusername=usernameTextField.text
        letpassword=passwordTextField.text
        
        letkeychainItem=KeychainItemWrapper(identifier:"SharingViaKeychain",accessGroup:"W6GNU64U6Q.com.natashatherobot.SharingViaKeychain")
        
// WatchKit extension
letkeychainItem=KeychainItemWrapper(identifier:"SharingViaKeychain",accessGroup:"W6GNU64U6Q.com.natashatherobot.SharingViaKeychain")
        
        
        letpasswordData=keychainItem.objectForKey(kSecValueData)asNSData
        letpassword=NSString(data:passwordData,encoding:NSUTF8StringEncoding)
        
        letusername=keychainItem.objectForKey(kSecAttrAccount)as?String

Q&A (暂不翻译)

 

WatchKit视图转换控制小结

    对于WatchKit App,是可以在Storyboard里建立多个InterfaceController并像在iOS应用一样直观的画出视图转换连接的,当然我们也可以通过视图控制器代码实现相应视图切换与跳转。

    简单来说视图转换连接有三种形式:

    1.Push/Pop - 提供Hierarchical形态下基于栈(Stack)的视图控制,Hierarchical意为分级视图架构,效果类似iOS应用里的Navigation界面,主要用于导航类的视图转换。首先必须有一个根视图控制器,通过触发Storyboard里拖拽生成pushSegue或用pushControllerWithNames:代码将下一级视图压入(Push)堆栈并弹出时,会在标题栏前加上<左箭头,点击会返回上一级视图,也可以执行popController()代码,而直接返回根视图可以用popToRootController()。

    2.Present - 与Push的方式比较类似,会跳转到一个Modal(模态)视图,区别是当前视图的控制器会被释放并执行didDeactivate方法,但我们仍然可以在模态视图里点标题栏或者在视图控制器代码调用dismissController()方法以重新激活上一个视图。

    3.Next page - 分页视图模式,多个视图的横向并列排列,左右扫会切换,下方有小圆点进行标识。现在很多iOS应用启动时都有这种分页导航用于操作方法或新功能的提示。Storyboard里按住control从视图A拖到视图B选择next page可以建立此关系。

   具体可以看下“WatchKit编程指南:WatchKit Apps--界面导航”这篇文章:

  http://www.cocoachina.com/ios/20141216/10642.html

  下面重点总结一些常见问题与解决方法:

  1.Q:主控制器为分页视图时,无法push分级的视图栈;主控制器为分级视图时,无法push分页视图。

   A:Page-based与Stack模式是互斥的,因此必须使用模态方式进行切换。主控制器为分页视图时,要正确弹出一个分级视图栈可以用presentControllerWithName:方法,而主控制器为分级视图时,要正确弹出单页视图,用

presentControllerWithName:conext:方法,而弹出多个页组成的分页视图,需要改为用presentControllerWithNames:contexts:方法。

  2.Q:怎么在视图间传递参数?

  A: 利用context参数封装参数对象,若在push模式下需要传递视图控制器delegate,可参考本人之前翻译的那篇“WatchKit的代理与上下文”文章。

3. Q: 定义了ForceTouch时的上下文菜单,结果切换视图时再激活上下文菜单并点击时报错说视图指针丢失。

  A: 因为你代码里是在awakeWithContext:里定义的上下文菜单,然后用模态方式弹出,这样切换视图后原视图被销毁就无法正确调用了菜单内容代码。解决方法是:I.用push方式切换视图,这样主视图存于视图栈并不会销毁对象;II.依旧用模态方式切换视图,但把上下文菜单定义代码放到willActivate()函数里,这样菜单定义会重新加载不至于失效。


【译】WatchKit的代理与上下文

有限的API
当你使用WatchKit时你会发现其API比UIKit受到更多限制,这事实上是件好事,毕竟我们是在为适合手腕的设备编程。因为控件里没有取值的API,界面元素只能设置值而不可读取。标签文本目前必须在代码里单独设置状态变化跟踪。另一方面你会发现没有prepareForSegue方法,然而有contextForSegueWithIdentifier方法。传给下个视图控制器的context(上下文)必须是AnyObject类型的单一对象,在这里我们可以传数据与代理对象。我(作者)发现一个直观方法去同时发送数据对象与代理。

Context

让我们看看WKInterfaceController怎么获取自身上下文对象的。

class InterfaceController: WKInterfaceController {
    override func awakeWithContext(context: AnyObject?) { 

   super.awakeWithContext(context)
    }
}

注意上面的代码里我们在storyboard加载完毕后调用了awakeWithContext方法,以提供AnyObject类型的上下文对象。但看起来不那么理想毕竟对象必须转换为我们需要的类型,而且AnyObject类型而不是Any类型导致了结构体没法使用,必须转化为类实例。

然而我们希望简单的将数据对象作为上下文发送给控制器时,却没道理让数据对象包含一个控制器代理属性。更合适的办法是包装它成为一个既有数据又有代理的对象,我们把这个对象类叫做Context。

我们开始吧,这样会有个问题看你们能发现否

class Context {
    var object: AnyObject?
    var delegate: AnyObject?
}

然而看起来这虽像个通用解决方案,但有个大缺点。我们不能像习惯的delegates那样将转换代理变量为协议(protocol)类型,Any类型也是无法实现的,找到通用的方法相当困难。让我们试着把Context变为通用协议这样其它上下文对象可以适配且可以指定具体协议类型。

protocol Context {
    typealias DelType
    typealias ObjType
    var delegate: DelType? { get set }
    var object: ObjType? { get set }
}

这个协议里我们不会指定代理或对象的类型,我们让适配协议的类去做这项工作。

class BookControllerContext: Context {
    typealias DelType = BookControllerDelegate
    typealias ObjType = Book
    var delegate: DelType?
    weak var object: ObjType?
}

这样上下文提供的信息不仅是模型而又有控制器代理,我们能够在控制器上下文里调用它了。

传值

现在我们看看怎么将给定上下文对象从一个控制器传给另一个。这个例子里我们假设外部控制器维护书的列表而内部控制器是描述某本具体书的。

这是我们书(Book)的模型对象类

class Book {
    var title: String
    var author: String
    var description: String
    var price: Double
    var owned: Bool
    init(title: String, author: String, description: String, price: Double, owned: Bool) {

    self.title = title

    self.author = author

    self.description = description

    self.price = price

    self.owned = owned
    }
}

这是数据行的视图类
class BookRow: NSObject {
    @IBOutlet weak var bookTitleLabel: WKInterfaceLabel!
    @IBOutlet weak var bookAuthorLabel: WKInterfaceLabel!
    @IBOutlet weak var bookPriceLabel: WKInterfaceLabel!
    @IBOutlet weak var bookBuyLabel: WKInterfaceLabel!
}

这是用于内部控制器通知外部控制器他们买了某书的代理协议

protocol BookControllerDelegate  {
    func didBuyBook(book: Book)
}

这是书列表的外部控制器

class BookListController: WKInterfaceController, BookControllerDelegate {

    // MARK: BookListController

    @IBOutlet var table: WKInterfaceTable!

    var books = [
// Pricing based on Amazon.com on Jan 27, 2014
Book(title: "NSHipster Obscure Topics in Cocoa & Objective-C", author: "Mattt Thompson", description: "To be an NSHipster is to care deeply about the craft of writing code.", price: 25.29, owned: false),
Book(title: "Functional Programming in Swift", author: "Chris Eidhof, Florian Kugler, Wouter Swierstra", description: "This book will teach you how to use Swift to apply functional programming techniques to your iOS or OS X projects", price: 53.07, owned: false)
    ]

func updateDisplay() {
table.setNumberOfRows(books.count, withRowType: "bookRow")

for i in 0..<table.numberOfRows {
    var row = table.rowControllerAtIndex(i) as BookRow
    row.bookTitleLabel.setText(books[i].title)
    row.bookAuthorLabel.setText(books[i].author)
    row.bookPriceLabel.setText("\(books[i].price)")
    if books[i].owned {
row.bookBuyLabel.setText("Read")
    } else {
row.bookBuyLabel.setText("Buy")
    }
}
 }

    // MARK: WKInterfaceController
override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
var context = BookControllerContext()
context.object = books[rowIndex]
context.delegate = self
return context
}

override func willActivate() {
super.willActivate()
updateDisplay()
}

    // MARK: BookControllerDelegate
func didBuyBook(book: Book) {
book.owned = true
updateDisplay()
 }
}
注意上面contextForSegueWithIdentifier方法允许我们建立上下文对象并返回它。现在我们看看内部控制器怎么获取并设置代理

class BookListController: WKInterfaceController, BookControllerDelegate {

    // MARK: BookListController

    @IBOutlet var table: WKInterfaceTable!

    var books = [
// Pricing based on Amazon.com on Jan 27, 2014
Book(title: "NSHipster Obscure Topics in Cocoa & Objective-C", author: "Mattt Thompson", description: "To be an NSHipster is to care deeply about the craft of writing code.", price: 25.29, owned: false),
Book(title: "Functional Programming in Swift", author: "Chris Eidhof, Florian Kugler, Wouter Swierstra", description: "This book will teach you how to use Swift to apply functional programming techniques to your iOS or OS X projects", price: 53.07, owned: false)
    ]

func updateDisplay() {
table.setNumberOfRows(books.count, withRowType: "bookRow")

for i in 0..<table.numberOfRows {
    var row = table.rowControllerAtIndex(i) as BookRow
    row.bookTitleLabel.setText(books[i].title)
    row.bookAuthorLabel.setText(books[i].author)
    row.bookPriceLabel.setText("\(books[i].price)")
    if books[i].owned {
     row.bookBuyLabel.setText("Read")
    } else {
     row.bookBuyLabel.setText("Buy")
    }
}
  }

    // MARK: WKInterfaceController
override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
var context = BookControllerContext()
context.object = books[rowIndex]
context.delegate = self
return context
 }

override func willActivate() {
super.willActivate()
updateDisplay()
 }

    // MARK: BookControllerDelegate
func didBuyBook(book: Book) {
book.owned = true
updateDisplay()
  }
}
全部源码

这里是全部代码

import WatchKit

/*
 * Model
 */
protocol Context {
    typealias DelType
    typealias ObjType
    var delegate: DelType? { get set }
    var object: ObjType? { get set }
}

class BookControllerContext: Context {
    typealias DelType = BookControllerDelegate
    typealias ObjType = Book
    var delegate: DelType?
    weak var object: ObjType?
}

class Book {
    var title: String
    var author: String
    var description: String
    var price: Double
    var owned: Bool
    init(title: String, author: String, description: String, price: Double, owned: Bool) {
self.title = title
self.author = author
self.description = description
self.price = price
self.owned = owned
    }
}

/*
 * View
 */
class BookRow: NSObject {
    @IBOutlet weak var bookTitleLabel: WKInterfaceLabel!
    @IBOutlet weak var bookAuthorLabel: WKInterfaceLabel!
    @IBOutlet weak var bookPriceLabel: WKInterfaceLabel!
    @IBOutlet weak var bookBuyLabel: WKInterfaceLabel!
}

/*
 * Controller
 */
protocol BookControllerDelegate  {
    func didBuyBook(book: Book)
}

class BookController: WKInterfaceController {

    var delegate: BookControllerDelegate?
    var book: Book!

    override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)

let ctx = context as BookControllerContext
book = ctx.object
delegate = ctx.delegate
    }

    @IBOutlet weak var buyBtn: WKInterfaceButton!
    @IBAction func buyPressed() {
delegate?.didBuyBook(book)
popController()
    }
}

class BookListController: WKInterfaceController, BookControllerDelegate {

    // MARK: BookListController

    @IBOutlet var table: WKInterfaceTable!

    var books = [
// Pricing based on Amazon.com on Jan 27, 2014
Book(title: "NSHipster Obscure Topics in Cocoa & Objective-C", author: "Mattt Thompson", description: "To be an NSHipster is to care deeply about the craft of writing code.", price: 25.29, owned: false),
Book(title: "Functional Programming in Swift", author: "Chris Eidhof, Florian Kugler, Wouter Swierstra", description: "This book will teach you how to use Swift to apply functional programming techniques to your iOS or OS X projects", price: 53.07, owned: false)
    ]

    func updateDisplay() {
table.setNumberOfRows(books.count, withRowType: "bookRow")

for i in 0..<table.numberOfRows {
    var row = table.rowControllerAtIndex(i) as BookRow
    row.bookTitleLabel.setText(books[i].title)
    row.bookAuthorLabel.setText(books[i].author)
    row.bookPriceLabel.setText("\(books[i].price)")
    if books[i].owned {
row.bookBuyLabel.setText("Read")
    } else {
row.bookBuyLabel.setText("Buy")
    }
}
    }

    // MARK: WKInterfaceController
    override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
var context = BookControllerContext()
context.object = books[rowIndex]
context.delegate = self
return context
    }

    override func willActivate() {
super.willActivate()

updateDisplay()
    }

    // MARK: BookControllerDelegate
    func didBuyBook(book: Book) {
book.owned = true
updateDisplay()
    }
}
结论

你们看到怎么在WatchKit里同时将数据与代理传给视图控制器。我们可以遵循Context协议适配新的上下文,比方我们有个作者视图控制器需要传包含Author(作者)数据与AuthorControllerDelegate(作者控制器代理)的AuthorControllerContext(作者控制器上下文)对象。这是我(作者)目前找到的最好办法来解决WatchKit的有关限制。我(作者)确定还有其它更好的办法来解决问题,如果您也有主意请联系作者Korey Hinton。

译自:http://koreyhinton.com/blog/watchkit-delegates-and-contexts.html



 

WatchKit Storyboard攻略(三)

  这是WatchKitStoryboard攻略的第三篇,重点自然是Notification(通知)视图这一部分的内容。

  首先转述一下有关概念及原理:AppleWatch应用的通知内容是与iPhone应用同步显示的,但分为ShortLook(短时查看)和LongLook(长时查看)两类进行展现。当某iOS应用的本地或远程通知抵达用户的iPhone 后,iOS将会自行判断显示该通知的设备(iPhone或AppleWatch),对于发送到 AppleWatch的通知来说,系统首先会通过TapticEngine触觉反馈提示用户,如果用户抬起手腕选择查看通知,系统将会首先展示该通知的短时概览(ShortLook),如果用户不放下手腕暂停一会后AppleWatch切换进入LongLook模式。而我们观察Storyboard可知通知是分静态和动态两种的,这里特别强调不要想当然的认为ShortLook就是对应的静态通知,ShortLook的通知目前在模拟器是无法测试的,Storyboard配置的静态与动态通知都是针对的LongLook通知,静态标签内容部分来自Notification payload,动态内容部分则来自于自定义WKUserNotificationInterfaceController类。

  还要注意系统的限制:iOS系统无法定义一个AppleWatch专用的通知,意味着通知必然是同时发送到iPhone与AppleWatch的,只不过可以针对AppleWatch开发专属的动态通知功能。

  我们通过开发一个提供AppleWatch喝水通知的简单示例应用来看看WatchKit提供的Notification有哪些功能。

  (教程运行环境:MacOSX10.10、Xcode6.2beta3、Swift)

  1.工程配置

  建立testNoti项目,新添加WatchKit App的target,


 复制一份Watch主应用的Scheme以建立Notification模板的启动Scheme,
  修改启动点,


  看一下WatchKit App的Inteface.storyboard:Static Notification InterfaceController Scene对应的是静态通知,Notification ControllerScene对应的是动态通知。


  2.静态通知配置

  我们可以看到静态通知的CustomClass是无法指定的,其主要界面实现都通过配置Storyboard里相关属性完成。我们对照Main类的Storyboard配置,可以理解为Notification整个模板的界面背景是一个不可见的Group,而且其上方有一层显示通知来源应用名称及其图标的蒙版区域。

   与Group组件一样,我们可以设置整个通知的背景图。


假设我们的通知界面设计为一个全屏带提醒文字的蓝色图片背景,首先针对42mm设备,做一个312*352的蓝色圆角矩形底图(高度减去了标题栏部分)并添加文字图案,然后设置一下Background的文件名,模式改为Aspect Fill:


  下图是运行效果,同时我们也在图上标注出各项目的对应设置路径。但这样显示不全,还达不到期望的全屏显示的强调效果,


那是因为通知标签高度小了,我们加一个固定高度(这里我们针对42mm设备设置为138)的Group并把Label拖进去,效果已经可以看得到:


接下来设置一下图标,我们简单的做一个宽高88px的作为42mm设备的图标,缩小为80px作为38mm设备图标,并将他们拖入AppIcon对应项

然后是自定义的按钮和通知文本,我们修改WatchKit Extension里的PushNotificationPayload.apns文件中对应内容以测试效果:


运行发现期望的全屏效果已经有了,但按钮需要滚动屏幕才能看到


而点击“Ok I did it”按钮会切换进入AppleWatch应用的主程序,Dismiss按钮则是系统自动生成的。

BTW:程序名称部分的半透明蒙版色彩可以通过修改Notification入口的SashColor属性进行修改。

3.动态通知配置

动态通知在Storyboard部分与静态通知配置方法类似,但我们获得了代码绑定通知界面元素的功能,可以后台动态获取并将其在界面上进行展现。

先测试一下,拖拽一个Label到动态通知控制器上,更改文本为hi:


要将模拟器切换为显示动态通知,必须把Controller代码里这段注释取消:

    overridefunc didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {

        completionHandler(.Custom)

    }

运行一下,确实可以显示出标签hi了,下面代码绑定此Label控件,

@IBOutletweakvar dynamicLabel: WKInterfaceLabel! 

再添加进一步显示喝水量的代码

overridefunc didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {

        let mount=1.2 //此值根据你的应用计算生成

        dynamicLabel.setText("Today you should drink \(mount)L water more.")

        completionHandler(.Custom)

    }

最后别忘了在Storyboard里修改dynamicLabel的Lines为3行,以免显示不下


运行即可看到正确的效果。



WatchKit Storyboard攻略(二)

    上一篇主要写到了WatchKit里对于Main入口界面设置Storyboard的一些要点和技巧,没想到被转载让那么多小伙伴们看到,因此我也会继续讲解这一部分的后续内容。本篇主要针对第二类视图界面也就是Glance(瞥览)进行示例介绍,编写之际恰逢WatchKit beta2的更新,因此也将顺便介绍有关更新内容。

   Glance的用途大家应该比较熟悉了,它是专门用来快速浏览信息的视图界面,所有信息在一屏内显示,而且界面元素无法交互,用户点击任意位置都会可开启Watch的主应用。Glance视图的激活方式是从边缘上划。

   我们现在打算构造一个显示用户当前位置的Glance。视图上应包括当前时间、当前位置描述与位置地图。因此我们会需要WKInterfaceDate、WKIntefaceLabel和WKInterfaceMap三个组件,考虑将他们垂直瀑布式排列。步骤如下:

    1.建立项目工程和设置启动方式。

    我们用Xcode6beta2建立一个TestWatchStoryboard的项目并增加target,添加WatchKit App扩展,这一步时记得勾上Glance场景的选项。
Xcode提示你激活Watch程序的scheme,确认激活,但用这个是无法启动Glance的。我们可以用Duplicate scheme复制一份,并将新scheme更名为Glance of TestWatchStoryboard2 WatchKit App。


然后修改启动界面为Glance,即可通过新scheme启动我们的Glance。


    2.配置Glance视图。

    打开WatchKitApp的Interface.storyboard,点击Glance的ViewController,可以看到右边出现Glance模板配置界面。


   右方只有模板选择的功能。目前模板都是分为上(Upper)和下(Lower)两部分。虽然选择很丰富但个人认为强制切割成2块而不能整体性的全屏是它的主要局限。前面说了我们有三个元素,因此把Upper上方部分模板改为Group-Body1(模板里的Body其实代表的是一个标签WKInterfaceLabel)。

   接着拖拽1个Date组件到Upper的Group中代表当前时间,再拖拽1个Map控件到Lower的Group代表当前地图位置,界面和结构如下:


    这时我们也可以发现Xcode6.2beta2与beta1的区别:点击各项属性的+号能分别为42mm和38mm设备指定不同的属性值,结合上一篇里我们提到的特性,界面布局的灵活性得到大大提升!

   比如我们运行在38mm表盘上时发现日期时间在一行内显示不下,可以为38mm表盘设置缩小一半的Scale,将Min Scale值中38mm设备的对应值设置为0.5,并同理设置Label元素:


   3.利用代码更新内容

   再简单介绍一下本示例的代码实现思路。

   首先建立所述三个Storyboard控件与控制器代码中对象的IBOutlet关联。名称分别为txtDate、txtLocation与map:

    @IBOutletweakvar txtDate: WKInterfaceDate!

    @IBOutletweakvar txtLocation: WKInterfaceLabel!

    @IBOutletweakvar map: WKInterfaceMap!

    然后增加各控件初始化时的设置代码,这里我们发现了beta2版的又一个差异,WKInterfaceController的初始化函数以前是init(context: AnyObject?),现在变更为awakeWithContext(context: AnyObject?) ,这个变动的原因是你其实无法在WatchKit Extension里进行视图控制器构造函数的调用,所以init是名不副实的,还是以awakeFromNib类似的名称命名更好理解。我们为了简化,静态的设置地图位置与地址标签为武汉大学:

    overridefunc awakeWithContext(context: AnyObject?) {

        super.awakeWithContext(context)

        txtLocation.setText("@:\WuhanUniversty")

        map.setRegion(MKCoordinateRegion(center: CLLocationCoordinate2DMake(30.541093, 114.360734), span: MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)))

    }

  已经完成!选择Glance的Scheme并运行,38mm设备上效果如下:


    Glance部分内容不算多,本系列下一篇我们会研究Notification。

后话:作者非常开心的看到Xcode6.2beta2所带WatchKit的功能得到进一步丰富,这证明Apple确实在不断完善SDK,相信等到AppleWatch发售时WatchKit正式版的功能将更加完整。这也给了作者更新WatchKit有关文章的动力,若未关注本人新浪微博(@WatchKit开发),也希望转载前先联系作者,以确保正确的内容出现于公众媒体,感谢大家!





WatchKit Storyboard攻略(一)

目前的WatchKit只有用户界面的渲染是靠AppleWatch进行,本文的目的主要在于探索WatchKit界面部分的功能开发与小技巧,内容层次上偏基础。

我们现阶段能用到的WatchKit是不完全体,能定制的界面元素都在唯一的Storyboard文件里,但是仔细一看我们就发现,XCode6.2里Watch的Storyboard和iPhone里的完全是两码事,且可动态交互的内容极少。

但是也不要气馁,至少在界面元素的排版方面,比起iPhone用的传统Storyboard其实更加简便。

首先我们了解一下WatchApp的界面组成的基本概念。我们知道,WatchKit目前提供的三种Storyboard模板有WatchApp用的Main模板、瞥览的Glance模板与静态/动态通知模板。这里重点先看一下Main模板,其实它分为两个部分--标题栏与内容frame,对于42mm和38mm的Watch来说,其尺寸是有区别的,见图1与图2:

图1 42mmAppleWatch界面尺寸与分辨率


图2 38mmAppleWatch界面尺寸与分辨率


红色表示标题栏,我们目前唯一能做的似乎就是设定标题文本。


不过还好也可以通过在ViewController的代码里通过setTitle来动态改变标题内容,颜色也是可以通过Global Tint来改变的(见之前的翻译文章)。

黄色部分是我们可以真正掌控的部分,在ViewController的代码里可以通过contentFrame属性进行访问(小小的表盘被标题占用了那么多空间- -!)。


XCode里针对Storyboard剩余能设置的属性都是针对它的了!我们可以设置Background定义背景图,指定图的多种显示模式(默认都是ScaleToFill,好处后面说),并通过设置Animate为true来实现在背景上自动播放图片帧动画。Insets设置为Custom后可以设置视图上下左右留白的空间,默认值都是0。

下面就是本文重点部分了,还是以一个实际的应用场景讲一下更多控件元素的具体使用和排版技巧:

比如说我们要做一个手表程序的启动画面和菜单,构思如下图(程序名Logo部分应与顶部间距40px,与左右边缘需要保持间距20px,水平位置居中;2个菜单项宽度180px,也是水平居中,垂直方向与底部间距24px)。为了精致我为程序名和菜单会制作一些图片资源。但很快就意识到了麻烦,前面已经发现2种大小的AppleWatch分辨率是不同的,这意味着如果做两种设备的适配并保持此排版,图片通常思路下是需要2种尺寸的。



其实完全没有必要,使用WatchKit专用的Storyboard布局功能仅使用一套资源图片就可以轻松的解决统一排版问题,这也将减少图片传输带来的开销并节省Watch宝贵的存储空间。

我们新建一个项目并加好WathApp的target,打开Inteface.storyboard文件,在Main模板里设置InterfaceController的Insets为Custom并按如下设置边距。


这里要注意的是AppleWatch使用的是2倍像素密度图像资源,Top的值减去了标题栏的38像素。

再拖一个Group控件上去,作为主要界面元素的容器。设定其Size的Width和Height均为Relative to Container,系数保持缺省值1,效果立现。


Relative to Container的作用是设定控件的Width属性或Height属性占父容器的比例(1代表100%)。

把Group的Layout设置为Vertical垂直排列,再加3个Image上去代表logo和2个按钮。


先来看看logo,我们现在边界已经确定好,因此只要它宽度与Group保持一致就行,而不同的设备,用控件自带的Scale功能就好!高度它可以自己计算!


运行程序并分别选用42mm和38mm设备,发现logo图确实是在保证边距的情况下在38mm设备上缩小了。

本文的情景里对于菜单按钮要求有点区别,为保证用户能方便按到按钮,按钮大小是必须保证的。因此这里我们设置2个按钮Image的size为fixed width与fixed height并填好图片对应宽高,并设置Position位置为向底端对齐。

预览里已经可以看到效果了,如果要修改2个按钮之间的间隔,修改Group的spacing值就OK了。

运行2种设备比较一下:



现在这套方案还是很简单的,按42mm的大尺寸设计,依据需求灵活对属性进行设置选择缩放或者固定尺寸,并利用Group进行各种对齐等等,跟Html排版很类似。

先就说这么多吧,也没有太多体系化的东西。主要还是靠自己的应用场景去设计并调整实践才能有更好的心得体会。


【翻译】WatchKit:开始动画吧!

文章来自:@NatashaTheRobot

WatchKit最另人惊讶的一件事之一就是不再依赖我们喜爱的UIKit组件库。现在我们是直接面向界面进行处理,不再有我们能拿来做动画的layer层属性。为了在Watch上动画,我们必须利用许多图像进行翻页动画。

为了测试这一功能,作者截取了小黄人踢足球的一段GIF动画,随便找了一个网上的Gif Splitter工具来提取GIF的各独立帧,然后把这些成果文件改成统一名称,但文件名末尾有着帧序号以及@2x.png,并把他们导入到WatchApp的 Images.xcassets文件夹:

竟然一共有131帧!

然后作者把图像加到Interface Controller里,并设置动画!


这样就行了,运行程序,就会播放动画了!

如果你喜欢更有控制力一些,也可以用代码控制动画播放,比如你可以指定播放哪些帧:


注意这次设定了整个动画的历时(duration)为15秒,现在动画有了慢放的效果!

http://natashatherobot.com/wp-content/uploads/MinionAnimationSlow.mp4?_=2

你应该注意到了,动画在循环播放.因为我设定了重复次数为Int.max。要让动画只播放1次,设定repeatCount为1!

你可以控制使得动画在任何时候暂停。对你的InterfaceImage调用stopAnimating()方法就好。

享受AppleWatch上动画的快感吧!

项目资源代码见:

https://github.com/NatashaTheRobot/WatchKitAnimationDemo

 

 



 

【翻译】WatchKit: 自定义Global Tint

Twitter上的

@benmorrow 提到了ShortLook通知的一项可自定义特性: 


Short-Look通知的界面不能直接进行自定义,当然,但它是基于你的WatchApp设定进行计算的。通知的App图标、名称以及标题字符明显的配置在WatchApp内容里(这是WatchApp的标准内容)。但你可能不知道App名称的文本颜色是基于你WatchApp的Global Tint颜色设置来生成的。

我(原作者)强烈推荐去设置Global Tint,因为缺省的暗灰色实在太丑了!


要更改Global Tint, 在你Watch App的Interface.storyboard里,选择任意元素,转到右边Utilities的 File Inspector面板,你就能更改Global Tint 了!


Global Tint的设置也会改变你Interface Controller的标题效果:

还有个你可能没注意到的有趣现象。从Table里点击出来的Interface Controller(明细控制器),标题是在返回按钮右边显示的。在iOS里,我们习惯了在Navigation栈的前一个ViewController里标识,但是Watch上,只需要设定Title(标题属性)就好!(反正也没有NavigationBar- -)