Swift
的基础知识基本学完,这次写的不是别的,是培训常模仿的微博项目,现在我也是边模仿边学习的心态。我会记录小码哥
中模仿微博
的从0
到1
的过程,希望以后看到自己写的烂文章会笑起来,哈哈。另外文章篇幅可能很长很长,所以想看的同学可以好好收藏一下。如果觉得可以帮助到你的话,请
Star
我,也可以Fort
一下工程关注项目的进展情况。当然过程中有什么问题的话,随时欢迎Issues
如有侵权,请联系删除。
工程地址
MyWeiBoProjectForSwift
另外资源可以在工程里面找
创建工程
相信大家都会了,这里就不在述说了
删除StoryBoard
使用纯代码完成这个项目,所以先把故事版删除,还有把info.plist
文件的main
删除。如下图
框架搭建
- 首先建立工程目录
按照微博的模块,可以分为首页(Home)
、消息(Message)
、广场(Discover)
、我(Profile)
四大模块,然后工程在同级别添加Main
、Common
、Tool
三个模块
如下图:
- 搭建框架
在Main
模块中新建一个继承UITabBarController
的自定义MainViewController
类
那么好了,我们只要在AppDelegate.swift
文件中创建Window,并把rootController
赋值MainViewController
就行了。
- 在AppDelegate.swift创建视图
1 |
|
- 在首页(Home)、消息(Message)、广场(Discover)、我(Profile)模块建立主要控制器
分别对应HomeTableViewController
、MessageTableViewController
、DiscoverTableViewController
、ProfileTableViewController
- MainViewController创建子控制器
1 | override func viewDidLoad() { |
好了,我们运行一下效果图
- MainViewController重构
你们有没有发现上面的代码除了类名、标题和图片的名称不一样,其他的都一样,其实很多重复的代码,我们可以使用一个方法把它搞定,而不同的可以通过参数来传递。这个就是简单的重构
。废话不多说,上代码
1 | /// 初始化子控制器 |
方法构建成功,那么在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 | addChildViewController("HomeTableViewController", title: "首页", imageName: "tabbar_home") |
然后再创建一个按钮表示中间的加号,使用懒加载的方式创建加号按钮1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fileprivate 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
,所以这里绑定一个函数,这里有个注意的地方:监听加号按钮点击,监听按钮点击方法不能私有, 按钮点击事件的调用是由 运行循环 监听并且以消息机制传递的,因此,按钮监听函数不能设置为fileprivate1
2
3
4
5
6/// 监听加好按钮点击,注意:监听按钮点击方法不能私有
/// 按钮点击事件的调用是由 运行循环 监听并且以消息机制传递的,因此,按钮监听函数不能设置为`fileprivate`
func composeBtnClick() {
print(#function)
print("点击按钮")
}
再把加号按钮添加到tababr上,这里我抽成一个函数,顺便调整加号按钮的frame1
2
3
4
5
6
7
8
9
10
11
12
13fileprivate 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
8override 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 | fileprivate func addChildViewControllers() { |
初始化每一个子控制器
1 | /// 初始化子控制器 |
添加VisitorView视图
添加一个自定义VisitorView游客视图,作为每个控制器的基本视图。当没有登录的情况下展示的是VisitorView视图。
为了方便布局,使用UIView+AutoLayout分类布局文件,具体可在工程中找到。
下面是VisitorView视图的关键的代码片段:
定义协议
1 | // Swift中如何定义协议:必须遵守NSObjectProtocol |
使用弱引用
1 | // 定义一个属性保存代理对象 |
重写初始化方法注意点
1 | override init(frame: CGRect) { |
添加BaseViewController控制器
1 | // 定义一个变量保存用户当前是否是登录 |
由于BaseViewController控制器遵循VisitorViewDelegate协议,那么VisitorViewDelegate的相应方法就要实现:
1 | func loginBtnWillClick() { |
主控制器继承基类
1 | class HomeTableViewController: BaseViewController |
导航条按钮
导航栏中使用UIBarButtonItem来显示每一个item,那么现在为了方便创建,使用扩展extension声明一个类型方法。
创建一个Swift文件,名为:UIBarButtonItem+Category
1 | import UIKit |
那么在HomeTableViewController中创建导航栏的UIBarButtonItem就可以这样:
1 | func setupNav() { |
更新时间:2018-04-27 16:53:34
CocoPods
第三方框架
项目中使用到以下第三方框架
AFNetworking
SDWebImage
SVProgressHUD
Pod 安装
- git 备份
- 打开终端
$ cd
进入项目目录- 输入以下终端命令建立或编辑
Podfile
1 | $ vim Podfile |
- 输入以下内容
1 | use_frameworks! |
:wq
保存退出
在 Swift 项目中,cocoapod 仅支持以 Framework 方式添加框架,因此需要在 Podfile 中添加
use_frameworks!
在终端提交添加的框架
1 | # 将修改添加至暂存区 |
安装第三方框架
1 | # 安装第三方框架 |
打开xcworkspace工程
退出当前工程,进入目录,打开xcworkspace工程即可
添加桥接文件DWeibo-Bridge
- 使用 Xcode 打开工作组文件
- 在
Supporting Files
下添加桥接文件Weibo-Bridge.h
- 输入以下内容
1 |
- 点击
项目
-TARGETS
-Build Settings
- 搜索
bridg
- 在
Objective-C Bridging Header
中输入DWeibo/DWeibo-Bridge.h
路径
然后在swift文件中导入SVProgressHUD,这样就可以使用OC的SVProgressHUD框架了。
更新时间:2018-04-28 15:28:34
微博API
接入微博API,里面的有许多接口,相对应实现即可。有一些分高级接口,可灵活选择。
授权
创建应用:我的应用,获取App Key
,App Secret
,授权回调页:http://daisuke.cn
这三个值,作为授权的参数。
创建OAuthViewController
- 声明需要的参数:
1 | let WB_App_Key = "2624860832" |
- 拼接获取未授权的RequestToken的路径并加载:
1 | let urlStr = "https://api.weibo.com/oauth2/authorize?client_id=\(WB_App_Key)&redirect_uri=\(WB_redirect_uri)" |
3.遵循UIWebViewDelegate实现方法,利用已经授权的RequestToken换取AccessToken
1 | extension OAuthViewController: UIWebViewDelegate |
UserAccount存储用户信息
1 | import UIKit |
归档用户信息
1 | // MARK: - 保存和读取 Keyed |
获取用户信息
1 | // 加载用户信息 |
更新时间:2018-05-08 15:07:39
新特性
创建NewFeatureViewController
NewFeatureViewController继承UICollectionViewController
实现UICollectionViewDataSource
1 | // MARK: - UICollectionViewDataSource |
创建NewFeatureCell
实现每一个cell的样式在NewFeatureViewController的显示
1 | private class NewFeatureCell: UICollectionViewCell { |
创建NewFeatureLayout
1 | /// 布局对象 |
完善&添加按钮
添加按钮
1 | lazy var startBtn: UIButton = { |
添加按钮显示动画
1 | /// 给按钮做个动画 |
在最后一个cell显示完毕的时候执行按钮动画
1 | // 完全显示完cell之后调用 |
更新时间:2018-05-08 16:00:03
添加欢迎界面
创建WelcomViewController继承于UIViewController
添加子控件
1 | // MARK: - 懒加载 |
添加显示动画
1 | override func viewWillAppear(_ animated: Bool) { |
- usingSpringWithDamping 的范围为 0.0f 到 1.0f,数值越小 弹簧 的振动效果越明显
- initialSpringVelocity 则表示初始的速度,数值越大一开始移动越快,初始速度取值较高而时间较短时,会出现反弹情况
初始化信息
1 | override func viewDidLoad() { |
跟控制器切换
原理图:
判断新版本
1 | func isNewUpdate() -> Bool { |
显示的默认控制器
1 | private func defaultController() -> UIViewController{ |
定义通知
1 | /// 切换控制器通知 |
注销通知
1 | deinit { |
发送通知
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 | class func loadStatuses(finishBlock:@escaping (_ models:[Status]?, _ error:NSError?) ->()){ |
HomeTableViewController填充数据
声明数组存储微博数据1
2
3
4
5
6
7// 保存微博数组
var statuses: [Status]? {
didSet{
// 设置数据完毕,刷新表格
tableView.reloadData()
}
}
注册cell
1 | // 注册cell |
加载数据
1 | /// 加载数据 |
扩展HomeTableViewController现实数据源
1 | extension HomeTableViewController { |
获取用户数据
创建User
声明user的属性
1 | /// 用户ID |
Status监听user的数据
1 | override func setValue(_ value: Any?, forKey key: String) { |
StatusTableViewCell
添加子控件
1 | contentView.addSubview(iconView) |
监听微博属性
1 | var status:Status? { |
替换UITableViewCell
1 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
封装UILabel和UIButton
扩展UILabel, UILabel+Category.swift
1 | extension UILabel { |
扩展UIButton, UIButton+Category.swift
1 | extension UIButton { |
StatusTableViewCell.swift重构
1 | // 昵称 |
更新时间:2018-05-10 10:11:16
数据处理
认证图标
User.swift1
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 | var status:Status? { |
会员图标
User.swift
1 | /// 会员等级 |
StatusTableViewCell.swift
1 | var status:Status? { |
来源处理
Status.swift1
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 | extension NSDate { |
Status.swift
1 | /// 微博创建时间 |
缓存配图
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 | /// 缓存微博图片 |
使用缓存微博图片方法
1 | /// 加载微博数据 |
计算配图大小
StatusTableViewCell.swift
1 | /// 计算配图的尺寸 |
显示配图
懒加载配图控件
1 | // 配图 |
初始化配图控件
1 | /// 初始化配图的相关属性 |
扩展StatusTableViewCell实现数据源
1 | extension StatusTableViewCell:UICollectionViewDataSource { |
自定义配图cell
1 | class PictureViewCell: UICollectionViewCell { |
完善计算配图方法,返回总试图的尺寸&每个子视图的尺寸
1 | /// 计算配图的尺寸 |
设置配图尺寸
1 | var status:Status? { |
缓存行高
在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 | /// 微博ID |
实现返回数据源方法,返回行高
1 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { |
重构StatusTableViewCell.swift
cell的子控件分为四个部分,工具条封装成一个个view,头部封装成一个view,如下图:
创建StatusCellTopView
分别包含有昵称、头像、认证图标、会员图标、来源、时间这个几个控件
部分代码,具体看实现1
2
3
4
5
6
7
8
9
10
11
12
13
14var 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
24var 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 | private lazy var retweetBtn: UIButton = UIButton.createButton(imageName: "timeline_icon_retweet", title: "转发") |
替换成StatusCellBottomView
1 | // 底部工具条 |
创建StatusCellPictureView
设置数据,初始化(具体查看文件)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var 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 | class StatusNormalTableViewCell: StatusTableViewCell { |
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 |
|
创建StatusForwardTableViewCell显示转发状态下的微博
创建两个控件属性1
2
3
4
5
6
7
8
9
10
11private 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
21override 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 | /** |
完善StatusCellPictureView.swift
1 | /// 计算配图的尺寸 |
完善HomeTableViewController.swift
注册两个cell1
2tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.NormalCell.rawValue)
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: StatusTableViewCellIdentifier.ForwardCell.rawValue)
代理方法
1 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
转发功能完成。
下拉刷新
添加刷新控件
在Profile添加pod 'MJRefresh'
,然后pod install
1 | weak var weakSelf = self |
优化
处理加载微博数据上拉还是下拉
1 | /// 定义变量记录当时上拉还是下拉 |
判断是上拉还是下拉
1 | tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { |
更新时间:2018-05-16 15:09:32
修改BaseViewController继承UIViewController
继承BaseViewController分别修改,首页创建UItableview
1 | lazy var tableView: UITableView! = { |
显示刷新提醒
创建显示文本
1 | private lazy var newStatusLabel: UILabel = { |
展示文本方法
1 | private func showNewStatusCount(count:Int){ |
调用展示方法
1 | // 显示刷新提醒 |
浏览大图
获取大图
在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 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
创建浏览器
1 | class PhotoBrowserViewController: UIViewController { |
添加通知
在StatusCellPictureView.swift声明三个通知字段1
2
3
4
5
6/// 选中照片通知
let kStatusCellSelectPictureNotify = "kStatusCellSelectPictureNotify"
/// 照片的URL
let kStatusCellSelectPictureURLNotify = "kStatusCellSelectPictureURLNotify"
/// 照片的index
let kStatusCellSelectPictureIndexNotify = "kStatusCellSelectPictureIndexNotify"
在HomeTableViewController.swift1
2// 添加通知
NotificationCenter.default.addObserver(self, selector: #selector(selectPicture(notify:)), name: NSNotification.Name(rawValue: kStatusCellSelectPictureNotify), object: nil)
并实现监听方法
1 | func selectPicture(notify:Notification) { |
在StatusCellPictureView.swift中点击照片的时候发出通知
1 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
显示大图
创建PhotoBrowserCell
1 | class PhotoBrowserCell: UICollectionViewCell { |
完善
注册cell
1 | // 注册cell |
实现数据源方法
1 | extension PhotoBrowserViewController: UICollectionViewDataSource { |
创建布局
1 | class PhotoBrowserLayout: UICollectionViewFlowLayout { |
点击图片指定位置展示
1 | // 滚动到指定位置 |
效果如下:
更新时间:2018-05-17 16:13:21
长短图计算
计算显示的图像尺寸
1 | /// 计算显示的图像尺寸,以scrollview的宽度为准 |
初始化位置
1 | private func setupImagePosition() { |
调用
1 | // 图片的URL |
缩放图片
重设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 | ·// 图片的URL |
遵循UIScrollViewDelegate
1 | lazy var scrollView: UIScrollView = { |
实现相关UIScrollViewDelegate方法
1 | extension PhotoBrowserCell: UIScrollViewDelegate { |
显示菊花
1 | // 菊花 |
1 | // 图片的URL |
显示gif图标
在StatusCellPictureView.swift中
1 | private class PictureViewCell: UICollectionViewCell { |
关闭浏览器
PhotoBrowserCell.swift
添加手势
1 | // 添加imageView手势监听 |
声明代理
1 | protocol PhotoBrowserCellDelegate: NSObjectProtocol { |
声明代理属性
1 | // 声明代理属性 |
在PhotoBrowserViewController类中实现PhotoBrowserCellDelegate
1 | extension PhotoBrowserViewController: UICollectionViewDataSource, PhotoBrowserCellDelegate { |
更新时间:2018-05-18 10:19:39
添加表情数据包
可在工程上找
表情控制器
主要控件
1 | // MARK: - 懒加载 |
初始化工具条
1 | private func setupToolbar() { |
布局CollectionView界面
设置layout
1 | private class EmoticonLayout: UICollectionViewFlowLayout { |
创建EmoticonCell
1 | class EmoticonCell: UICollectionViewCell { |
设置collectionView
1 | private func setupCollectionView() { |
实现数据源方法
1 | extension EmoticonViewController: UICollectionViewDataSource{ |
加载数据模型准备
1 | import UIKit |
emoji字符扫描及转换原理
1 | // 十六进制的字符串 |
那么在Emoticon中(注意点:需要设置id参数)
1 | /// 表情模型 |
显示图片和emoji
加载数据1
lazy var packages: [EmoticonPackage] = EmoticonPackage.packages()
设置EmoticonCell数据
1 | var emoticon: Emoticon? { |
数据源方法修改
1 | extension EmoticonViewController: UICollectionViewDataSource{ |
效果如下:
完善模型
由于零散点修改多,这里只贴出部分,具体参看EmoticonPackage.swift
1 | /// 追加空白按钮,方便界面布局,如果一个界面的图标不足20个,补足,最后添加一个删除按钮 |
在EmoticonViewController.swift中
1 | var emoticon: Emoticon? { |
调整表情分组
1 | func clickItem(item: UIBarButtonItem) { |
插入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 | lazy var collectionView: UICollectionView = { |
实现UICollectionViewDelegate代理方法
1 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
关闭cell中按钮的点击,否则与点击cell的事件冲突
1 | override init(frame: CGRect) { |
测试:
1 | class DiscoverTableViewController: BaseViewController { |
效果如下:
插入图片表情
1 | // weak 相当于OC中的__weak,特点对象释放之后会将变量设置nil |
更新时间:2018-05-19 10:48:31
简单的图文混排(测试)
1 | override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
效果:
获取发送文本
创建附件类
1 | class EmoticonTextAttachment: NSTextAttachment { |
NSTextAttachment替换为EmoticonTextAttachment1
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 | var strM = String() |
重构
传入表情模型,获取图片文本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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 | extension UITextView { |
删除按钮处理
1 | /// 插入表情 |
处理最近表情
在Emoticon类中声明属性记录使用次数
1 | /// 记录当前表情被使用次数 |
在EmoticonPackage类中添加方法用来添加表情
1 | /// 用于添加最近表情 |
在EmoticonViewController类中点击表情时候进行记录存储
1 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
更新时间:2018-05-19 16:00:31
布局发送微博界面
创建ComposeViewController.swift
设置导航栏按钮
1 | private func setupNav() { |
设置输入框
1 | private func setupInputView() { |
监听文本输入变化,改变按钮是否可以点击状态
1 | extension ComposeViewController: UITextViewDelegate { |
点击tabbar按钮,跳转ComposeViewController页面
1 | /// 监听加好按钮点击,注意:监听按钮点击方法不能私有 |
发送微博
1 | func send() { |
继承表情键盘
声明键盘属性
1 | /// 表情键盘 |
添加键盘控制器
1 | // 添加子控制器 |
切换键盘
1 | /// 切换表情键盘 |
效果:
更新时间:2018-05-21 10:04:56
创建浏览器&布局
懒加载三个属性
1 | // MARK: - 懒加载 |
创建cell
1 | class PhotoSelectorCell: UICollectionViewCell { |
PhotoSelectorController初始化UI
1 | private func setupUI() { |
实现数据源方法
1 | extension PhotoSelectorController: UICollectionViewDataSource{ |
显示图片浏览器
创建代理
1 |
|
协议的某些属性或方法是可选实现的。遵循协议的类,就可以灵活实现相关属性和方法了。协议中使用optional关键字作为前缀定义可选类型。因为要跟OC打交道,所以协议以及属性、方法前面都要加上@objc。那么也推导出只有OC类型的类或者是@objc类遵循,其他类以及结构体和枚举都不能遵循这种协议
代理属性
1 | /// 定义代理 |
使用
1 | func selectPhoto() { |
实现PhotoSelectorCellDelegate
1 | extension PhotoSelectorController: UICollectionViewDataSource, PhotoSelectorCellDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate{ |
显示照片
在PhotoSelectorCell中添加图片属性
1 | // 不能解包可选项 -> 将nil使用了惊叹号 |
数据源方法&cell代理方法相关修改
1 | extension PhotoSelectorController: UICollectionViewDataSource, PhotoSelectorCellDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate{ |
内存问题处理
UIImageJPEGRepresentation
使用UIImageJPEGRepresentation(, )来压缩图片会严重影响图片质量
- 解压缩性能消耗太大,苹果不推荐
- JPEG压缩图片是不保真的,压缩后可能很难看
写入到本地
1 | let image = info[UIImagePickerControllerOriginalImage] as! UIImage |
内存警告
- 关于应用程序内存,UI的APP空的程序运行占用20M左右,一般程序消耗100M以内都是可以接受的
- 内存飙升到500M接受到第一次内存警告,内存释放后的结果120M,程序仍然能够正常运行
缩放图片
1 | extension UIImage { |
使用
1 | // 将图片保存到数组中 |
更新时间: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 | func urlRegex(str:String) { |
匹配表情、用户名、话题、URL
1 | func contentRegex(str:String) { |
效果
1 | override func viewDidLoad() { |
生成表情文字
1 | func emoticonRegex(str:String) { |
测试效果:
1 | emoticonRegex(str: "我[爱你]好久了[好爱哦]") |
生成表情文字重构
在EmoticonPackage.swift中把生成表情字符串的方法封装在里面
1 | static let packageList: [EmoticonPackage] = EmoticonPackage.packages() |
更新时间:2018-05-22 14:42:22
TextKit基本使用
初衷:创建一个文本控件,自动识别URL并可以点击。
创建DDLabel继承与UILabel
声明三个属性
1 | // MARK: - 懒加载 |
重写初始化方法
1 | override init(frame: CGRect) { |
重写布局方法
1 | override func layoutSubviews() { |
重写绘制方法
1 | /// 如果UILabel调用setNeedsDisplay方法,系统会触发drawText方法 |
重写text属性
1 | override var text: String?{ |
测试
键盘变化处理
添加键盘通知
1 | // 注册通知监听键盘 |
注销通知
1 | deinit { |
keyboardChange方法
1 | func keyboardChange(notify: Notification) { |
集成图片选择器
懒加载
1 | /// 图片选择器 |
添加photoSelectorVC
1 | addChildViewController(photoSelectorVC) |
初始化
1 | private func setupPhotoView(){ |
工具条事件
1 | /// 切换图片选择器 |
发送图片微博
发送图片微博
1 | func send() { |
重构发送图片微博
在NetworkTools.swift类中
1 | func sendStatus(text:String, image:UIImage?, successCallBack:@escaping (_ status: Status) ->(), errorCallBack:@escaping (_ error: Error)->()) { |
使用
1 | func send() { |
发布页面字数提醒
1、提示控件
1 | lazy var tipLabel: UILabel = UILabel() |
2、添加到视图,并布局
1 | view.addSubview(tipLabel) |
3、设置提示数据
1 | func textViewDidChange(_ textView: UITextView) { |
显示首页表情
在StatusTableViewCell.swift中
1 | var status:Status? { |
更新时间:2018-05-23 10:28:27
SQLite基本使用
- 创建数据库管理类
1 | private static let manager:SQLiteManager = SQLiteManager() |
- 打开数据库
1 | /// 打开数据库, SQLiteName数据路名称 |
- 创建表
1 | func createTable() -> Bool { |
- 执行SQL语句
1 | /// 执行除查询以外的SQL语句 |
- 执行查询SQL语句
1 | /// 查询所有的数据, 返回字典数组 |
创建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
47class 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 | // 打开数据库,创建表格 |
进行10000次插入数据操作1
2
3
4
5
6
7
8
9let 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 | /// 插入一条记录 |
测试:
1 | let start = CFAbsoluteTimeGetCurrent() |
这样就不会阻塞主线程了。
更新时间:2018-05-25 09:59:27
数据库事务
1 | // MARK: - 事务相关 |
测试:
1 | let manager = SQLiteManager.share() |
预编译绑定
1 | // MARK: - 预编译相关 |
测试:
1 | let start = CFAbsoluteTimeGetCurrent() |
FMDB
导入FMDB
1、在Podfile文件中添加pod 'FMDB'
2、pod install
3、在桥接文件DWeibo-Bridge中导入头文件#import <FMDB/FMDB.h>
基本使用
1 | var db: FMDatabase? |
TestPerson类中
1 | class func loadPersons() ->[TestPerson]{ |
FMDBQueue使用
1 | var dbQueue:FMDatabaseQueue? |
在TestPerson中
1 | class func loadPersons(finished: @escaping ([TestPerson]) ->()){ |
缓存首页数据
- 在AppDelegate类中打开数据库
1 | // 打开数据库,创建表格 |
- 在SQLiteManager中创建表格
1 | private func createTable() { |
- 创建StatusDAO类作为数据层
缓存微博数据方法
1 | /// 缓存微博数据 |
请求数据回来时缓存微博数据
1 | // 缓存微博数据 |
读取缓存数据
读取缓存方法
1 | /// 读取缓存数据 |
使用:
1 | StatusDAO.loadCacheStatuses(since_id: since_id, max_id: max_id) { (statuses) in |
完善缓存逻辑
1 | /* |
在Status类中加载数据就变得这样简单
1 | /// 加载微博数据 |
清除缓存数据
- 定义清除数据方法
1 | /// 清除过期的数据 |
- 当应用进入后台是清理数据
1 | func applicationDidEnterBackground(_ application: UIApplication) { |
结束
课程到这里已经全部结束。感谢小码哥
的资料,让我对swift更加巩固,也对很多知识点更加深刻。感谢