0%

iWatch 原生态支持的控件主要有以下类型,下面对每个类型一一解刨。

  • Labels
  • Images
  • Groups
  • Pickers
  • Tables
  • Buttons
  • Switches
  • Sliders
  • Maps
  • Movies
  • Date and Timer Labels
  • Menus

Labels – WKInterfaceLabel

Labels 使用静态的文本来传达简短的消息,是你App中最常见的一个元素之一。它能支持多行显示也可通过代码动态更新内容。

iWatch ,设计一个标签事,首先应该把精力集中在 易读性 ,使用更亮的颜色和使用动态类型( Dynamic Type )来保证文本尺寸的正确,内置已经提供了最佳的易读性和自动的动态的类型支持。假如需要使用自定义的字体,应该避免使用过度格式化的。

更多信息查看:Typography

Images – WKInterfaceImage

一个 Image 元素用来显示单张图片或者一系列动画图片。Images 支持在 iOS 上的的任何格式,但是跟偏向于 PNG 格式。动画系列图片可以通过代码控制启停。

38mm42mm 共用一套图片资源,只要内容能够表达清晰。

Apple Watch 支持高清屏显示,所有没有必要创建一个标准的分辨率图片,在图片名应该包括 @2x

当图片正在加载时,应该使用占位图片来占据它的空间

优化图片查看:Image Optimizations

Groups – WKInterfaceGroup

Group 这个概念是专为有限空间的 iWatch 打造的,可以帮你对其他元素进行布局。它有 postion , size , marigns 以及其他布局相关的接口,一个 Group 对象本身也是一个可见的元素,它也有背景图片,颜色,圆角半径等属性。

支持水平 horizontally 和垂直 vertically 布局,可以让放在里面的项目实现相应的布局,可以使用 spacing 属性来设置内部项目之间的间距,使用 margins 来控制 Group 和它周边的元素的距离。

Group 支持镶嵌操作,你可以通过镶嵌来指定你想要的布局样式。

你应该优化你的 Group 的背景图片来获得更好的体验,因为太大可能使你的 App 变得卡顿。优化请参考:Image Optimizations

外观大概这样:

Pickers – WKInterfacePicker

Pickers 能够显示一个可以使用数字表冠导航的列表。这意味着你可以使用一种更精确和迷人的方式选择。 Pickers 可以通过下面三种方式的一种来展示它们的项目:

  • List style ,可以显示文本或者图片在一个滚动列表里面
  • Stack style ,栈风格以一个卡片风格的界面显示图片,当用户滚动,被选择的图片动画进入最上方,这种风格适合浏览照片。
  • Sequence style ,序列显示,可以显示一系列的图片。当用户转动表冠时, Picker 显示先前或下一张图片在这个序列中没有动画,这种风格适合于构建自定义的界面使用你自己的图片。

Pickers 还能被配置成显示一个 轮廓标题滚动指示器 。这些元素可以让你分辨出屏幕上的 Pickers 并且可以帮助用户导航它的内容。

使用标题来指明你选择项目的意义或者 picker 本身的作用

你可以给项目指定一个独特的标题当那些项目的意思表达不清晰的时候,可选,你也可以设置所有的项目相同的标题来标明 Picker 它本身的作用。

显示一个滚动指示器当所有的内容不是全部可见时

Tabels – WKInterfaceTable

Tabels 在一列中展示多行的内容。一个表的一行是动态配置的,它的内容可以在随时改变。 Tabels 天生可滚动,支持各种交互,能够设置背景颜色或者图片。

使用注意事项:

  • 统一使用行的风格类型,即使一个表可以包含多行,它应该呈现一个全局统一的外观,每一行使用你的主要内容开始,然后随你的需要添加更多支持的其他类型的内容,例如 headers 或者 footers ,总是用原来设计的目的来使用行类型。例如,你不应该用一行来显示 headersfooters 的内容。

  • 限制行数最大 20,显示最重要的行在顶部,让用户能够浏览尽可能的多。行数越少越容易快速浏览。

  • **不要在 groups 里面镶嵌 tables **, Tabels 会动态改变它的尺寸基于它所包含的行的数目。它会忽略 groups 对高度的限制。

  • 不要在 table 行内包含 或者 符号,行后台实现可以自然点击,你不需要包含一个 或者 符号或者文字来表明行是可点击的。

Buttons – WKInterfaceButton

一个 button 可以执行一个 app 指定的动作, button 可以自定背景和圆角半径。它还可以充当容器使用,包含一个标签 labe; ,图片 image ,或者一个组 group 对象。

使用事项:

  • 创建横跨屏幕的 Buttons ,全宽的 buttons 看起来更容易点击,假如有两个 buttons,必须有相同的水平间距,两者使用相同的高度使用相同的图或者简短的文本标签。

  • 使用相同的高度对于 vertical stacks 风格布局一行和两行文本按钮,原则是,尽可能的,使用一致到高度让他们在视觉上统一。

  • 使用标准的圆角半径,标准的圆角半径的是 6点 ,使用标准的圆角半径可以提升整个 app 的视觉的一致性并且可以增强 button 的内容。

  • 让 butotns 足够大以便容易点击,创建让用户很容易点击的 button,buttons 应该遵循下面最小的值。

38mm (minimum) 42mm (minimum)
Circular buttons 75 pixels 80 pixels
Round rectangular buttons 50 pixels high 52 pixels high

Switches – WKInterfaceSwitch

开关控件更 iOS 类似,让用户在量个互斥的选择或者状态中选择,例如开或者关,一个靠近 switch 的标签应该指定响应的效果当,开关切换时。

效果像这样:

Sliders – WKInterfaceSlider

一个滑动条让用户来调整到某一个值。一个 slider 用来显示它设置的一系列的值或者是一个持续的 bar , 它可以在一个有限的范围内根据预定的数量自增或者自减。

Note:你可以像使用 slider 那样使用自定图片,系统默认显示 +- 符号。

Maps – WKInterfaceMap

Maps 是地理位置的静态快照。你可以放一张地图在你的界面里面在预计的时间,但是你 WatchKit extension 必须配置显示的区域在运行时。显示区域是不可交互的,但是点击一个地图将会打开你 Apple Watch 上的 Maps app。

你可以在地图上用一个高亮的点对你感兴趣或者其他相关信息进行标注。地图能显示标准的 绿 大头针,它也可以显示自定义的图片,系统允许你最多添加 5个 在一张地图上。

使用注意实现:

  • 让你地图的元素根据屏幕自动适应。用户应该能够看到整个地图元素在 Apple Watch 上而不用滚动屏幕。
  • 配置显示的地图区域到包含兴趣点的最小范围,地图元素内容本身不能滚动,所有所有内容必须附着在一个指定的地图区域

Movies – WKInterfaceMovie

一个 movie 对象会显示一张海报图片,当点击时,模态呈现一份视频或者音频对象,使用这个元素来放置简短的媒体剪切和你内容内联。媒文件的重放由系统来管理。

使用注意事项:

  • 使用一种海报图片来呈现剪切的内容,海报图片能让用户提前做决定是否观看与这个剪切关联的媒体,不要使用没有意义的剪切图片。
  • 不要创建海报图片像系统控件Movies 会让视频和音频内容看起来像你节目的一部分,所以不要通过让他们看起来像其他东西的方式把他们隐藏起来。你应该使用一个 Button (而不是一个 movie )来呈现媒体回放界面假如你喜欢的话。
  • 保证视频或者音频剪辑得足够短,剪辑不应该超过 30 秒,首选更短的。长的剪辑会占用更多的磁盘空间并且要求用户保持他们的手腕抬起更长时间,这样可能导致疲劳。
  • 适当的设置剪辑视频的尺寸,无论时候都应该使用推荐的尺寸来剪切,缩放视频剪辑将影响性能导致不理想的外观。更多信息关于推荐的尺寸和编码的值,参考Audio & Video

Date & Timer Labels – WKInterfaceDate,WKInterfaceTimer

Date 和 Timer 标签用来显示真实的时间在 Apple Watch 上。

一个 date label 显示了现在的日期,现在的时间或者两者的组合。它可以使用各种样式,日历和时区来配置,在你配置好后,日期标签直接跟新它的值而不需要通过你 app 的进一步输入。

一个 timer label 用来显示一个精准的倒计时和和计数定时器。它能够配置它显示的计数的值以各种形式,在你配置完成后,一个 timer label 能够计数或者倒计时不用从你的 app 的进一步输入。

强力触摸 Apple Watch 将调出当前屏幕的 menu (假如有点话)。一个菜单能够显示多达 4 个有关的动作对于当前屏幕,而不用从你的界面离开。

使用注意事项:

  • 包含一个 Menu 当现在屏幕有相关的操作的时候,菜单是可选的。假如没有菜单呈现,系统将播放一段动画来响应用户的强力触摸操作。
  • 使用一个 label 和一个 icon 来传达每个菜单动作的意图,label 和 icon 都需要,label 限制 2 行,所以文本应该尽量的短。
  • 使用菜单来响应它们预期的目的Menus 是一种能够修改当前界面控制器内容的方式,不要使用它们来作为主要的导航形式在你的 app 中。
  • 避免复制你的其他 app 中菜单的视觉风格,假如你必须使用一个相似的布局,加上颜色给你的界面或者排列的项目,用一种不同于其他菜单的方式。

更多 menu icons 设计的信息,参考 Menu Icons

参考资料:

MVVM,作为一种新的架构设计,有别于 MVC,MVP,它有很多自己的特性和优势,例如易于调试,业务逻辑集中方便管理,统一各种通信机制,将业务和控制器解耦。这框架现在很多公司都在尝试使用,像美团等,以后可能是一种趋势,鉴于它学习成本比较高,此文意在记录和收集相关的资料,以便以后查阅。

Github 地址ReactiveCocoa

比较不错的学习资料:

此文整理一下 TableView 优化相关的方案和思路。

TableView 为什么会卡?

主要由以下原因:

  • cellForRowAtIndexPath: 方法中处理了过多业务
  • tableviewCell 的 subview 层级太复杂,做了大量透明处理
  • cell 的 height 动态变化时计算方式不对

优化核心思想: UITableViewCell 重用机制

简单的理解就是:UITableView 只会创建一屏幕(或一屏幕多一点)的 UITableViewCell,其他都是从中取出来重用的。每当 Cell 滑出屏幕时,就会放入到一个集合(或数组)中(这里就相当于一个重用池),当要显示某一位置的 Cell 时,会先去集合(或数组)中取,如果有,就直接拿来显示;如果没有,才会创建。这样做的好处可想而知,极大的减少了内存的开销。

Tips:

  1. 提前计算并缓存好高度(布局),因为 heightForRowAtIndexPath: 是调用最频繁的方法;
  2. 异步绘制,遇到复杂界面,参考 FacebookAsyncDisplayKitYYAsyncLayer异步绘制框架;
  3. 缓存图片( SDWebImage ),提前处理好 UIImageView 图片的尺寸按需加载而不是加载原图;
  4. 计算等耗时操作异步处理,处理完再回主线程更新 UI;
  5. 图文混排不定高度采用 CoreText 排版,缓存 Cell 高度参考YYKit
  6. 实现 Cell 的 drawRect: 方法直接绘制,减少 UIViewUIImageViewUILabel 等容器的使用。

Bonus:

  1. 正确使用 reuseIdentifier 来重用 Cell;
  2. 尽量少用或不用透明图层或 View;
  3. 如果 Cell 内现实的内容来自 web,使用异步加载,缓存请求结果;
  4. 减少 subviews 的数量在 heightForRowAtIndexPath: 中尽量不使用 cellForRowAtIndexPath: ,如果你需要用到它,只用一次然后缓存结果;
  5. 尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过 hide 来控制是否显示;
  6. 固定高度不要实现 heightForRowAtIndexPath: 方法。

可以通过实现以下方法,可以减少高度计算次数

1
2
3
4
5
6
@property (nonatomic) CGFloat rowHeight;             // will return the default value if unset
@property (nonatomic) CGFloat sectionHeaderHeight; // will return the default value if unset
@property (nonatomic) CGFloat sectionFooterHeight; // will return the default value if unset
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is 0, which means there is no estimate
@property (nonatomic) CGFloat estimatedSectionHeaderHeight NS_AVAILABLE_IOS(7_0); // default is 0, which means there is no estimate
@property (nonatomic) CGFloat estimatedSectionFooterHeight NS_AVAILABLE_IOS(7_0); // default is 0, which means there is no estimate

参考资料:

coreAnimation

简介

此文主要记录我学习 iOS 的动画相关内容用 Swift 语言,记录下来以供参考,不定时更新。

目录结构

  • Section I: View 动画
  • View animation
  • 弹簧动画 - Springs
  • 转场动画 - Transitions
  • 关键帧动画 - Keyframe
  • Section II: Auto Layout 动画
  • Section III: Layer 动画
  • Section IV: 3D 动画
  • Section V: 未来类型的动画
  • Section VI: View Controller 动画
  • Section VII:第三方动画库
  • Section VIII: Apple Watch 动画

Section I: View 动画

1.View animation

简介:view 的动画主要是通过 UIView 的类方法创建,动画内容一般
放在 block 里面,可以镶嵌使用,构成链式动画,主要有 3 个方法,三个访法只需会参数最多的那个就行,函数名 animateWithDuration(_:delay:options:animations:completion:) ,其他的使用起来都类似。

1
2
3
class func animateWithDuration(_ duration: NSTimeInterval, animations animations: () -> Void)
class func animateWithDuration(_ duration: NSTimeInterval, animations animations: () -> Void, completion completion: ((Bool) -> Void)?)
class func animateWithDuration(_ duration: NSTimeInterval, delay delay: NSTimeInterval, options options: UIViewAnimationOptions, animations animations: () -> Void, completion completion: ((Bool) -> Void)?)

使用:

1
2
3
4
5
UIView.animateWithDuration(0.5, delay: 0, options: [], animations: {
//do something
}, completion: {_ in
//do something
})

说明: options ,动画选项,这个运行你设置一个动画选项集合,传 [] 表示不需要选项,单个时可以省略放括号例如 .Repeat ,多个用逗号连接 [.Repeat, .CurveEaseInOut] > completion 闭包需要一个参数,不需要使用参数可以用 _ 表示,不需要实现可以直接传 nil 或者像下面这样写

1
2
3
4
5
UIView.animateWithDuration(0.5, delay: 0, options: [], animations: { () -> Void in
//do something
}) { (_) -> Void in
//do something
}
2.弹簧动画 - Springs

简介:弹簧动画是 iOS 7.0 新增的 API,函数名 animateWithDuration(_:delay:usingSpringWithDamping:initialSpringVelocity:opti ons:animations:completion:)

1
class func animateWithDuration(_ duration: NSTimeInterval, delay delay: NSTimeInterval, usingSpringWithDamping dampingRatio: CGFloat, initialSpringVelocity velocity: CGFloat, options options: UIViewAnimationOptions, animations animations: () -> Void, completion completion: ((Bool) -> Void)?)

使用:

1
2
3
UIView.animateWithDuration(0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {
// do something
}, completion: nil)

说明: usingSpringWithDamping :设置弹簧的阻尼,范围(0.0-1.0),越接近 0.0 弹簧越有弹性,越接近 1.0,效果越僵硬,你可以把当成弹簧的刚度来理解,
initialSpringVelocity :设置弹簧初始化的速度,设置 1.0 表示用 1 秒在动画跨度上完成整个动画距离,值越大或者越小会导致动画有相应的大的或者较小的速度,可以当成弹簧的初始动量来理解。

3.转场动画 - Transitions

简介:前两种创建的动画是基于可动画的接口例如,position,alpha,frame 等等,Transitions 是专门处理添加或者移除一个 view 的动画,系统有 2 个标准函数,一个是对单个 view 的处理,一个是处理 2 个 view 替换。

1
2
class func transitionWithView(_ view: UIView, duration duration: NSTimeInterval, options options: UIViewAnimationOptions, animations animations: (() -> Void)?, completion completion: ((Bool) -> Void)?)
class func transitionFromView(_ fromView: UIView, toView toView: UIView, duration duration: NSTimeInterval, options options: UIViewAnimationOptions, completion completion: ((Bool) -> Void)?)

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//add the new view via transition
UIView.transitionWithView(animationContainerView!, duration: 0.33,
options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.animationContainerView!.addSubview(newView) }, completion: nil)

//remove the view via transition
UIView.transitionWithView(animationContainerView!, duration: 0.33,
options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.newView.removeFromSuperview() }, completion: nil)

//hide the view via transition
UIView.transitionWithView(self.newView, duration: 0.33, options: [.CurveEaseOut, .TransitionFlipFromBottom], animations: {
self.newView.hidden = true }, completion: nil)

//replace via transition
UIView.transitionFromView(self.oldView!, toView: self.newView!, duration: 0.33, options: [.TransitionFlipFromTop],
completion: nil)
4.关键帧动画 - Keyframe

简介:关键帧顾名思义就是设定关键的一些帧然后系统会根据一套算法计算中间的其他帧,这样改变的可动画属性时看看起来更加流畅自然,主要有两个函数,一个创建关键帧动画,一个设置具体每帧内容

1
2
class func animateKeyframesWithDuration(_ duration: NSTimeInterval, delay delay: NSTimeInterval, options options: UIViewKeyframeAnimationOptions, animations animations: () -> Void, completion completion: ((Bool) -> Void)?)
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func planeDepart() {

UIView.animateKeyframesWithDuration(1.5, delay: 0.0, options: [], animations: {
//add keyframes

UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.25, animations: {
self.planeImage.center.x += 100.0
self.planeImage.center.y -= 10.0
})

UIView.addKeyframeWithRelativeStartTime(0.1, relativeDuration: 0.4) {
self.planeImage.transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_4/2))
}

UIView.addKeyframeWithRelativeStartTime(0.25, relativeDuration: 0.25) {
self.planeImage.center.x += 100.0
self.planeImage.center.y -= 60.0
self.planeImage.alpha = 0.0
}

UIView.addKeyframeWithRelativeStartTime(0.51, relativeDuration: 0.01) {
self.planeImage.transform = CGAffineTransformIdentity
self.planeImage.center = CGPoint(x: 0.0, y: originalCenter.y)
}

UIView.addKeyframeWithRelativeStartTime(0.55, relativeDuration: 0.45) {
self.planeImage.alpha = 1.0
self.planeImage.center = originalCenter
}

}, completion: nil)
}

说明:这是设计一个飞机沿一条路径起飞然后再归位的一段动画, addKeyframeWithRelativeStartTime(_:relativeDuration:animations:) ,第一个参数相对开始时间,是相对于动画持续时间的百分比,例如 0.1 就是 10%,0.25 就是 25%,如果整个动画持续 2 秒,0.1 就是在 2*0.1=0.2 秒的时候开始,后面的一个参数是相对持续时间,范围更第一参数类似也(0.0-1.0),只从相对开始时间起,往后推移的相对时间。

Section II: Auto Layout 动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// change Constraint

// init layout
view.layoutIfNeeded()

UIView.animateWithDuration(0.8, delay: 0.0,
usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,
options: [], animations: {

// do something change constraint

// layout
self.view.layoutIfNeeded()
}, completion: nil)

原理就是,修改约束,让添加一个动画,在动画 block 里面执行布局更新, block 里面也可以添加修改约束代码,修改约束的本质是修改相关 Viewframebounds ,由于修改 View 的这个属性是支持动画的,所以修改约束其实是间接修改 View 的这些属性,所以也是支持动画的,这个动画的强大的之处在于,可以实现一系列 View 相互协作的动画。

Section VIII: Apple Watch 动画

概述:在 Apple Watch 中有两种技术来创建动画,一个是一次性创建一系列静态可动画的图片序列或者循环动画,另一种是对标签和其他项目通过改变尺寸,对齐,颜色,透明度进行布局和外观动画。

Animated Image Sequences

Animated Image Sequences 是由两个或者更多的独立图片组成的序列来随时间按序列显示。每张图片组成了动画的一个单独的帧,整个动画运行在一个循环中除了你在运行的时候修改了播放的行为。主要把它安装在你界面中的 imagegroup , button 等元素中。通过 WKImageAnimatable 协议来实现动画,也就是说凡是遵循这个协议的对象都支持动画, WatchKit 中遵循这个协议的对象有: WKInterfaceGroupWKInterfaceImage 你可以控制 imagegroup 元素 Animated Image Sequences 的播放的速度,方向,帧率,否则界面元素将会无限循环显示一个完整的动画。

Notes:

  1. Animated Image Sequences 可以正向播放动画也可以反向执行动画在运行的时候,你不应该提供一套一样的图片以反向的方式,你应该使用这个技术来降低 App 的尺寸。
  2. 如果你是从 storyboard 创建序列图片对象,可以通过 func startAnimating() 方法开始执行动画,通过 func stopAnimating() 来停止动画的执行。

实例:

实现 WKInterfaceGroup 加载进度动画

1.在 Watch 里面的 Interface.storyboard 里面拖拽一个 WKInterfaceGroup 命名为 backgroundGroup 2.创建一个 WKInterfaceButton 按钮来触发这个动画事件,在控制器里面实现下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@IBAction func checkInButtonTapped() {
// 1
let duration = 0.35
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64((duration + 0.15) * Double(NSEC_PER_SEC)))
// 2
backgroundGroup.setBackgroundImageNamed("Progress")
// 3
backgroundGroup.startAnimatingWithImagesInRange(NSRange(location: 0, length: 10), duration: duration, repeatCount: 1)
// 4
dispatch_after(delay, dispatch_get_main_queue()) { () -> Void in
// 5
dosomething
}
}

参数说明: imageRange ,将要执行动画的图片范围, 0 表示第一张图片, 1 表示第二张图片,以此类推; duration ,单位秒,表示动画执行单个循环的时间, 正值 表示从第一帧到最后一帧执行, 负值 表示以相反的顺序执行动画,以第一帧结束; repeatCount ,动画的重复次数,设置 0 表示无限循环。

3.将素材文件拖入 WatchAsset.xcassets 的文件里面,以 Progress 开始命名后面接一串连续的数字,效果应该像下面这样。

iWatchProgress

4.然后在模拟器上运行就 OK 了。

Layout and Appearance Animations

你可以动画改变所有界面元素的布局和外观。动画改变布局让你修改元素的尺寸或者动态的改变布局的方向。你可以移动在屏幕上的元素或者让它们的内容重新对齐。你也可以动画改变元素的外观,包括改变他们的 backgroud color 或者 opactiy 。这些类型的动画让你创建动态的界面用来响应用户的交互并且提供一个更好的反馈。

所有的 layout- and appearance-based animations 在动画开始和结束都以 easing 的曲线方式自动构建 。你不能关闭或者自定义缓动曲线。相当于 UIView 的动画选项里面的 easeInOut

可动画属性:

  • Opacity
  • Height
  • Width
  • Group Insets
  • Alignment
  • Background Color
  • Tint Color

核心代码:

1
2
3

class func animateWithDuration(_ duration: NSTimeInterval, animations animations: () -> Void)

说明:这个方法在 WKInterfaceController 中,目前发现只有这一个动画 API,你可以在这个闭包里面执行相应的动画操作,实现跟 UIView 的 block 动画类似。

iWatchAnimation

实例:

在界面已经出现的时候对 planeImage 进行着色动画,改变 separator 的颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

var planeImage: WKInterfaceImage!
var separator: WKInterfaceSeparator!

override func didAppear() {
super.didAppear()
// 1
animateWithDuration(0.35, animations: { () -> Void in
// 2
let color = UIColor(red: 90/255, green: 200/255, blue: 250/255, alpha: 1)
planeImage.setTintColor(color)
separator.setColor(color)
})
}
}

参考资料:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

本文翻译自 http://www.raywenderlich.com/51127/nsurlsession-tutorial

原作者: Charlie Fulton

译者:@oneruofeng

注意:这是一篇来自我们作为即将发布 iOS 7 Feast的一部分iOS 7 by Tutorials的简短版本的章节。我们希望你们喜欢。

每一个新的 iOS 版本发布都会包含一些极好的新的网络 APIs,iOS7 也不例外,在 iOS7 中,Apple 引入了 NSURLSession ,这是为了取代 NSURLConnection 作为偏好的网络请求的一系列类。

在这个 NSURLSession 的教程中,你将了解到这个新类究竟是什么,为什么你要使用它以及你怎样使用它,它和以前的类库比较而言怎样,最后最重要的是:获得一个整合到一个真正的 App 的实践。

请注意:这个课程是假设你熟悉基本的网络概念。假如网络对你来说完全是新的,你仍然可以跟我一起一步一步的学习,但是可能有些你不熟悉的概念需要需要自己查询在这个过程中。

为什么使用 NSURLSession 为什么你要使用 NSURLSession ? 饿,因为它可以带给你一些好处和优势相比以前的:

  • 后台上传和下载 : 当你创建 NSURLSession 的时候你只需配置一个选项即可,你便可以进行所有的后台网络任务。这将有对你的电池寿命有利,它支持 UIKit 多任务并且当切换线程的时候使用相同的代理模型。
  • 使你的网络操作可以暂停和恢复 :你稍后将会看到,任何使用 NSURLSession API 的网络任务都可以被暂停,停止,重新开始。而没有使用 NSOperation 子类的必要。
  • 可配置的容器:对于放进里面的请求而言每一个 NSURLSession 都是可配置的。例如,假如你需要设置一个 HTTP header 选项,你只需要设置一次然后每个在 session 中的请求就都会有相同的配置了。
  • 可子类化和私密存储NSURLSession 是可子类化的并且你可以配置一个 session 用来作为私密存储在某个会话中。这允许你拥有私密存储对象在全局状态下。
  • 优化的授权处理机制:授权被完成基于某个特定的连接。当使用 NSURLConnection 的时候假如发生了一处授权改变,这个改变将返回一个随意的请求,你不能确定你具体得到的那个。使用 NSURLSession 的话,代理回来处理授权。
  • 丰富的代理模型NSURLConnection 有些基于 block 的同步方法,然而代理就不能使用它们了。当一个请求建立了无论它是成功还是失败,哪怕需要授权。使用 NSURLSession 就就可以混合接入,使用基于 block 的异步方法同时也可以设置代理来处理授权。
  • 通过文件系统上传和下载:苹果鼓励把(文件内容)数据跟(URL 和一些设置)元数据分开。

NSURLSession vs NSURLConnection “哇哦, NSURLSession 听起来很复杂呀!”,你可能这么想。”我可能还是继续使用我的老朋友 NSURLConnection 。”

别担心–使用 NSURLSession 使用起来其实和它的前辈 NSURLConnection 一样简单对于简单的任务来说。例如,让我来看一个在获取伦敦最新的天气的一个简单网络请求,通过它来获取 JSON 数据的例子。

假设你用这个 NSString 来构造这个 NSURL :

1
NSString *londonWeatherUrl = @"http://api.openweathermap.org/data/2.5/weather?q=London,uk";

这里是第一步你使用 NSURLConnection 来调用:

1
2
3
4
5
6
7
8
9
10
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:londonWeatherUrl]];

[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *connectionError) {
// handle response
}];

现在让我们来使用 NSURLSession 。注意这是使用 NSURLSession 的最简单的方式来快速构造一个请求。在后面的课程你将看到怎样配置 session 和设置其他特征比如像代理。

1
2
3
4
5
6
7
8
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
// handle response

}] resume];

注意你并不需要指定它运行在那个队列中。除了你特别指定,这个调用将会在后台线程。你可能很难注意到这两者之间有什么不同,它就是故意这样的。Apple 提到打算使用 dataTaskWithURL 来取代在 NSURLConnection 中的 sendAsynchronousRequest

所有从根本上来讲–对于简单的任务使用 NSURLSession 就和使用 NSURLConnection 一样简单,并且它还有一些列额外的定制功能当你需要它的时候。

NSURLSession vs AFNetworking 不提到 AFNetworking 框架就谈不上谈论网络编程。这个事在 iOS/OS X 上最流行的框架,有杰出的 Mattt Thompson 创建。

注意:想了解更多关于 AFNetworking ,请检出在https://github.com/AFNetworking/AFNetworking的 github 页面。我们也有一个关于它的课程:http://www.raywenderlich.com/30445/afnetworking-crash-course

下面是的使用 AFNetworking 1.x 版本处理相同的数据任务的代码:

1
2
3
4
5
6
7
8
9
10
11
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:londonWeatherUrl]];

AFJSONRequestOperation *operation =
[AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request,
NSHTTPURLResponse *response,
id JSON) {
// handle response
} failure:nil];
[operation start];

使用 AFNetworking 的一个好处是处理响应的数据的数据根据类型被归类。使用 AFJSONRequestOperation (或者诸如 XML 和 plist 相似的类),成功 block 已经被解析根据响应并且为你返回你想要的数据。使用 NSURLSession 你将收到一个 NSData 对象在 completion handler ,所有你只需要把 NSData 转换成 JOSN 或者其他形式。

注意:你能够很方便的把数据从 NSData 转换成 JSON 使用在 iOS 5 引入的 NSJSONSerialization 这个类。如果你想了解更多,请查看 23 章的 iOS 5 教程,“Working with JSON”。

你或许想知道你是应该使用 AFNetworking 还是仅仅是继续使用 NSURLSession

就个人而言,我认为对于简单的需求最好还是继续使用 NSURLSession –这样可以避免不必要的引入一个第三方库在你的工程中。另外,使用新的代理,配置,和基于很多添加到 AFNetworking 中的的“遗失特征”现在都被包括了的 API 的任务。

然而,假如你想使用一些在 AFNetworking 中 2.0 版本的新特性,诸如序列化和将来对
UIKit 的整合(添加到 UIIImageView 分类中),然后这样很难争辩不使用它!

注意:在 AFNetworking 2.0 分支中,它们已经转换到使用 NSURLSession 。更多信息看这篇帖子:https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-2.0-Migration-Guide

介绍 Byte Club

在这篇 NSURLSession 教程中,你将探索这个新的 API 通过构建一篇日记好图片分享 app 基于 Dropbox Core API ,因为是顶级机密组织因此姑且命名它 Byte Club

考虑到这个课程是你接受到 Byte Club 的官方邀请!什么是你可能会问到的关于 Byte Club 的第一条规则?没人谈论 Byte Club –除了那些足够炫酷的人将会阅读这个教程。并且那些 Android 用户完全不知道;他们被他们俩的生活劫持了。 :]

开始构建 app 迎战下一个章节,将充当被 Byte Club 组织邀请。

主要到这个教程是假设你对基本的网络有基本的了解在先前的版本 iOS。它非常有用假如你已经使用过诸如 NSURLConnection 或者 NSURLSession 在过去。假如在 iOS 方面你是网络方面的新手,在继续这个课程之前你应该查询我们的 iOS 学徒系列作为最初的开发者。

现在开始吧

Byte Club 是iOS 开发者专有的组织,一起来加入挑战你的编程吧。由于每个成员都是远程工作,在这个挑战中有一个是跨越世界,成员通过分享他们“战场”的全景照片也能找到它的乐趣。

例如,下面是 Ray 的办公场所的全景照片:
Ray's office setup

注意:你可能想创建你自己办公室的全景照片–它很有趣,在这个课程的后续中我们将会处理。

在 iOS 7 中,你可以通过打开相机选择一个叫 Pano (全景)的标签照一张全景照片。

加入你喜欢那张,把它设置成你锁屏界面的墙纸通过打开 设置 让后选择 墙纸 \选择墙纸 \我的全景照片。

当然- Byte Club 有它自己的 app,我们来见证奇迹。你可以和其他成员使用 app 来完成编程挑战或者分享全景照片。在幕后,这是通过网络实现-明确的说,就是通过 Dropbox API 来分享文件。

开始的工程概述

首先,下载教程开始的工程

开始的工程包含了为你预先准备好的 UI,所以你只需把精力集中在这个教程中 app 的网络部分。开始的工程也包含一些处理 Dropbox 授权的代码,在后面你将学到更多。

在 Xcode 中打开工程让后在你设备或者模拟器上运行,你应该看到像下面这样:

networking2

然而你还并不能登录它-你不得不先配置 app,你将做一点。

下一步打开 Main.storyboard 纵览一下整个 app 的设计:

networking2

这是一个最基本的使用 2 个标签的的 TabBarController app:一个是为了挑战编程,另
一个是为了放全景照片。这里也有预先让用户登录到 app 中的一步。你将要配置登录在你创建完 Dropbox 平台下的 App 后。

感到很轻松浏览一下 App 剩余的部分并且找到到目前为止相似的地方。你将会注意到除了授权组件,这里没有检索挑战编程或者全景照片的网络代码-那就是你的工作!

创建一个新的 Dropbox 平台 App

为了开始你的新的 Dropbox App,打开 Dropbox App 位于https://www.dropbox.com/developers/apps的控制台

用你的 Dropbox 账号登录,假如没有,没问题:马上创建一个免费的 Dropbox 账号。假如这是你第一次使用 Dropbox 的 API,你需要通知 Dropbox 的条款和条件。

经过这个法定的材料以后就是上路了,选择创建 App 选项。将呈现给你一系列问题-提供下面的答案

  • What type of app do you want to create?
  • Choose: Dropbox API app
  • What type of data does your app need to store on Dropbox?
  • Choose: Files and Datastore
  • Can your app be limited to its own, private folder?
  • Choose: No – My App needs access to files already on Dropbox
  • What type of files does your app need access to?
  • Choose: All File Types

最终,为你的 App 准备一个名字,选择什么并没有关系只有它是唯一的。假如你选择了一个别人已经在使用的名字 Dropbox 将会告诉你。你的屏幕应该看起来像下面这样:

networking3

点击 Create App ,你将开始上路了!

下一个屏幕你将看到显示到屏幕中的包含 App keyApp secret :

networking5

先不要关闭这个屏幕,你将需要需要下一步的 App KeyApp Secret

打开 Dropbox.m 文件找到下面这些行:

1
2
3
#warning INSERT YOUR OWN API KEY and SECRET HERE
static NSString *apiKey = @"YOUR_KEY";
static NSString *appSecret = @"YOUR_SECRET";

填写你的 app key 和 secret,让后删除 #warning line,现在你可以关闭 Dropbox Web App 页面。

下面,创建一个文件夹在你 Dropbox 主文件下的根目录给它命一个你想要的名字。假如你把这个文件夹个和其他的 Dropbox 用户分享,发送他们在构建 Byte Club App 的时候,他们将能够创建笔记并且能够上传所有人都能看得见的照片。

在 Dropbox.m 中找打下面这些行:

1
2
#warning THIS FOLDER MUST BE CREATED AT THE TOP LEVEL OF YOUR DROPBOX FOLDER, you can then share this folder with others
NSString * const appFolder = @"byteclub";

改变字符串的值,设置成你创建的 Dropbox 文件夹的名字,让后删除 #warning pragma.

为了把这个 app 分发给其他用户,给他们接入 access tokens ,你将需要为你的 Dropbox 平台 App 打开 Enable additional users 设置。

去在https://www.dropbox.com/developers/appsDropbox app 的控制台。点击你 app 名称。然后点击 Enable Additional Users 按钮。将出现一个状态对话框表明你已经增加了你的用户限制。点击 Okay 关闭对话框。你的 App 页面将像下面这样显示:

networking5

注意:你可能注意到当你正在开发你的 app 的时候,你可以接入多达 100 个用户。当你准备发布 app 销售的时候,你必须申请生产状态,你可以通过点击 Apply for production 按钮来发送给 Dropbox 一些额外的信息。

Dropbox 将随后审核你的 App 来确保它遵守指南,假如所有的一切进行得顺利的话,你将打开你的 app 的 API 接入无线的用户。

Dropbox 授权: 概览

假如你曾经使用过第三方 twitter 客服端 app,像 TweetBot ,你将会熟悉 OAuth 授权处理步骤从一个用户的角度。 OAuth 授权接入过程对你 app 来说是完全一样的。

构建运行你的 app,按照步骤登录。你将看到一个有 2 个标签的空白屏幕,一个是 Notes,一个是 PanoPhotos,如下图显示:

networking7

OAuth 授权发生在3和高级的步骤:

  1. 获取用来处理剩下的授权一个 OAuth 请求 token。这是请求 token。
  2. 一个 web 页面被呈现到用户面前通过他们的 web 浏览器。没有这一步的用户授权,对你的应用想获取一个第三步中的接入 token 几乎不可能。
  3. 在第二步完成后,应用调用 web 服务来交换临时请求 token(从第一步中的)为了一个将存储在 app 里面的持久接入 token。

注意:为了保证这个教程的简洁,我们不打算进行更详细的讲解关于这里 Dropbox 授权工作。然而,假如你想了解更多点击整个教程的完全版本,它是iOS 7 by Tutorials.的一部分。

NSURLSession 的一系列类

Apple 已经把 NSURLSession 描述成一个新类和一系列旧类的组合。这些新的工具是为了处理 上传,下载,处理授权已经处理在 HTTP 协议里面的任何事情。

networking12

一个 NSURLSession 用一个带可选代理的 NSURLSessionConfiguration 构造。在你创建会话以后,你应该能够满足你的网络需要通过创建 NSURLSessionTask 的任务。

NSURLSessionConfiguration

这里有三种方式创建 NSURLSessionConfiguration :

  • defaultSessionConfiguration - 创建一个使用全局缓存,cookie 的配置对象和凭证存储的对象。这个配置会使你的会话最像 NSURLConnection
  • ephemeralSessionConfiguration - 这个配置是用来作为‘私有的’会话并且不会持久化存储缓存,cookie,或者信用存储对象。
  • backgroundSessionConfiguration - 当你想要从远程推送或者当 app 被暂时挂起时进行网络业务使用这个这个配置。参考 17 章和 18 章在 iOS 7 by Tutorials, Beginning and Intermediate Multitasking ,有更详细的讲解。

一旦你创建一个 NSURLSessionConfiguration 对象,你就可以在它上面设置各种接口像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];

// 1
sessionConfig.allowsCellularAccess = NO;

// 2
[sessionConfig setHTTPAdditionalHeaders:
@{@"Accept": @"application/json"}];

// 3
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
  1. 你限制了网络操作只有 wifi 才能进行。
  2. 这将设置所有的请求只接受 JSON 类型的响应。
  3. 这些接口将配置资源或者请求超时时间。你也可以限制你的 app 对你的主机只能有一个网络连接。

这些仅仅是你能配置的一些东西,确保检查所有列表的文档。

NSURLSession

NSURLSession 被设计成替代 NSURLConnection 的API。Sessions做了他们的工作通过他们的部下,也就是非常出名的 NSURLSessionTask 对象。使用 NSURLSession 你能够创建任务使用基于block的便利方法,设置一个代理,或者同时两者。例如,假如你想要下载一张
图片( *challenge hint *),你就需要创建一个 NSURLSessionDownloadTask

第一步,你需要创建(会话)session。 这里有一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1
NSString *imageUrl =
@"http://www.raywenderlich.com/images/store/iOS7_PDFonly_280@2x_authorTBA.png";

// 2
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];

// 3
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];

Ok,这个仅仅是你目前所看到的一点点不同。让我们一步步重温。

  1. 用这个代码片段我们将一样进行下载在两个任务中。
  2. 你总是以创建 NSURLConfiguration 开始。
  3. 这里创建一个会话使用现在的类作为代理。

在你创建会话后,你可以通过创建一个带一个 completion handler 的任务下载这张图片,想下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1
NSURLSessionDownloadTask *getImageTask =
[session downloadTaskWithURL:[NSURL URLWithString:imageUrl]

completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error) {
// 2
UIImage *downloadedImage =
[UIImage imageWithData:
[NSData dataWithContentsOfURL:location]];
//3
dispatch_async(dispatch_get_main_queue(), ^{
// do stuff with image
_imageWithBlock.image = downloadedImage;
});
}];

// 4
[getImageTask resume];

Ah ha!现在这个看起来有点像网络代码!

  1. 任务总是被 sessions 创建。任务一旦被基于 block 的方法创建。记住你仍然可以使用 NSURLSessionDownloadDelegate 来跟踪下载进度。所以你将获得最好的两个单词!( *hint for challenge *)

-URLSession:downloadTask
:didWriteData:totalBytesWritten
:totalBytesExpectedToWrite:

  1. 这里你使用在 completion handler 提供的本地变量来获取一个指向图片的指针。
  2. 最终你能够,例如,更新 UIIImageView 的图片来显示新的文件。(hint hint ☺)
  3. 你总得自己启动任务!
  4. 记住我在前面所说的,一个会话也可以创建将要发送消息给代理方法来通知你完成等的任务。

应该长成这样,使用相同的会话从上面:

1
2
3
4
5
// 1
NSURLSessionDownloadTask *getImageTask =
[session downloadTaskWithURL:[NSURL URLWithString:imageUrl]];

[getImageTask resume];
  1. 这当然是确定使用更少的代码☺ 然而,假如你只这样做,你将什么都看不到。
    你需要让你的代理实现一些 NSURLSessionDownloadDelegate 协议的方法。

首先我们需要获得通知当下载完成时:

1
2
3
4
5
6
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
// use code above from completion handler
}

再有你需要提供将要下载的文件存放的位置,然后你就可以使用这个来处理图片。

最后,假如你需要跟踪下载进度,对于任务创建方法,你需要像下面这样用:

1
2
3
4
5
6
7
8
9
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"%f / %f", (double)totalBytesWritten,
(double)totalBytesExpectedToWrite);
}

正如你所见, NSURLSessionTask 是一匹通过网络来干活的真实的驮马。

NSURLSessionTask

目前为止你已经知道 NSURLSessionDataTaskNSURLSessionDownloadTask 怎样使用了。这两个的任务是来自他们共同的基类 NSURLSessionTask ,你可以在这类看到:

networking13

NSURLSessionTask 在你的会话中是任务的基类;他们只能通过一个会话创建并且它们是下面子类中的一个。

NSURLSessionDataTask

这个任务发起 HTTP GET 请求来从服服务器拉取数据。数据被返回以 NSData 的形式返回。你应该在随后将其把这个数据转换成正确的数据类型比如 XML , JSON ,UIImage,plist 等等。

1
2
3
4
5
6
NSURLSessionDataTask *jsonData = [session dataTaskWithURL:yourNSURL
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
// handle NSData
}];

NSURLSessionUploadTask

使用这个类当你需要上传一些东西到 web 服务器时,使用 HTTP POST 或者 PUT 命令。任务带来也允许你监视网络状况当它正在传输的时候。

上传一张图片:

1
2
3
4
5
NSData *imageData = UIImageJPEGRepresentation(image, 0.6);

NSURLSessionUploadTask *uploadTask =
[upLoadSession uploadTaskWithRequest:request
fromData:imageData];

在这类任务被创建从一个会话中并且图片以 NSData 的形式上传。这里也可以通使用一个文件或者流的方法来进行上传。

NSURLSessionDownloadTask

NSURLSessionDownloadTask 让通过远程服务下载文件变得超级简单,并且可以暂停和恢复下载只要你想。这个子类有别于其他两个。

  • 这个类型的任务直接写入一个临时文件。
  • 在下载会话中将调用 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: 来更新状态信息。
  • 当任务完成时, URLSession:downloadTask:didFinishDownloadingToURL: 被调用。这就是你该保存文件从临时位置到一个永久位置的时候。
  • 当下载失败或者取消时,你可以让数据重新开始下载。

这个特性将极其有用当你在下载一个 Byte Club 定位 全景照片给你的设备的相机胶卷。你看到的一个下载任务例子在上面下载图片片段中。

上述全部

所有的上述任务被创建在一个暂停的状态;在创建一个任务时你需要调用它的继续方法像下面演示的那样:

1
[uploadTask resume];

当你一次不只管理一个任务时, taskIdentifier 接口允许你唯一标示一个在会话中的任务

这就是!既然你已经知道了 NSURLSession 系列的主要类,让我们尝试一下。

Sharing notes with NSURLSession

OK,这不是死亡诗社,这是 Byte Club !是时候开始看看一下这个的网络代码在起作用了。

你需要一个方法来给 Byte Club 的其他成员发送消息。既然你已经设置了接入 token,下一步就是实例化 NSURLSesssion 对象,让后调用你的第一个 Dropbox API。

Creating an NSURLSession

添加下面的接口到 NotesViewController.m 文件,就在 NSArray *notes 行的后面:

1
@property (nonatomic, strong) NSURLSession *session;

你将创造所有你的下属从上面的 session 中。

添加下面的方法到 NotesViewController.m 就在 initWithStyle 方法的上面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// 1
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];

// 2
[config setHTTPAdditionalHeaders:@{@"Authorization": [Dropbox apiAuthorizationHeader]}];

// 3
_session = [NSURLSession sessionWithConfiguration:config];
}
return self;
}

下面是上面代码注释的注释的解释:

  1. 你的 app 调用 initWithCoder 当你实例化一个控制器从一个故事版中;因此这是一个完美的时刻初始化和创建 NSURLSession 。你并不想积极缓存或者持久化这里,所有你使用 ephemeralSessionConfiguration 便利方法,它返回一个没有持久化缓存,cookies,或者认证存储的会话。这是一个”私有浏览”配置。
  2. 下一步,你添加授权 HTTP 头道配置对象中。apiAuthorizationHeader 是一个我写的辅助方法,返回一个字符串,以授权制定的格式。这个字符串包含 access token,token secret 和你的 Dropbox App API 秘钥。记住这是必要的因为每个对 Dropbox API 的调用都需要被授权。
  3. 最后,你使用上面的配置创建了 NSURLSession

会话现在准备好创建在你 app 中你所需要的任何网络任务。

GET Notes through the Dropbox API

为了模拟一条笔记被另一个用户添加,添加任何你在设置在你的 Dropbox 根目录下的文件夹中选择的文本文件。例如位于 Dropbox 文件夹下 byteclub 下面显示的 test.txt

networking14

等待直到 Dropbox 确认它已经同步完你的文件,让后继续下面代码。

添加下面的代码到空的 notesOnDropBox 方法中在 NotesViewController.m :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// 1
NSURL *url = [Dropbox appRootURL];

// 2
NSURLSessionDataTask *dataTask =
[self.session dataTaskWithURL:url
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (!error) {
// TODO 1: More coming here!
}
}];

// 3
[dataTask resume];

这个方法的目标是检索在 app 的 Dropbox 文件下的文件列表。让我们来重温一下这是怎样工作的一步步。

  1. 在 Dropbox 中,你能看到一个文件夹的内容通过进行一个已经授权的 GET 请求到某个特别的 URL - 像 https://api.dropbox.com/1/metadata/dropbox/byteclub. 我已经创建了一个便利的方法在 Dropbox 类中来为你产生这个 URL。
  2. NSURLSession 用便利构造方法来简单的创建各种类型的任务。这是你创建的一个数据任务为了执行一个 GET 请求到那个 URL。当请求完成时,你的 completionHandler block 被调用。一会儿你将添加一下代码到这里。
  3. 记住一个任务默认是一个 暂停 的状态,因此你需要调用恢复方法来启动运行。

那就是所有你需要做的来开始一个 GET 请求-现在让我们添加代码到解析的结果中。添加下面的这些行到”TUDO 1”注释的后面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *) response;
if (httpResp.statusCode == 200) {

NSError *jsonError;

// 2
NSDictionary *notesJSON =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error:&jsonError];

NSMutableArray *notesFound = [[NSMutableArray alloc] init];

if (!jsonError) {
// TODO 2: More coming here!
}
}

下面是两个主要的部分:

  1. 你知道你已经发送一个 HTTP 请求,所以响应将是一个 HTTPP 响应。因此这里你可以抛出 NSURLResponse 到一个 NSHTTPURLRequest 响应以便你能够接入到接口的状态码。
    假如你收到了一个 HTTP 状态码 200,然后一切正常。

    HTTP 错误码举例:

    • 400 - 输入参数错误。错误消息将表明那个和为什么错误。
    • 401 - token 错误或者失效。这个可能发生假如用户或者 Dropbox 被撤销或者接入 token 过期。你可以通过重新授权修复这个用户。
    • 403 - 错误的授权请求(错误的用户键,坏的随机数,时间戳过期…)。不信的是,重新授权用户在这里并没有用。
    • 404 - 指定路径下的文件或者文件夹没找到。
    • 405 - 未知的请求方法(通常应该是 GET 或者 POST)。
    • 429 - 你的 app 发送太多请求超出了限制的速率,429 能够触发在每个 app 或者每个用户根部。
    • 503 - 假如响应包括重发后的头,这就意味做你的 OAuth 1.0 app 正在被限速。否则,这个表明了一个短暂的服务器错误,并且你的 app 应该重新发送这这个请求。
    • 507 - 用户超出 Dropbox 储存配额。
    • 5xx - 服务器错误。
  2. Dropbox API 返回 JSON 类型的数据。所有你收到一个 200 的响应,然后应该把数据转发成 JSON 使用 iOS 的构建 JSON 序列化的方法。了解更多关于 JSON 和 NSJSONSerialization,查看第 23 章在 iOS 5 by Tutorials ,”Working with JSON.”

JSON 数据返回从 Dropbox 将看起来像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"hash": "6a29b68d106bda4473ffdaf2e94c4b61",
"revision": 73052,
"rev": "11d5c00e1cf6c",
"thumb_exists": false,
"bytes": 0,
"modified": "Sat, 10 Aug 2013 21:56:50 +0000",
"path": "/byteclub",
"is_dir": true,
"icon": "folder",
"root": "dropbox",
"contents": [{
"revision": 73054,
"rev": "11d5e00e1cf6c",
"thumb_exists": false,
"bytes": 16,
"modified": "Sat, 10 Aug 2013 23:21:03 +0000",
"client_mtime": "Sat, 10 Aug 2013 23:21:02 +0000",
"path": "/byteclub/test.txt",
"is_dir": false,
"icon": "page_white_text",
"root": "dropbox",
"mime_type": "text/plain",
"size": "16 bytes"
}],
"size": "0 bytes"
}

所有最后一段添加的代码是拉取的部分你感兴趣的从 JSON 中。特别的,你想循环遍历“contents”数组来把“is_dir”设置成 false

这样做,添加下面的代码到“TODO 2”注释后面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1
NSArray *contentsOfRootDirectory = notesJSON[@"contents"];

for (NSDictionary *data in contentsOfRootDirectory) {
if (![data[@"is_dir"] boolValue]) {
DBFile *note = [[DBFile alloc] initWithJSONData:data];
[notesFound addObject:note];
}
}

[notesFound sortUsingComparator:
^NSComparisonResult(id obj1, id obj2) {
return [obj1 compare:obj2];
}];

self.notes = notesFound;

// 6
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self.tableView reloadData];
});

这里有两部分:

  1. 你拉取数组对象从“contents”键中,让后循环便利数组。每个数组入口是一个文件,所以你创建一个相应的 DBFile 文件模型为每一个文件。

DBFile 是一个我为你创建的辅助类,为了拉取信息从一个JSON字典到一个文件中 - 轻轻一瞥就能看到它是怎样工作的。

当你完成时,你添加所有的笔记到 self.notes 接口中。表视图被设置来显示数组中的任何记录。

既然你的表视图数据源已经更新了,你需要重载表的数据。无论何时你正在处理异步网络请求时,你必须保证更新 UIKit 在主线程。

机敏的读者将会注意到在上述代码中没有错误处理;假如你感觉你像一个哭丧女(大多数 Byte Club 都是!)添加一些代码在这里(在随后的代码块中你将添加),代码在错误和警告用户的时候将重试。

构建运行你的 app;你应该看你添加到你的 Dropbox 文件夹的文件是否显示在列表中,就像下面例子一样显示:

networking16

事情本来是小事,当时这证明了你正确的调用了 Dropbox API。

下一步是发布比较然后向其他俱乐部成员发起挑战,再一次使用 Dropbox API 就当你是传送机构。

POST Notes through the Dropbox API

轻点右上角的 + 号,你将看到笔记 add/edit 屏幕出现,就像下面演示的一样:

networking17

开始的 app 已经安装了 DBFile 模型对象到 NoteDetailsViewController 在 prepareForSegue:sender: 方法中:

加入你瞥一眼这个方法,你将看到 NoteViewController 被设置成 NoteDetailsViewController 的代理。这种方法, NoteDetailsViewController 能够通知 NoteViewController 当用户完成编辑一篇笔记或者取消编辑一篇笔记时。

打开 NotesViewController.m ,添加下面这行到 prepareForSegue:sender: 中,就在 showNote.delegate = self 行的后面;

1
showNote.session = _session;

NoteDetailsViewController 已经有一个 NSURLSession 的接口名字叫做 session ,因此你能够设置它在 prepareForSegue:sender: 载入之前。

现在 detail view controller 将获得相同的 NSURLSession ,所有 detail view controller 能够使用它来进行 DropBox 的 API 调用。

CancelDone 按钮已经呈现在你的app中;你只需要添加一些在他们背后保存或者取消在尚未完工的笔记逻辑。

NoteDetailsViewController.m ,找到下面这一行在 (IBAction)done:(id)sender: 方法中:

1
2
// - UPLOAD FILE TO DROPBOX - //
[self.delegate noteDetailsViewControllerDoneWithDetails:self];

…用下面的替换它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1
NSURL *url = [Dropbox uploadURLForPath: _note.path];

// 2
NSMutableURLRequest *request =
[[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"PUT"];

// 3
NSData *noteContents = [_note.contents dataUsingEncoding:NSUTF8StringEncoding];

// 4
NSURLSessionUploadTask *uploadTask = [_session
uploadTaskWithRequest:request
fromData:noteContents
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error)
{
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;

if (!error && httpResp.statusCode == 200) {

[self.delegate noteDetailsViewControllerDoneWithDetails:self];
} else {
// alert for error saving / updating note
}
}];

// 5
[uploadTask resume];

这个实现了你需要保存和分享你的笔记的所有事情。假如仔细观察每一块的注释,你将发现做了下面的事:

  1. 为了上传一个文件到 Dropbox,你需要再次使用某个 API URL。就像先前你需要一个 URL 来列出在一个文件夹中的文件,我已经构造了一个辅助方法来为你产生 URL。你可以在这里调用。
  2. 下一步是你的老朋友 NSMutableURLRequest ,新的 APIs 能够同时使用普通的 URL 是和 NSURLRequest 对象,但是这里你需要可变的形式来使 Dropbox API 让它的请求变成 PUT 请求。设置 HTTP 方法作为 PUT 发信号给 Dropbox 来让它为你创建一个新的文件。
  3. 下一步是将文本从你的 UITextView 编码成 NSData 对象。
  4. 既然你已经创建好了请求和 NSData 数据,你下一步就是创建一个 NSURLSessionUploadTask 然后设置 completion handler block.一旦成功,你就调用代理方法 noteDetailsViewControllerDoneWithDetails: 来关闭呈现的内容。在生产级别的应用中你可以回传一个新的 BDFile 给代理然后同步你需要持久化的数据。为了这个应用,你只需要刷新 NotesViewController 用一个网络调用。
  5. 再次提到,所有的任务以暂停的状态被创建,所以你必须调用恢复来启动他们。

构建然后运行你的 App,点击笔记标签上的+号。在 challenge name 字段上输入你的名字,输入一些文本在 note 字段想 Ray 发布一份挑战书,和下面的例子相似:

networking17

当你清点 Done 时, NoteViewController 将返回并且给你列出新的笔记像下面显示的那样:

networking18

你已经正式的给 Ray 下发挑战书;然而,他有朋友在非常高的位置所有你最好尽力完成这场比赛。

但是这里有一条非常重要的消息遗漏了。你能告诉我是什么么?

轻点笔记包含的挑战; NoteDetailsViewController 自己呈现,当时笔记的内容确实空白的。

Ray 并不会找到你的发的非常有威胁的挑战假如他没有读的话!

现在,app 只是调用 Dropbox 元数据 API 来检索文件列表。你也需要添加一些代码来抓取笔记的内容。

打开 NoteDetailsViewController.m ,用下面的实现替换空白的 retreiveNoteText 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-(void)retreiveNoteText
{
// 1
NSString *fileApi =
@"https://api-content.dropbox.com/1/files/dropbox";
NSString *escapedPath = [ _note.path
stringByAddingPercentEscapesUsingEncoding:
NSUTF8StringEncoding];

NSString *urlStr = [NSString stringWithFormat: @"%@/%@",
fileApi,escapedPath];

NSURL *url = [NSURL URLWithString: urlStr];

[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

// 2
[[ _session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

if (!error) {
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
// 3
NSString *text =
[[NSString alloc]initWithData:data
encoding:NSUTF8StringEncoding];
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.textView.text = text;
});

} else {
// HANDLE BAD RESPONSE //
}
} else {
// ALWAYS HANDLE ERRORS :-] //
}
// 4
}] resume];
}

上面在笔记中的代码(没有错误检查)下面来解释:

  1. 设置请求的路径和你期望检索的文件的 URL 地址;/文件的端点在 Dropbox API 中将会返回给你一个指定文件的内容。
  2. 用指向感兴趣的文件的 URL 创建数据任务。这个调用应该开始,只要你纵览整个 app 你会相当熟悉。
  3. 假如你响应的代码表明所有的都是好的,在主线程用你在先前的步骤中检索到的文件内容设置 textView。记住,UI 更新必须切换到主线程。
  4. 一旦这个任务被初始化,调用恢复。这里有写不一样的方法和以前的相比,当恢复被直接调用时在任务还没有指派时。

构建运行你的 App,在列表中对你的挑战轻点,内容将直接正确的显示在 view 中,像下面这样:

networking19

你可以扮演 Ray 然后通过向笔记中输入文本响应这个挑战;文件将很快更新当你轻点 Done

使用 NSURLSessionTask 代理发送照片

你已经看到怎样使用 NSURLSession 异步便利构造方法。但是假如你想把注意力集中在文件传输上,例如上传一个大文件并且显示一个进度条怎么样?

对于这种异步的,耗时任务类型你需要实现 NSURLSessionTaskDelegate 协议方。通过实现这个方法,你能够检索回调当一个任务接收到数据和完成接收数据时。

你可能已经注意到 PanoPhotos 标签是空的当你启动 App 的时候。然而, Byte Club 组织的创办成员已经慷慨的提供了一些他们自己的全景照片,你可以用它来填充你的 app。

下载这些 我们为你放在一起的全景照片。解压文件,拷贝到你的 app 在 Dropbox 目录下的照片目录。你文件夹的内容应该和下面一样:

networking20

Dropbox 和核心 API 可以提供照片的缩略图;使用一个 UITableView cell 这听起来想一件非常完美的事。

打开 PhotosViewController.m 然后在 “GO GET THUMBNAILS 注释后面添加下面的代码到 tableView:cellForRowAtIndexPath: ```objc
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSURLSessionDataTask *dataTask = [_session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response,
NSError *error) {
if (!error) {
UIImage *image = [[UIImage alloc] initWithData:data];
photo.thumbNail = image;
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
cell.thumbnailImage.image = photo.thumbNail;
});
} else {
// HANDLE ERROR //
}
}];
[dataTask resume];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

上面的代码显示了照片的缩略图图在表视图的 cell 中...或者至少它会是,假如 `_photoThumbnails` 现在不是空的话。

找到 `refreshPhotos` 用下面的实现替换:

```objc
- (void)refreshPhotos
{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSString *photoDir = [NSString stringWithFormat:@"https://api.dropbox.com/1/search/dropbox/%@/photos?query=.jpg",appFolder];
NSURL *url = [NSURL URLWithString:photoDir];

[[ _session dataTaskWithURL:url completionHandler:^(NSData
*data, NSURLResponse *response, NSError *error) {
if (!error) {
NSHTTPURLResponse *httpResp =
(NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {

NSError *jsonError;
NSArray *filesJSON = [NSJSONSerialization
JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error:&jsonError];
NSMutableArray *dbFiles =
[[NSMutableArray alloc] init];

if (!jsonError) {
for (NSDictionary *fileMetadata in
filesJSON) {
DBFile *file = [[DBFile alloc]
initWithJSONData:fileMetadata];
[dbFiles addObject:file];
}

[dbFiles sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1 compare:obj2];
}];

_photoThumbnails = dbFiles;

dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
[self.tableView reloadData];
});
}
} else {
// HANDLE BAD RESPONSE //
}
} else {
// ALWAYS HANDLE ERRORS :-] //
}
}] resume];
}

这个和你早期载入挑战笔记时写的代码很像。这次,API 调用来查找在 photos 目录的内容,并且只会以.jpg 拓展的文件。

既然 _photoThumbnails 数组已经填充好了,缩略图将出现在表视图中并且异步更新。

构建运行你的 app,然后切换到 PanoPhotos 标签;缩略图将载入并且像下面这样出现:

networking21

照片看起来非常的棒–只是请当心 Matthijs 家撕裂代码的猫🐱!

上传一张全景照片

你的 app 能够下载相片,如果它也能上传照片并且显示上传进度的话就非常棒了。

为了跟踪上传的进度, PhotosViewController 必须成为 NSURLSessionDelegateNSURLSessionTaskDelegate 协议的代理,以便你能收到进度回调。

修改在 PhotosViewController.mPhotosViewController 的接口声明,添加 NSURLSessionTaskDelegate ,像下面这样:

1
@ interface PhotosViewController ()UITableViewDelegate, UITableViewDataSource, UIImagePickerControllerDelegate, UINavigationControllerDelegate, NSURLSessionTaskDelegate>

下一步,添加下面的私有接口:

1
2
@property (nonatomic, strong)
NSURLSessionUploadTask *uploadTask;

上面的指针引用了任务对象;通过哪种方式,你就可以接入到对象的成员中来跟踪上传任务的进度了。

当用户选择一张图片上传时, didFinishPickingMediaWithInfo 调用 uploadImage: 方法来执行文件上传。
现在,那个方法空了-这是你的工作让它丰满起来。

替换 uploadImage: 用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)uploadImage:(UIImage*)image
{
NSData *imageData = UIImageJPEGRepresentation(image, 0.6);

// 1
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPMaximumConnectionsPerHost = 1;
[config setHTTPAdditionalHeaders:@{@"Authorization": [Dropbox apiAuthorizationHeader]}];

// 2
NSURLSession *upLoadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

// for now just create a random file name, dropbox will handle it if we overwrite a file and create a new name..
NSURL *url = [Dropbox createPhotoUploadURL];

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"PUT"];

// 3
self.uploadTask = [upLoadSession uploadTaskWithRequest:request fromData:imageData];

// 4
self.uploadView.hidden = NO;
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];

// 5
[ _uploadTask resume];
}

下面是上面的代码做的事:

  1. 起先,你使用设置在 initWithCoder 的会话和相关的便利方法来创建异步任务。这个时候,你使用一个 NSURLSessionConfiguration ,它只允许一个连接连接到远程主机,因为你上传进度处理一次就是一个文件。
  2. 上传和下载任务报告信息通过它们的代理返回;你将简短的实现。
  3. 这里你设置了 uploadTask 接口使用从 UIImagePicker 获得的 JPEG 图片。
  4. 下一步,你显示 UIProgressView 让它隐藏在 PhotosViewController 内部。
  5. 开始任务–额,抱歉,是恢复任务。

既然代理已经设置了,你就可以实现 NSURLSessionTaskDelegate 方法来更新进度视图。

添加下面的代码到 PhotosViewController.m 文件末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - NSURLSessionTaskDelegate methods

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
dispatch_async(dispatch_get_main_queue(), ^{
[ _progress setProgress:
(double)totalBytesSent /
(double)totalBytesExpectedToSend animated:YES];
});
}

上面的代理方法将定期报告信息给调用者关于上传任务的信息。它同时会更新 UIProgressView ( _progress)的进度以便显示 totalBytesSent/totalBytesExpectedToSend,这比显示一个完成的百分比跟有意义(也更极客)。

剩下唯一的事就是当上传任务结束时指示一下。添加下面的代码到 PhotosViewController.m 文件末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
// 1
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
_uploadView.hidden = YES;
[ _progress setProgress:0.5];
});

if (!error) {
// 2
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshPhotos];
});
} else {
// Alert for error
}
}

这里没有很多代码,但是它执行了两项非常重要的任务:

  1. 打开网络指示器,然后隐藏 _uploadView 作为上传完成的一点点清理工作。
  2. 刷新 PhotosViewController 以便包含你刚刚上传的照片,由于 demo app 是不能进行任何本地储存的上传
    。在一个真正的 app 中,你应该把图片在本地进行储存和缓存。

构建运行你的 app,导航到 PanoPhotos 标签,点击照相图标选择一张照片。

networking22

注意:假如你说使用模拟器测试 app,很显然你不能用你的 Mac 拍照,所有仅仅是拷贝一张全景照片
给模拟器然后上传。这样做,可以确保没有其他的 Xcode 工程现在连接到这个模拟器,在 Xcode 中选择 Xcode Open Developer Tool iOS Simulator

从 Finder 中拖拽一张全景照片带模拟器中,在模拟器中图片将会在 Safari 中打开。长按图片然后保存图片到图库中。

在选择一张图片后上传,uploadView 显示在屏幕的中央,并且带有上传进度,乡下面这一样显示:

networking23

你可能注意到一张图片上传需要花一些时间由于上传任务设置了 better quality 缩放因子。对于那些 A 类性格的人,你应该提供一个取消函数假如上传花费太长的时间。

取消按钮在 uploadView 已经被封装起来在故事板中,所有你只需实现清楚逻辑来杀死下载操作就行。

用下面的代码替换 PhotosViewController.mcancelUpload:

1
2
3
4
5
- (IBAction)cancelUpload:(id)sender {    
if ( _uploadTask.state == NSURLSessionTaskStateRunning) {
[ _uploadTask cancel];
}
}

在这类你会看到,取消一个任务相当简单就是调用一个取消方法。

现在构建运行你的 app,选择一张照片上传然后点击 Cancel 。图片上传将会停止并且 uploadView 将会被隐藏。

就这样– Byte Club 完成了!

何去何从?

这里是完成的工程在这个 NSURLSession 教程中。

假如你做到这一步,恭喜你可以享受到 Byte Club 的时光!不要告诉任何 Android 的小伙伴们! :]
你现在能够处理在你 app 需要的任何网络任务了。

假如你喜欢这个课程,你可能想要查阅我们的新书 iOS 7 by Tutorials,这是这本书的一个简略版本,这本书几乎涵盖了在 iOS 7 中最新和最多的 APIs,这些你应该清楚的知道作为一个开发者。

我几乎忘记了…

/slap have been slapped around a bit with a large trout.

(不要问我为什么 hehe!😄)

假如你有任何问题或者评论关于这个教程或者 NSURLSession ,请加入到下面的论坛讨论!

参考资料:

译者注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

Objective-C

使用 @synchronized 类实现锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 实例类 person
Person *person = [[Person alloc] init];
// 线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(person) {
[person personA];
[NSThread sleepForTimeInterval:3];
// 线程休眠 3 秒
}
});
// 线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(person) {
[person personB];
}
});

使用 NSRecursiveLock 类实现锁

递归锁,递归或循环方法时使用此方法实现锁,可避免死锁等问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 实例类 person
Person *person = [[Person alloc] init];

// 创建锁对象
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

// 创建递归方法
static void (^testCode)(int);
testCode = ^(int value) {
[theLock tryLock];
if (value > 0) {
[person personA];
[NSThread sleepForTimeInterval:1];
testCode(value - 1);
}
[theLock unlock];
};

//线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
testCode(5);
});

//线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[theLock lock];
[person personB];
[theLock unlock];
});

使用 NSConditionLock(条件锁)类实现锁

使用此方法可以指定,只有满足条件的时候才可以解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 实例类 person
Person *person = [[Person alloc] init];
// 创建条件锁
NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
// 线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lock];
[person personA];
[NSThread sleepForTimeInterval:5];
[conditionLock unlockWithCondition:10];
});
// 线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lockWhenCondition:10];
[person personB];
[conditionLock unlock];
});

NSDistributedLock(分布式锁)

在 iOS 中不需要用到,也没有这个方法,因此本文不作介绍,这里写出来只是想让大家知道有这个锁存在。
如果想要学习 NSDistributedLock 的话,你可以创建 MAC OS 的项目自己演练,方法请自行 Google,谢谢

C 语言

使用 pthread_mutex_t 实现锁

注意:必须在头文件导入:#import <pthread.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实例类 person
Person *person = [[Person alloc] init];
// 创建锁对象
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
[person personA];
[NSThread sleepForTimeInterval:5];
pthread_mutex_unlock(&mutex);
});
// 线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
[person personB];
pthread_mutex_unlock(&mutex);
});

使用 GCD 实现“锁”(信号量)

GCD 提供一种信号的机制,使用它我们可以创建“锁”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 实例类 person
Person *person = [[Person alloc] init];

// 创建并设置信量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

// 线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[person personA];
[NSThread sleepForTimeInterval:5];
dispatch_semaphore_signal(semaphore);
});

// 线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[person personB];
dispatch_semaphore_signal(semaphore);
});

我在这里解释一下代码。 dispatch_semaphore_wait 方法是把信号量加 1, dispatch_semaphore_signal 是把信号量减 1。

我们把信号量当作是一个计数器,当计数器是一个非负整数时,所有通过它的线程都应该把这个整数减 1。

如果计数器大于 0,那么则允许访问,并把计数器减 1。如果为 0,则访问被禁止,所有通过它的线程都处于
等待的状态。

使用 POSIX(条件锁)创建锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实例类 person
Person *person = [[Person alloc] init];

// 创建互斥锁
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

// 创建条件锁
__block pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

// 线程 A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
[person personA];
pthread_mutex_unlock(&mutex);
});

// 线程 B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&mutex);
[person personB];
[NSThread sleepForTimeInterval:5];
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
});

效果:程序会首先调用线程 B,在 5 秒后再调用线程 A。因为在线程 A 中创建了等待条件锁,线程 B 有激活锁,只有当线程 B 执行完后会激活线程 A。

pthread_cond_wait 方法为等待条件锁。

pthread_cond_signal 方法为激活一个相同条件的条件锁。

注意:自旋锁 OSSpinLock 已经不再线程安全

参考资料

SQLite Order By

SQLite 的 ORDER BY 子句是用来基于一个或多个列按升序或降序顺序排列数据。


语法

ORDER BY 子句的基本语法如下:

SELECT column-list
FROM table_name
[WHERE condition]
[ORDER BY column1, column2, .. columnN] [ASC | DESC];

您可以在 ORDER BY 子句中使用多个列。确保您使用的排序列在列清单中。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它会将结果按 SALARY 升序排序:

sqlite> SELECT * FROM COMPANY ORDER BY SALARY ASC;

这将产生以下结果:

    ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
7 James 24 Houston 10000.0
2 Allen 25 Texas 15000.0
1 Paul 32 California 20000.0
3 Teddy 23 Norway 20000.0
6 Kim 22 South-Hall 45000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面是一个实例,它会将结果按 NAME 和 SALARY 升序排序:

sqlite> SELECT * FROM COMPANY ORDER BY NAME, SALARY ASC;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
5 David 27 Texas 85000.0
7 James 24 Houston 10000.0
6 Kim 22 South-Hall 45000.0
4 Mark 25 Rich-Mond 65000.0
1 Paul 32 California 20000.0
3 Teddy 23 Norway 20000.0

下面是一个实例,它会将结果按 NAME 降序排序:

sqlite> SELECT * FROM COMPANY ORDER BY NAME DESC;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
3 Teddy 23 Norway 20000.0
1 Paul 32 California 20000.0
4 Mark 25 Rich-Mond 65000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0
5 David 27 Texas 85000.0
2 Allen 25 Texas 15000.0

SQLite Group By

SQLite 的 GROUP BY 子句用于与 SELECT 语句一起使用,来对相同的数据进行分组。
在 SELECT 语句中,GROUP BY 子句放在 WHERE 子句之后,放在 ORDER BY 子句之前。


语法

下面给出了 GROUP BY 子句的基本语法。GROUP BY 子句必须放在 WHERE 子句中的条件之后,必须放在 ORDER BY 子句之前。

SELECT column-list
FROM table_name
WHERE [ conditions ]
GROUP BY column1, column2....columnN
ORDER BY column1, column2....columnN

您可以在 GROUP BY 子句中使用多个列。确保您使用的分组列在列清单中。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

如果您想了解每个客户的工资总额,则可使用 GROUP BY 查询,如下所示:

sqlite> SELECT NAME, SUM(SALARY) FROM COMPANY GROUP BY NAME;

这将产生以下结果:

NAME SUM(SALARY)
---------- -----------
Allen 15000
David 85000
James 20000
Kim 45000
Mark 65000
Paul 40000
Teddy 20000

让我们把 ORDER BY 子句与 GROUP BY 子句一起使用,如下所示:

sqlite> SELECT NAME, SUM(SALARY)
 FROM COMPANY GROUP BY NAME ORDER BY NAME DESC;

这将产生以下结果:

NAME SUM(SALARY)
---------- -----------
Teddy 20000
Paul 40000
Mark 65000
Kim 45000
James 20000
David 85000
Allen 15000

SQLite Having 子句

HAVING 子句允许指定条件来过滤将出现在最终结果中的分组结果。

WHERE 子句在所选列上设置条件,而 HAVING 子句则在由 GROUP BY 子句创建的分组上设置条件。


语法

下面是 HAVING 子句在 SELECT 查询中的位置:

SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY

在一个查询中,HAVING 子句必须放在 GROUP BY 子句之后,必须放在 ORDER BY 子句之前。下面是包含 HAVING 子句的 SELECT 语句的语法:

SELECT column1, column2
FROM table1, table2
WHERE [ conditions ]
GROUP BY column1, column2
HAVING [ conditions ]
ORDER BY column1, column2

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0
8 Paul 24 Houston 20000.0
9 James 44 Norway 5000.0
10 James 45 Texas 5000.0

下面是一个实例,它将显示名称计数小于 2 的所有记录:

sqlite > SELECT * FROM COMPANY GROUP BY name HAVING count(name) < 2;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000
5 David 27 Texas 85000
6 Kim 22 South-Hall 45000
4 Mark 25 Rich-Mond 65000
3 Teddy 23 Norway 20000

下面是一个实例,它将显示名称计数大于 2 的所有记录,注意其实这里只返回最后一条记录:

sqlite > SELECT * FROM COMPANY GROUP BY name HAVING count(name) > 2;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
10 James 45 Texas 5000

SQLite Distinct 关键字

SQLite 的 DISTINCT 关键字与 SELECT 语句一起使用,来消除所有重复的记录,并只获取唯一一次记录。
有可能出现一种情况,在一个表中有多个重复的记录。当提取这样的记录时,DISTINCT 关键字就显得特别有意义,它只获取唯一一次记录,而不是获取重复记录。


语法

用于消除重复记录的 DISTINCT 关键字的基本语法如下:

SELECT DISTINCT column1, column2,.....columnN
FROM table_name
WHERE [condition]

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0
8 Paul 24 Houston 20000.0
9 James 44 Norway 5000.0
10 James 45 Texas 5000.0

首先,让我们来看看下面的 SELECT 查询,它将返回重复的工资记录:

sqlite> SELECT name FROM COMPANY;

这将产生以下结果:

NAME
----------
Paul
Allen
Teddy
Mark
David
Kim
James
Paul
James
James

现在,让我们在上述的 SELECT 查询中使用 DISTINCT 关键字:

sqlite> SELECT DISTINCT name FROM COMPANY;

这将产生以下结果,没有任何重复的条目:

NAME
----------
Paul
Allen
Teddy
Mark
David
Kim
James

资料整理于:RUNOOB.COM-SQLite 教程
转载请注明出处。

SQLite Delete 语句

SQLite 的 DELETE 查询用于删除表中已有的记录。可以使用带有 WHERE 子句的 DELETE 查询来删除选定行,否则所有的记录都会被删除。


语法

带有 WHERE 子句的 DELETE 查询的基本语法如下:

DELETE FROM table_name
WHERE [condition];

您可以使用 AND 或 OR 运算符来结合 N 个数量的条件。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

现在,COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0

如果您想要从 COMPANY 表中删除所有记录,则不需要使用 WHERE 子句,DELETE 查询如下:

sqlite> DELETE FROM COMPANY;

现在,COMPANY 表中没有任何的记录,因为所有的记录已经通过 DELETE 语句删除。

SQLite Like 子句

SQLite 的 LIKE 运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,LIKE 运算符将返回真(true),也就是 1。这里有两个通配符与 LIKE 运算符一起使用:

  • 百分号 (%)
  • 下划线 (_)

百分号(%)代表零个、一个或多个数字或字符。下划线(_)代表一个单一的数字或字符。这些符号可以被组合使用。


语法

% 和 _ 的基本语法如下:

SELECT FROM table_name
WHERE column LIKE 'XXXX%'

or

SELECT FROM table_name
WHERE column LIKE '%XXXX%'

or

SELECT FROM table_name
WHERE column LIKE 'XXXX_'

or

SELECT FROM table_name
WHERE column LIKE '_XXXX'

or

SELECT FROM table_name
WHERE column LIKE '_XXXX_'

您可以使用 AND 或 OR 运算符来结合 N 个数量的条件。在这里,XXXX 可以是任何数字或字符串值。

实例

下面一些实例演示了 带有 ‘%’ 和 ‘_’ 运算符的 LIKE 子句不同的地方:

语句 描述
WHERE SALARY LIKE ‘200%’ 查找以 200 开头的任意值
WHERE SALARY LIKE ‘%200%’ 查找任意位置包含 200 的任意值
WHERE SALARY LIKE ‘_00%‘ 查找第二位和第三位为 00 的任意值
WHERE SALARY LIKE ‘2_%_%’ 查找以 2 开头,且长度至少为 3 个字符的任意值
WHERE SALARY LIKE ‘%2’ 查找以 2 结尾的任意值
WHERE SALARY LIKE ‘_2%3’ 查找第二位为 2,且以 3 结尾的任意值
WHERE SALARY LIKE ‘2___3’ 查找长度为 5 位数,且以 2 开头以 3 结尾的任意值

让我们举一个实际的例子,假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它显示 COMPANY 表中 AGE 以 2 开头的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE LIKE '2%';

这将产生以下结果:

    ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它显示 COMPANY 表中 ADDRESS 文本里包含一个连字符(-)的所有记录:

sqlite> SELECT * FROM COMPANY WHERE ADDRESS LIKE '%-%';

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
6 Kim 22 South-Hall 45000.0

SQLite Glob 子句

SQLite 的 GLOB 运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,GLOB 运算符将返回真(true),也就是 1。与 LIKE 运算符不同的是,GLOB 是大小写敏感的,对于下面的通配符,它遵循 UNIX 的语法。

  • 星号 (*)
  • 问号 (?)

星号(*)代表零个、一个或多个数字或字符。问号(?)代表一个单一的数字或字符。这些符号可以被组合使用。


语法

* 和 ? 的基本语法如下:

SELECT FROM table_name
WHERE column GLOB 'XXXX*'

or

SELECT FROM table_name
WHERE column GLOB '*XXXX*'

or

SELECT FROM table_name
WHERE column GLOB 'XXXX?'

or

SELECT FROM table_name
WHERE column GLOB '?XXXX'

or

SELECT FROM table_name
WHERE column GLOB '?XXXX?'

or

SELECT FROM table_name
WHERE column GLOB '????'

您可以使用 AND 或 OR 运算符来结合 N 个数量的条件。在这里,XXXX 可以是任何数字或字符串值。


实例

下面一些实例演示了 带有 ‘*’ 和 ‘?’ 运算符的 GLOB 子句不同的地方:

语句 描述
WHERE SALARY GLOB ‘200*‘ 查找以 200 开头的任意值
WHERE SALARY GLOB ‘*200*‘ 查找任意位置包含 200 的任意值
WHERE SALARY GLOB ‘?00*‘ 查找第二位和第三位为 00 的任意值
WHERE SALARY GLOB ‘2??’ 查找以 2 开头,且长度至少为 3 个字符的任意值
WHERE SALARY GLOB ‘*2’ 查找以 2 结尾的任意值
WHERE SALARY GLOB ‘?2*3’ 查找第二位为 2,且以 3 结尾的任意值
WHERE SALARY GLOB ‘2???3’ 查找长度为 5 位数,且以 2 开头以 3 结尾的任意值

让我们举一个实际的例子,假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它显示 COMPANY 表中 AGE 以 2 开头的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE GLOB '2*';

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它显示 COMPANY 表中 ADDRESS 文本里包含一个连字符(-)的所有记录:

sqlite> SELECT * FROM COMPANY WHERE ADDRESS GLOB '*-*';

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
6 Kim 22 South-Hall 45000.0

SQLite Limit 子句

SQLite 的 LIMIT 子句用于限制由 SELECT 语句返回的数据数量。


语法

带有 LIMIT 子句的 SELECT 语句的基本语法如下:

SELECT column1, column2, columnN
FROM table_name
LIMIT [no of rows]

下面是 LIMIT 子句与 OFFSET 子句一起使用时的语法:

SELECT column1, column2, columnN
FROM table_name
LIMIT [no of rows] OFFSET [row num]

SQLite 引擎将返回从下一行开始直到给定的 OFFSET 为止的所有行,如下面的最后一个实例所示。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它限制了您想要从表中提取的行数:

sqlite> SELECT * FROM COMPANY LIMIT 6;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0

但是,在某些情况下,可能需要从一个特定的偏移开始提取记录( OFFSET下标从0开始 )。下面是一个实例,从第三位开始提取 3 个记录:

sqlite> SELECT * FROM COMPANY LIMIT 3 OFFSET 2;

这将产生以下结果:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

资料整理于:RUNOOB.COM-SQLite 教程
转载请注明出处。

SQL 表达式

表达式是一个或多个值、运算符和计算值的 SQL 函数的组合。

SQL 表达式与公式类似,都写在查询语言中。您还可以使用特定的数据集来查询数据库。


语法

假设 SELECT 语句的基本语法如下:

SELECT column1, column2, columnN
FROM table_name
WHERE [CONTION | EXPRESSION];

有不同类型的 SQLite 表达式,具体讲解如下:

SQLite - 布尔表达式

SQLite 的布尔表达式在匹配单个值的基础上获取数据。语法如下:

SELECT column1, column2, columnN
FROM table_name
WHERE SINGLE VALUE MATCHTING EXPRESSION;

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的实例演示了 SQLite 布尔表达式的用法:

sqlite> SELECT * FROM COMPANY WHERE SALARY = 10000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 James 24 Houston 10000.0

SQLite - 数值表达式

这些表达式用来执行查询中的任何数学运算。语法如下:

SELECT numerical_expression as OPERATION_NAME
[FROM table_name WHERE CONDITION] ;

在这里,numerical_expression 用于数学表达式或任何公式。下面的实例演示了 SQLite 数值表达式的用法:

sqlite> SELECT (15 + 6) AS ADDITION
ADDITION = 21

有几个内置的函数,比如 avg()、sum()、count(),等等,执行被称为对一个表或一个特定的表列的汇总数据计算。

sqlite> SELECT COUNT(*) AS "RECORDS" FROM COMPANY;
RECORDS = 7

SQLite - 日期表达式

日期表达式返回当前系统日期和时间值,这些表达式将被用于各种数据操作。

sqlite> SELECT CURRENT_TIMESTAMP;
CURRENT_TIMESTAMP = 2016-01-13 16:29:56

SQLite Where 子句

SQLite 的 WHERE 子句用于指定从一个表或多个表中获取数据的条件。

如果满足给定的条件,即为真(true)时,则从表中返回特定的值。您可以使用 WHERE 子句来过滤记录,只获取需要的记录。

WHERE 子句不仅可用在 SELECT 语句中,它也可用在 UPDATE、DELETE 语句中,等等,这些我们将在随后的章节中学习到。


语法

SQLite 的带有 WHERE 子句的 SELECT 语句的基本语法如下:

SELECT column1, column2, columnN
FROM table_name
WHERE [condition]

实例

您还可以使用比较或逻辑运算符指定条件,比如 >、<、=、LIKE、NOT,等等。假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的实例演示了 SQLite 逻辑运算符的用法。下面的 SELECT 语句列出了 AGE 大于等于 25 且工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 AND SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 AGE 大于等于 25 或工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 OR SALARY >= 65000;

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 AGE 不为 NULL 的所有记录,结果显示所有的记录,意味着没有一个记录的 AGE 等于 NULL:

sqlite> SELECT * FROM COMPANY WHERE AGE IS NOT NULL;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 NAME 以 ‘Ki’ 开始的所有记录,’Ki’ 之后的字符不做限制(不区分大小写):

sqlite> SELECT * FROM COMPANY WHERE NAME LIKE 'Ki%';
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
6 Kim 22 South-Hall 45000.0

下面的 SELECT 语句列出了 NAME 以 ‘Ki’ 开始的所有记录,’Ki’ 之后的字符不做限制(区分大小写):

sqlite> SELECT * FROM COMPANY WHERE NAME GLOB 'Ki*';
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
6 Kim 22 South-Hall 45000.0

下面的 SELECT 语句列出了 AGE 的值为 25 或 27 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE IN ( 25, 27 );
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

SQLite AND/OR 运算符

SQLite 的 AND 和 OR 运算符用于编译多个条件来缩小在 SQLite 语句中所选的数据。这两个运算符被称为连接运算符。

这些运算符为同一个 SQLite 语句中不同的运算符之间的多个比较提供了可能。


AND 运算符

AND 运算符允许在一个 SQL 语句的 WHERE 子句中的多个条件的存在。使用 AND 运算符时,只有当所有条件都为真(true)时,整个条件为真(true)。例如,只有当 condition1 和 condition2 都为真(true)时,[condition1] AND [condition2] 为真(true)。

语法

带有 WHERE 子句的 AND 运算符的基本语法如下:

SELECT column1, column2, columnN
FROM table_name
WHERE [condition1] AND [condition2]...AND [conditionN];

您可以使用 AND 运算符来结合 N 个数量的条件。SQLite 语句需要执行的动作是,无论是事务或查询,所有由 AND 分隔的条件都必须为真(TRUE)。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 AGE 大于等于 25 且工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 AND SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

OR 运算符

OR 运算符也用于结合一个 SQL 语句的 WHERE 子句中的多个条件。使用 OR 运算符时,只要当条件中任何一个为真(true)时,整个条件为真(true)。例如,只要当 condition1 或 condition2 有一个为真(true)时,[condition1] OR [condition2] 为真(true)

语法

带有 WHERE 子句的 OR 运算符的基本语法如下:

SELECT column1, column2, columnN
FROM table_name
WHERE [condition1] OR [condition2]...OR [conditionN]

您可以使用 OR 运算符来结合 N 个数量的条件。SQLite 语句需要执行的动作是,无论是事务或查询,只要任何一个由 OR 分隔的条件为真(TRUE)即可。

实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 AGE 大于等于 25 或工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 OR SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

SQLite Update 语句

SQLite 的 **UPDATE **查询用于修改表中已有的记录。可以使用带有 WHERE 子句的 UPDATE 查询来更新选定行,否则所有的行都会被更新。


语法

带有 WHERE 子句的 UPDATE 查询的基本语法如下:

UPDATE table_name
SET column1 = value1, column2 = value2...., columnN = valueN
WHERE [condition];

您可以使用 AND 或 OR 运算符来结合 N 个数量的条件。


实例

假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面是一个实例,它会更新 ID 为 6 的客户地址:

sqlite> UPDATE COMPANY SET ADDRESS = 'Texas' WHERE ID = 6;

现在,COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 Texas 45000.0
7 James 24 Houston 10000.0

如果您想修改 COMPANY 表中 ADDRESS 和 SALARY 列的所有值,则不需要使用 WHERE 子句,UPDATE 查询如下:

sqlite> UPDATE COMPANY SET ADDRESS = 'Texas', SALARY = 20000.00;

现在,COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 Texas 20000.0
2 Allen 25 Texas 20000.0
3 Teddy 23 Texas 20000.0
4 Mark 25 Texas 20000.0
5 David 27 Texas 20000.0
6 Kim 22 Texas 20000.0
7 James 24 Texas 20000.0

资料整理于:RUNOOB.COM-SQLite 教程
转载请注明出处。

SQLite 运算符

SQLite 运算符是什么?

运算符是一个保留字或字符,主要用于 SQLite 语句的 WHERE 子句中执行操作,如比较和算术运算。

运算符用于指定 SQLite 语句中的条件,并在语句中连接多个条件。

  • 算术运算符
  • 比较运算符
  • 逻辑运算符
  • 位运算符

SQLite 算术运算符

假设变量 a=10,变量 b=20,则:

运算符 描述 实例
+ 加法 - 把运算符两边的值相加 a + b 将得到 30
- 减法 - 左操作数减去右操作数 a - b 将得到 -10
* 乘法 - 把运算符两边的值相乘 a * b 将得到 200
/ 除法 - 左操作数除以右操作数 b / a 将得到 2
% 取模 - 左操作数除以右操作数后得到的余数 b % a will give 0

实例

下面是 SQLite 算术运算符的简单实例:

sqlite> .mode line
sqlite> select 10 + 20;
10 + 20 = 30


sqlite> select 10 - 20;
10 - 20 = -10


sqlite> select 10 * 20;
10 * 20 = 200


sqlite> select 10 / 5;
10 / 5 = 2


sqlite> select 12 % 5;
12 % 5 = 2

SQLite 比较运算符

假设变量 a=10,变量 b=20,则:

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (a == b) 不为真。
= 检查两个操作数的值是否相等,如果相等则条件为真。 (a = b) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (a != b) 为真。
<> 检查两个操作数的值是否相等,如果不相等则条件为真。 (a <> b) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (a > b) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (a < b) 为真。
>= 检查左操作数的值是否大于等于右操作数的值,如果是则条件为真。 (a >= b) 不为真。
<= 检查左操作数的值是否小于等于右操作数的值,如果是则条件为真。 (a <= b) 为真。
!< 检查左操作数的值是否不小于右操作数的值,如果是则条件为真。 (a !< b) 为假
!> 检查左操作数的值是否不大于右操作数的值,如果是则条件为真。 (a !> b) 为真

实例
假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的实例演示了各种 SQLite 比较运算符的用法。

在这里,我们使用 WHERE 子句,这将会在后边单独的一个章节中讲解,但现在您需要明白,WHERE 子句是用来设置 SELECT 语句的条件语句。

下面的 SELECT 语句列出了 SALARY 大于 50,000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE SALARY > 50000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 SALARY 等于 20,000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE SALARY = 20000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
3 Teddy 23 Norway 20000.0

下面的 SELECT 语句列出了 SALARY 不等于 20,000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE SALARY != 20000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 SALARY 不等于 20,000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE SALARY <> 20000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 SALARY 大于等于 65,000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

SQLite 逻辑运算符

下面是 SQLite 中所有的逻辑运算符列表。

运算符 描述
AND AND 运算符允许在一个 SQL 语句的 WHERE 子句中的多个条件的存在。
BETWEEN BETWEEN 运算符用于在给定最小值和最大值范围内的一系列值中搜索值。
EXISTS EXISTS 运算符用于在满足一定条件的指定表中搜索行的存在。
IN IN 运算符用于把某个值与一系列指定列表的值进行比较。
NOT IN IN 运算符的对立面,用于把某个值与不在一系列指定列表的值进行比较。
LIKE LIKE 运算符用于把某个值与使用通配符运算符的相似值进行比较。
GLOB GLOB 运算符用于把某个值与使用通配符运算符的相似值进行比较。GLOB 与 LIKE 不同之处在于,它是大小写敏感的。
NOT NOT 运算符是所用的逻辑运算符的对立面。比如 NOT EXISTS、NOT BETWEEN、NOT IN,等等。它是否定运算符。
OR OR 运算符用于结合一个 SQL 语句的 WHERE 子句中的多个条件。
IS NULL NULL 运算符用于把某个值与 NULL 值进行比较。
IS IS 运算符与 = 相似。
IS NOT IS NOT 运算符与 != 相似。
|| 连接两个不同的字符串,得到一个新的字符串。
UNIQUE UNIQUE 运算符搜索指定表中的每一行,确保唯一性(无重复)。

实例
假设 COMPANY 表有以下记录:

ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的实例演示了 SQLite 逻辑运算符的用法。
下面的 SELECT 语句列出了 AGE 大于等于 25 且工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 AND SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 AGE 大于等于 25 或工资大于等于 65000.00 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE >= 25 OR SALARY >= 65000;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 AGE 不为 NULL 的所有记录,结果显示所有的记录,意味着没有一个记录的 AGE 等于 NULL:

sqlite> SELECT * FROM COMPANY WHERE AGE IS NOT NULL;
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
2 Allen 25 Texas 15000.0
3 Teddy 23 Norway 20000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句列出了 NAME 以 ‘Ki’ 开始的所有记录,’Ki’ 之后的字符不做限制:

sqlite> SELECT * FROM COMPANY WHERE NAME LIKE 'Ki%';
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
6 Kim 22 South-Hall 45000.0

下面的 SELECT 语句列出了 NAME 以 ‘Ki’ 开始的所有记录,’Ki’ 之后的字符不做限制:

sqlite> SELECT * FROM COMPANY WHERE NAME GLOB 'Ki*';
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
6 Kim 22 South-Hall 45000.0

下面的 SELECT 语句列出了 AGE 的值为 25 或 27 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE IN ( 25, 27 );
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
2 Allen 25 Texas 15000.0
4 Mark 25 Rich-Mond 65000.0
5 David 27 Texas 85000.0

下面的 SELECT 语句列出了 AGE 的值既不是 25 也不是 27 的所有记录:

sqlite> SELECT * FROM COMPANY WHERE AGE NOT IN ( 25, 27 );
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0
3 Teddy 23 Norway 20000.0
6 Kim 22 South-Hall 45000.0
7 James 24 Houston 10000.0

下面的 SELECT 语句使用 SQL 子查询,子查询查找 SALARY > 65000 的带有 AGE 字段的所有记录,后边的 WHERE 子句与 EXISTS 运算符一起使用,列出了外查询中的 AGE 存在于子查询返回的结果中的所有记录:

sqlite> SELECT AGE FROM COMPANY
 WHERE EXISTS (SELECT AGE FROM COMPANY WHERE SALARY > 65000);
AGE
----------
32
25
23
25
27
22
24

下面的 SELECT 语句使用 SQL 子查询,子查询查找 SALARY > 65000 的带有 AGE 字段的所有记录,后边的 WHERE 子句与 > 运算符一起使用,列出了外查询中的 AGE 大于子查询返回的结果中的年龄的所有记录:

sqlite> SELECT * FROM COMPANY
 WHERE AGE > (SELECT AGE FROM COMPANY WHERE SALARY > 65000);
ID NAME AGE ADDRESS SALARY
---------- ---------- ---------- ---------- ----------
1 Paul 32 California 20000.0

SQLite 位运算符

位运算符作用于位,并逐位执行操作。真值表 & 和 | 如下:

p q p&q p|q
0 0 0 0
0 1 0 1
1 1 1 1
1 0 0 1

假设如果 A = 60,且 B = 13,现在以二进制格式,它们如下所示:

A = 0011 1100

B = 0000 1101


A&B = 0000 1100

A|B = 0011 1101

~A = 1100 0011

下表中列出了 SQLite 语言支持的位运算符。假设变量 A=60,变量 B=13,则:

运算符 描述 实例
& 如果同时存在于两个操作数中,二进制 AND 运算符复制一位到结果中。 (A & B) 将得到 12,即为 0000 1100
| 如果存在于任一操作数中,二进制 OR 运算符复制一位到结果中。 (A
~ 二进制补码运算符是一元运算符,具有”翻转”位效应。 (~A ) 将得到 -61,即为 1100 0011,2 的补码形式,带符号的二进制数。
<< 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 A << 2 将得到 240,即为 1111 0000
>> 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 A >> 2 将得到 15,即为 0000 1111

实例

下面的实例演示了 SQLite 位运算符的用法:

sqlite> .mode line
sqlite> select 60 | 13;
60 | 13 = 61

sqlite> select 60 & 13;
60 & 13 = 12

sqlite> select 60 ^ 13;
10 * 20 = 200


sqlite> select (~60);
(~60) = -61

sqlite> select (60 << 2);
(60 << 2) = 240

sqlite> select (60 >> 2);
(60 >> 2) = 15

资料整理于:RUNOOB.COM-SQLite 教程
转载请注明出处。