Swift-微博

Swift的基础知识基本学完,这次写的不是别的,是培训常模仿的微博项目,现在我也是边模仿边学习的心态。我会记录小码哥中模仿微博的从01的过程,希望以后看到自己写的烂文章会笑起来,哈哈。

另外文章篇幅可能很长很长,所以想看的同学可以好好收藏一下。如果觉得可以帮助到你的话,请Star我,也可以Fort一下工程关注项目的进展情况。当然过程中有什么问题的话,随时欢迎Issues

如有侵权,请联系删除。

image

工程地址

MyWeiBoProjectForSwift
另外资源可以在工程里面找

创建工程

相信大家都会了,这里就不在述说了

删除StoryBoard

使用纯代码完成这个项目,所以先把故事版删除,还有把info.plist文件的main删除。如下图

框架搭建

  • 首先建立工程目录

按照微博的模块,可以分为首页(Home)消息(Message)广场(Discover)我(Profile)四大模块,然后工程在同级别添加MainCommonTool三个模块
如下图:

  • 搭建框架

Main模块中新建一个继承UITabBarController的自定义MainViewController
那么好了,我们只要在AppDelegate.swift文件中创建Window,并把rootController赋值MainViewController就行了。

  • 在AppDelegate.swift创建视图
1
2
3
4
5
6
7
8
9
10
11
12
13

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.

// 创建window
window = UIWindow(frame:UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
// 创建根控制器
window?.rootViewController = MainViewController()
window?.makeKeyAndVisible()

return true
}
  • 在首页(Home)、消息(Message)、广场(Discover)、我(Profile)模块建立主要控制器

分别对应HomeTableViewControllerMessageTableViewControllerDiscoverTableViewControllerProfileTableViewController

  • MainViewController创建子控制器
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
55
56
57
58
59
60
61
62
63
override func viewDidLoad() {
super.viewDidLoad()

// 设置当前控制器对应tabbar的颜色
// 注意:在iOS7以前如果设置来了titColor只有文字会变,图片不会变
tabBar.tintColor = UIColor.orange

// 创建首页子控制器
let homeVC = HomeTableViewController()
homeVC.title = "首页"
homeVC.tabBarItem.image = UIImage(named: "tabbar_home")
homeVC.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 创建导航控制器包装一下
let homeNavC = UINavigationController()
homeNavC.addChildViewController(homeVC)

// 将导航控制器添加到当前控制器
addChildViewController(homeNavC)


// 创建消息子控制器
let messageVC = MessageTableViewController()
messageVC.title = "消息"
messageVC.tabBarItem.image = UIImage(named: "tabbar_message_center")
messageVC.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 创建导航控制器包装一下
let messageNavC = UINavigationController()
messageNavC.addChildViewController(messageVC)

// 将导航控制器添加到当前控制器
addChildViewController(messageNavC)


// 创建广场子控制器
let discoverVC = DiscoverTableViewController()
discoverVC.title = "广场"
discoverVC.tabBarItem.image = UIImage(named: "tabbar_discover")
discoverVC.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 创建导航控制器包装一下
let discoverNavC = UINavigationController()
discoverNavC.addChildViewController(discoverVC)

// 将导航控制器添加到当前控制器
addChildViewController(discoverNavC)


// 创建我子控制器
let profileVC = ProfileTableViewController()
profileVC.title = "我"
profileVC.tabBarItem.image = UIImage(named: "tabbar_profile")
profileVC.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 创建导航控制器包装一下
let profileNavC = UINavigationController()
profileNavC.addChildViewController(profileVC)

// 将导航控制器添加到当前控制器
addChildViewController(profileNavC)

}

好了,我们运行一下效果图

  • MainViewController重构

你们有没有发现上面的代码除了类名、标题和图片的名称不一样,其他的都一样,其实很多重复的代码,我们可以使用一个方法把它搞定,而不同的可以通过参数来传递。这个就是简单的重构。废话不多说,上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 初始化子控制器
///
/// - parameter childController: 需要初始化的子控制器
/// - parameter title: 标题
/// - parameter imageName: 图片
fileprivate func addChildViewController(_ childController: UIViewController, title:String, imageName:String) {
// 这里提示一下,fileprivate是swift3.0才有的,以前的是使用private

childController = title
childController.tabBarItem.image = UIImage(named:imageName)
childController.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 包装一个导航器
let navController = UINavigationController()
navController.addChildViewController(childController)

// 将导航器添加到当前控制器
addChildViewController(navController)

}

方法构建成功,那么在viewDidLoad中使用就非常简单了

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

override func viewDidLoad() {
super.viewDidLoad()

// 设置当前控制器对应tabbar的颜色
// 注意:在iOS7以前如果设置来了titColor只有文字会变,图片不会变
tabBar.tintColor = UIColor.orange

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")

}

运行一下,是不是效果一样,然后代码立即少了许多是不是?

添加中间按钮

这个按钮比较特殊,所以要特殊处理一下。

首先,我创建一个新的控制器,作为中间按钮的占位控制器

1
2
3
4
5
6
addChildViewController("HomeTableViewController", title: "首页", imageName: "tabbar_home")
addChildViewController("MessageTableViewController", title: "消息", imageName: "tabbar_message_center")
// 再添加一个占位控制器
addChildViewController("NullViewController", title: "", imageName: "")
addChildViewController("DiscoverTableViewController", title: "广场", imageName: "tabbar_discover")
addChildViewController("ProfileTableViewController", title: "我", imageName: "tabbar_profile")

然后再创建一个按钮表示中间的加号,使用懒加载的方式创建加号按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fileprivate lazy var composeBtn:UIButton = {
let button = UIButton()

// 设置前置图片
button.setImage(UIImage(named:"tabbar_compose_icon_add"), for: UIControlState.normal)
button.setImage(UIImage(named:"tabbar_compose_icon_add_highlighted"), for: UIControlState.highlighted)

// 设置背景图片
button.setBackgroundImage(UIImage(named:"tabbar_compose_button"), for: UIControlState.normal)
button.setBackgroundImage(UIImage(named:"tabbar_compose_button_highlighted"), for: UIControlState.highlighted)

// 添加监听事件
button.addTarget(self, action: #selector(MainViewController.composeBtnClick), for: UIControlEvents.touchUpInside)

return button

}()

注意按钮的函数绑定是#selector不是@selector,所以这里绑定一个函数,这里有个注意的地方:监听加号按钮点击,监听按钮点击方法不能私有, 按钮点击事件的调用是由 运行循环 监听并且以消息机制传递的,因此,按钮监听函数不能设置为fileprivate

1
2
3
4
5
6
/// 监听加好按钮点击,注意:监听按钮点击方法不能私有
/// 按钮点击事件的调用是由 运行循环 监听并且以消息机制传递的,因此,按钮监听函数不能设置为`fileprivate`
func composeBtnClick() {
print(#function)
print("点击按钮")
}

再把加号按钮添加到tababr上,这里我抽成一个函数,顺便调整加号按钮的frame

1
2
3
4
5
6
7
8
9
10
11
12
13
fileprivate func addComposeButton(){

// 添加按钮
tabBar.addSubview(composeBtn)

// 调整加好按钮的位置
print("\((viewControllers?.count)! + 1)")
let width = UIScreen.main.bounds.size.width / CGFloat(viewControllers!.count)
let rect = CGRect(x: 0, y: 0, width: width, height: 49)

composeBtn.frame = rect.offsetBy(dx: 2 * width, dy: 0)

}

一般都是在viewDidLoad这里添加并在设置frame,我也这么一直认为,但是我在viewDidLoad打印一下print(tabBar.subviews),发现是空的,而在viewWillAppear打印却有值并且有frame从iOS7开始就不推荐大家在viewDidLoad中设置frame,所以在viewWillAppear添加加号按钮

1
2
3
4
5
6
7
8
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// print("-----")
// print(tabBar.subviews)
// 有值,有frame
addComposeButton()
}

好了,运行一下看看效果。


更新时间:2018-04-27 14:24:34

重构MainViewController

把子控制器的类型、标题、图片用一个json文件管理,创建一个为MainVCSetting.json文件。内容如下:

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
[
{
"vcName": "HomeTableViewController",
"title": "首页",
"imageName": "tabbar_home"
},
{
"vcName": "MessageTableViewController",
"title": "消息",
"imageName": "tabbar_message_center"
},
{
"vcName": "NullViewController",
"title": "",
"imageName": ""
},
{
"vcName": "DiscoverTableViewController",
"title": "广场",
"imageName": "tabbar_discover"
},
{
"vcName": "ProfileTableViewController",
"title": "我",
"imageName": "tabbar_profile"
}
]

那么我们就可以通过加载MainVCSetting.json文件来获取数据:

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
fileprivate func addChildViewControllers() {
// 获取json文件的路径
let path = Bundle.main.path(forResource: "MainVCSetting.json", ofType: nil)

if let pathName = path {
let jsonData = try!Data(contentsOf:URL(fileURLWithPath: pathName))

do {
// 有可能异常代码放在这里
// 序列化json数据 --> Array
// try: 发生异常会跳到catch中继续执行
// try!: 发生过异常程序直接崩溃
let dicArr = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.mutableContainers)

// 遍历数组,动态创建控制器和设置数据
// 在swift中,如果需要遍历一个数组,必须明确数据类型
for dic in dicArr as! [[String: String]] {
// 报错的原因是因为addChildViewController参数必须有值,但是字典返回的是可选值
// addChildViewController(dic["vcName"], title: dic["title"], imageName: dic["imageName"])
addChildViewController(dic["vcName"]!, title: dic["title"]!, imageName: dic["imageName"]!)
}
} catch {

// 发生异常之后会执行的代码
print(error)

//从本地创建子控制器
addChildViewController("HomeTableViewController", title: "首页", imageName: "tabbar_home")
addChildViewController("MessageTableViewController", title: "消息", imageName: "tabbar_message_center")
// 再添加一个占位控制器
addChildViewController("NullViewController", title: "", imageName: "")
addChildViewController("DiscoverTableViewController", title: "广场", imageName: "tabbar_discover")
addChildViewController("ProfileTableViewController", title: "我", imageName: "tabbar_profile")
}
}
}

初始化每一个子控制器

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
/// 初始化子控制器
///
/// - parameter childController: 需要初始化的子控制器
/// - parameter title: 标题
/// - parameter imageName: 图片
fileprivate func addChildViewController(_ childControllerName: String, title:String, imageName:String) {

// 设置首页对应的数据
// childController.title = title
// childController.tabBarItem.image = UIImage(named:imageName)
// childController.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")
// 动态获取命名空间
let bundleString = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String

// 将字符串转为类
let className:AnyClass? = NSClassFromString(bundleString + "." + childControllerName)

// 通过类创建对象
// 将AnyClass转为指定的类型
let vcClass = className as! UIViewController.Type
// 通过class创建对象
let vc = vcClass.init()

vc.title = title
// vc.tabBarItem.image = UIImage(named:imageName)
vc.tabBarItem.image = UIImage.init(named: imageName)
vc.tabBarItem.selectedImage = UIImage(named:imageName+"_highlighted")

// 包装一个导航器
let navController = UINavigationController()
navController.addChildViewController(vc)

// 将导航器添加到当前控制器
addChildViewController(navController)
}

添加VisitorView视图

添加一个自定义VisitorView游客视图,作为每个控制器的基本视图。当没有登录的情况下展示的是VisitorView视图。

为了方便布局,使用UIView+AutoLayout分类布局文件,具体可在工程中找到。

下面是VisitorView视图的关键的代码片段:

定义协议

1
2
3
4
5
6
7
8
9
// Swift中如何定义协议:必须遵守NSObjectProtocol
protocol VisitorViewDelegate: NSObjectProtocol
{

// 登录回调
func loginBtnWillClick()

// 注册回调
func registerBtnWillClick()
}

使用弱引用

1
2
3
// 定义一个属性保存代理对象
// 一定要加上weak,避免循环引用
weak var delegate: VisitorViewDelegate?

重写初始化方法注意点

1
2
3
4
5
6
7
8
9
10
override init(frame: CGRect) {
super.init(frame: frame)

}

// Swift推荐我们自定义一个空间,要么用纯代码,要么使用xib/stroyboard
required init?(coder aDecoder: NSCoder) {
// 如果用过xib/stroyboard创建该类,那么就会崩溃
fatalError("init(coder:) has not been implemented")
}

添加BaseViewController控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义一个变量保存用户当前是否是登录
var userLogin = true

// 定义属性保存未登录界面
var visitorView: VisitorView?

override func loadView() {
userLogin ? super.loadView() : setupVisitorView()
}

fileprivate func setupVisitorView(){
// 初始化未登录界面
let visitorView = VisitorView()
// 设置当前控制器为代理,那么BaseViewController就要遵循VisitorViewDelegate协议
visitorView.delegate = self
view = visitorView

// 设置导航条未登录按钮
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "注册", style: UIBarButtonItemStyle.plain, target: self, action: #selector(BaseViewController.registerBtnWillClick))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "登录", style: UIBarButtonItemStyle.plain, target: self, action: #selector(BaseViewController.loginBtnWillClick))
}

由于BaseViewController控制器遵循VisitorViewDelegate协议,那么VisitorViewDelegate的相应方法就要实现:

1
2
3
4
5
6
7
func loginBtnWillClick() {
print(#function)
}

func registerBtnWillClick() {
print(#function)
}

主控制器继承基类

1
2
3
4
class HomeTableViewController: BaseViewController
class DiscoverTableViewController: BaseViewController
class MessageTableViewController: BaseViewController
class ProfileTableViewController: BaseViewController

导航条按钮

导航栏中使用UIBarButtonItem来显示每一个item,那么现在为了方便创建,使用扩展extension声明一个类型方法。

创建一个Swift文件,名为:UIBarButtonItem+Category

1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit

extension UIBarButtonItem {
// 声明一个类型方法,类方法使用class关键字
class func createBarButtonItem(_ imageName:String, target:AnyObject?, action:Selector) -> UIBarButtonItem {
let btn = UIButton()
btn.setImage(UIImage(named: imageName), for: UIControlState.normal)
btn.setImage(UIImage(named: imageName), for: UIControlState.highlighted)
btn.addTarget(target, action: action, for: UIControlEvents.touchUpInside)
btn.sizeToFit()
return UIBarButtonItem(customView: btn)
}
}

那么在HomeTableViewController中创建导航栏的UIBarButtonItem就可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
func setupNav() {
navigationItem.leftBarButtonItem = UIBarButtonItem.createBarButtonItem("navigationbar_friendattention", target: self, action: #selector(leftItemClick))
navigationItem.rightBarButtonItem = UIBarButtonItem.createBarButtonItem("navigationbar_pop", target: self, action: #selector(rightItemClick))

let titleBtn = UIButton()
titleBtn.setTitleColor(UIColor.darkGray, for: UIControlState.normal)
titleBtn.setImage(UIImage(named: "navigationbar_arrow_down"), for: UIControlState.normal)
titleBtn.setImage(UIImage(named: "navigationbar_arrow_up"), for: UIControlState.selected)
titleBtn.setTitle(" WeiBo", for: UIControlState.normal)
titleBtn.addTarget(self, action: #selector(titleItemClick(_:)), for: UIControlEvents.touchUpInside)
titleBtn.sizeToFit()
navigationItem.titleView = titleBtn
}

更新时间:2018-04-27 16:53:34

CocoPods

第三方框架

项目中使用到以下第三方框架
  • AFNetworking
  • SDWebImage
  • SVProgressHUD

Pod 安装

  • git 备份
  • 打开终端
  • $ cd 进入项目目录
  • 输入以下终端命令建立或编辑 Podfile
1
$ vim Podfile
  • 输入以下内容
1
2
3
4
use_frameworks!
pod 'AFNetworking'
pod 'SDWebImage'
pod 'SVProgressHUD'
  • :wq 保存退出

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

在终端提交添加的框架

1
2
3
4
5
6
7
8
# 将修改添加至暂存区
$ git add .

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

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

安装第三方框架

1
2
# 安装第三方框架
pod install

打开xcworkspace工程

退出当前工程,进入目录,打开xcworkspace工程即可

添加桥接文件DWeibo-Bridge

  • 使用 Xcode 打开工作组文件
  • Supporting Files 下添加桥接文件 Weibo-Bridge.h
  • 输入以下内容
1
2
#import <SDWebImage/UIImageView+WebCache.h>
#import <SVProgressHUD/SVProgressHUD.h>
  • 点击 项目 - TARGETS - Build Settings
  • 搜索 bridg
  • Objective-C Bridging Header 中输入 DWeibo/DWeibo-Bridge.h路径

然后在swift文件中导入SVProgressHUD,这样就可以使用OC的SVProgressHUD框架了。


更新时间:2018-04-28 15:28:34

微博API

接入微博API,里面的有许多接口,相对应实现即可。有一些分高级接口,可灵活选择。

授权

使用OAuth 2.0授权接口

创建应用:我的应用,获取App Key,App Secret,授权回调页:http://daisuke.cn这三个值,作为授权的参数。

创建OAuthViewController

  1. 声明需要的参数:
1
2
3
let WB_App_Key = "2624860832"
let WB_App_Secret = "75430fcbef20686409d7b775b1f10f5e"
let WB_redirect_uri = "http://www.daisuke.cn"
  1. 拼接获取未授权的RequestToken的路径并加载:
1
2
3
4
let urlStr = "https://api.weibo.com/oauth2/authorize?client_id=\(WB_App_Key)&redirect_uri=\(WB_redirect_uri)"
let url = NSURL(string: urlStr)
let request = NSURLRequest(URL: url!)
webView.loadRequest(request)

3.遵循UIWebViewDelegate实现方法,利用已经授权的RequestToken换取AccessToken

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
extension OAuthViewController: UIWebViewDelegate
{

// 返回true正常加载,返回false不加载
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
// 判断是否授权回调页面,如果不是继续加载
let urlStr = request.url!.absoluteString
if !urlStr.hasPrefix(kRedirectUrl) {
// 继续加载
return true
}

// 判断是否授权
let codeStr = "code="
if request.url!.query!.hasPrefix(codeStr) {
// 授权成功
// 取出已经授权的RequestToken
let code = request.url!.query!.substring(from: codeStr.endIndex)

// 利用已经授权的RequestToken换取AccessToken
loadAccessToken(code: code)
} else {
close()
}
return false
}

func webViewDidStartLoad(_ webView: UIWebView) {
SVProgressHUD.show(withStatus: "正在加载...")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
}

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


// MARK: - 自定方法
/// 换取AccessToken
private func loadAccessToken(code:String) {
// 定义路径
let path = "oauth2/access_token"
// 封装参数
let params = ["client_id":kAppKey, "client_secret":kAppSecret, "grant_type":"authorization_code", "code":code, "redirect_uri":kRedirectUrl]

NetworkTools.shareNetworkTools().post(path, parameters: params, progress: { (_) in

}, success: { (_, json) in
print(json as Any)
/*
字典转模型
plist:特点只能存储系统自带的数据类型
将对象转化为json之后写入文件中
偏好设置:本质plist
归档:可以存储自定义对象
数据库:用于存储大数据,特点效率高
*/

let user = UserAccount(dict: json as! [String : AnyObject])
print(user)
user.loadUserInfo(finishBlock: { (account, error) in
if account != nil {
account!.saveAccount()
SVProgressHUD.dismiss()
return;
}
SVProgressHUD.show(withStatus: "网络不给力")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
})
// 由于加载用户信息是异步的,所以不能在这里保存
// // 归档模型
// user.saveAccount()

}) { (_, error) in
print(error)
}

}
}

UserAccount存储用户信息

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
import UIKit

// Swift2.0 打印对象需要重写CustomStringConvertible协议中的description
class UserAccount: NSObject {
/// 用于调用access_token,接口获取授权后的access token。
var access_token: String?
/// access_token的生命周期,单位是秒数。
var expires_in: NSNumber?
/// 当前授权用户的UID。
var uid:String?


override init() {

}
init(dict: [String: AnyObject])
{
access_token = dict["access_token"] as? String
expires_in = dict["expires_in"] as? NSNumber
uid = dict["uid"] as? String
}

override var description: String{
// 1.定义属性数组
let properties = ["access_token", "expires_in", "uid"]
// 2.根据属性数组, 将属性转换为字典
let dict = self.dictionaryWithValuesForKeys(properties)
// 3.将字典转换为字符串
return "\(dict)"
}
}

归档用户信息

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
// MARK: - 保存和读取  Keyed
/**
保存授权模型
*/

func saveAccount()
{
let path = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!
let filePath = (path as NSString).stringByAppendingPathComponent("account.plist")
print("filePath \(filePath)")
NSKeyedArchiver.archiveRootObject(self, toFile: filePath)
}

/// 加载授权模型
class func loadAccount() -> UserAccount? {
let path = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!
let filePath = (path as NSString).stringByAppendingPathComponent("account.plist")
print("filePath \(filePath)")

let account = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? UserAccount
return account
}


// MARK: - NSCoding
// 将对象写入到文件中
func encodeWithCoder(aCoder: NSCoder)
{
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeObject(expires_in, forKey: "expires_in")
aCoder.encodeObject(uid, forKey: "uid")
}
// 从文件中读取对象
required init?(coder aDecoder: NSCoder)
{
access_token = aDecoder.decodeObjectForKey("access_token") as? String
expires_in = aDecoder.decodeObjectForKey("expires_in") as? NSNumber
uid = aDecoder.decodeObjectForKey("uid") as? String
}

获取用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 加载用户信息
func loadUserInfo(finishBlock: @escaping (_ account: UserAccount?, _ error: NSError?)->()) {
assert(access_token != nil, "没有授权")

let path = "2/users/show.json"
let params = ["access_token":access_token, "uid":uid]

NetworkTools.shareNetworkTools().get(path, parameters: params, progress: nil, success: { (_, json) in
print(json as Any)
if let dict = json as? [String: AnyObject] {
self.avatar_large = dict["avatar_large"] as? String
self.screen_name = dict["screen_name"] as? String

// 保存信息
finishBlock(self, nil)
return
}
finishBlock(nil, nil)
}) { (_, error) in
print(error)
finishBlock(nil, error as NSError)
}
}

更新时间:2018-05-08 15:07:39

新特性

创建NewFeatureViewController

NewFeatureViewController继承UICollectionViewController

实现UICollectionViewDataSource

1
2
3
4
5
6
7
8
9
10
11
// MARK: - UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pageCount
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// 获取cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! NewFeatureCell
cell.imgaeIndex = indexPath.item
return cell
}

创建NewFeatureCell

实现每一个cell的样式在NewFeatureViewController的显示

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
private class NewFeatureCell: UICollectionViewCell {
private lazy var iconView: UIImageView = {
let view = UIImageView()
return view
}()

// 保存图片的索引
// Swift中被private修饰的东西,如果同一个文件中是可以访问的
var imgaeIndex:Int?{
didSet {
iconView.image = UIImage(named: "new_feature_\(imgaeIndex! + 1)")
}
}

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

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

func setupUI() {
// 添加到contentView控件上
contentView.addSubview(iconView)
iconView.xmg_Fill(contentView)
}
}

创建NewFeatureLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// 布局对象
private class NewFeatureLayout: UICollectionViewFlowLayout {

// 重谢准备布局方法
override func prepare() {
// 设置layout布局
itemSize = UIScreen.main.bounds.size
minimumLineSpacing = 0
minimumInteritemSpacing = 0
scrollDirection = UICollectionViewScrollDirection.horizontal

// 设置collectionView的属性
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.bounces = false
collectionView?.isPagingEnabled = true
}
}

完善&添加按钮

添加按钮

1
2
3
4
5
6
7
8
lazy var startBtn: UIButton = {
let btn = UIButton()
btn.setBackgroundImage(UIImage(named: "new_feature_button"), for: UIControlState.normal)
btn.setBackgroundImage(UIImage(named: "new_feature_button"), for: UIControlState.highlighted)
btn.isHidden = true
btn.addTarget(self, action: #selector(clickStartBtn), for: UIControlEvents.touchUpInside)
return btn
}()

添加按钮显示动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 给按钮做个动画
func startBtnAnimation() {
startBtn.isHidden = false

// 执行动画
startBtn.transform = CGAffineTransform.init(scaleX: 0, y: 0)
startBtn.isUserInteractionEnabled = false

UIView.animate(withDuration: 2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: UIViewAnimationOptions(rawValue: 0), animations: {
// 清空形变
self.startBtn.transform = CGAffineTransform.identity
}) { (_) in
self.startBtn.isUserInteractionEnabled = true
}
}

在最后一个cell显示完毕的时候执行按钮动画

1
2
3
4
5
6
7
8
9
10
11
// 完全显示完cell之后调用
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 拿到当前显示的cell对应的索引
let path = collectionView.indexPathsForVisibleItems.last
print(path as Any)
if path?.item == (pageCount - 1) {
// 拿到当前索引对应的cell
let cell = collectionView.cellForItem(at: path!) as! NewFeatureCell
cell.startBtnAnimation()
}
}

image


更新时间:2018-05-08 16:00:03

添加欢迎界面

创建WelcomViewController继承于UIViewController

添加子控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MARK: - 懒加载
private lazy var bgImageView: UIImageView = UIImageView(image: UIImage(named: "ad_background"))
private lazy var iconView: UIImageView = {
let view = UIImageView(image: UIImage(named: "avatar_default_big"))
view.layer.cornerRadius = 50
view.layer.masksToBounds = true
return view
}()
private lazy var message: UILabel = {
let label = UILabel()
label.text = "欢迎归来"
label.alpha = 0.0
label.sizeToFit()
return label
}()

添加显示动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// 提示:修改约束不会立即生效,添加了一个标记,统一由自动布局系统更新约束
bottomConstaint?.constant = -UIScreen.main.bounds.height - bottomConstaint!.constant
print(-UIScreen.main.bounds.height)
print(bottomConstaint!.constant)

UIView.animate(withDuration: 1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: UIViewAnimationOptions(rawValue: 0), animations: {
// 强制更新约束
self.view.layoutIfNeeded()
}) { (finish) in
UIView.animate(withDuration: 1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.message.alpha = 1.0
}) { (finish) in
print("OK")
}
}
}
  • usingSpringWithDamping 的范围为 0.0f 到 1.0f,数值越小 弹簧 的振动效果越明显
  • initialSpringVelocity 则表示初始的速度,数值越大一开始移动越快,初始速度取值较高而时间较短时,会出现反弹情况

初始化信息

1
2
3
4
5
6
7
8
9
10
11
override func viewDidLoad() {
super.viewDidLoad()

// 初始化子控件
setupUI()

// 设置用户信息
if let iconUrl = UserAccount.loadAccount()?.avatar_large {
iconView.sd_setImage(with: NSURL(string: iconUrl)! as URL)
}
}

跟控制器切换

原理图:
image

判断新版本

1
2
3
4
5
6
7
8
9
10
11
12
13
func isNewUpdate() -> Bool {
// 获取当前版本信息
let currentVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let version = NumberFormatter().number(from: currentVersion)?.doubleValue ?? 0

// 获取沙盒版本信息
let versionKey = "versionKey"
let sandboxVersion = UserDefaults.standard.double(forKey: versionKey)

// 保存当前版本
UserDefaults.standard.set(version, forKey: versionKey)
return version > sandboxVersion
}

显示的默认控制器

1
2
3
4
5
6
7
private func defaultController() -> UIViewController{
// 判断用户是否登录
if UserAccount.userLogin() {
return isNewUpdate() ? NewFeatureViewController() : WelcomViewController()
}
return MainViewController()
}

定义通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 切换控制器通知
let kSwitchRootViewControllerKey = "kSwitchRootViewControllerKey"

// 注册通知
NotificationCenter.default.addObserver(self, selector: #selector(switchRootViewController(notify:)), name: NSNotification.Name(rawValue: kSwitchRootViewControllerKey), object: nil)

/// 监听方法
func switchRootViewController(notify:Notification) {
if notify.object as! Bool {
window?.rootViewController = MainViewController()
} else {
window?.rootViewController = WelcomViewController()
}
}

注销通知

1
2
3
deinit {
NotificationCenter.default.removeObserver(self)
}

发送通知

1
NotificationCenter.default.post(name: Notification.Name(rawValue: kSwitchRootViewControllerKey), object: true)

更新时间:2018-05-09 10:13:03

获取微博数据

创建Status类

基本属性

1
2
3
4
5
6
7
8
9
10
/// 微博创建时间
var created_at:String?
/// 微博ID
var id:Int?
/// 微博信息内容
var text:String?
/// 微博来源
var source:String?
/// 配图数组
var pic_urls: [[String:AnyObject]]?

加载微博数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class func loadStatuses(finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()){
let path = "2/statuses/home_timeline.json"
let params = ["access_token":UserAccount.loadAccount()!.access_token]

SVProgressHUD.setStatus("loading...")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

NetworkTools.shareNetworkTools().get(path, parameters: params, progress: nil, success: { (_, json) in
if let dict = json as? [String: AnyObject]{
// 取出字典数组,转为model数组
let dictList = dict["statuses"] as! [[String: AnyObject]]
let models = dictToModel(list: dictList)
finishBlock(models, nil)
SVProgressHUD.dismiss()
return
}
SVProgressHUD.dismiss()
}) { (_, error) in
finishBlock(nil, error as NSError)
SVProgressHUD.dismiss()
}
}

HomeTableViewController填充数据

声明数组存储微博数据

1
2
3
4
5
6
7
// 保存微博数组
var statuses: [Status]? {
didSet{
// 设置数据完毕,刷新表格
tableView.reloadData()
}
}

注册cell

1
2
// 注册cell
tableView.register(UITableViewCell.self, forCellReuseIdentifier: kHomeCellReuseIdentifier)

加载数据

1
2
3
4
5
6
7
8
9
/// 加载数据
func loadData() {
Status.loadStatuses { (models, error) in
if error != nil {
return
}
self.statuses = models
}
}

扩展HomeTableViewController现实数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension HomeTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return statuses?.count ?? 0
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: kHomeCellReuseIdentifier, for: indexPath)
if statuses!.count > indexPath.row {
let status = statuses![indexPath.row]
cell.textLabel?.text = status.text
}
return cell
}
}

image

获取用户数据

创建User

声明user的属性

1
2
3
4
5
6
7
8
9
10
/// 用户ID
var id: Int = 0
/// 名称
var name:String?
/// 用户头像地址(中图)
var profile_image_url:String?
/// 认证,ture是
var verified: Bool = false
/// 用户的认证类型, -1没有认证, 0认证用户, 2.3.5企业认证, 220达人
var verified_type: Int = -1

Status监听user的数据

1
2
3
4
5
6
7
8
9
override func setValue(_ value: Any?, forKey key: String) {
// 判断当前是否给字典的user赋值
if "user" == key {
user = User(dict: value as! [String : AnyObject])
return
}
// 调用父类的方法,按照系统默认处理
super.setValue(value, forKey: key)
}

StatusTableViewCell

添加子控件

1
2
3
4
5
6
7
8
contentView.addSubview(iconView)
contentView.addSubview(verifiedView)
contentView.addSubview(nameLabel)
contentView.addSubview(vipView)
contentView.addSubview(timeLabel)
contentView.addSubview(sourceLabel)
contentView.addSubview(contentLabel)
contentView.addSubview(footerView)

监听微博属性

1
2
3
4
5
6
7
8
9
10
11
var status:Status? {
didSet{
nameLabel.text = status?.user?.name
// timeLabel.text = status?.created_at
// sourceLabel.text = status?.source
timeLabel.text = "刚刚"
sourceLabel.text = "来自:daisuke"
contentLabel.text = status?.text
iconView.sd_setImage(with: URL(string: (status?.user?.profile_image_url)!))
}
}

替换UITableViewCell

1
2
3
4
5
6
7
8
9
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: kHomeCellReuseIdentifier, for: indexPath) as! StatusTableViewCell
if statuses!.count > indexPath.row {
let status = statuses![indexPath.row]
// cell.textLabel?.text = status.text
cell.status = status
}
return cell
}

image

封装UILabel和UIButton

扩展UILabel, UILabel+Category.swift

1
2
3
4
5
6
7
8
9
10
11
extension UILabel {

/// 快速创建一个label
class func createLabel(color:UIColor, fontSize:CGFloat) -> UILabel {
let label = UILabel()
label.textColor = color
label.font = UIFont.systemFont(ofSize: fontSize)
return label
}

}

扩展UIButton, UIButton+Category.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension UIButton {
/// 快速创建一个Button
class func createButton(imageName: String, title: String) -> UIButton
{

let btn = UIButton()
btn.setTitle(title, for: UIControlState.normal)
btn.setImage(UIImage(named: imageName), for: UIControlState.normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 10)
btn.setBackgroundImage(UIImage(named: "timeline_card_bottom_background"), for: UIControlState.normal)
btn.setTitleColor(UIColor.darkGray, for: UIControlState.normal)
btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
return btn
}
}

StatusTableViewCell.swift重构

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
// 昵称
// private lazy var nameLabel: UILabel = {
// let name = UILabel()
// name.textColor = UIColor.darkGray
// name.font = UIFont.systemFont(ofSize: 14)
// return name
// }()

private lazy var nameLabel: UILabel = {
let name = UILabel.createLabel(color: UIColor.darkGray, fontSize: 14)
return name
}()

// ----------------------------------------------

// private lazy var commonBtn: UIButton = {
// let btn = UIButton()
// btn.setImage(UIImage(named: "timeline_icon_comment"), for: UIControlState.normal)
// btn.setTitle("评论", for: UIControlState.normal)
// btn.titleLabel?.font = UIFont.systemFont(ofSize: 10)
// btn.setTitleColor(UIColor.darkGray, for: UIControlState.normal)
// btn.titleEdgeInsets = UIEdgeInsetsMake(0, 10, 0, 0)
// btn.setBackgroundImage(UIImage(named: "timeline_card_bottom_background"), for: UIControlState.normal)
// return btn
// }()

private lazy var commonBtn: UIButton = {
let btn = UIButton.createButton(imageName: "timeline_icon_comment", title: "评论")
return btn
}()

更新时间:2018-05-10 10:11:16

数据处理

认证图标

User.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// 用户的认证类型, -1没有认证, 0认证用户, 2.3.5企业认证, 220达人
var verified_type: Int = -1 {
didSet{
switch verified_type {
case 0:
verifiedImage = UIImage(named: "avatar_vip")
case 2,3,5:
verifiedImage = UIImage(named: "avatar_enterprise_vip")
case 220:
verifiedImage = UIImage(named: "avatar_grassroot")
default:
verifiedImage = nil
}
}
}

/// 保存当前用户的认证图片
var verifiedImage:UIImage?

StatusTableViewCell.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var status:Status? {
didSet{
nameLabel.text = status?.user?.name
// timeLabel.text = status?.created_at
// sourceLabel.text = status?.source
timeLabel.text = "刚刚"
sourceLabel.text = "来自:daisuke"
contentLabel.text = status?.text
if let url = status?.user?.profile_image_url {
iconView.sd_setImage(with: URL(string: url))
}

// 设置会员图标
verifiedView.image = status?.user?.verifiedImage

}
}

会员图标

User.swift

1
2
3
4
5
6
7
8
9
10
11
/// 会员等级
var mbrank: Int = 0 {
didSet{
if mbrank > 0 && mbrank < 7 {
mbrankImage = UIImage(named: "common_icon_membership_level\(mbrank)")
}
}
}

/// 会员图标
var mbrankImage:UIImage?

StatusTableViewCell.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var status:Status? {
didSet{
nameLabel.text = status?.user?.name
// timeLabel.text = status?.created_at
// sourceLabel.text = status?.source
timeLabel.text = "刚刚"
sourceLabel.text = "来自:daisuke"
contentLabel.text = status?.text
if let url = status?.user?.profile_image_url {
iconView.sd_setImage(with: URL(string: url))
}

// 设置会员图标
verifiedView.image = status?.user?.verifiedImage
vipView.image = status?.user?.mbrankImage
}
}

来源处理

Status.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 微博来源
var source:String? {
didSet{
// <a href=\"http://app.weibo.com/t/feed/4fuyNj\" rel=\"nofollow\">即刻笔记</a>
// 截取字符串
if let str = source {
// 获取开始截取的位置
let startLocation = (str as NSString).range(of: ">").location+1
// 获取截取的长度
let length = (str as NSString).range(of: "<", options: NSString.CompareOptions.backwards).location - startLocation
source = "来自:"+(str as NSString).substring(with: NSRange(location: startLocation, length: length))
}
}
}

创建时间处理

扩展NSDate,创建Date+Category.swift

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
55
56
57
58
59
60
61
62
extension NSDate {

/// 字符串转date
class func dateWithString(time:String) -> NSDate {
// 创建fomatter
let fomatter = DateFormatter()
// 设置时间格式
fomatter.dateFormat = "EEE MMM d HH:mm:ss Z yyyy"
// 设置时间的区域(真机必须设置,否则可能转换不成功)
fomatter.locale = Locale(identifier: "en")
let createDate = fomatter.date(from: time)!
return createDate as NSDate
}


/**
刚刚(一分钟内)
X分钟前(一小时内)
X小时前(当天)
昨天 HH:mm(昨天)
MM-dd HH:mm(一年内)
yyyy-MM-dd HH:mm(更早期)
*/

var descDate:String {
let calendar = NSCalendar.current

// 判断是否今天
if calendar.isDateInToday(self as Date) {
// 获取当前时间和系统时间之间的差距
let since = Int(NSDate().timeIntervalSince(self as Date))
if since < 60 {
return "刚刚"
}
if since < 60 * 60 {
return "\(since/60)分钟前"
}
return "\(since/60/60)小时前"
}

// 判断是否是昨天
var fomatterStr = "HH:mm"
if calendar.isDateInYesterday(self as Date) {
fomatterStr = "昨天:" + fomatterStr
} else {
// 处理一年以内
fomatterStr = "MM-dd" + fomatterStr

// 处理更早时间
let comps = calendar.dateComponents([Calendar.Component.year], from: (self as Date), to: Date())
if comps.year! >= 1 {
fomatterStr = "yyyy-" + fomatterStr
}
}

// 创建fomatter
let fomatter = DateFormatter()
fomatter.dateFormat = fomatterStr
fomatter.locale = Locale(identifier: "en")
return fomatter.string(from: self as Date)
}

}

Status.swift

1
2
3
4
5
6
7
/// 微博创建时间
var created_at:String? {
didSet{
let date = NSDate.dateWithString(time: created_at!)
created_at = date.descDate
}
}

缓存配图

Status.swift

添加storedPicURLs数组&重写pic_urls的set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// 配图数组
var pic_urls: [[String:AnyObject]]? {
didSet {
// 初始化数组
storedPicURLs = [NSURL]()
for dict in pic_urls! {
if let urlStr = dict["thumbnail_pic"]{
// 将字符串转为URL保存到数组中
storedPicURLs?.append(NSURL(string: urlStr as! String)!)
}
}
}
}

/// 保存当前微博所有配图的URL
var storedPicURLs:[NSURL]?

添加类方法缓存微博图片

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
/// 缓存微博图片
class func cacheStatusImages(list:[Status], finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()) {

// 创建一个数组
let group = DispatchGroup()
for status in list {
// 判断当前微博是否有配图,没有就直接跳过
// if status.storedPicURLs == nil {
// continue
// }

// 新语法
// 如果条件为nil,那么久会执行else后面的语句
guard status.storedPicURLs != nil else {
continue
}
for url in status.storedPicURLs! {
// 将当前的下载操作添加到组
group.enter()

SDWebImageManager.shared().downloadImage(with: url as URL, options: SDWebImageOptions(rawValue: 0), progress: nil) { (_, _, _, _, _) in
// 离开当前组
group.leave()
}
}
}

// 当所有图片都下载完毕再通过闭包通用调用者
group.notify(queue: DispatchQueue.main) {
// 能够来到这个地方,一定是所有图片都下载完毕
finishBlock(list, nil)
}
}

使用缓存微博图片方法

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
/// 加载微博数据
class func loadStatuses(finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()){
let path = "2/statuses/home_timeline.json"
let params = ["access_token":UserAccount.loadAccount()!.access_token]

SVProgressHUD.setStatus("loading...")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

NetworkTools.shareNetworkTools().get(path, parameters: params, progress: nil, success: { (_, json) in
if let dict = json as? [String: AnyObject]{
// 取出字典数组,转为model数组
let dictList = dict["statuses"] as! [[String: AnyObject]]
let models = dictToModel(list: dictList)

// finishBlock(models, nil)
// 替换成缓存微博图片方法
cacheStatusImages(list: models, finishBlock: finishBlock)

SVProgressHUD.dismiss()
return
}
SVProgressHUD.dismiss()
}) { (_, error) in
finishBlock(nil, error as NSError)
SVProgressHUD.dismiss()
}
}

计算配图大小

StatusTableViewCell.swift

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
/// 计算配图的尺寸
private func calculateImageSize() -> CGSize {
// 取出配图个数
let count = status?.storedPicURLs?.count
// 如果没有配图zero
if count == 0 || count == nil {
return CGSize.zero
}

// 如果只有一张配图,返回图片的实际大小
if count == 1 {
// 取出缓存的图片
let key = status?.storedPicURLs?.first?.absoluteString
let image = SDWebImageManager.shared().imageCache.imageFromDiskCache(forKey: key!)
return (image?.size)!
}

// 如果有4张配图,计算字格的大小
let width = 90
let margin = 10
if count == 4 {
let viewWidth = width * 2 + margin
return CGSize(width: viewWidth, height: viewWidth)
}

// 如果是其他(多张),计算九宫格的大小
let colNumber = 3
let rowNumber = (count! - 1) / 3 + 1
let viewWidth = colNumber * width + (colNumber - 1) * margin
let viewHeight = rowNumber * width + (rowNumber - 1) * margin
return CGSize(width: viewWidth, height: viewHeight)
}

显示配图

懒加载配图控件
1
2
3
// 配图
private lazy var pictureLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
private lazy var pictureView: UICollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.pictureLayout)
初始化配图控件
1
2
3
4
5
6
7
8
9
10
11
/// 初始化配图的相关属性
private func setupPictureView() {
// 注册cell
pictureView.register(PictureViewCell.self, forCellWithReuseIdentifier: kPictureViewCellId)
pictureView.dataSource = self
// 设置cell之间的间隙
pictureLayout.minimumLineSpacing = 10
pictureLayout.minimumInteritemSpacing = 10
// 设置配图的背景颜色
pictureView.backgroundColor = UIColor.darkGray
}
扩展StatusTableViewCell实现数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension StatusTableViewCell:UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return status?.storedPicURLs?.count ?? 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPictureViewCellId, for: indexPath) as! PictureViewCell
if (status?.storedPicURLs?.count)! > indexPath.item {
cell.imageUrl = status?.storedPicURLs![indexPath.item]
}

return cell
}
}
自定义配图cell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PictureViewCell: UICollectionViewCell {

var imageUrl:NSURL? {
didSet{
iconView.sd_setImage(with: imageUrl! as URL!)
}
}

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

contentView.addSubview(iconView)

iconView.xmg_Fill(contentView)
}

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

private lazy var iconView = UIImageView ()
}
完善计算配图方法,返回总试图的尺寸&每个子视图的尺寸
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
/// 计算配图的尺寸
private func calculateImageSize() -> (viewSize:CGSize, itemSize:CGSize) {
// 取出配图个数
let count = status?.storedPicURLs?.count
// 如果没有配图zero
if count == 0 || count == nil {
return (CGSize.zero, CGSize.zero)
}

// 如果只有一张配图,返回图片的实际大小
if count == 1 {
// 取出缓存的图片
let key = status?.storedPicURLs?.first?.absoluteString
let image = SDWebImageManager.shared().imageCache.imageFromDiskCache(forKey: key!)
return (image!.size, image!.size)
}

// 如果有4张配图,计算字格的大小
let width = 90
let margin = 10
if count == 4 {
let viewWidth = width * 2 + margin
return (CGSize(width: viewWidth, height: viewWidth), CGSize(width: width, height: width))
}

// 如果是其他(多张),计算九宫格的大小
let colNumber = 3
let rowNumber = (count! - 1) / 3 + 1
let viewWidth = colNumber * width + (colNumber - 1) * margin
let viewHeight = rowNumber * width + (rowNumber - 1) * margin
return (CGSize(width: viewWidth, height: viewHeight), CGSize(width: width, height: width))
}
设置配图尺寸
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var status:Status? {
didSet{
nameLabel.text = status?.user?.name
timeLabel.text = status?.created_at
sourceLabel.text = status?.source
contentLabel.text = status?.text
if let url = status?.user?.profile_image_url {
iconView.sd_setImage(with: URL(string: url))
}

// 设置会员图标
verifiedView.image = status?.user?.verifiedImage
vipView.image = status?.user?.mbrankImage

// 设置配图尺寸
let size = calculateImageSize()
pictureWidthCons?.constant = size.viewSize.width
pictureHeightCons?.constant = size.viewSize.height
pictureLayout.itemSize = size.itemSize
pictureView.reloadData()
}
}

缓存行高

在StatusTableViewCell.swift添加获取行高的方法

1
2
3
4
5
6
7
8
9
10
/// 获取行高
func rowHeight(status:Status) -> CGFloat {
// 为了调用didSet, 计算配图的高度
self.status = status
// 强制更新页面
self.layoutIfNeeded()

// 返回底部试图最大的Y值
return footerView.frame.maxY
}

HomeTableViewController.swift

声明缓存行高的数组

1
2
3
4
5
6
7
/// 微博行高的缓存,利用字典作为容器,key是微博的ID,值就是对应微博的行高
var rowCache: [Int:CGFloat] = [Int:CGFloat]()

override func didReceiveMemoryWarning() {
// 清空缓存
rowCache.removeAll()
}

在Status.swift中吧id属性可选属性去掉,给默认值

1
2
3
/// 微博ID
// var id:Int?
var id:Int = 0

实现返回数据源方法,返回行高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// 取出对应的模型
let status = statuses![indexPath.row]
// 判断缓存中有没有
if let height = rowCache[status.id] {
return height
}

// 拿到cell
let cell = tableView.dequeueReusableCell(withIdentifier: kHomeCellReuseIdentifier) as! StatusTableViewCell

// 拿到行高
let rowHeight = cell.rowHeight(status: status)
// 缓存行高
rowCache[status.id] = rowHeight
return rowHeight
}

image

重构StatusTableViewCell.swift

cell的子控件分为四个部分,工具条封装成一个个view,头部封装成一个view,如下图:
image

创建StatusCellTopView

分别包含有昵称、头像、认证图标、会员图标、来源、时间这个几个控件

部分代码,具体看实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var status:Status? {
didSet{
nameLabel.text = status?.user?.name
timeLabel.text = status?.created_at
sourceLabel.text = status?.source
if let url = status?.user?.profile_image_url {
iconView.sd_setImage(with: URL(string: url))
}

// 设置会员图标
verifiedView.image = status?.user?.verifiedImage
vipView.image = status?.user?.mbrankImage
}
}

然后我们把topView替换成cell里面的控件

1
private lazy var topView:StatusCellTopView = StatusCellTopView()

数据赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var status:Status? {
didSet{
// nameLabel.text = status?.user?.name
// timeLabel.text = status?.created_at
// sourceLabel.text = status?.source
// if let url = status?.user?.profile_image_url {
// iconView.sd_setImage(with: URL(string: url))
// }
//
// // 设置会员图标
// verifiedView.image = status?.user?.verifiedImage
// vipView.image = status?.user?.mbrankImage
topView.status = status

contentLabel.text = status?.text

// 设置配图尺寸
let size = calculateImageSize()
pictureWidthCons?.constant = size.viewSize.width
pictureHeightCons?.constant = size.viewSize.height
pictureLayout.itemSize = size.itemSize
pictureView.reloadData()
}
}

创建StatusCellBottomView

工具条包含三个按钮

1
2
3
4
5
private lazy var retweetBtn: UIButton = UIButton.createButton(imageName: "timeline_icon_retweet", title: "转发")

private lazy var unLikeBtn: UIButton = UIButton.createButton(imageName: "timeline_icon_unlike", title: "赞")

private lazy var commonBtn: UIButton = UIButton.createButton(imageName: "timeline_icon_comment", title: "评论")

替换成StatusCellBottomView

1
2
// 底部工具条
private lazy var footerView: StatusCellBottomView = StatusCellBottomView()

创建StatusCellPictureView

设置数据,初始化(具体查看文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var status:Status? {
didSet{
reloadData()
}
}

private var pictureLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()

init() {
super.init(frame: CGRect.zero, collectionViewLayout: pictureLayout)

// 注册cell
register(PictureViewCell.self, forCellWithReuseIdentifier: kPictureViewCellId)
dataSource = self
// 设置cell之间的间隙
pictureLayout.minimumLineSpacing = 10
pictureLayout.minimumInteritemSpacing = 10
// 设置配图的背景颜色
backgroundColor = UIColor.white
}

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

替换成StatusCellPictureView

1
private lazy var pictureView:StatusCellPictureView = StatusCellPictureView()

重构结束。。。


更新时间:2018-05-11 09:51:39

转发微博

转发与正常发布的cell样式有所不同,所以创建两个cell继承于基类cell(StatusTableViewCell)

创建StatusNormalTableViewCell显示普通状态下的微博

由于显示普通状态下的cell,基类已具备样式,所以只需要重写setupUI()方法,调整布局即可。(先把基类的setupUI()使用private声明去掉,不然重写不了, 因为调整配图的布局,把声明pictureView和contentLabel的private关键字也去掉)

1
2
3
4
5
6
7
8
9
10
11
12
class StatusNormalTableViewCell: StatusTableViewCell {

override func setupUI() {
super.setupUI()

let cons = pictureView.xmg_AlignVertical(type: XMG_AlignType.bottomLeft, referView: contentLabel, size: CGSize.zero, offset: CGPoint(x: 0, y: 10))

pictureWidthCons = pictureView.xmg_Constraint(cons, attribute: NSLayoutAttribute.width)
pictureHeightCons = pictureView.xmg_Constraint(cons, attribute: NSLayoutAttribute.height)

}
}

Status.swift

在Status.swift中声明两个属性

1
2
3
4
5
6
7
8
/// 转发微博
var retweeted_status:Status?

/// 如果有转发,原创就没有配图
/// 定义一个计算属性,用于返回原创获取转发配图的URL数组
var pictureURLs:[NSURL]? {
return retweeted_status != nil ? retweeted_status?.storedPicURLs : storedPicURLs
}

修改相关方法

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
55
56
57

/// 缓存微博图片
class func cacheStatusImages(list:[Status], finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()) {

// 创建一个数组
let group = DispatchGroup()
for status in list {

// 判断当前微博是否有配图,没有就直接跳过
// if status.storedPicURLs == nil {
// continue
// }

// 新语法
// 如果条件为nil,那么久会执行else后面的语句
// guard status.storedPicURLs != nil else {
// continue
// }

guard let _ = status.pictureURLs else {
continue
}

for url in status.pictureURLs! {
// 将当前的下载操作添加到组
group.enter()

SDWebImageManager.shared().downloadImage(with: url as URL, options: SDWebImageOptions(rawValue: 0), progress: nil) { (_, _, _, _, _) in
// 离开当前组
group.leave()
}
}
}

// 当所有图片都下载完毕再通过闭包通用调用者
group.notify(queue: DispatchQueue.main) {
// 能够来到这个地方,一定是所有图片都下载完毕
finishBlock(list, nil)
}
}


override func setValue(_ value: Any?, forKey key: String) {
// 判断当前是否给字典的user赋值
if "user" == key {
user = User(dict: value as! [String : AnyObject])
return
}

if "retweeted_status" == key {
retweeted_status = Status(dict: value as! [String : AnyObject])
return
}

// 调用父类的方法,按照系统默认处理
super.setValue(value, forKey: key)
}

创建StatusForwardTableViewCell显示转发状态下的微博

创建两个控件属性

1
2
3
4
5
6
7
8
9
10
11
private lazy var foreardLabel: UILabel = {
let label = UILabel.createLabel(color: UIColor.darkGray, fontSize: 15)
label.numberOfLines = 0
label.preferredMaxLayoutWidth = UIScreen.main.bounds.width - 20
return label
}()
private lazy var forwardButton: UIButton = {
let btn = UIButton()
btn.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return btn
}()

重写setupUI()方法。(因为调整配图的布局,把声明footerView的private关键字去掉, 另外需要声明一个成员变量:var pictureTopCons:NSLayoutConstraint?(保存配图的顶部约束
))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func setupUI() {
super.setupUI()

// 添加子控件
contentView.insertSubview(forwardButton, belowSubview: pictureView)
contentView.insertSubview(foreardLabel, aboveSubview: forwardButton)

// 布局
forwardButton.xmg_AlignVertical(type: XMG_AlignType.bottomLeft, referView: contentLabel, size: nil, offset: CGPoint(x: -10, y: 10))
forwardButton.xmg_AlignVertical(type: XMG_AlignType.topRight, referView: footerView, size: nil)

foreardLabel.text = "上大号实打实大师"
foreardLabel.xmg_AlignInner(type: XMG_AlignType.topLeft, referView: forwardButton, size: nil, offset: CGPoint(x: 10, y: 20))

// 重新调整配图
let cons = pictureView.xmg_AlignVertical(type: XMG_AlignType.bottomLeft, referView: foreardLabel, size: CGSize(width: 290, height: 290), offset: CGPoint(x: 0, y: 10))

pictureWidthCons = pictureView.xmg_Constraint(cons, attribute: NSLayoutAttribute.width)
pictureHeightCons = pictureView.xmg_Constraint(cons, attribute: NSLayoutAttribute.height)
pictureTopCons = pictureView.xmg_Constraint(cons, attribute: NSLayoutAttribute.top)
}

重写status属性,设置相关数据

  • 重写父类的属性的didSet并不会阀盖父类的操作
  • 只需要在重写方法中,做自己想做的事情即可
  • 注意点:如果父类是didSet,那么子类重写也只能重写didSet
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /*
    重写父类的属性的didSet并不会阀盖父类的操作
    只需要在重写方法中,做自己想做的事情即可
    注意点:如果父类是didSet,那么子类重写也只能重写didSet
    */

    override var status: Status? {
    didSet{
    let name = status?.retweeted_status?.user?.name ?? ""
    let text = status?.retweeted_status?.text ?? ""
    foreardLabel.text = name + ":" + text

    }
    }

创建切换不同cell的标记

在StatusTableViewCell.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
保存cell的重用标志

NormalCell:原创微博的重用标志
ForwardCell:转发微博的重用标志
*/

enum StatusTableViewCellIdentifier:String {
case NormalCell = "NormalCell"
case ForwardCell = "ForwardCell"

/*
如果在枚举中利用static修饰一个方法,相当于类中的class修饰方法
如果调用枚举值的rawValue,那么意味着拿到枚举对应的原始值
*/

static func cellID(status:Status) ->String{
return status.retweeted_status != nil ? ForwardCell.rawValue : NormalCell.rawValue
}
}

完善StatusCellPictureView.swift

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
/// 计算配图的尺寸
func calculateImageSize() -> CGSize {
// 取出配图个数
let count = status?.storedPicURLs?.count
// 如果没有配图zero
if count == 0 || count == nil {
return CGSize.zero
}

// 如果只有一张配图,返回图片的实际大小
if count == 1 {
// 取出缓存的图片
let key = status?.storedPicURLs?.first?.absoluteString
let image = SDWebImageManager.shared().imageCache.imageFromDiskCache(forKey: key!)
pictureLayout.itemSize = image!.size
return image!.size
}

// 如果有4张配图,计算字格的大小
let width = 90
let margin = 10
pictureLayout.itemSize = CGSize(width: width, height: width)
if count == 4 {
let viewWidth = width * 2 + margin
return CGSize(width: viewWidth, height: viewWidth)
}

// 如果是其他(多张),计算九宫格的大小
let colNumber = 3
let rowNumber = (count! - 1) / 3 + 1
let viewWidth = colNumber * width + (colNumber - 1) * margin
let viewHeight = rowNumber * width + (rowNumber - 1) * margin
return CGSize(width: viewWidth, height: viewHeight)
}

完善HomeTableViewController.swift

注册两个cell

1
2
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.NormalCell.rawValue)
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.ForwardCell.rawValue)

代理方法

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
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let status = statuses![indexPath.row]

let cell = tableView.dequeueReusableCell(withIdentifier: StatusTableViewCellIdentifier.cellID(status: status), for: indexPath) as! StatusTableViewCell
cell.status = status
return cell
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// 取出对应的模型
let status = statuses![indexPath.row]
// 判断缓存中有没有
if let height = rowCache[status.id] {
return height
}

// 拿到cell
// let cell = tableView.dequeueReusableCell(withIdentifier: kHomeCellReuseIdentifier) as! StatusTableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: StatusTableViewCellIdentifier.cellID(status: status)) as! StatusTableViewCell

// 拿到行高
let rowHeight = cell.rowHeight(status: status)
// 缓存行高
rowCache[status.id] = rowHeight
return rowHeight
}

转发功能完成。

下拉刷新

添加刷新控件

在Profile添加pod 'MJRefresh',然后pod install

1
2
3
4
5
6
7
8
weak var weakSelf = self
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
weakSelf?.loadData()
})
tableView.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
weakSelf?.loadData()
})
tableView.mj_header.beginRefreshing()

优化

处理加载微博数据上拉还是下拉

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
/// 定义变量记录当时上拉还是下拉
var pullupRefreshFalg = false

/// 加载数据
private func loadData() {
/*
1、默认最新返回20条数据
2、since_id: 会返回比since_id大的微博
3、max_id:会返回小鱼等于max_id的微博

每条微博都有一个微博id,而且微博ID越后面发送的微博,他的微博ID越大
*/


var since_id = statuses?.first?.id ?? 0
var max_id = 0
// 判断是否上拉
if pullupRefreshFalg {
since_id = 0
max_id = statuses?.last?.id ?? 0
}


weak var weakSelf = self
Status.loadStatuses(since_id:since_id, max_id:max_id) { (models, error) in

weakSelf?.tableView.mj_header.endRefreshing()
weakSelf?.tableView.mj_footer.endRefreshing()
if error != nil {
SVProgressHUD.showError(withStatus: "服务器错误")
return
}

if since_id > 0 {
// 下拉刷新
weakSelf?.statuses = models! + (weakSelf?.statuses!)!

} else if max_id > 0 {
// 如果是上拉加载更多,将数据取到的数据拼在原有的数据后面
weakSelf?.statuses = (weakSelf?.statuses!)! + models!
} else {
weakSelf?.statuses = models
}
}
}

判断是上拉还是下拉

1
2
3
4
5
6
7
8
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
weakSelf?.pullupRefreshFalg = false
weakSelf?.loadData()
})
tableView.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
weakSelf?.pullupRefreshFalg = true
weakSelf?.loadData()
})

更新时间:2018-05-16 15:09:32

修改BaseViewController继承UIViewController

继承BaseViewController分别修改,首页创建UItableview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lazy var tableView: UITableView! = {
let view = UITableView(frame: CGRect.zero, style: UITableViewStyle.plain)
view.dataSource = self
view.delegate = self;
// 注册cell
view.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.NormalCell.rawValue)
view.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.ForwardCell.rawValue)
view.separatorStyle = UITableViewCellSeparatorStyle.none
weak var weakSelf = self
view.mj_header = MJRefreshNormalHeader(refreshingBlock: {
weakSelf?.pullupRefreshFalg = false
weakSelf?.loadData()
})
view.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
weakSelf?.pullupRefreshFalg = true
weakSelf?.loadData()
})
view.mj_header.beginRefreshing()
return view
}()

显示刷新提醒

创建显示文本

1
2
3
4
5
6
7
8
9
10
11
private lazy var newStatusLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.orange
label.textColor = UIColor.white
label.textAlignment = NSTextAlignment.center
label.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44)
// 加到导航栏上
self.navigationController?.navigationBar.insertSubview(label, at: 0)
label.isHidden = true
return label
}()

展示文本方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private func showNewStatusCount(count:Int){
newStatusLabel.isHidden = false
newStatusLabel.text = (count == 0) ? "没有刷新到新的微博数据" : "已更新\(count)条数据"
UIView.animate(withDuration: 2, animations: {
self.newStatusLabel.transform = CGAffineTransform(translationX: 0, y: self.newStatusLabel.frame.size.height)
}) { (finish) in
UIView.animate(withDuration: 2, animations: {
self.newStatusLabel.transform = CGAffineTransform.identity
}, completion: { (_) in
self.newStatusLabel.isHidden = true
})
}
}

调用展示方法

1
2
// 显示刷新提醒
weakSelf?.showNewStatusCount(count: models?.count ?? 0)

浏览大图

获取大图

在Status.swift中声明storeLargePictureURLs属性

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
/// 配图数组
var pic_urls: [[String:AnyObject]]? {
didSet {
// 判断数组中是否有数据 nil
if pic_urls?.count == 0 {
return
}

// 初始化数组
storedPicURLs = [NSURL]()
storeLargePictureURLs = [NSURL]()

for dict in pic_urls! {
if let urlStr = dict["thumbnail_pic"]{
// 将字符串转为URL保存到数组中
// 生成小图
storedPicURLs?.append(NSURL(string: urlStr as! String)!)

// 生成大图
let largeUrlStr = urlStr.replacingOccurrences(of: "thumbnail", with: "large")
storeLargePictureURLs?.append(NSURL(string: largeUrlStr)!)

}
}
}
}

/// 保存当前微博所有配图的URL
var storedPicURLs:[NSURL]?
/// 保存配图的大图URL数组
var storeLargePictureURLs: [NSURL]?
var largePictureURLs: [NSURL]? {
return retweeted_status == nil ? storeLargePictureURLs : retweeted_status?.storeLargePictureURLs
}

获取当前点击图片

在StatusCellPictureView.swift中实现UICollectionViewDelegate代理方法

1
2
3
4
5
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 拿到当前点击图片
print(status?.pictureURLs![indexPath.item])
print(status?.storedPicURLs![indexPath.item])
}

创建浏览器

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
class PhotoBrowserViewController: UIViewController {

/// 图片数组
var imageUrls: [NSURL]?
/// 用户选中的图片索引
var currentIndex: Int

init(urls:[NSURL], index:Int) {
imageUrls = urls
currentIndex = index
super.init(nibName: nil, bundle: nil)
}

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

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = UIColor.white
setupUI()
}

func setupUI() {
view.addSubview(collectionView)
view.addSubview(closeBtn)
view.addSubview(saveBtn)

closeBtn.xmg_AlignInner(type: XMG_AlignType.bottomLeft, referView: view, size: CGSize(width: 100, height: 32), offset: CGPoint(x: 8, y: -8))
saveBtn.xmg_AlignInner(type: XMG_AlignType.bottomRight, referView: view, size: CGSize(width: 100, height: 32), offset: CGPoint(x: 8, y: -8))
collectionView.frame = UIScreen.main.bounds

saveBtn.addTarget(self, action: #selector(save), for: UIControlEvents.touchUpInside)
closeBtn.addTarget(self, action: #selector(close), for: UIControlEvents.touchUpInside)
}

func close() {
dismiss(animated: true, completion: nil)
}

func save() {
print("保存图片")
}

private lazy var closeBtn: UIButton = UIButton(tittle: "关闭", fontSize: 14, color: UIColor.white, backColor: UIColor.darkGray)
private lazy var saveBtn: UIButton = UIButton(tittle: "保存", fontSize: 14, color: UIColor.white, backColor: UIColor.darkGray)
private lazy var collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())

}

添加通知

在StatusCellPictureView.swift声明三个通知字段

1
2
3
4
5
6
/// 选中照片通知
let kStatusCellSelectPictureNotify = "kStatusCellSelectPictureNotify"
/// 照片的URL
let kStatusCellSelectPictureURLNotify = "kStatusCellSelectPictureURLNotify"
/// 照片的index
let kStatusCellSelectPictureIndexNotify = "kStatusCellSelectPictureIndexNotify"

在HomeTableViewController.swift

1
2
// 添加通知
NotificationCenter.default.addObserver(self, selector: #selector(selectPicture(notify:)), name: NSNotification.Name(rawValue: kStatusCellSelectPictureNotify), object: nil)

并实现监听方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func selectPicture(notify:Notification) {
// 获取参数。提示:从自定义通知获取参数,一定记住检查参数值
guard let urls = notify.userInfo![kStatusCellSelectPictureURLNotify] as? [NSURL] else {
print("url数组不存在")
return
}
guard let indexPath = notify.userInfo![kStatusCellSelectPictureIndexNotify] as? NSIndexPath else {
print("indexPath不存在")
return
}
let vc = PhotoBrowserViewController(urls: urls, index: indexPath.item)
present(vc, animated: true, completion: nil)

}

在StatusCellPictureView.swift中点击照片的时候发出通知

1
2
3
4
5
6
7
8
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 拿到当前点击图片
// print(status?.pictureURLs![indexPath.item])
// print(status?.storedPicURLs![indexPath.item])

NotificationCenter.default.post(name: NSNotification.Name(rawValue: kStatusCellSelectPictureNotify), object: self, userInfo: [kStatusCellSelectPictureURLNotify:status!.storeLargePictureURLs!, kStatusCellSelectPictureIndexNotify:indexPath])

}

显示大图

创建PhotoBrowserCell
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
class PhotoBrowserCell: UICollectionViewCell {

// 图片的URL
var imageURL: NSURL? {
didSet{

imageView.sd_setImage(with: imageURL as URL?, placeholderImage: nil, options: SDWebImageOptions.retryFailed) { (_, _, _, _) in
self.imageView.sizeToFit()
}
}
}

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

setupUI()

}

private func setupUI() {
contentView.addSubview(scrollView)
scrollView.addSubview(imageView)

scrollView.frame = UIScreen.main.bounds
}

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


lazy var scrollView = UIScrollView()
lazy var imageView = UIImageView()
}
完善

注册cell

1
2
3
// 注册cell
collectionView.dataSource = self
collectionView.register(PhotoBrowserCell.self, forCellWithReuseIdentifier: kCellReuseIdentifier)

实现数据源方法

1
2
3
4
5
6
7
8
9
10
11
12
extension PhotoBrowserViewController: UICollectionViewDataSource {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageUrls!.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCellReuseIdentifier, for: indexPath) as! PhotoBrowserCell
cell.imageURL = imageUrls![indexPath.item]
return cell
}
}

创建布局

1
2
3
4
5
6
7
8
9
10
11
class PhotoBrowserLayout: UICollectionViewFlowLayout {
override func prepare() {
itemSize = UIScreen.main.bounds.size
minimumLineSpacing = 0
minimumInteritemSpacing = 0
scrollDirection = UICollectionViewScrollDirection.horizontal
collectionView?.isPagingEnabled = true
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.bounces = false
}
}
点击图片指定位置展示
1
2
3
// 滚动到指定位置
let indexPath = IndexPath(item: currentIndex, section: 0)
collectionView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition.left, animated: false)

效果如下:

image


更新时间:2018-05-17 16:13:21

长短图计算

计算显示的图像尺寸

1
2
3
4
5
6
7
/// 计算显示的图像尺寸,以scrollview的宽度为准
private func displaySize(image:UIImage) -> CGSize {
// 图像宽高比
let scale = image.size.height / image.size.width
let height = scale * scrollView.bounds.size.width
return CGSize(width: scrollView.bounds.size.width, height: height)
}

初始化位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private func setupImagePosition() {
let size = self.displaySize(image: imageView.image!)
// 图像高度比视图高度小,短图
if size.height < scrollView.bounds.size.height {
// 垂直居中
let y = (scrollView.bounds.size.height - size.height) * 0.5
imageView.frame = CGRect(origin: CGPoint.zero, size: size)
// 设置间距,能够保证缩放完成后,同样能够显示完整画面
scrollView.contentInset = UIEdgeInsetsMake(y, 0, y, 0)
} else {
// 长图
imageView.frame = CGRect(origin: CGPoint.zero, size: size)
scrollView.contentSize = size
}
}

调用

1
2
3
4
5
6
7
8
9
10
// 图片的URL
var imageURL: NSURL? {
didSet{

imageView.sd_setImage(with: imageURL as URL?, placeholderImage: nil, options: SDWebImageOptions.retryFailed) { (_, _, _, _) in

self.setupImagePosition()
}
}
}

缩放图片

重设scrollview的偏移属性

1
2
3
4
5
6
7
/// 重设scrollview的偏移属性
private func resetScrollView() {
imageView.transform = CGAffineTransform.identity
scrollView.contentInset = UIEdgeInsets.zero
scrollView.contentOffset = CGPoint.zero
scrollView.contentSize = CGSize.zero
}

加载图片前重设偏移属性

1
2
3
4
5
6
7
8
9
10
11
12
·// 图片的URL
var imageURL: NSURL? {
didSet{
// 重设scrollview
resetScrollView()

imageView.sd_setImage(with: imageURL as URL?, placeholderImage: nil, options: SDWebImageOptions.retryFailed) { (_, _, _, _) in

self.setupImagePosition()
}
}
}

遵循UIScrollViewDelegate

1
2
3
4
5
6
7
8
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.delegate = self
// 最大/最小缩放比例
view.minimumZoomScale = 0.5
view.maximumZoomScale = 2.0
return view
}()

实现相关UIScrollViewDelegate方法

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
extension PhotoBrowserCell: UIScrollViewDelegate {

/// 告诉scrollview要缩放的控件
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}

/**
在缩放完成后,会被调用一次
view:被缩放的view
scale:缩放完成后的缩放比例
*/

func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
// 重新计算间距
// 通过transform改变view的缩放,bound本身没有变化,frame会变化
var offsetX = (scrollView.bounds.width - view!.frame.size.width) * 0.5
// 如果边距小于0,需要修正
offsetX = offsetX < 0 ? 0 : offsetX

var offsetY = (scrollView.bounds.size.height - view!.frame.size.height) * 0.5
offsetY = offsetY < 0 ? 0 : offsetY

scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0, 0)

}
}

显示菊花

1
2
// 菊花
lazy var indicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.white)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 图片的URL
var imageURL: NSURL? {
didSet{
self.indicator.startAnimating()

// 重设scrollview
resetScrollView()

imageView.sd_setImage(with: imageURL as URL?, placeholderImage: nil, options: SDWebImageOptions.retryFailed) { (_, _, _, _) in
self.indicator.stopAnimating()

self.setupImagePosition()
}
}
}

显示gif图标

在StatusCellPictureView.swift中

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
private class PictureViewCell: UICollectionViewCell {

var imageUrl:NSURL? {
didSet{
iconView.sd_setImage(with: imageUrl! as URL?)

// 设置时候显示gif图标
gifImageView.isHidden = ((imageUrl!.absoluteString! as NSString).pathExtension.lowercased() != "gif")
}
}

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

contentView.addSubview(iconView)
contentView.addSubview(gifImageView)

iconView.xmg_Fill(contentView)
gifImageView.xmg_AlignInner(type: XMG_AlignType.bottomRight, referView: iconView, size: nil)
}

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

private lazy var iconView = UIImageView ()
lazy var gifImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "timeline_image_gif")
view.isHidden = true
return view
}()
}

关闭浏览器

PhotoBrowserCell.swift

添加手势

1
2
3
4
// 添加imageView手势监听
let tap = UITapGestureRecognizer(target: self, action: #selector(clickImageView))
imageView.addGestureRecognizer(tap)
imageView.isUserInteractionEnabled = true

声明代理

1
2
3
4
protocol PhotoBrowserCellDelegate: NSObjectProtocol {
/// 结束缩放
func photoBrowserCellDidTapImageView()
}

声明代理属性

1
2
// 声明代理属性
weak var photoDelegate:PhotoBrowserCellDelegate?

在PhotoBrowserViewController类中实现PhotoBrowserCellDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension PhotoBrowserViewController: UICollectionViewDataSource, PhotoBrowserCellDelegate {

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageUrls!.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCellReuseIdentifier, for: indexPath) as! PhotoBrowserCell
cell.imageURL = imageUrls![indexPath.item]
cell.backgroundColor = UIColor.randomColor()
cell.photoDelegate = self
return cell
}

// MARK: - PhotoBrowserCellDelegate
func photoBrowserCellDidTapImageView() {
close()
}
}

更新时间:2018-05-18 10:19:39

添加表情数据包

可在工程上找
image

表情控制器

主要控件

1
2
3
// MARK: - 懒加载
private lazy var toolBar = UIToolbar()
private lazy var collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())

初始化工具条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private func setupToolbar() {
toolBar.tintColor = UIColor.darkGray

var items = [UIBarButtonItem]()
var index = 0
for str in ["最近", "默认", "emoji", "浪小花"] {
let item = UIBarButtonItem(title: str, style: UIBarButtonItemStyle.plain, target: self, action: #selector(clickItem(item:)))
index += 1
item.tag = index
items.append(item)
items.append(UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil))
}
items.removeLast()
toolBar.items = items
}

布局CollectionView界面

设置layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private class EmoticonLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()

let width = collectionView!.bounds.size.width / 7
itemSize = CGSize(width: width, height: width)
minimumLineSpacing = 0
minimumInteritemSpacing = 0
scrollDirection = UICollectionViewScrollDirection.horizontal

// 由于不CGFloat准确,所以不要写0.5,可能出现只显示2
let y = (collectionView!.bounds.size.height - 3 * width) * 0.45
collectionView?.contentInset = UIEdgeInsetsMake(y, 0, y, 0)

collectionView?.isPagingEnabled = true
collectionView?.bounces = false
}
}

创建EmoticonCell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class EmoticonCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)

contentView.addSubview(emoticonBtn)
emoticonBtn.frame = bounds.insetBy(dx: 4, dy: 4)
emoticonBtn.backgroundColor = UIColor.white
}

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

private lazy var emoticonBtn = UIButton()
}

设置collectionView

1
2
3
4
private func setupCollectionView() {
collectionView.register(EmoticonCell.self, forCellWithReuseIdentifier: kEmoticonCellReuseIdentifier)
collectionView.dataSource = self
}

实现数据源方法

1
2
3
4
5
6
7
8
9
10
11
12
extension EmoticonViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 21 * 3
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kEmoticonCellReuseIdentifier, for: indexPath) as! EmoticonCell
cell.backgroundColor = (indexPath.item % 2 == 0) ? UIColor.red : UIColor.blue
return cell
}

}

image

加载数据模型准备

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import UIKit

/*
说明:
1、Emoticons.bundle的根目录下存放emoticons.plist保存了packages表情包信息
> package是一个数组,数组中存放的是字典
> 字典中的属性id对应的分组路径的名称
2、在id对应的目录下,各自都保存有info.plist
> group_name_cn 保存的是分组名称
> emoticons 保存的是表情信息数组
> code UNICODE编码字符串
> chs 表情文字,发送给新浪微博服务器的文本内容
> png 报请图片,在APP中进行图文混排使用的图片
*/


/// 表情包,存储每一组表情数据
class EmoticonPackage: NSObject {

/// 表情路径
var id:String?
/// 分组名称
var groupName:String?
/// 表情数组
var emoticons: [Emoticon]?

init(id: String) {
self.id = id
}

/// 加载表情包·数组·
class func packages() -> [EmoticonPackage] {
var list = [EmoticonPackage]()

// 读取emoticons.plist文件
let path = Bundle.main.path(forResource: "emoticons.plist", ofType: nil, inDirectory: "Emoticons.bundle")
let dict = NSDictionary(contentsOfFile: path!)!
let array = dict["packages"] as! [[String : AnyObject]]

// 遍历数组
for dic in array {
// 创建表情包,并初始化对应的路径
let package = EmoticonPackage(id: dic["id"] as! String)
// 加载表情数组
package.loadEmoticons()
// 添加表情包
list.append(package)
}
return list
}

/// 加载对应的表情数组
private func loadEmoticons() {
// 加载info.plist数据
let dict = NSDictionary(contentsOfFile: plistPath())!
// 设置分组名
groupName = dict["group_name_cn"] as? String
// 获取表情数组
let array = dict["emoticons"] as! [[String : String]]

// 实例化表情数组
emoticons = [Emoticon]()
// 遍历数组
for dic in array {
emoticons?.append(Emoticon(dict: dic))
}
}

/// 返回info.plist路径
private func plistPath() -> String {
return (EmoticonPackage.bundlePath().appendingPathComponent(id!) as NSString).appendingPathComponent("info.plist")
}

/// 返回表情包路径
private class func bundlePath() -> NSString {
return (Bundle.main.bundlePath as NSString).appendingPathComponent("Emoticons.bundle") as NSString
}

}

/// 表情模型
class Emoticon: NSObject {
/// 表情路径
var id:String?
/// 表情文字,发送给新浪微博服务器的文本内容
var chs:String?
/// 表情图片,在APP中进行图文混排的图片
var png:String?
/// UNICODE编码字符串
var code:String?

init(dict:[String : String]) {
super.init()
// 使用KVC设置属性之前,必须调用super.init()
setValuesForKeys(dict)
}

override func setValue(_ value: Any?, forUndefinedKey key: String) {

}

}

emoji字符扫描及转换原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 十六进制的字符串
let code = "0x2600"

// 扫描器,可以扫描指定字符串中特定文字
let scannser = Scanner(string: code)

// 扫描整数
var result: UInt32 = 0
scannser.scanHexInt32(&result)

print(result) // 9728

// 生成字符串
let str = "\(Character(UnicodeScalar(result)!))"
print(str) // ☀

那么在Emoticon中(注意点:需要设置id参数)

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
/// 表情模型
class Emoticon: NSObject {
/// 表情路径
var id:String?
/// 表情文字,发送给新浪微博服务器的文本内容
var chs:String?
/// 表情图片,在APP中进行图文混排的图片
var png:String?
/// UNICODE编码字符串
var code:String? {
didSet{
// 扫描器,可以扫描指定字符串中特定的文字
let scanner = Scanner(string: code!)
// 扫描整数
var result: UInt32 = 0
scanner.scanHexInt32(&result)
// 生成字符串
emoji = "\(Character(UnicodeScalar(result)!))"
}
}

/// emoji字符串
var emoji:String?

/// 图片的完整路径
var imagePath:String? {
return png == nil ? nil : (EmoticonPackage.bundlePath().appendingPathComponent(id!) as NSString).appendingPathComponent(png!)
}


init(id:String, dict:[String : String]) {
super.init()
self.id = id
// 使用KVC设置属性之前,必须调用super.init()
setValuesForKeys(dict)
}

override func setValue(_ value: Any?, forUndefinedKey key: String) {

}

}

显示图片和emoji

加载数据

1
lazy var packages: [EmoticonPackage] = EmoticonPackage.packages()

设置EmoticonCell数据

1
2
3
4
5
6
7
8
9
10
11
12
13
var emoticon: Emoticon? {
didSet{
// 设置图片
if let path = emoticon?.imagePath {
emoticonBtn.setImage(UIImage(named: path), for: UIControlState.normal)
} else {
// 防止cell的重用
emoticonBtn.setImage(nil, for: UIControlState.normal)
}
// 设置emoji,直接过滤调用cell重用的问题
emoticonBtn.setTitle(emoticon?.emoji ?? "", for: UIControlState.normal)
}
}

数据源方法修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension EmoticonViewController: UICollectionViewDataSource{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return packages.count
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return packages[section].emoticons?.count ?? 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kEmoticonCellReuseIdentifier, for: indexPath) as! EmoticonCell
cell.backgroundColor = (indexPath.item % 2 == 0) ? UIColor.red : UIColor.blue
cell.emoticon = packages[indexPath.section].emoticons![indexPath.item]
return cell
}

}

效果如下:
image

完善模型

由于零散点修改多,这里只贴出部分,具体参看EmoticonPackage.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 追加空白按钮,方便界面布局,如果一个界面的图标不足20个,补足,最后添加一个删除按钮
private func appendEmptyEmoticons() {
if emoticons == nil {
emoticons = [Emoticon]()
}
let count = emoticons!.count % 21
if count > 0 || emoticons!.count == 0 {
for _ in count..<20 {
// 追加空白按钮
emoticons?.append(Emoticon(isRemoveBtn: false))
}
// 追加一个删除按钮
emoticons?.append(Emoticon(isRemoveBtn: true))
}
}

在EmoticonViewController.swift中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var emoticon: Emoticon? {
didSet{
// 设置图片
if let path = emoticon?.imagePath {
emoticonBtn.setImage(UIImage(named: path), for: UIControlState.normal)
} else {
// 防止cell的重用
emoticonBtn.setImage(nil, for: UIControlState.normal)
}
// 设置emoji,直接过滤调用cell重用的问题
emoticonBtn.setTitle(emoticon?.emoji ?? "", for: UIControlState.normal)

// 设置删除按钮
if emoticon!.removeBtn {
emoticonBtn.setImage(UIImage(named: "compose_emoticon_delete"), for: UIControlState.normal)
emoticonBtn.setImage(UIImage(named: "compose_emoticon_delete_highlighted"), for: UIControlState.highlighted)
}
}
}

调整表情分组

1
2
3
4
5
func clickItem(item: UIBarButtonItem) {
print(item.tag)
let indexPath = NSIndexPath(item: 0, section: item.tag)
collectionView.scrollToItem(at: indexPath as IndexPath, at: UICollectionViewScrollPosition.left, animated: true)
}

插入emoji表情

定义一个闭包属性

1
2
3
4
5
6
7
8
/// 定义一个闭包属性,用于传递选中的表情模型
var emoticonDidSelectedCallBack: (_ emoticon: Emoticon)->()

// 初始化属性值
init(callBack:@escaping (_ emoticon: Emoticon)->()) {
self.emoticonDidSelectedCallBack = callBack
super.init(nibName: nil, bundle: nil)
}

修改懒加载collectionView

1
2
3
4
5
6
7
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: EmoticonLayout())
collectionView.register(EmoticonCell.self, forCellWithReuseIdentifier: kEmoticonCellReuseIdentifier)
collectionView.dataSource = self
collectionView.delegate = self
return collectionView
}()

实现UICollectionViewDelegate代理方法

1
2
3
4
5
6
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 获取表情模型
let emoticon = packages[indexPath.section].emoticons![indexPath.item]
// 执行闭包
emoticonDidSelectedCallBack(emoticon)
}

关闭cell中按钮的点击,否则与点击cell的事件冲突

1
2
3
4
5
6
7
8
override init(frame: CGRect) {
super.init(frame: frame)

contentView.addSubview(emoticonBtn)
emoticonBtn.frame = bounds.insetBy(dx: 4, dy: 4)
emoticonBtn.backgroundColor = UIColor.white
emoticonBtn.isUserInteractionEnabled = false
}

测试:

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
class DiscoverTableViewController: BaseViewController {

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(textView)
textView.xmg_AlignInner(type: XMG_AlignType.topCenter, referView: view, size: CGSize(width: 150, height: 50), offset: CGPoint(x: 0, y: navHeight+50))

addChildViewController(emojiController)
textView.inputView = emojiController.view

}

lazy var textView: UITextView = {
let view = UITextView()
view.text = "start"
view.backgroundColor = UIColor.red
return view
}()

// weak 相当于OC中的__weak,特点对象释放之后会将变量设置nil
// unowned相当于OC中的unsafe_unretained ,特点对象释放之后不会将变量设置nil
lazy var emojiController: EmoticonViewController = EmoticonViewController {
[unowned self]
(emoticon) in
if emoticon.chs != nil {
self.textView.replace(self.textView.selectedTextRange!, withText: emoticon.chs!)
}
}

}

效果如下:
image

插入图片表情

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
// weak 相当于OC中的__weak,特点对象释放之后会将变量设置nil
// unowned相当于OC中的unsafe_unretained ,特点对象释放之后不会将变量设置nil
lazy var emojiController: EmoticonViewController = EmoticonViewController {
[unowned self]
(emoticon) in
if emoticon.emoji != nil {
self.textView.replace(self.textView.selectedTextRange!, withText: emoticon.emoji!)
}

// 判断当前点击的是否是表情图片
if emoticon.png != nil {
// 创建附件
let attachment = NSTextAttachment()
attachment.image = UIImage(contentsOfFile: emoticon.imagePath!)
// 设置附件大小
attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)

// 根据附件创建属性字符串
let imageText = NSAttributedString(attachment: attachment)

// 拿到当前所有内容
let strM = NSMutableAttributedString(attributedString: self.textView.attributedText)

// 插入表情到当前光标所在的位置
let range = self.textView.selectedRange
strM.replaceCharacters(in: range, with: imageText)

// 属性字符串有自己默认的尺寸
strM.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 19), range: NSMakeRange(range.location, 1))

// 将替换后的字符串赋值给textView
self.textView.attributedText = strM

// 恢复光标所在的位置
// 两个参数:第一个是指定光标所在的位置,第二个是选中文本的个数
self.textView.selectedRange = NSMakeRange(range.location+1, 0)

}
}

image


更新时间:2018-05-19 10:48:31

简单的图文混排(测试)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

let attributed = NSMutableAttributedString()
let str1 = NSAttributedString(string: "我")

let attachment = NSTextAttachment()
attachment.image = UIImage(named: "preview_like_icon")
// 修改附件尺寸
attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)
let imageText = NSAttributedString(attachment: attachment)

let str2 = NSMutableAttributedString(string: "LOVE YOU")
str2.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSMakeRange(5, 3))

attributed.append(str1)
attributed.append(imageText)
attributed.append(str2)

self.customLabel.attributedText = attributed

}

效果:
image

获取发送文本

创建附件类

1
2
3
4
5
class EmoticonTextAttachment: NSTextAttachment {

/// 保存对应表情的文字
var chs:String?
}

NSTextAttachment替换为EmoticonTextAttachment

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
// weak 相当于OC中的__weak,特点对象释放之后会将变量设置nil
// unowned相当于OC中的unsafe_unretained ,特点对象释放之后不会将变量设置nil
lazy var emojiController: EmoticonViewController = EmoticonViewController {
[unowned self]
(emoticon) in
if emoticon.emoji != nil {
self.textView.replace(self.textView.selectedTextRange!, withText: emoticon.emoji!)
}

// 判断当前点击的是否是表情图片
if emoticon.png != nil {
// 创建附件
// let attachment = NSTextAttachment()
let attachment = EmoticonTextAttachment()
attachment.chs = emoticon.chs
attachment.image = UIImage(contentsOfFile: emoticon.imagePath!)
// 设置附件大小
attachment.bounds = CGRect(x: 0, y: -4, width: 20, height: 20)

// 根据附件创建属性字符串
let imageText = NSAttributedString(attachment: attachment)

// 拿到当前所有内容
let strM = NSMutableAttributedString(attributedString: self.textView.attributedText)

// 插入表情到当前光标所在的位置
let range = self.textView.selectedRange
strM.replaceCharacters(in: range, with: imageText)

// 属性字符串有自己默认的尺寸
strM.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 19), range: NSMakeRange(range.location, 1))

// 将替换后的字符串赋值给textView
self.textView.attributedText = strM

// 恢复光标所在的位置
// 两个参数:第一个是指定光标所在的位置,第二个是选中文本的个数
self.textView.selectedRange = NSMakeRange(range.location+1, 0)

}
}

获取发送文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var strM = String()
// 后台需要发送给服务器的数据
self.textView.attributedText.enumerateAttributes(in: NSMakeRange(0, self.textView.attributedText.length), options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (objc, range, _) in

// 遍历的时候传递给我们的objc是一个字典,如果字典中的NSAttachment这个可以有值,那么久证明当前是一个图片
print(objc["NSAttachment"] as Any)

// range就是春字符串的范围,如果春字符串中间有图片表情,那么range就会传递多次
print(range)
let les = (self.textView.text as NSString).substring(with: range)
print(les)

if objc["NSAttachment"] != nil {
// 图片
let attachment = objc["NSAttachment"] as! EmoticonTextAttachment
strM += attachment.chs!
} else {
// 文字
strM += (self.textView.text as NSString).substring(with: range)
}

}
print(strM)
// shshasdaHAH双手合十[馋嘴][馋嘴][馋嘴][馋嘴][馋嘴][馋嘴][馋嘴][可爱]

重构

传入表情模型,获取图片文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class EmoticonTextAttachment: NSTextAttachment {

/// 保存对应表情的文字
var chs:String?

class func imageText(emoticon:Emoticon, font:CGFloat) -> NSAttributedString {
// 创建附件
let attachment = EmoticonTextAttachment()
attachment.chs = emoticon.chs
attachment.image = UIImage(contentsOfFile: emoticon.imagePath!)
// 设置附件大小
attachment.bounds = CGRect(x: 0, y: -4, width: font, height: font)
// 根据附件创建属性字符串
return NSAttributedString(attachment: attachment)
}
}

扩展UITextView

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
55
extension UITextView {
/// 插入表情
func insertEmoticon(emoticon:Emoticon, font:CGFloat) {
if emoticon.emoji != nil {
self.replace(self.selectedTextRange!, withText: emoticon.emoji!)
}

// 判断当前点击的是否是表情图片
if emoticon.png != nil {

// 根据附件创建属性字符串
let imageText = EmoticonTextAttachment.imageText(emoticon: emoticon, font: font)

// 拿到当前所有内容
let strM = NSMutableAttributedString(attributedString: self.attributedText)

// 插入表情到当前光标所在的位置
let range = self.selectedRange
strM.replaceCharacters(in: range, with: imageText)

// 属性字符串有自己默认的尺寸
strM.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 19), range: NSMakeRange(range.location, 1))

// 将替换后的字符串赋值给textView
self.attributedText = strM

// 恢复光标所在的位置
// 两个参数:第一个是指定光标所在的位置,第二个是选中文本的个数
self.selectedRange = NSMakeRange(range.location+1, 0)

}
}

func emoticonAttributeText() -> String {
var strM = String()
// 后台需要发送给服务器的数据
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: NSAttributedString.EnumerationOptions(rawValue: 0)) { (objc, range, _) in

// 遍历的时候传递给我们的objc是一个字典,如果字典中的NSAttachment这个可以有值,那么久证明当前是一个图片

// range就是春字符串的范围,如果春字符串中间有图片表情,那么range就会传递多次

if objc["NSAttachment"] != nil {
// 图片
let attachment = objc["NSAttachment"] as! EmoticonTextAttachment
strM += attachment.chs!
} else {
// 文字
strM += (self.text as NSString).substring(with: range)
}

}
return strM
}
}

删除按钮处理

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
/// 插入表情
func insertEmoticon(emoticon:Emoticon, font:CGFloat) {
// 处理删除按钮
if emoticon.removeBtn {
deleteBackward()
}

// 判断当前点击是否是emoji表情
if emoticon.emoji != nil {
self.replace(self.selectedTextRange!, withText: emoticon.emoji!)
}

// 判断当前点击的是否是表情图片
if emoticon.png != nil {

// 根据附件创建属性字符串
let imageText = EmoticonTextAttachment.imageText(emoticon: emoticon, font: font)

// 拿到当前所有内容
let strM = NSMutableAttributedString(attributedString: self.attributedText)

// 插入表情到当前光标所在的位置
let range = self.selectedRange
strM.replaceCharacters(in: range, with: imageText)

// 属性字符串有自己默认的尺寸
strM.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 19), range: NSMakeRange(range.location, 1))

// 将替换后的字符串赋值给textView
self.attributedText = strM

// 恢复光标所在的位置
// 两个参数:第一个是指定光标所在的位置,第二个是选中文本的个数
self.selectedRange = NSMakeRange(range.location+1, 0)

}
}

处理最近表情

在Emoticon类中声明属性记录使用次数

1
2
/// 记录当前表情被使用次数
var times:Int = 0

在EmoticonPackage类中添加方法用来添加表情

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
/// 用于添加最近表情
func appendEmoticons(emoticon: Emoticon) {
// 判断是否是删除按钮
if emoticon.removeBtn {
return
}

// 判断当前点击的表情是否已经添加到最近数组中
let contains = emoticons!.contains(emoticon)
if !contains {
// 删除删除按钮
emoticons?.removeLast()
emoticons?.append(emoticon)
}

// 对数组进行排序
var result = emoticons?.sorted(by: { (e1, e2) -> Bool in
return e1.times > e2.times
})

// 删除多余的表情
if !contains {
result?.removeLast()
result?.append(Emoticon(isRemoveBtn: true))
}

emoticons = result
}

在EmoticonViewController类中点击表情时候进行记录存储

1
2
3
4
5
6
7
8
9
10
11
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 获取表情模型
let emoticon = packages[indexPath.section].emoticons![indexPath.item]

// 处理最近表情,将当前使用的表情添加到最近表情的数组中
emoticon.times += 1
packages[0].appendEmoticons(emoticon: emoticon)

// 执行闭包
emoticonDidSelectedCallBack(emoticon)
}

更新时间:2018-05-19 16:00:31

布局发送微博界面

创建ComposeViewController.swift

设置导航栏按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private func setupNav() {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: UIBarButtonItemStyle.plain, target: self, action: #selector(close))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "发送", style: UIBarButtonItemStyle.plain, target: self, action: #selector(send))
navigationItem.rightBarButtonItem?.isEnabled = false

// 中间视图
let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 32))
let label = UILabel()
label.text = "发送微博"
label.font = UIFont.systemFont(ofSize: 15)
label.sizeToFit()
titleView.addSubview(label)

let label1 = UILabel()
label1.text = UserAccount.loadAccount()?.screen_name
label1.font = UIFont.systemFont(ofSize: 15)
label1.textColor = UIColor.darkGray
label1.sizeToFit()
titleView.addSubview(label1)

label.xmg_AlignInner(type: XMG_AlignType.topCenter, referView: titleView, size: nil)
label1.xmg_AlignInner(type: XMG_AlignType.bottomCenter, referView: titleView, size: nil)
navigationItem.titleView = titleView
}

设置输入框

1
2
3
4
5
6
7
8
private func setupInputView() {
view.addSubview(textView)
textView.addSubview(placeholderLabel)

// 布局子控件
textView.xmg_Fill(view)
placeholderLabel.xmg_AlignInner(type: XMG_AlignType.topLeft, referView: textView, size: nil, offset: CGPoint(x: 5, y: 8))
}

监听文本输入变化,改变按钮是否可以点击状态

1
2
3
4
5
6
7
extension ComposeViewController: UITextViewDelegate {

func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = textView.hasText
navigationItem.rightBarButtonItem?.isEnabled = textView.hasText
}
}

点击tabbar按钮,跳转ComposeViewController页面

1
2
3
4
5
6
7
8
9
10
/// 监听加好按钮点击,注意:监听按钮点击方法不能私有
/// 按钮点击事件的调用是由 运行循环 监听并且以消息机制传递的,因此,按钮监听函数不能设置为fileprivate
func composeBtnClick() {
print(#function)
print("点击按钮")
let composeVC = ComposeViewController()
let nav = UINavigationController(rootViewController: composeVC)
present(nav, animated: true, completion: nil)

}

发送微博

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func send() {
SVProgressHUD.show(withStatus: "正在发送。。。")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
let path = "2/statuses/update.json"
let params = ["access_token": UserAccount.loadAccount()?.access_token, "status":textView.text]
NetworkTools.shareNetworkTools().post(path, parameters: params, progress: nil, success: { (_, json) in

SVProgressHUD.dismiss()
// 提示用户发送成功
SVProgressHUD.showSuccess(withStatus: "发送成功")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

// 关闭页面
self.close()
}) { (_, error) in
print(error)
// 提示发送失败
SVProgressHUD.showSuccess(withStatus: "发送失败")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
}
}

继承表情键盘

声明键盘属性

1
2
3
4
5
/// 表情键盘
lazy var emoticonKeyboradVC: EmoticonViewController = EmoticonViewController { [unowned self] (emoticon) in
self.textView.insertEmoticon(emoticon: emoticon)
self.textViewDidChange(self.textView)
}

添加键盘控制器

1
2
// 添加子控制器
addChildViewController(emoticonKeyboradVC)

切换键盘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// 切换表情键盘
func inputEmoticon() {
// 如果输入视图是nul,说明使用的是系统键盘
print(textView.inputView)

// 要切换键盘之前,需要先关闭键盘
textView.resignFirstResponder()

// 更换键盘输入视图
textView.inputView = (textView.inputView == nil) ? emoticonKeyboradVC.view : nil

// 重新设置检点
textView.becomeFirstResponder()
}

效果:
image


更新时间:2018-05-21 10:04:56

创建浏览器&布局

懒加载三个属性

1
2
3
4
// MARK: - 懒加载
private lazy var layout = UICollectionViewFlowLayout()
private lazy var collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.layout)
private lazy var photos = [UIImage]()

创建cell

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
55
56
57
class PhotoSelectorCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)

setupUI()
}

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

private func setupUI() {
contentView.addSubview(photoBtn)
contentView.addSubview(removeBtn)

photoBtn.translatesAutoresizingMaskIntoConstraints = false
removeBtn.translatesAutoresizingMaskIntoConstraints = false
var cons = [NSLayoutConstraint]()
// 背景
cons += NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[photoButton]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["photoButton": photoBtn])
cons += NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[photoButton]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["photoButton": photoBtn])

// 删除按钮
cons += NSLayoutConstraint.constraints(withVisualFormat: "H:[removeButton]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["removeButton":removeBtn])
cons += NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[removeButton]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["removeButton":removeBtn])

contentView.addConstraints(cons)

// 设置监听方法
photoBtn.addTarget(self, action: #selector(selectPhoto), for: UIControlEvents.touchUpOutside)
removeBtn.addTarget(self, action: #selector(removePhoto), for: UIControlEvents.touchUpOutside)
}



func selectPhoto() {

}

func removePhoto() {

}

// MARK: - 懒加载
private lazy var photoBtn = PhotoSelectorCell.createBtn(imageName: "compose_ic_add")
private lazy var removeBtn = PhotoSelectorCell.createBtn(imageName: "compose_ic_close")



class func createBtn(imageName:String) -> UIButton {
let button = UIButton()
button.setImage(UIImage(named: imageName), for: UIControlState.normal)
button.setImage(UIImage(named: imageName + "_highlighted"), for: UIControlState.highlighted)
return button
}

}

PhotoSelectorController初始化UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private func setupUI() {

view.addSubview(collectionView)

// 设置layout
layout.itemSize = CGSize(width: 80, height: 80)
layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10)

// 设置collectionView
collectionView.backgroundColor = UIColor.lightGray
collectionView.register(PhotoSelectorCell.self, forCellWithReuseIdentifier: kPhotoSelectorCellReuseIdentifier)
collectionView.dataSource = self

// 设置布局
collectionView.translatesAutoresizingMaskIntoConstraints = false
var cons = [NSLayoutConstraint]()
cons += NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[collectionView]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["collectionView": collectionView])
cons += NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[collectionView]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["collectionView": collectionView])
view.addConstraints(cons)
}

实现数据源方法

1
2
3
4
5
6
7
8
9
10
11
extension PhotoSelectorController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPhotoSelectorCellReuseIdentifier, for: indexPath)
cell.backgroundColor = UIColor.randomColor()
return cell
}
}

显示图片浏览器

创建代理

1
2
3
4
5
6
7
@objc
protocol PhotoSelectorCellDelegate: NSObjectProtocol {
/// 要选择照片
@objc optional func photoSelectorSelectPhoto(cell: PhotoSelectorCell)
/// 要删除照片
@objc optional func photoSelectorRemovePhoto(cell: PhotoSelectorCell)
}

协议的某些属性或方法是可选实现的。遵循协议的类,就可以灵活实现相关属性和方法了。协议中使用optional关键字作为前缀定义可选类型。因为要跟OC打交道,所以协议以及属性、方法前面都要加上@objc。那么也推导出只有OC类型的类或者是@objc类遵循,其他类以及结构体和枚举都不能遵循这种协议

代理属性

1
2
/// 定义代理
weak var delegate: PhotoSelectorCellDelegate?

使用

1
2
3
4
5
6
7
8
func selectPhoto() {

delegate?.photoSelectorSelectPhoto!(cell: self)
}

func removePhoto() {
delegate?.photoSelectorRemovePhoto!(cell: self)
}

实现PhotoSelectorCellDelegate

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
extension PhotoSelectorController: UICollectionViewDataSource, PhotoSelectorCellDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPhotoSelectorCellReuseIdentifier, for: indexPath) as! PhotoSelectorCell
cell.backgroundColor = UIColor.randomColor()
cell.delegate = self
return cell
}

// MARK: - PhotoSelectorCellDelegate
func photoSelectorSelectPhoto(cell: PhotoSelectorCell) {

/*
UIIimagePickerController专门用来选择照片的
PhotoLibrary : 照片库(所有的照片,拍照&用iTunes & photo 同步的照片,不能删除)
SavePPhotosAlbum 相册(自助机拍摄的,可以删除)
Camera 相机
*/

if !UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
// 没有授权
return
}

let vc = UIImagePickerController()
vc.delegate = self

// 允许编辑 - 如果上传头像,记得把这个属性打开
// 能够建立一个正方形的图像,方便后续的头像处理
// vc.allowsEditing = true

present(vc, animated: true, completion: nil)

}

func photoSelectorRemovePhoto(cell: PhotoSelectorCell) {

}

// MARK: - UIImagePickerControllerDelegate, UINavigationControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// 提示:所有关于相册的处理,都需要处理内存
print(info)
// 获取图片
print(info[UIImagePickerControllerOriginalImage] as Any)

// 关闭
dismiss(animated: true, completion: nil)
}
}

显示照片

在PhotoSelectorCell中添加图片属性

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不能解包可选项 -> 将nil使用了惊叹号
var image:UIImage? {
didSet{
if image != nil {
photoBtn.setImage(image, for: UIControlState.normal)
} else {
photoBtn.setImage(UIImage(named: "compose_pic_add"), for: UIControlState.normal)
photoBtn.setImage(UIImage(named: "compose_pic_add_highlighted"), for: UIControlState.highlighted)
}

removeBtn.isHidden = (image == nil)
}
}

数据源方法&cell代理方法相关修改

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
55
56
57
58
59
60
61
62
63
64
65
66
    extension PhotoSelectorController: UICollectionViewDataSource, PhotoSelectorCellDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photos.count + 1
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPhotoSelectorCellReuseIdentifier, for: indexPath) as! PhotoSelectorCell
cell.backgroundColor = UIColor.randomColor()
cell.delegate = self
// 设置图片
cell.image = indexPath.item < photos.count ? photos[indexPath.item] : nil
return cell
}

// MARK: - PhotoSelectorCellDelegate
func photoSelectorSelectPhoto(cell: PhotoSelectorCell) {

/*
UIIimagePickerController专门用来选择照片的
PhotoLibrary : 照片库(所有的照片,拍照&用iTunes & photo 同步的照片,不能删除)
SavePPhotosAlbum 相册(自助机拍摄的,可以删除)
Camera 相机
*/

if !UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
// 没有授权
return
}

let vc = UIImagePickerController()
vc.delegate = self

// 允许编辑 - 如果上传头像,记得把这个属性打开
// 能够建立一个正方形的图像,方便后续的头像处理
// vc.allowsEditing = true

present(vc, animated: true, completion: nil)

}

func photoSelectorRemovePhoto(cell: PhotoSelectorCell) {
// indexpatch
let indexPath = collectionView.indexPath(for: cell)

// 删除照片
photos.remove(at: indexPath!.item)

// 更新
collectionView.reloadData()
}

// MARK: - UIImagePickerControllerDelegate, UINavigationControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// 提示:所有关于相册的处理,都需要处理内存
// print(info)
// 获取图片
// print(info[UIImagePickerControllerOriginalImage] as Any)

// 将图片保存到数组中
if photos.count < kMaxPhotoSelectCount - 1 {
photos.append(info[UIImagePickerControllerOriginalImage] as! UIImage)
collectionView.reloadData()
}
// 关闭
dismiss(animated: true, completion: nil)
}
}

image

内存问题处理

UIImageJPEGRepresentation

使用UIImageJPEGRepresentation(, )来压缩图片会严重影响图片质量

  • 解压缩性能消耗太大,苹果不推荐
  • JPEG压缩图片是不保真的,压缩后可能很难看

写入到本地

1
2
3
4
5
6
7
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
let data1 = UIImageJPEGRepresentation(image, 1.0)
do {
try data1?.write(to: URL(string: "Users/Daisuke/Desktop/1.jpg")!, options: Data.WritingOptions.atomic)
} catch {
print("捕捉到异常")
}

内存警告

  • 关于应用程序内存,UI的APP空的程序运行占用20M左右,一般程序消耗100M以内都是可以接受的
  • 内存飙升到500M接受到第一次内存警告,内存释放后的结果120M,程序仍然能够正常运行

缩放图片

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
extension UIImage {

/// 将图片缩放到指定宽度
func scaleImageToWidth(width:CGFloat) -> UIImage {
// 提示:不要使用比例,如果所有照片都按照指定比例来缩放,图片太小就看不见了
// 判断宽度,如果小于指定宽度直接返回对象
if size.width < width {
return self
}

// 计算等比例缩放的高度
let height = width * size.height / size.width

// 图像的上下文
let sizeContext = CGSize(width: width, height: height)
UIGraphicsBeginImageContext(sizeContext)
// 在指定区域中缩放绘制完整图像
draw(in: CGRect(origin: CGPoint.zero, size: sizeContext))
// 获取绘制结果
let result = UIGraphicsGetImageFromCurrentImageContext()
// 关闭上下文
UIGraphicsEndImageContext()
return result!
}
}

使用

1
2
3
4
5
6
7
// 将图片保存到数组中
if photos.count < kMaxPhotoSelectCount - 1 {
var image = info[UIImagePickerControllerOriginalImage] as! UIImage
image = image.scaleImageToWidth(width: 300)
photos.append(image)
collectionView.reloadData()
}

更新时间:2018-05-21 15:25:27

正则表达式

NSRegularExpression

  • NSRegularExpressionCaseInsensitive = 1 << 0, 忽略大小写
  • NSRegularExpressionAllowCommentsAndWhitespace = 1 << 1, 忽略空白字符,以及前缀是 # 开始的注释
  • NSRegularExpressionIgnoreMetacharacters = 1 << 2, 将整个匹配方案作为文字字符串
  • NSRegularExpressionDotMatchesLineSeparators = 1 << 3, 允许 . 匹配任意字符,包括回车换行
  • NSRegularExpressionAnchorsMatchLines = 1 << 4, 允许 ^ 和 $ 匹配多行文本的开始和结尾
  • NSRegularExpressionUseUnixLineSeparators = 1 << 5, 仅将 \n 作为换行符
  • NSRegularExpressionUseUnicodeWordBoundaries = 1 << 6 使用 Unicode TR#29 指定单词边界

方法介绍

  • open func numberOfMatches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> Int(返回正确匹配的个数)
  • open func firstMatch(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> NSTextCheckingResult?(返回第一个匹配的结果)
  • open func rangeOfFirstMatch(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> NSRange(返回第一个正确匹配结果字符串的NSRange)
  • open func matches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> [NSTextCheckingResult](返回所有匹配结果的集合(适合从一段字符串中提取我们想要匹配的所有数据))

练习匹配微博正文

匹配URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func urlRegex(str:String) {
do {
// 创建匹配对象
/*
NSDataDetector:是NSRegularExpression的子类,里面能够匹配URL,电话,日期,地址
*/

let dataDetector = try NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingResult.CheckingType.link.rawValue))

// 匹配所有的URL
let array = dataDetector.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count))
if array.count > 0 {
for checking in array {
print((str as NSString).substring(with: checking.range))
}
}

} catch {
print(error)
}
}

匹配表情、用户名、话题、URL

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 contentRegex(str:String) {
do {
/*
- . 匹配任意字符,除了换行
- * 匹配任意长度的内容
- ? 尽量少的匹配
*/

// 1.创建规则
// 1.1表情规则
let emoticonPattern = "\\[.*?\\]"
// 1.2用户名规则
let atPattern = "@.*?:"
// 1.3话题规则
let topicPattern = "#.*?#"
// 1.4url规则
let urlPattern = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?"
let pattern = emoticonPattern + "|" + atPattern + "|" + topicPattern + "|" + urlPattern

// 创建正则表达式对象
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options(rawValue: 0))
// 开始匹配
let array = regex.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count))
if array.count > 0 {
for checking in array {
print((str as NSString).substring(with: checking.range))
}
}

} catch {
print(error)
}
}

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let str = "@jack12:【动物尖叫合辑】#肥猪流#猫头鹰这么尖叫[偷笑]、@南哥: 老鼠这么尖叫、兔子这么尖叫[吃惊]、@花满楼: 莫名奇#小笼包#妙的笑到最后[挖鼻屎]!~ http://t.cn/zYBuKZ8"
urlRegex(str: str) // http://t.cn/zYBuKZ8
contentRegex(str: str)
/*
@jack12:
#肥猪流#
[偷笑]
@南哥:
[吃惊]
@花满楼:
#小笼包#
[挖鼻屎]
http://t.cn/zYBuKZ8
*/

}

生成表情文字

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
func emoticonRegex(str:String) {
var strM = NSMutableAttributedString(string: str)

do {
// 创建规则
let pattern = "\\[.*?\\]"
// 创建正则对象
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options(rawValue: 0))
// 开始匹配
let array = regex.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count))
// 遍历结果数组
var count = array.count
while count > 0 {
// 拿到匹配结果
count -= 1
let res = array[count]
// 拿到range
let range = res.range
// 拿到匹配的字符串
let tempStr = (str as NSString).substring(with: range)
// 查找模型
if let emoticon = emotcionWithStr(str: tempStr){
// 生成属性字符串
let attstr = EmoticonTextAttachment.imageText(emoticon: emoticon, font: UIFont.systemFont(ofSize: 17))
// 替换表情
strM.replaceCharacters(in: range, with: attstr)
}
print(strM)
self.customLabel.attributedText = strM
}

} catch {
print(error)
}

}

/// 根据指定字符串获得表情模型
func emotcionWithStr(str:String) -> Emoticon? {
var emoticon:Emoticon?
for package in EmoticonPackage.packages() {
emoticon = package.emoticons?.filter({ (emo) -> Bool in
return emo.chs == str
}).last
if emoticon != nil {
break
}
}
return emoticon
}

测试效果:

1
emoticonRegex(str: "我[爱你]好久了[好爱哦]")

image

生成表情文字重构

在EmoticonPackage.swift中把生成表情字符串的方法封装在里面

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
static let packageList: [EmoticonPackage] = EmoticonPackage.packages()
class func emoticonRegex(str:String) ->NSAttributedString?{
let strM = NSMutableAttributedString(string: str)

do {
// 创建规则
let pattern = "\\[.*?\\]"
// 创建正则对象
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options(rawValue: 0))
// 开始匹配
let array = regex.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count))
// 遍历结果数组
var count = array.count
while count > 0 {
// 拿到匹配结果
count -= 1
let res = array[count]
// 拿到range
let range = res.range
// 拿到匹配的字符串
let tempStr = (str as NSString).substring(with: range)
// 查找模型
if let emoticon = emotcionWithStr(str: tempStr){
// 生成属性字符串
let attstr = EmoticonTextAttachment.imageText(emoticon: emoticon, font: UIFont.systemFont(ofSize: 17))
// 替换表情
strM.replaceCharacters(in: range, with: attstr)
}
}
return strM
} catch {
print(error)
return nil
}
}

/// 根据指定字符串获得表情模型
class func emotcionWithStr(str:String) -> Emoticon? {
var emoticon:Emoticon?
for package in EmoticonPackage.packageList {
emoticon = package.emoticons?.filter({ (emo) -> Bool in
return emo.chs == str
}).last
if emoticon != nil {
break
}
}
return emoticon
}

更新时间:2018-05-22 14:42:22

TextKit基本使用

初衷:创建一个文本控件,自动识别URL并可以点击。

创建DDLabel继承与UILabel

声明三个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MARK: - 懒加载
/*
只要textStorage中的内容发生变化,就可以通知layoutManager重新布局
layoutManager重新布局需要知道绘制到什么地方,所以layoutManager就会找textContainer绘制的区域
*/


// 专门用于存储内容的。textStorage 中有 layoutManager
private lazy var textStorage = NSTextStorage()

// 专门用于管理布局的。layoutManager 中有 textContainer
private lazy var layoutManager = NSLayoutManager()

// 专门用于指定绘制的区域
private lazy var textContainer = NSTextContainer()

重写初始化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override init(frame: CGRect) {
super.init(frame: frame)
setupSystem()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupSystem()
}

private func setupSystem() {
// 1、将layoutManager添加到textStorage中
textStorage.addLayoutManager(layoutManager)

// 2、将textContainer添加到layoutManager
layoutManager.addTextContainer(textContainer)
}

重写布局方法

1
2
3
4
5
6
override func layoutSubviews() {
super.layoutSubviews()

// 3、指定区域
textContainer.size = bounds.size
}

重写绘制方法

1
2
3
4
5
6
7
8
9
10
/// 如果UILabel调用setNeedsDisplay方法,系统会触发drawText方法
override func drawText(in rect: CGRect) {
/*
重绘字形,理解为一个小的UIView

forGlyphRange: 指定绘制的范围
at:指定从什么位置开始绘制
*/

layoutManager.drawGlyphs(forGlyphRange: NSMakeRange(0, text!.count), at: CGPoint.zero)
}

重写text属性

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
override var text: String?{
didSet{
// 1、修改textStorage存储的内容
textStorage.setAttributedString(NSAttributedString(string: text!))

// 2、设置textStorage的属性
textStorage.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, text!.count))

// 处理URL
self.URLRegex()

// 通知layoutManager重新布局
setNeedsDisplay()
}
}

/// 匹配URL
func URLRegex() {
do {
// 创建正则表达式对象
let dataDerector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
// 匹配
let resArray = dataDerector.matches(in: textStorage.string, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, textStorage.string.count))

// 取出结果
for checkingRes in resArray {
let str = (textStorage.string as NSString).substring(with: checkingRes.range)
let tempStr = NSMutableAttributedString(string: str)

// 设置属性
tempStr.addAttributes([NSFontAttributeName : font!, NSForegroundColorAttributeName : UIColor.red], range: NSMakeRange(0, str.count))

textStorage.replaceCharacters(in: checkingRes.range, with: tempStr)
}
} catch {
print(error)
}
}

测试

image

键盘变化处理

添加键盘通知

1
2
// 注册通知监听键盘
NotificationCenter.default.addObserver(self, selector: #selector(keyboardChange(notify:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)

注销通知

1
2
3
4
deinit {
// 注销通知
NotificationCenter.default.removeObserver(self)
}

keyboardChange方法

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
func keyboardChange(notify: Notification) {
// 获取最终的frame - oc中将结构体保存在字典中,存成NSValue
let value = notify.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
let rect = value.cgRectValue

let height = UIScreen.main.bounds.height
toolbarBottomCons?.constant = -(height - rect.origin.y)

// 更新页面
let duration = notify.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! NSNumber

/*
工具条回弹是因为执行了两次动画,而系统自带的键盘的动画节奏(曲线) 7
7 在Apple API中并没有提供我们,但是我们可以用7 这种节奏的特点
:如果连续执行两次动画,不管上一次有没有执行完毕,都会立刻执行下一次,也就是说上一次可能会被忽略

如果将动画节奏设置为7,那么动画的时长无论如何都会自动修改为0.5
UIView动画的本质是核心动画,所以可以给核心动画设置动画节奏
*/


// 取出键盘的动画节奏
let curve = notify.userInfo![UIKeyboardAnimationCurveUserInfoKey] as! NSNumber

UIView.animate(withDuration: duration.doubleValue) {
// 设置动画节奏
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: curve.intValue)!)
self.view.layoutIfNeeded()
}
}

集成图片选择器

懒加载

1
2
/// 图片选择器
private lazy var photoSelectorVC:PhotoSelectorController = PhotoSelectorController()

添加photoSelectorVC

1
addChildViewController(photoSelectorVC)

初始化

1
2
3
4
5
6
7
8
9
10
11
private func setupPhotoView(){
view.insertSubview(photoSelectorVC.view, belowSubview: toolbar)

// 布局
let size = UIScreen.main.bounds.size
let width = size.width
let height:CGFloat = 0
let cons = photoSelectorVC.view.xmg_AlignInner(type: XMG_AlignType.bottomLeft, referView: view, size: CGSize(width: width, height: height))
photoViewHeightCons = photoSelectorVC.view.xmg_Constraint(cons, attribute: NSLayoutAttribute.height)

}

工具条事件

1
2
3
4
5
6
7
8
/// 切换图片选择器
func selectPicture() {
// 关闭键盘
textView.resignFirstResponder()

// 调整图片选择器的高度
photoViewHeightCons?.constant = UIScreen.main.bounds.height * 0.6
}

发送图片微博

发送图片微博

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
func send() {
SVProgressHUD.show(withStatus: "正在发送。。。")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

let params = ["access_token": UserAccount.loadAccount()?.access_token, "status":textView.emoticonAttributeText()]
if let image = photoSelectorVC.photos.first {
// 发送图片微博

let path = "2/statuses/upload.json"
NetworkTools.shareNetworkTools().post(path, parameters: params, constructingBodyWith: { (formData) in
// 将数据转为二进制
let data = UIImagePNGRepresentation(image)

// 上传数据
/*
第一个参数:需要上传的二进制数据
第二个参数:服务器对应哪个字段名称
第三个参数:文件的名称(大部分服务器可以随便写)
第四个参数:数据类型,通用类型application/octet-stream
*/

formData.appendPart(withFileData: data!, name: "pic", fileName: "pic.png", mimeType: "application/octet-stream")

}, progress: nil, success: { [unowned self] (_, json) in
SVProgressHUD.dismiss()
// 提示用户发送成功
SVProgressHUD.showSuccess(withStatus: "发送成功")
// 关闭发送页面
self.close()
}) { (_, error) in
print(error)
SVProgressHUD.showError(withStatus: "发送失败")
}

} else {

let path = "2/statuses/update.json"
NetworkTools.shareNetworkTools().post(path, parameters: params, progress: nil, success: { (_, json) in
SVProgressHUD.dismiss()
// 提示用户发送成功
SVProgressHUD.showSuccess(withStatus: "发送成功")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

// 关闭页面
self.close()
}) { (_, error) in
print(error)
// 提示发送失败
SVProgressHUD.showSuccess(withStatus: "发送失败")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
}
}

}

重构发送图片微博

在NetworkTools.swift类中

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
func sendStatus(text:String, image:UIImage?, successCallBack:@escaping (_ status: Status) ->(), errorCallBack:@escaping (_ error: Error)->()) {
var path = "2/statuses/"
let params = ["access_token": UserAccount.loadAccount()?.access_token, "status":text]
if image != nil {
// 发送图片微博
path += "upload.json"
post(path, parameters: params, constructingBodyWith: { (formData) in
// 将数据转为二进制
let data = UIImagePNGRepresentation(image!)

// 上传数据
/*
第一个参数:需要上传的二进制数据
第二个参数:服务器对应哪个字段名称
第三个参数:文件的名称(大部分服务器可以随便写)
第四个参数:数据类型,通用类型application/octet-stream
*/

formData.appendPart(withFileData: data!, name: "pic", fileName: "pic.png", mimeType: "application/octet-stream")

}, progress: nil, success: { (_, json) in

let status = Status(dict: (json as! [String : AnyObject]))
successCallBack(status)
}) { (_, error) in
errorCallBack(error)
}

} else {
path += "update.json"
post(path, parameters: params, progress: nil, success: { (_, json) in
let status = Status(dict: (json as! [String : AnyObject]))
successCallBack(status)
}) { (_, error) in
errorCallBack(error as NSError )
}
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func send() {
let text = textView.emoticonAttributeText()
let image = photoSelectorVC.photos.first

SVProgressHUD.show(withStatus: "正在发送。。。")
SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
NetworkTools.shareNetworkTools().sendStatus(text: text, image: image, successCallBack: { (status) in
// 提示用户发送成功
SVProgressHUD.showSuccess(withStatus: "发送成功")
}) { (error) in
print(error)
SVProgressHUD.showError(withStatus: "发送失败")
}
}

发布页面字数提醒

1、提示控件

1
lazy var tipLabel: UILabel = UILabel()

2、添加到视图,并布局

1
2
3
view.addSubview(tipLabel)

tipLabel.xmg_AlignVertical(type: XMG_AlignType.topRight, referView: toolbar, size: nil, offset: CGPoint(x: -10, y: -10))

3、设置提示数据

1
2
3
4
5
6
7
8
9
10
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = textView.hasText
navigationItem.rightBarButtonItem?.isEnabled = textView.hasText

// 当已经输入的内容长度
let count = textView.emoticonAttributeText().count
let res = maxTipLength - count
tipLabel.textColor = (res > 0) ? UIColor.darkGray : UIColor.red
tipLabel.text = res == maxTipLength ? "" : "\(res)"
}

显示首页表情

在StatusTableViewCell.swift中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var status:Status? {
didSet{
topView.status = status

// contentLabel.text = status?.text
contentLabel.attributedText = EmoticonPackage.emoticonRegex(str: status?.text ?? "")

// 设置配图数据
pictureView.status = status?.retweeted_status != nil ? status?.retweeted_status : status

// 设置配图尺寸
let size = pictureView.calculateImageSize()
pictureWidthCons?.constant = size.width
pictureHeightCons?.constant = size.height
pictureTopCons?.constant = size.height == 0 ? 0 : 10
}
}

更新时间:2018-05-23 10:28:27

SQLite基本使用

  • 创建数据库管理类
1
2
3
4
5
private static let manager:SQLiteManager = SQLiteManager()
/// 单例
class func share() ->SQLiteManager{
return manager
}
  • 打开数据库
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
/// 打开数据库, SQLiteName数据路名称
func openDB(SQLiteName:String) {
// 0、拿到数据库的路径
let path = SQLiteName.docDir()
print(path)
let cPath = path.cString(using: String.Encoding.utf8)

// 1、打开数据库
/*
1、需要打开的数据库文件的路径,C语言字符串
2、打开之后的数据库对象(指针),以后所有的数据库操作,都必须拿到这个指针才能进行相关操作

open方法特点:
1、如果指定路径对应的数据库文件已存在,就会直接打开
2、如果指定路径对应的数据库文件不存在,就会创建一个新的
*/

if sqlite3_open(cPath, &db) != SQLITE_OK {
print("打开数据库失败")
return
}

// 创建表
if createTable() {
print("创建表成功")
} else {
print("创建表失败")
}
}
  • 创建表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func createTable() -> Bool {
// 1、编写SQL语句
// 建议:在开发中编写SQL语句,如果语句过长,不要写在一行
// 开发技巧:在做数据库开发是,如果遇到错误,可以先将SQL打印出来,拷贝到PC工具中验证之后再进行调试
let sql = "CREATE TABLE IF NOT EXISTS T_Person( \n" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, \n" +
"name TEXT, \n" +
"age INTEGER \n" +
"); \n"
print(sql)

// 执行SQL语句
return execSQL(sql: sql)
}
  • 执行SQL语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// 执行除查询以外的SQL语句
func execSQL(sql:String) -> Bool {
// 1、将swift字符串转为C语言字符串
let cSQL = sql.cString(using: String.Encoding.utf8)

// 在sqlite3中,除了查询以外(创建、删除、新增、更新)都是用同一个函数
/*
参数1:已经打开的数据库对象
参数2:需要执行的SQL语句,C语言字符串
参数3:执行SQL语句之后的回调,一般传nil
参数4:是第三个参数的第一个参数,一般传nil
参数5:错误信息,一般传nil
*/

if sqlite3_exec(db, cSQL, nil, nil, nil) != SQLITE_OK {
return false
}
return true
}
  • 执行查询SQL语句
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/// 查询所有的数据, 返回字典数组
func execRecordSQL(sql:String) -> [[String:AnyObject]] {
// 1、将swift字符串转为C语言字符串
let cSQL = sql.cString(using: String.Encoding.utf8)

// 准备:理解为预编译SQL语句,检测里面是否有错误等等,他可以提供性能
/*
参数1:已经打开的数据库对象
参数2:需要执行的SQL语句,C语言字符串
参数3:需要执行的SQL语句的长度,传入-1系统自动计算
参数4:预编译之后的句柄,已经要想去除数据,就需要这个句柄
参数5:错误信息,一般传nil
*/

var stmt: OpaquePointer? = nil
if sqlite3_prepare(db, cSQL, -1, &stmt, nil) != SQLITE_OK {
print("准备失败")
}

// 准备成功
var records = [[String: AnyObject]]()

// 查询数据
// sqlite3_step代表去除一条数据,如果去到了数据就会返回SQLITE_ROW
while sqlite3_step(stmt) == SQLITE_ROW {
// 获取一条记录的数据
let record = recordWithStmt(stmt: stmt!)
// 将当前获取到的这一条记录添加到数组
records.append(record)
}
// 返回查询到的数据
return records
}

/// 获取一条疾苦的值,返回字典
private func recordWithStmt(stmt:OpaquePointer) -> [String:AnyObject] {
// 拿到当前这条数据所有的列
let count = sqlite3_column_count(stmt)

// 定义字典存储查询到的数据
var record = [String:AnyObject]()

for index in 0..<count {
// 拿到每一列的名称
let cName = sqlite3_column_name(stmt, index)
let name = String(cString: cName!, encoding: String.Encoding.utf8)

// 拿到每一列的类型 SQLITE_INTERGER
let type = sqlite3_column_type(stmt, index)

switch type {
case SQLITE_INTEGER:
// 整形
let num = sqlite3_column_int64(stmt, index)
record[name!] = Int(num) as AnyObject
case SQLITE_FLOAT:
// 浮点型
let double = sqlite3_column_double(stmt, index)
record[name!] = Double(double) as AnyObject
case SQLITE_TEXT:
// 文本类型
let cText = sqlite3_column_text(stmt, index)
let text = String(cString: cText!)
record[name!] = text as AnyObject
case SQLITE_NULL:
// 空类型
record[name!] = NSNull()

default:
// 二进制类型 SQLITE_BLOB
// 一般情况下,不会往数据库中存储二进制数据
print("")
}

}
return record

}

创建TestPerson类测试

创建TestPerson类对上述方法进行测试

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
class TestPerson: NSObject {

var id:Int = 0
var age:Int = 0
var name:String?

// MARK: - 系统内部方法
init(dict:[String:AnyObject]) {
super.init()
setValuesForKeys(dict)
}

override func setValue(_ value: Any?, forUndefinedKey key: String) {

}

// MARK: - 执行数据源CRUD的操作
/// 查询所有TestPerson
class func loadPersons() -> [TestPerson] {
let sql = "SELECT * FROM T_Person;"
let result = SQLiteManager.share().execRecordSQL(sql: sql)
var models = [TestPerson]()
for dict in result {
models.append(TestPerson(dict: dict))
}
return models
}

/// 删除记录
func deletePerson() -> Bool {
let sql = "DELETE FROM T_Person WHERE age IS \(self.age);"
return SQLiteManager.share().execSQL(sql: sql)
}

/// 更新
func updatePerson(name:String) -> Bool {
let sql = "UPDATE T_Person SET NAME = '\(name)' WHERE age = \(self.age);"
return SQLiteManager.share().execSQL(sql: sql)
}

/// 插入一条记录
func insertPerson() -> Bool {
assert(name != nil, "name必须有值")
let sql = "INSERT INTI T_Person (name, age) VALUES (\(name!), \(self.age);"
return SQLiteManager.share().execSQL(sql: sql)
}
}

压力测试

进行测试的时候确保已经打开过数据库(也就是创建表格)

1
2
// 打开数据库,创建表格
SQLiteManager.share().openDB(SQLiteName: "person.sqlite")

进行10000次插入数据操作

1
2
3
4
5
6
7
8
9
let start = CFAbsoluteTimeGetCurrent()

for i in 0..<10000{
let person = TestPerson(dict: ["name":"daisuke" as AnyObject, "age":(2+i as AnyObject)])
person.insertPerson()
}

print("耗时:\(CFAbsoluteTimeGetCurrent() - start)")
// 耗时:14.7124910354614

发现这个操作是非常耗时的,理应放在子线程中操作。放在主线程操作妨碍其他操作,体验不好

使用线程

串行队列 -SQLite本质就是一个文件,如果多个线程同时对数据库进行读写会造成数据混乱,可以使用一个串行队列,顺序调度任务,就不会出现数据混乱的问题。注意:串行队列不能嵌套,一旦嵌套,同样会出现互相等待情况,造成死锁!

1
2
3
4
5
6
7
8
9
10
11
/// 创建一个串行队列
private let dbQueue = DispatchQueue(label: "com.daisuke.test")

func execQueueSql(action:@escaping (_ manager:SQLiteManager) ->()) {
// 开启一个子线程
dbQueue.async {
print(Thread.current)
// 执行闭包
action(self)
}
}

插入数据方法修改

1
2
3
4
5
6
7
8
9
/// 插入一条记录
func insertQueuePerson() {
assert(name != nil, "name必须有值")
let sql = "INSERT INTO T_Person (name, age) VALUES ('\(name!)', \(self.age));"

SQLiteManager.share().execQueueSql { (manager) in
manager.execSQL(sql: sql)
}
}

测试:

1
2
3
4
5
6
7
8
9
let start = CFAbsoluteTimeGetCurrent()

for i in 0..<10000{
let person = TestPerson(dict: ["name":"daisuke" as AnyObject, "age":(2+i as AnyObject)])
person.insertQueuePerson()
}

print("耗时:\(CFAbsoluteTimeGetCurrent() - start)")
// 耗时:0.696339964866638

这样就不会阻塞主线程了。


更新时间:2018-05-25 09:59:27

数据库事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MARK: - 事务相关
// 1、开启事务
func beginTransaction() {
execSQL(sql: "BEGIN TRANSACTION")
}

// 2、提交事务
func commitTransaction() {
execSQL(sql: "COMMIT TRANSACTION")
}

// 3、回滚事务
func rollbackTransaction() {
execSQL(sql: "ROLLBACK TRANSACTION")
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let manager = SQLiteManager.share()
// 开启事务
manager.beginTransaction()

for i in 0..<10000{
let person = TestPerson(dict: ["name":"daisuke" as AnyObject, "age":(2+i as AnyObject)])
person.insertQueuePerson()
if i == 1000 {
manager.rollbackTransaction()
// 注意点:回滚之后一定要跳出循环停止更新
return
}
}

// 提交事务
manager.commitTransaction()

预编译绑定

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
55
56
57
58
59
60
61
62
63
64
65
66
67
// MARK: - 预编译相关

/// 自定义一个SQLITE_TRANSIENT, 覆盖系统的
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)

/// 预编译
func batchExecSQL(sql:String, args: CVarArg...) -> Bool {
// 1、将SQL语句转为C语言字符串
let cSQL = sql.cString(using: String.Encoding.utf8)

// 2、预编译SQL语句
var stmt:OpaquePointer? = nil
if sqlite3_prepare(db, cSQL, -1, &stmt, nil) != SQLITE_OK {
print("预编译失败")
sqlite3_finalize(stmt)
return false
}

// 3、绑定数据
var index: Int32 = 1
for objc in args {
if objc is Int {
print("通过int方法绑定数据 \(objc)")
// 第二个参数就是SQL中('?', ?)的位置,注意:从1开启
sqlite3_bind_int64(stmt, index, sqlite3_int64(objc as! Int))
} else if objc is Double {
print("通过Double方法绑定数据 \(objc)")
sqlite3_bind_double(stmt, index, objc as! Double)
} else if objc is String {
print("通过String方法绑定数据 \(objc)")
let text = objc as! String
let cText = text.cString(using: String.Encoding.utf8)
// 第三个参数: 需要绑定的字符串, C语言
// 第四个参数: 第三个参数的长度, 传入-1系统自动计算
// 第五个参数: OC中直接传nil, 但是Swift传入nil会有大问题
/*
typedef void (*sqlite3_destructor_type)(void*);

#define SQLITE_STATIC ((sqlite3_destructor_type)0)
#define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1)

第五个参数如果传入SQLITE_STATIC/nil, 那么系统不会保存需要绑定的数据, 如果需要绑定的数据提前释放了, 那么系统就随便绑定一个值
第五个参数如果传入SQLITE_TRANSIENT, 那么系统会对需要绑定的值进行一次copy, 直到绑定成功之后再释放
*/

sqlite3_bind_text(stmt, index, cText, -1, SQLITE_TRANSIENT)
}
index += 1
}

// 4、执行SQL语句
if sqlite3_step(stmt) != SQLITE_DONE {
print("执行SQL语句失败")
return false
}

// 5、重置stmt
if sqlite3_reset(stmt) != SQLITE_OK {
print("重置失败")
return false
}

// 6、关闭stmt
// 注意点:只要用到了stmt,一定要关闭
sqlite3_finalize(stmt)

return true
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let start = CFAbsoluteTimeGetCurrent()

let manager = SQLiteManager.share()
// 开启事务
manager.beginTransaction()

for i in 0..<10000{
let sql = "INSERT INTO T_Person (name, age) VALUES (?, ?);"
manager.batchExecSQL(sql: sql, args: "yy + \(i)", 1+i)
}

// 提交事务
manager.commitTransaction()

print("耗时:\(CFAbsoluteTimeGetCurrent() - start)")

FMDB

导入FMDB

1、在Podfile文件中添加pod 'FMDB'

2、pod install

3、在桥接文件DWeibo-Bridge中导入头文件#import <FMDB/FMDB.h>

基本使用

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
var db: FMDatabase?

/// 打开数据库
func openDB(DBName:String){
// 根据传入的数据库拼接数据库路径
let path = DBName.docDir()

// 创建数据库对象
db = FMDatabase(path: path)

// 打开数据库
// open方法特点:
// 1、如果指定路径对应的数据库文件已存在,就会直接打开
// 2、如果指定路径对应的数据库文件不存在,就会创建一个新的
if !db!.open() {
print("数据库打开失败")
return
}

// 创建表
createTable()
}

private func createTable() {
// 1、编写SQL语句
let sql = "CREATE TABLE IF NOT EXISTS T_Person( \n" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, \n" +
"name TEXT, \n" +
"age INTEGER \n" +
"); \n"
print(sql)

// 执行SQL语句
// 注意点:在FMDB中除了查询以外,都称之为更新
if db!.executeUpdate(sql, withArgumentsIn: nil) {
print("创建表成功")
} else {
print("创建表失败")
}
}

TestPerson类中

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
class func loadPersons() ->[TestPerson]{
let sql = "SELECT * FROM T_Person;"
let res = SQLiteManager.share().db!.executeQuery(sql, withArgumentsIn: nil)

var models = [TestPerson]()
while res!.next() {
let p = TestPerson()
let num = res?.int(forColumn: "id")
let name = res?.string(forColumn: "name")
let age = res?.int(forColumn: "age")

p.id = Int(num!)
p.name = name
p.age = Int(age!)
models.append(p)
}
return models
}

/// 插入一条记录
func insertQueuePerson() ->Bool{
assert(name != nil, "name必须有值")
let sql = "INSERT INTO T_Person (name, age) VALUES ('\(name!)', \(self.age));"

return SQLiteManager.share().db!.executeUpdate(sql, withArgumentsIn: nil)
}

FMDBQueue使用

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
var dbQueue:FMDatabaseQueue?
/// 打开数据库
func openDB(DBName:String){
// 根据传入的数据库拼接数据库路径
let path = DBName.docDir()

// 创建数据库对象
// 注意点:如果是使用FMDatabaseQueue创建数据库对象,那么就不用打开数据库
dbQueue = FMDatabaseQueue(path: path)

// 创建表
createTable()
}

private func createTable() {
// 1、编写SQL语句
let sql = "CREATE TABLE IF NOT EXISTS T_Person( \n" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, \n" +
"name TEXT, \n" +
"age INTEGER \n" +
"); \n"
print(sql)

// 执行SQL语句
dbQueue!.inDatabase({ (db) in
db?.executeUpdate(sql, withArgumentsIn: nil)
})
}

在TestPerson中

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
class func loadPersons(finished: @escaping ([TestPerson]) ->()){
let sql = "SELECT * FROM T_Person;"
SQLiteManager.share().dbQueue?.inDatabase({ (db) in
let res = db?.executeQuery(sql, withArgumentsIn: nil)

var models = [TestPerson]()
while res!.next() {
let p = TestPerson()
let num = res!.int(forColumn: "id")
let name = res!.string(forColumn: "name")
let age = res!.int(forColumn: "age")

p.id = Int(num)
p.name = name
p.age = Int(age)
models.append(p)
}
finished(models)
})
}

/// 插入一条记录
func insertQueuePerson(){
assert(name != nil, "name必须有值")
let sql = "INSERT INTO T_Person (name, age) VALUES ('\(name!)', \(self.age));"
// 执行SQL语句
/*
只要在inTransaction闭包中执行的语句都是已经开启事务的
第一个参数:已经打开的数据库对象
第二个参数:用于设置是否需要回滚数据
*/

SQLiteManager.share().dbQueue?.inTransaction({ (db, rollback) in
if !db!.executeUpdate(sql, withArgumentsIn: nil){
// 如果插入数据失败,就回滚
rollback?.pointee = true
}
})
}

缓存首页数据

  • 在AppDelegate类中打开数据库
1
2
// 打开数据库,创建表格
SQLiteManager.share().openDB(DBName: "status.sqlite")
  • 在SQLiteManager中创建表格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private func createTable() {
// 1、编写SQL语句
let sql = "CREATE TABLE IF NOT EXISTS T_Status( \n" +
"statusId INTEGER PRIMARY KEY, \n" +
"statusText TEXT, \n" +
"userId INTEGER \n" +
"); \n"
print(sql)

// 执行SQL语句
dbQueue!.inDatabase({ (db) in
db?.executeUpdate(sql, withArgumentsIn: nil)
})
}
  • 创建StatusDAO类作为数据层

缓存微博数据方法

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
/// 缓存微博数据
class func cacheStatuses(statuses: [[String:AnyObject]]){
// 准备数据
let userId = UserAccount.loadAccount()!.uid!

// 定义SQL语句
let sql = "INSERT INTO T_Status (statusId, statusText, userId) VALUES (?, ?, ?);"

// 执行SQL语句
SQLiteManager.share().dbQueue?.inTransaction({ (db, rollback) in

for dict in statuses {
let statusId = dict["id"]!
// JSON -> 二进制 -> 字符串
let data = try! JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions.prettyPrinted)
let statusText = String(data: data, encoding: String.Encoding.utf8)!
print(statusText)
if !db!.executeUpdate(sql, withArgumentsIn: [statusId, statusText, userId]){
// 如果数据插入失败,就回滚
print("数据插入失败")
rollback?.pointee = true
}
print("数据插入成功")
}
})

}

请求数据回来时缓存微博数据

1
2
// 缓存微博数据
StatusDAO.cacheStatuses(statuses: dict["statuses"] as! [[String: AnyObject]])

读取缓存数据

读取缓存方法

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
/// 读取缓存数据
class func loadCacheStatuses(since_id:Int, max_id:Int, finished:@escaping ([[String:AnyObject]])->()){
// 定义SQL语句
var sql = "SELECT * FROM T_Status \n"
if since_id > 0 {
sql += "WHERE statusId > \(since_id) \n"
} else if max_id > 0 {
sql += "WHERE statusId <= \(since_id) \n"
}

sql += "ORDER BY statusId DESC \n"
sql += "LIMIT 20; \n"

// 执行SQL语句
SQLiteManager.share().dbQueue?.inDatabase({ (db) in
// 查询数据
let res = db?.executeQuery(sql, withArgumentsIn: nil)

// 遍历取出查询的数据
/*
返回字典数组的原因:通过网络获取返回的也是字典数组
让本地和网络返回的数据类型保持一致,以便于我们后期处理
*/

var statuses = [[String:AnyObject]]()
while res!.next() {
// 取出数据库存储的一条微博字符串
let dictStr = res?.string(forColumn: "statusText") as! String
// 将微博字符串转为字典
let data = dictStr.data(using: String.Encoding.utf8)!
let dict = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as! [String:AnyObject]
statuses.append(dict)
}
// 返回数据
finished(statuses)
})
}

使用:

1
2
3
4
StatusDAO.loadCacheStatuses(since_id: since_id, max_id: max_id) { (statuses) in
let models = dictToModel(list: statuses)
finishBlock(models, nil)
}

完善缓存逻辑

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
/*
1、从本地数据中获取
2、如果本地有,直接返回
3、从网络获取
4、将从网络获取的数据缓存起来
*/

/// 加载微博数据
class func loadStatuses(since_id:Int, max_id:Int, finished:@escaping ([[String:AnyObject]]?, _ error:Error?) ->()){
// 1、从本地数据库中获取
loadCacheStatuses(since_id: since_id, max_id: max_id) { (array) in
// 如果本地有,直接返回
if !array.isEmpty{
print("从数据库中获取")
finished(array, nil)
return
}

// 从网络获取
let path = "2/statuses/home_timeline.json"
var params = ["access_token":UserAccount.loadAccount()!.access_token]
// 下拉刷新
if since_id > 0 {
params["since_id"] = "\(since_id)"
}

// 上拉刷新
if max_id > 0 {
params["max_id"] = "\(max_id - 1)"
}

NetworkTools.shareNetworkTools().get(path, parameters: params, progress: nil, success: { (_, json) in
let dict = json as! [String: AnyObject]
let array = dict["statuses"] as! [[String: AnyObject]]

// 缓存微博数据
cacheStatuses(statuses: array)

// 返回数据
finished(array, nil)
}) { (_, error) in
finished(nil, error)
}
}
}

在Status类中加载数据就变得这样简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// 加载微博数据
class func loadStatuses(since_id:Int, max_id:Int, finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()){

StatusDAO.loadStatuses(since_id: since_id, max_id: max_id) { (array, error) in
if array == nil {
finishBlock(nil, error! as NSError)
}
if error != nil {
finishBlock(nil, error! as NSError)
}

// 遍历数组,将字典转为模型
let models = dictToModel(list: array!)

// 缓存微博配图
cacheStatusImages(list: models, finishBlock: finishBlock)
}
}

清除缓存数据

  • 定义清除数据方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// 清除过期的数据
class func cleanStatuses() {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.locale = Locale(identifier: "en")

let date = NSDate(timeIntervalSinceNow: -60)
let dateStr = formatter.string(from: date as Date)

// 定义SQL语句
let sql = "DELETE FROM T_Status WHERE createDate <= '\(dateStr)';"

// 执行SQL语句
SQLiteManager.share().dbQueue?.inDatabase({ (db) in
db?.executeUpdate(sql, withArgumentsIn: nil)
})
}
  • 当应用进入后台是清理数据
1
2
3
4
func applicationDidEnterBackground(_ application: UIApplication) {
// 清理缓存数据
StatusDAO.cleanStatuses()
}

结束

课程到这里已经全部结束。感谢小码哥的资料,让我对swift更加巩固,也对很多知识点更加深刻。感谢