您的位置:首页 > 其它

微博开发笔记上(未完待续)

2015-09-04 23:43 316 查看

新浪微博开发笔记

iPhone 项目目标

项目掌控能力

工具使用能力

开发技巧能力

课程提纲



新浪微博接口地址

微博开放平台地址

http://open.weibo.com

微博接口文档地址

http://open.weibo.com/wiki/微博API

项目主题框架

走向工作岗位之后,一般会遇到两种工作情况:

新项目开发

通常在项目开始之前,公司的产品经理会提供完整的产品原型图,或功能设计文档

通过对这些文档的解读,能够梳理出目标项目的整体架构,从而协助项目框架的搭建

旧项目维护

很多老项目是缺乏文档的,这种情况在一些小公司中表现的尤为突出

要想快速上手一个老项目,首先运行项目,并且整理项目整体框架结构

然后用整理出的框架结构与代码结构相互印证,无疑可以对了解项目的整体架构起到重要的辅助

综上所述,无论是新项目,还是老项目,在开发之前确定项目的主体架构都是非常重要,也是十分必要的!

主体架构确认的好处

开发之前,明确项目的主体架构具有以下好处:

明确开发目标,项目一旦启动,始终锁定目标前进!

明确功能模块的数量,方便工期核算

根据开发进度,预判开发周期,及时与相关部门沟通、协调

根据主体架构搭建项目框架,方便团队开发,各个功能模块齐头并进,提高开发效率!

确定项目开发中的重点难点,提前安排攻关能力强的同事进行技术攻关,待需要时能够享受攻关成果,或者及时调整产品设计

新增或调整功能时,能够高屋建瓴,在最合适的位置添加相关功能模块

新浪微博

作为中国移动互联网的代表性产品之一,新浪微博涵盖了大量的移动互联网元素,通过对新浪微博的研究及模仿,可以:

对这些元素在实际产品中的应用有深入的了解和认识

知道如何在一个真实的项目中运用相关技术点

对大型项目的架构、开发及掌控有更全面的认识和理解

正如前文所述,在开始模仿之前,首先运行产品,掌握项目的整体架构,确定开发的主体功能非常重要!

新浪微博主体架构

对界面预览之后,可以发现新浪微博符合经典应用程序架构设计:

主视图控制器是一个
UITabbarController


包含四个
UINavigationController
,分别是

首页

消息

发现



特殊之处:

-
UITabbarController
中间有一个 “+” 按钮,点击该按钮能够 Modal 显示
微博类型选择
界面,方便用户选择自己需要的微博类型

- 四个
UINavigationController
在用户登录前后显示的界面格式是不一样的

根原版新浪微博的区别

由于必须使用新浪微博官方的 API 才能够正常开发,换言之,如果没有登录系统是无法使用新浪微博提供的接口的!

基于上述原因,在实际开发中对未登录之前的界面设计进行简化

开源中国社区

官方网站

https://git.oschina.net/

开源中国社区成立于2008年8月,其目的是为中国的IT技术人员提供一个全面的、快捷更新的用来检索开源软件以及交流使用开源经验的平台

目前国内有很多公司会将公司的项目部署在
OSChina


GitHUB
的对比

服务器在国内,速度更快

免费账户同样可以建立
私有
项目,而
GitHUB
上要建立私有项目必须
付费


使用

注册账号

建议使用网易的邮箱,使用其他免费邮箱可能会收不到验证邮件

添加 SSH 公钥,进入终端,并输入以下命令

开源中国帮助文档地址:https://git.oschina.net/oschina/git-osc/wikis/帮助#ssh-keys

# 切换目录,MAC中目录的第一个字符如果是 `.` 表示改文件夹是隐藏文件夹
$ cd ~/.ssh
# 查看当前目录文件
$ ls

# 生成 RSA 密钥对
# 1> "" 中输入个人邮箱
# 2> 提示输入私钥文件名称,直接回车
# 3> 提示输入密码,可以随便输入,只要本次能够记住即可
$ ssh-keygen -t rsa -C "xxx@126.com"

# 查看公钥内容
$ cat id_rsa.pub


将公钥内容复制并粘贴至 https://git.oschina.net/profile/sshkeys

测试公钥

# 测试 SSH 连接
$ ssh -T git@git.oschina.net

# 终端提示 `Welcome to Git@OSC, 刀哥!` 说明连接成功


新建项目

克隆项目

# 切换至项目目录
$ cd 项目目录

# 克隆项目,地址可以在项目首页复制
$ git clone git@git.oschina.net:xxx/ProjectName.git


添加
gitignore


# ~/dev/github/gitignore/ 是保存 gitignore 的目录
$ cp ~/dev/github/gitignore/Swift.gitignore .gitignore


提示:

可以从
https://github.com/github/gitignore
获取最新版本的
gitignore
文件

添加
.gitignore
文件之后,每次提交时不会将个人的项目设置信息(例如:末次打开的文件,调试断点等)提交到服务器,在团队开发中非常重要

图片素材

素材对应的设备

1x2x3x
大小对应开发中的
宽高是
1x
的两倍
宽高时
1x
的三倍
iPhone 3GS,可以省略iPhone 4
iPhone 4s
iPhone 5
iPhone 5s
iPhone 6
iPhone 6+

与美工的配合

让美工在设计原型图时,按照
iPhone 6+
的分辨率设计

然后切图的时候,切两套即可

一套以 @3x 结尾,供 iPhone 6+ 使用

一套缩小 2/3,以 @2x 结尾,供小屏视网膜手机使用

提示:现在大多数应用程序还适配 iOS 6,下载的 ipa 包能够拿到图片素材,但是如果今后应用程序只支持 iOS 7+,解压缩包之后,择无法再获得对应的图片素材。

请妥善保管好一些优秀作品的 IPA 文件

图标素材 & App 名称

图标素材

设置图标选项

如下图所示,删除
Launch Screen File
&
Main.storyboard
,并且设置
启动图片
应用方向


提示:iPhone 项目一般不需要支持横屏,游戏除外



添加图标



App 名称



提示

此处修改的内容是
Info.plist
CFBundleName
对应的内容

注意不要超过6个中文,否则会影响用户体验

启动程序

AppDelegate
didFinishLaunchingWithOptions
函数中添加以下代码:

window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()

window?.makeKeyAndVisible()


运行测试

添加启动图片



提示

关于启动图片的设置,需要注意上课的操作细节

关于各个设备的实际屏幕尺寸,注意一下不同类型的启动图片即可

项目搭建

课程目标

熟悉 swift 语法

搭建系统主体框架结构

对比与 OC 开发的异同

纯代码搭建框架

创建文件

准备工作

删除模板文件

ViewController.swift

Main.storyboard

LaunchScreen.xib

创建项目结构

主目录
Classes

二级目录

目录名说明
Module功能模块
Model业务逻辑模型
Tools工具类

Module
子目录

目录名说明
Main主要
Home首页
Message消息
Discover发现
Profile

创建项目文件

Main

目录Controller
MainMainViewController.swift(
:UITabBarController
)

功能模块

目录Controller
HomeHomeTableViewController.swift
MessageMessageTableViewController.swift
DiscoverDiscoverTableViewController.swift
ProfileProfileTableViewController.swift

细节

每个 ViewController 继承自
UITableViewController


搭建完成的文件结构图如下:



修改
AppDelegate
中的
didFinishLaunchingWithOptions
函数,设置启动控制器

window?.rootViewController = MainViewController()


添加子控制器

功能需求

由于采用了多视图控制器的设计方式,因此需要通过代码的方式向主控制器中添加子控制器

文件准备

将素材文件夹中的
TabBar
拖拽到
Images.xcassets
目录下

代码实现

添加第一个视图控制器

override func viewDidLoad() {
super.viewDidLoad()

addChildViewController()
}

private func addChildViewController() {
tabBar.tintColor = UIColor.orangeColor()

let vc = HomeTableViewController()
vc.title = "首页"
vc.tabBarItem.image = UIImage(named: "tabbar_home")

let nav = UINavigationController(rootViewController: vc)

addChildViewController(nav)
}


重构代码抽取参数

/// 添加控制器
///
/// - parameter vc       : 视图控制器
/// - parameter title    : 标题
/// - parameter imageName: 图像名称
private func addChildViewController(vc: UIViewController, title: String, imageName: String) {
tabBar.tintColor = UIColor.orangeColor()

let vc = HomeTableViewController()
vc.title = title
vc.tabBarItem.image = UIImage(named: imageName)

let nav = UINavigationController(rootViewController: vc)

addChildViewController(nav)
}


扩充调用函数,添加其他控制器

/// 添加所有子控制器
private func addChildViewControllers() {
addChildViewController(HomeTableViewController(), title: "首页", imageName: "tabbar_home")
addChildViewController(MessageTableViewController(), title: "消息", imageName: "tabbar_message_center")
addChildViewController(DiscoverTableViewController(), title: "发现", imageName: "tabbar_discover")
addChildViewController(ProfileTableViewController(), title: "我", imageName: "tabbar_profile")
}


自定义 TabBar

功能需求

在 4 个控制器切换按钮中间增加一个撰写按钮

点击撰写按钮能够弹出对话框撰写微博

需求分析

自定义 TabBar

计算控制器按钮位置,在中间添加一个
撰写
按钮

思路

加号按钮的大小与其他
tabBarItem
的大小是一致的

如果不考虑 modal 的方式,其所在位置应该同样有一个
tabBarItem


建立一个空的视图控制器形成占位

然后在该位置添加一个按钮遮挡

代码实现

添加空的视图控制器

/// 添加所有子控制器
private func addChildViewControllers() {
// ...

addChildViewController(UIViewController())

// ...
}


注意 UIViewController() 的位置

添加按钮

// MARK: - 懒加载
/// 撰写按钮
private lazy var composedButton: UIButton = {
let btn = UIButton()

btn.setImage(UIImage(named: "tabbar_compose_icon_add"), forState: UIControlState.Normal)
btn.setImage(UIImage(named: "tabbar_compose_icon_add_highlighted"), forState: UIControlState.Highlighted)
btn.setBackgroundImage(UIImage(named: "tabbar_compose_button"), forState: UIControlState.Normal)
btn.setBackgroundImage(UIImage(named: "tabbar_compose_button_highlighted"), forState: UIControlState.Highlighted)

self.tabBar.addSubview(btn)

return btn
}()


设置按钮位置

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

setupComposeButton()
}

/// 设置撰写按钮位置
private func setupComposeButton() {
let w = tabBar.bounds.width / CGFloat(childViewControllers.count)
let rect = CGRect(x: 0, y: 0, width: w, height: tabBar.bounds.height)

composedButton.frame = CGRectOffset(rect, 2 * w, 0)
}


添加按钮监听方法

btn.addTarget(self, action: "clickComposeButton", forControlEvents: UIControlEvents.TouchUpInside)


按钮监听方法

/// 点击撰写按钮
func clickComposeButton() {
print(__FUNCTION__)
}


注意:按钮的监听方法不能使用
private


阶段性小结

整体开发思路与使用 OC 几乎一致

Swift 语法更加简洁

Swift 对类型校验更加严格,不同类型的变量不允许直接计算

let w = tabBar.bounds.width / CGFloat(childViewControllers.count)


Swift 中的懒加载本质上是一个闭包,因此引用当前控制器的对象时需要使用 self.

不希望暴露的方法,应该使用
private
修饰符

按钮点击事件的调用是由
运行循环
监听并且以
消息机制
传递的,因此,按钮监听函数不能设置为
private




第三方框架

项目中使用到以下第三方框架

AFNetworking


SDWebImage


SVProgressHUD


Pod 安装

git 备份

打开终端

$ cd
进入项目目录

输入以下终端命令建立或编辑
Podfile


$ vim Podfile


输入以下内容

use_frameworks!
platform :ios, '8.0'
pod 'AFNetworking'
pod 'SDWebImage'
pod 'SVProgressHUD'


:wq
保存退出

输入以下命令安装第三方框架

$ pod install


如果第三方框架不能正常工作或者升级,可以输入以下命令更新

$ pod update


在 Swift 项目中,cocoapod 仅支持以 Framework 方式添加框架,因此需要在 Podfile 中添加
use_frameworks!


在终端提交添加的框架

# 将修改添加至暂存区
$ git add .

# 提交修改并且添加备注信息
$ git commit -m "添加第三方框架"

# 将修改推送到远程服务器
$ git push


修改项目版本



AFNetworking

建立
NetworkTools
单例

import AFNetworking

/// 网络工具类
class NetworkTools: AFHTTPSessionManager {

// 全局访问点
static let sharedNetworkTools: NetworkTools = {
let instance = NetworkTools(baseURL: NSURL(string: "https://api.weibo.com/")!)

return instance
}()
}


SDWebImage & SVProgressHUD

SVProgressHUD

SVProgressHUD
是使用 OC 开发的指示器

使用非常广泛

框架地址

https://github.com/TransitApp/SVProgressHUD

MBProgressHUD
对比

SVProgressHUD


只支持
ARC


支持较新的苹果 API

提供有素材包

使用更简单

MBProgressHUD


支持
ARC
&
MRC


没有素材包,程序员需要针对框架进行一定的定制才能使用

使用

import SVProgressHUD

SVProgressHUD.showInfoWithStatus("正在玩命加载中...", maskType: SVProgressHUDMaskType.Gradient)


SDWebImage

import SDWebImage

let url = NSURL(string: "http://img0.bdstatic.com/img/image/6446027056db8afa73b23eaf953dadde1410240902.jpg")!
SDWebImageManager.sharedManager().downloadImageWithURL(url, options: SDWebImageOptions.allZeros, progress: nil) { (image, _, _, _, _) in
let data = UIImagePNGRepresentation(image)
data.writeToFile("/Users/liufan/Desktop/123.jpg", atomically: true)
}


单例

单例的目标

内存中只有一个对象实例

提供一个全局访问点

OC 中的单例

+ (instancetype)sharedManager {
static id instance;

static dispatch_once_t onceToken;
NSLog(@"%ld", onceToken);

dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});

return instance;
}


Swift 中的单例

static var instance: NetworkTools?
static var token: dispatch_once_t = 0

/// 在 swift 中类变量不能是存储型变量
class func sharedSoundTools() -> SoundTools {
dispatch_once(&token) { () -> Void in
instance = SoundTools()
}
return instance!
}


不过!在 Swift 中
let
本身就是线程安全的

改进过的单例代码

private static let instance = NetworkTools()
/// 在 swift 中类变量不能是存储型变量
class var sharedNetworkTools: NetworkTools {
return instance
}


单例其实还可以更简单

static let sharedSoundTools = SoundTools()




OAuth

基本概念

OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准

OAuth 的授权不会使第三方触及到用户的帐号信息

OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据

每一个令牌授权一个
特定的网站
特定的时段内
访问
特定的资源


OAuth 授权流程图



注册应用程序

注册应用程序

注册新浪微博账号

访问 http://open.weibo.com

点击
微连接
-
移动应用


填写基本信息,如下图所示:



点击
应用信息
-
高级信息
,设置回调地址,如下图所示:



应用程序信息

Key
client_id113773579
client_secreta34f52ecaad5571bfed41e6df78299f6
redirect_urihttp://www.baidu.com
access_token2.00ml8IrF0jh4hHe09f471dc4C_L3nC
注意:授权回调地址一定要完全一致

加载授权页面

功能需求

通过浏览器访问新浪授权页面,获取授权码

接口文档

http://open.weibo.com/wiki/Oauth2/authorize

测试授权 URL

https://api.weibo.com/oauth2/authorize?client_id=479651210&redirect_uri=http://itheima.com

注意:回调地址必须与注册应用程序保持一致

功能实现

准备工作

新建
OAuth
文件夹

新建
OAuthViewController.swift
继承自
UIViewController


加载 OAuth 视图控制器

修改
BaseTableViewController
中用户登录部分代码

///  用户登录
func visitorLoginViewWillLogin() {
let nav = UINavigationController(rootViewController: OAuthViewController())

presentViewController(nav, animated: true, completion: nil)
}


OAuthViewController
中添加以下代码

lazy var webView: UIWebView = {
return UIWebView()
}()

override func loadView() {
view = webView

title = "新浪微博"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "关闭", style: UIBarButtonItemStyle.Plain, target: self, action: "close")
}

///  关闭
func close() {
dismissViewControllerAnimated(true, completion: nil)
}


运行测试

加载授权页面

NetworkTools
中定义应用程序授权相关信息

// MARK: - 应用程序信息
private var clientId = "113773579"
private var clientSecret = "a34f52ecaad5571bfed41e6df78299f6"
var redirectUri = "http://www.baidu.com"

/// 授权 URL
var oauthURL: NSURL {
return NSURL(string: "https://api.weibo.com/oauth2/authorize?client_id=\(clientId)&redirect_uri=\(redirectUri)")!
}


info.plist
中增加
ATS
设置

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>


加载授权页面

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

webView.loadRequest(NSURLRequest(URL: NetworkTools.sharedNetworkTools.oauthURL))
}


实现代理方法,跟踪重定向 URL

// MARK: - UIWebView 代理方法
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
print(request)

return true
}


结果分析

如果 URL 以回调地址开始,需要检查查询参数

其他 URL 均加载

修改代码

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {

// 判断请求的 URL 中是否包含回调地址
let urlString = request.URL!.absoluteString
if !urlString.hasPrefix(NetworkTools.sharedNetworkTools.redirectUri) {
return true
}

guard let query = request.URL?.query where query.hasPrefix("code=") else {
print("取消授权")
close()

return false
}

let code = query.substringFromIndex(advance(query.startIndex, "code=".characters.count))
print("授权成功 \(code)")

NetworkTools.sharedNetworkTools.loadAccessToken(code)

return false
}


加载指示器

导入
SVProgressHUD


import SVProgressHUD


WebView 代理方法

func webViewDidStartLoad(webView: UIWebView) {
SVProgressHUD.show()
}

func webViewDidFinishLoad(webView: UIWebView) {
SVProgressHUD.dismiss()
}


关闭

///  关闭
func close() {
SVProgressHUD.dismiss()
dismissViewControllerAnimated(true, completion: nil)
}


AccessToken

课程目标

自定义对象

构造函数

归档 & 接档

接口定义

文档地址

http://open.weibo.com/wiki/OAuth2/access_token

接口地址

https://api.weibo.com/oauth2/access_token

HTTP 请求方式

POST

请求参数

参数描述
client_id申请应用时分配的AppKey
client_secret申请应用时分配的AppSecret
grant_type请求的类型,填写
authorization_code
code调用authorize获得的code值
redirect_uri回调地址,需需与注册应用里的回调地址一致

返回数据

返回值字段字段说明
access_token用于调用access_token,接口获取授权后的access token
expires_inaccess_token的生命周期,单位是秒数
remind_inaccess_token的生命周期(该参数即将废弃,开发者请使用expires_in)
uid当前授权用户的UID

UserAccount 模型

加载 AccessToken

NetworkTools
中增加函数加载
AccessToken


/// 使用 code 获取 accessToken
///
/// - parameter code: 请求码
func loadAccessToken(code: String) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let parames = ["client_id": clientId,
"client_secret": clientSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]

POST(urlString, parameters: parames, success: { (_, JSON) -> Void in
print(JSON)
}) { (_, error) -> Void in
print(error)
}
}


OAuthViewController
中获取授权码成功后调用网络方法

NetworkTools.sharedNetworkTools.loadAccessToken(code)


运行测试

返回错误信息

Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable content-type: text/plain"


NetworkTools
中增加反序列化数据格式

// 设置反序列化数据格式集合
instance.responseSerializer.acceptableContentTypes = NSSet(objects: "application/json", "text/json", "text/javascript", "text/plain") as Set<NSObject>


增加闭包回调

/// 使用 code 获取 accessToken
///
/// - parameter code: 请求码
func loadAccessToken(code: String, finished: (result: [String: AnyObject]?, error: NSError?)->()) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let parames = ["client_id": clientId,
"client_secret": clientSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]

POST(urlString, parameters: parames, success: { (_, JSON) in
finished(result: JSON as? [String: AnyObject], error: nil)
}) { (_, error) in
finished(result: nil, error: error)
}
}


修改调用代码

private func loadAccessToken(code: String) {
NetworkTools.sharedNetworkTools.loadAccessToken(code) { (result, error) -> () in
if error != nil result == nil {
SVProgressHUD.showInfoWithStatus("网络不给力")

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * Int64(NSEC_PER_SEC)), dispatch_get_main_queue()) {
self.close()
}
return
}

print(result)
}
}


定义 UserAcount 模型

Model
目录下添加
UserAccount


定义模型属性

/// 用于调用access_token,接口获取授权后的access token
var access_token: String?
/// access_token的生命周期,单位是秒数
var expires_in: String?
/// 当前授权用户的UID
var uid: String?

init(dict: [String: AnyObject]) {
super.init()

self.setValuesForKeysWithDictionary(dict)
}

override func setValue(value: AnyObject?, forUndefinedKey key: String) {}


字典转模型

let account = UserAccount(dict: result!)
print(account)


运行测试程序会崩溃!

因为从新浪服务器返回的
expires_in
是整数而不是字符串

调整代码,验证
expires_in
数据类型

responseSerializer = AFHTTPResponseSerializer()
POST(urlString, parameters: parames, success: { (_, JSON) in
print(NSString(data: JSON as! NSData, encoding: NSUTF8StringEncoding))
finished(result: JSON as? [String: AnyObject], error: nil)
}) { (_, error) in
finished(result: nil, error: error)
}


再次运行测试

调试模型信息

与 OC 不同,如果要在 Swift 1.2 中调试模型信息,需要遵守
Printable
协议,并且重写
description
getter
方法,在 Swift 2.0 中,
description
属性定义在
CustomStringConvertible
协议中

override var description: String {
let dict = ["access_token", "expires_in", "uid"]

return "\(dictionaryWithValuesForKeys(dict))"
}


目前的版本需要先遵守
CustomStringConvertible
协议,重写了
description
属性后,再删除,相信后续版本中会得到改进

设置过期日期

过期日期

在新浪微博返回的数据中,过期日期是以当前系统时间加上秒数计算的,为了方便后续使用,增加过期日期属性

定义属性

/// token过期日期
var expiresDate: NSDate?


修改构造函数

expiresDate = NSDate(timeIntervalSinceNow: expires_in)


修改
description


let properties = ["access_token", "expires_in", "expiresDate", "uid"]


归档 & 解档

课程目标

对比 OC 的
归档 & 解档
实现

利用
归档 & 解档
保存用户信息

遵守协议

class UserAccount: NSObject, NSCoding


实现协议方法

// MARK: - NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeDouble(expires_in, forKey: "expires_in")
aCoder.encodeObject(expiresDate, forKey: "expiresDate")
aCoder.encodeObject(uid, forKey: "uid")
}

required init?(coder aDecoder: NSCoder) {
access_token = aDecoder.decodeObjectForKey("access_token") as? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = aDecoder.decodeObjectForKey("uid") as? String
}


定义归档路径

/// 归档保存路径
private static let accountPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!.stringByAppendingPathComponent("account.plist")


保存账户信息

/// 保存账号
func saveAccount() {
NSKeyedArchiver.archiveRootObject(self, toFile: UserAccount.accountPath)
}


加载账户信息

/// 加载账号
class func loadAccount() -> UserAccount? {
let account = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount

return account
}


调整
OAuthViewController.swift
中的
loadAccessToken
函数

// 保存用户账号信息
UserAccount(dict: result!).saveAccount()


修改加载账号函数

/// 用户账号
private static var userAccount: UserAccount?

/// 加载账号
class func loadAccount() -> UserAccount? {
if userAccount == nil {
// 解档用户账户信息
userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
}

// 如果用户账户存在,判断是否过期
if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {
userAccount = nil
}

return userAccount
}


由于后续所有网络访问都基于用户账户中的
access_token
,因此定义一个全局变量,可以避免重复加载,而且能够在每次调用 AccessToken 时都判断是否过期

修改 BaseTableViewController 中的用户是否登录判断

/// 用户登录标记
var userLogon = UserAccount.loadAccount() != nil


加载用户信息

课程目标

通过
AccessToken
获取新浪微博网络数据

接口定义

文档地址

http://open.weibo.com/wiki/2/users/show

接口地址

https://api.weibo.com/2/users/show.json

HTTP 请求方式

GET

请求参数

参数描述
access_token采用OAuth授权方式为必填参数,其他授权方式不需要此参数,OAuth授权后获得
uid需要查询的用户ID

返回数据

返回值字段字段说明
name友好显示名称
avatar_large用户头像地址(大图),180×180像素

测试 URL

https://api.weibo.com/2/users/show.json?access_token=2.00ml8IrF0qLZ9W5bc20850c50w9hi9&uid=5365823342

代码实现

NetworkTools
中封装 GET 方法

/// 错误域
private let errorDomainName = "com.itheima.network.errorDomain"

// MARK: - 封装网络请求方法
/// 完成回调类型
typealias HMFinishedCallBack = (result: [String: AnyObject]?, error: NSError?) -> ()

/// GET 请求
///
/// - parameter urlString: URL 地址
/// - parameter params   : 参数字典
/// - parameter finished : 完成回调
private func requestGET(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {

GET(urlString, parameters: params, success: { _, JSON in

if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))
}

}) { _, error in
finished(result: nil, error: error)
}
}


定义通知常量

/// AccessToken 不存在通知
let HMAccessTokenEmptyNotification = "HMAccessTokenEmptyNotification"


生成 Token 参数字典

/// 生成 Token 参数字典
private func tokenDict() -> [String: AnyObject]? {
if let token = UserAccount.loadAccount()?.access_token {
return ["access_token": token]
}
NSNotificationCenter.defaultCenter().postNotificationName(HMAccessTokenEmptyNotification, object: nil)
return nil
}


NetworkTools
中增加加载用户信息函数

// MARK: - 加载用户信息
func loadUserInfo(uid: Int, finished: (result: [String: AnyObject]?, error: NSError?) -> ()) {
let urlString = "2/users/show.json"

guard var params = tokenDict() else {
return
}

params["uid"] = uid
requestGET(urlString, params: params) { (result, error) -> () in
finished(result: result, error: error)
}
}


UserAccount
中增加加载用户信息函数

func loadUserInfo() {
NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () in
print(result)
}
}


测试加载用户信息

UserAccount(dict: result!).loadUserInfo()


增加属性定义

/// 友好显示名称
var name: String?
/// 用户头像地址(大图),180×180像素
var avatar_large: String?


调整加载用户信息函数

// MARK: - 加载用户信息
func loadUserInfo(finished: (error: NSError?) -> ()) {
NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () in
if let dict = result {
self.name = dict["name"] as? String
self.avatar_large = dict["avatar_large"] as? String

self.saveAccount()
}
finished(error: error)
}
}


修改
description
属性

let properties = ["access_token", "expires_in", "uid", "expiresDate", "name", "avatar_large"]


修改归档&解档函数,增加用户名和图像地址属性

func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeDouble(expires_in, forKey: "expires_in")
aCoder.encodeObject(expiresDate, forKey: "expiresDate")
aCoder.encodeObject(uid, forKey: "uid")
aCoder.encodeObject(name, forKey: "name")
aCoder.encodeObject(avatar_large, forKey: "avatar_large")
}

required init?(coder aDecoder: NSCoder) {
access_token = aDecoder.decodeObjectForKey("access_token") as? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = aDecoder.decodeObjectForKey("uid") as? String
name = aDecoder.decodeObjectForKey("name") as? String
avatar_large = aDecoder.decodeObjectForKey("avatar_large") as? String
}


修改
loadAccessToken
方法

/// 使用授权码换取 AccessToken
private func loadAccessToken(code: String) {
NetworkTools.sharedTools.loadAccessToken(code) { (result, error) -> () in
if error != nil || result == nil {
self.loadError()

return
}

// 加载用户账号信息
UserAccount(dict: result!).loadUserInfo() { (error) -> () in
if error != nil {
self.loadError()

return
}

print(UserAccount.loadAccount())
}
}
}

/// 数据加载错误
private func loadError() {
SVProgressHUD.showInfoWithStatus("您的网络不给力")

// 延时一段时间再关闭
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * NSEC_PER_SEC))
dispatch_after(when, dispatch_get_main_queue()) {
self.close()
}
}


每一个令牌授权一个
特定的网站
特定的时段内
访问
特定的资源


调整网络代码

封装 POST 请求方法

/// POST 请求
///
/// - parameter urlString: URL 地址
/// - parameter params   : 参数字典
/// - parameter finished : 完成回调
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {

POST(urlString, parameters: params, success: { _, JSON in

if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))
}

}) { _, error in
print(error)
finished(result: nil, error: error)
}
}


修改加载 token 函数

/// 加载 Token
func loadAccessToken(code: String, finished: HMFinishedCallBack) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let params = ["client_id": clientId,
"client_secret": appSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]

requestPOST(urlString, params: params) { (result, error) -> () in
finished(result: result, error: error)
}
}


新特性

新特性是现在很多应用程序中包含的功能,主要用于在系统升级后,用户第一次进入系统时获知新升级的功能

课程目标

UICollectionView 使用

根视图控制器
切换

新特性功能

准备文件

将新特性图片素材拖拽到 Images.xcsets 中

Module
下建立
NewFeature
目录

新建
NewFeatureViewController.swift
继承自
UICollectionViewController


NewFeatureViewController.swift
的末尾添加如下代码:

代码实现

修改
AppDelegate
的根视图控制器

window?.rootViewController = NewFeatureViewController()


运行测试,崩溃!

原因:实例化
CollectionViewController
时必须指定布局参数

实现
init()
简化外部调用

/// 界面布局
private let layout = UICollectionViewFlowLayout()

init() {
super.init(collectionViewLayout: layout)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


定义 NewFeatureCell

/// 新特性 Cell
class NewFeatureCell: UICollectionViewCell {
var imageIndex: Int = 0 {
didSet {
iconView.image = UIImage(named: "new_feature_\(imageIndex + 1)")
}
}

override init(frame: CGRect) {
super.init(frame: frame)

contentView.addSubview(iconView)

// 自动布局
// 1> 图片视图
iconView.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// 懒加载控件
lazy var iconView: UIImageView = UIImageView()
}


注册可重用 Cell

override func viewDidLoad() {
super.viewDidLoad()

// 注册可重用 Cell
self.collectionView!.registerClass(NewFeatureCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}


运行测试,需要设置布局属性

设置布局属性

/// 新特性布局
private class NewFeatureLayout: UICollectionViewFlowLayout {

private override func prepareLayout() {
itemSize = collectionView!.bounds.size
minimumInteritemSpacing = 0
minimumLineSpacing = 0
scrollDirection = UICollectionViewScrollDirection.Horizontal

collectionView?.pagingEnabled = true
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.bounces = false
}
}


prepareLayout
函数中定义 collectionView 的布局属性是最佳位置

修改布局属性

/// 界面布局
private let layout = NewFeatureLayout()


定义按钮

/// 按钮
lazy var startButton: UIButton = {
let button = UIButton()

button.setBackgroundImage(UIImage(named: "new_feature_finish_button"), forState: UIControlState.Normal)
button.setBackgroundImage(UIImage(named: "new_feature_finish_button_highlighted"), forState: UIControlState.Highlighted)
button.setTitle("开始体验", forState: UIControlState.Normal)

return button
}()


设置按钮布局

// 2> 开始按钮
startButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: -160))


动画显示
开始体验
按钮

NewFeatureCell
中添加
showStartButton
函数

/// 动画显示按钮
func showStartButton() {
startButton.hidden = false

startButton.transform = CGAffineTransformMakeScale(0, 0)
startButton.userInteractionEnabled = false

UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {

self.startButton.transform = CGAffineTransformIdentity

}) { _ in
self.startButton.userInteractionEnabled = true
}
}


collectionView
完成显示Cell
代理方法中添加以下代码:

// 参数 cell, indexPath 是前一个 cell 和 indexPath
override func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {

let indexPath = collectionView.indexPathsForVisibleItems().last!

if indexPath.item == imageCount - 1 {
(collectionView.cellForItemAtIndexPath(indexPath) as! NewFeatureCell).showStartButton()
}
}


注意:参数中的
cell
&
indexPath
是之前消失的
cell
,而不是当前显示的
cell


隐藏状态栏

override func prefersStatusBarHidden() -> Bool {
return true
}


欢迎界面

在新浪微博中,如果用户登录成功会显示一个欢迎界面

特例:如果用户的系统刚刚升级或者第一次登录,会显示
新特性
界面,而不是
欢迎
界面

准备文件

NewFeature
目录下新建
WelcomeViewController.swift
继承自
UIViewController


新建
Welcome.storyboard
,初始视图控制器的自定义类为
WelcomeViewController


代码实现

修改
AppDelegate
的根视图控制器

window?.rootViewController = WelcomeViewController()


懒加载控件

// MARK: - 懒加载控件
/// 背景图片
private lazy var backImageView: UIImageView = UIImageView(image: UIImage(named: "ad_background"))
/// 头像视图
private lazy var iconView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "avatar_default_big"))

iv.layer.masksToBounds = true
iv.layer.cornerRadius = 45

return iv
}()
/// 文本标签
private lazy var messageLabel: UILabel = {
let label = UILabel()

label.text = "欢迎归来"

return label
}()


搭建界面

/// 头像底部约束
private var iconBottomCons: NSLayoutConstraint?

override func viewDidLoad() {
super.viewDidLoad()

prepareUI()
}

/// 准备 UI
private func prepareUI() {
view.addSubview(backImageView)
view.addSubview(iconView)
view.addSubview(messageLabel)

// 自动布局
// 1> 背景图片
backImageView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))
// 2> 头像
iconView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))
iconBottomCons = view.constraints.last
// 3> 标签
messageLabel.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
view.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 20))
}


界面动画

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

iconBottomCons?.constant = UIScreen.mainScreen().bounds.height - 240

UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}


参数说明

usingSpringWithDamping
的范围为
0.0f
1.0f
,数值越小
弹簧
的振动效果越明显

initialSpringVelocity
则表示初始的速度,数值越大一开始移动越快,初始速度取值较高而时间较短时,会出现反弹情况

设置用户头像

if let urlString = UserAccount.loadAccount()?.avatar_large {
iconView.sd_setImageWithURL(NSURL(string: urlString)!)
}


添加图像宽高约束

view.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 90))
view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))


代码评审(Code Review)

通常在企业开发中,会定期
面对面
(face to face)对代码进行评审

Code Review的意识

作为一个
Developer
,不仅要
提交可工作的代码
(Deliver working code),更要
提交可维护的代码
(Deliver maintainable code)

必要时进行重构,随着项目的迭代,在计划新增功能的同时,开发要主动计划重构的工作项

开放的心态,虚心接受大家的
评审建议
(Review Comments)

代码评审的方式

开 Code Review 会议

团队内部会整理 Check List

团队内部成员交换代码

找出可优化方案

多问问题,例如:“这块儿是怎么工作的?”、“如果有XXX 情况,你这个怎么处理?”

区分重点,优先抓住
设计
可读性
健壮性
等重点问题

整理好的编码实践,用来作为
Code Review
的参考

评审内容

架构/设计

单一职责原则

这是经常被违背的原则。一个类只能干一个事情,一个方法最好也只干一件事情。比较常见的违背是
一个类既干UI的事情,又干逻辑的事情
,这个在低质量的客户端代码里很常见

行为是否统一,例如:

缓存是否统一

错误处理是否统一

错误提示是否统一

弹出框是否统一

……

代码污染

代码有没有对其他模块强耦合

重复代码

开闭原则

面向接口编程

健壮性

是否考虑线程安全

数据访问是否一致性

边界处理是否完整

逻辑是否健壮

是否有内存泄漏

有没有循环依赖

有没有野指针

……

错误处理

改动是不是对代码的提升

新的改动是打补丁,让代码质量继续恶化,还是对代码质量做了修复

效率/性能

关键算法的时间复杂度多少?有没有可能有潜在的性能瓶颈

客户端程序对频繁消息和较大数据等耗时操作是否处理得当

代码风格

可读性

衡量可读性的可以有很好实践的标准,就是 Reviewer 能否非常容易的理解这个代码。如果不是,那意味着代码的可读性要进行改进

命名

命名对可读性非常重要

英语用词尽量准确一点,必要时可以查字典

函数长度/类长度

函数太长的不好阅读

类太长了,检查是否违反的
单一职责
原则

注释

恰到好处的注释

参数个数

不要太多,一般不要超过 3 个

Review Your Own Code First

每次提交前整体把自己的代码过一遍非常有帮助,尤其是看看有没有犯低级错误

OAuthViewController

删除多余的 print

删除 // TODO: 换取 TOKEN

修改
loadAccessToken
函数中的注释

提示:在实际开发中,代码中的注释一定要及时调整!

UserAccount

知识点:
类属性
vs
类函数


都是
通过类名调用


类属性作为属性一定有返回值

类函数不一定有返回值

类本质上只是对
对象
的描述,从面相对象的角度而言,类不应该有存储功能

类属性是只读的,可以返回一个函数计算结果

也可以返回一个私有静态成员记录的内容

通过类属性,能够提高代码的可读性

演练 & 体会

loadAccount()
类函数修改为
sharedUserAccount
类属性

class var sharedUserAccount: UserAccount? {
// 1. 判断账户是否存在
if userAccount == nil {
// 解档 - 如果没有保存过,解档结果可能仍然是 nil
userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
}

// 2. 判断日期
if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {
// 如果已经过期,需要清空账号记录
userAccount = nil
}

return userAccount
}


利用编译器提示修改出错的代码

对比前后两种方式的代码可读性的提高

说明:类属性是 Swift 特有的语法,仅供体会

NetworkTools

移动
HMNetFinishedCallBack
声明的位置

定义网络访问错误枚举

定义网络访问错误枚举

/// 网络访问错误
private enum HMNetworkError: Int {
case emptyDataError = -1
case emptyTokenError = -2

private var description: String {
switch self {
case .emptyDataError:
return "空数据"
case .emptyTokenError:
return "AccessToken 错误"
}
}

private var error: NSError {
return NSError(domain: HMErrorDomainName, code: rawValue, userInfo: [HMErrorDomainName: description])
}
}


可以在 Playground 中测试枚举类型

修改
requestGET
中的空数据错误

finished(result: nil, error: HMNetworkError.emptyDataError.error)


修改
loadUserInfo
中 token 为空的检测代码,增加错误回调

// 判断 token 是否存在
if UserAccount.sharedUserAccount?.access_token == nil {
let error = HMNetworkError.emptyTokenError.error
print(error)
finished(result: nil, error: error)
return
}


注释
UserAccount
中为全局账号赋值的代码,并且调试运行效果

封装 AFN 的 POST 方法

复制 GET 代码,并且修改部分单词

/// POST 请求
///
/// :param: urlString URL 地址
/// :param: params    参数字典
/// :param: finished  完成回调
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {

POST(urlString, parameters: params, success: { (_, JSON) -> Void in

if let result = JSON as? [String: AnyObject] {
// 有结果的回调
finished(result: result, error: nil)
} else {
// 没有错误,同时没有结果
print("没有数据 GET Request \(urlString)")
finished(result: nil, error: HMNetworkError.emptyDataError.error)
}

}) { (_, error) -> Void in
print(error)

finished(result: nil, error: error)
}
}


修改 函数并运行测试

/// 加载 Token
func loadAccessToken(code: String, finished: HMNetFinishedCallBack) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let params = ["client_id": clientId,
"client_secret": appSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]

requestPOST(urlString, params: params, finished: finished)
}


整合网络访问方法

定义网络方法枚举

/// 网络访问方法
private enum HMNetworkMethod: String {
case GET = "GET"
case POST = "POST"
}


封装网络访问方法

/// 网络请求
///
/// - parameter method   : 访问方法
/// - parameter urlString: URL 地址
/// - parameter params   : 参数自带呢
/// - parameter finished : 完成回调
private func request(method: HMNetworkMethod, urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {

let successCallBack: (NSURLSessionTask!, AnyObject!) -> Void = { _, JSON in
if let result = JSON as? [String: AnyObject] {
// 有结果的回调
finished(result: result, error: nil)
} else {
// 没有错误,同时没有结果
print("没有数据 \(method) Request \(urlString)")
finished(result: nil, error: HMNetworkError.emptyDataError.error)
}
}
let failedCallBack: (NSURLSessionTask!, NSError!) -> Void = { _, error in
print(error)

finished(result: nil, error: error)
}

switch method {
case .GET:
GET(urlString, parameters: params, success: successCallBack, failure: failedCallBack)
case .POST:
POST(urlString, parameters: params, success: successCallBack, failure: failedCallBack)
}
}


运行测试

自动布局框架

为简化纯代码布局,抽取了常用的自动布局代码

将 UIView+AutoLayout 拖拽到项目中的
Tools
目录下

调整
NewFeatureCell


iconView.ff_Fill(contentView)
startButton.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: contentView, size: nil, offset: CGPoint(x: 0, y: -160))


调整
WelcomeViewController


// 1> 背景图片
backImageView.ff_Fill(view)
// 2> 头像
let cons = iconView.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: view, size: CGSize(width: 90, height: 90), offset: CGPoint(x: 0, y: -160))
// 记录底边约束
iconBottomCons = iconView.ff_Constraint(cons, attribute: NSLayoutAttribute.Bottom)

// 3> 标签
label.ff_AlignVertical(type: ff_AlignType.BottomCenter, referView: iconView, size: nil, offset: CGPoint(x: 0, y: 16))


修改动画方法中的约束数值

iconBottomCons?.constant = -UIScreen.mainScreen().bounds.height - iconBottomCons!.constant
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: