第五天
昨天我们主要体验了access_token
的作用,今天我们将学习怎么从新浪官方获取access_token
今天我们需要用到昨天从官方得到的Appkey
和AppSecret
以及RedirectURI
从官方文档我们可以看到,获取access_token
有一下流程
1.新建一个XQWBCommon.swift
,保存我们应用的信息
// 应用程序信息
let XQWBAppKey = "xxxxxxx"
let XQWBAppSecret = "xxxxxxxxxxxxxxxxxxxxxxxx"
let XQWBRedirectURI = "http://baidu.com"
// 登录通知
let XQWBUserShouldLoginNotification = "XQWBUserShouldLoginNotification"
// 登录成功
let XQWBUserLoginSuccessNotification = "XQWBUserLoginSuccessNotification"
2.获取access_token
需要用户登录并且授权,当用户点击登录按钮,或者access_token
过期的时候,使用通知,让用户进行登录
-
2.1 注册通知
NotificationCenter.default.addObserver(self, selector: #selector(userLogin), name: NSNotification.Name(rawValue: XQWBUserShouldLoginNotification), object: nil)
-
2.2 注册通知以后,要记得注销通知
deinit { NotificationCenter.default.removeObserver(self) }
-
2.3 实现通知方法
// 登录 @objc func userLogin(n: Notification) { }
-
2.3 在需要登录的时候响应通知
NotificationCenter.default.post(name: NSNotification.Name(XQWBUserShouldLoginNotification), object: nil)
3.授权登录的页面是新浪官方的登录页面,我们使用一个webView
加载就可以
-
3.1 新建一个
XQWBOAuthViewController.swift
文件,加载webView
// 重写 loadView, 将webview设置为controller的view override func loadView() { view = webView view.backgroundColor = UIColor.white title = "登录新浪微博" // 设置返回按钮 navigationItem.leftBarButtonItem = UIBarButtonItem(title: "返回", target: self, action: #selector(close), isBackButton: true) } // 实现返回方法 @objc func close() { dismiss(animated: true, completion:nil) }
-
3.2 在
viewDidload()
中加载授权页面override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. webView.delegate = self // 加载授权页面 let urlString = "https://api.weibo.com/oauth2/authorize?client_id=\(XQWBAppKey)&redirect_uri=\(XQWBRedirectURI)" // 1. 确定URL guard let url = URL(string: urlString) else { return } // 2. 请求 let request = URLRequest(url: url) // 3. 加载请求 webView.loadRequest(request) }
-
3.3 实现
UIWebViewDelegate
方法,在页面开始加载的时候,拿到我们在文章开头分析的code
,并且获取access_token
/// webView将要加载请求 /// /// - Parameters: /// - webView: webview /// - request: 要加载的请求 /// - navigationType: 导航类型 /// - Returns: 是否加载request func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { }
- 因为上面的代理方法中可以得到
URLRequest
,这个方法返回结果为是否加载request
,我们可以通过URLRequest
进行判断,先写一个伪代码分析
// 1.如果URLRequest的absoluteString包含我们的`XQWBRedirectURI`,返回true // 2.如果URLRequest的query包含`code`,返回true // 3.如果都成功,我们就可以拿到`code` // 4.拿到`code`我们就可以换取`access_token`即登录成功
- 具体代码如下:
// 判断是否授权成功 if request.url?.absoluteString.hasPrefix(XQWBRedirectURI) == false { return true } if request.url?.query?.hasPrefix("code") == false { // 取消授权 close() return false } // 获取code let code = request.url?.query?.substring(from: "code=".endIndex) ?? "" // 发起网络请求 // 获取accessToken // 获取成功以后,发送登录的通知 return false
- 因为上面的代理方法中可以得到
-
3.4 封装单独的方法,用来在得到
code
以后换取access_token
/// 加载token /// /// - Parameters: /// - code: 授权码 /// - completion: 完成回调 func loadAccessToken(code: String, completion: @escaping (_ isSuccess:Bool)->()) { let urlString = "https://api.weibo.com/oauth2/access_token" let params = ["client_id":XQWBAppKey, "client_secret":XQWBAppSecret, "grant_type":"authorization_code", "code":code, "redirect_uri":XQWBRedirectURI] request(method: .POST, URLString: urlString, parameters: params) { (json, isSuccess) in // json就是我们获取到的用户信息 // 将用户信息保存到沙盒 } }
-
3.5 当获取到用户信息以后,我们将用户信息保存到沙盒,每次进入的时候,检测沙盒,就可以判断是否登录,从而展示不同的页面,为此,我们建立一个用户模型
XQWBUserAccount
- 声明相关属性
var access_token:String? //= "2.00PsIxFDFLz_lCe099af3af15lX_dB" var uid:String? var expires_in:TimeInterval = 0 { didSet { expiresDate = Date(timeIntervalSinceNow: expires_in) } } // 过期日期 // 过期日期 var expiresDate: Date?
- 重写
init()
方法,从沙盒读取文件
override init() { super.init() // 1. 从磁盘加载保存的文件, guard let path = accountFile.cz_appendDocumentDir(), let data = NSData(contentsOfFile: path), let dict = try?JSONSerialization.jsonObject(with: data as Data, options: []) as? [String:Any] else { return } // 2. 使用字典设置属性值 yy_modelSet(with: dict ?? [:]) // 3. 判断token是否过期 if expiresDate?.compare(Date()) != .orderedDescending { print("token过期") // 清空token access_token = nil; uid = nil; // 删除文件 _ = try?FileManager.default.removeItem(atPath: path) }
- 声明一个方法,将登录成功以后的数据保存到沙盒
func saveAccount() { // 1.模型转字典 var dict = (self.yy_modelToJSONObject() as? [String:Any]) ?? [:] // 2.字典序列化 dict.removeValue(forKey: "expires_in") guard let data = try? JSONSerialization.data(withJSONObject: dict, options: []), let fileName = accountFile.cz_appendDocumentDir() else { return } // 3.写入磁盘 (data as NSData).write(toFile: fileName, atomically: true) print("写入磁盘:\(fileName)") }
4.注册登录成功的通知,当登陆成功以后,重新加载首页
- 4.1 注册通知
NotificationCenter.default.addObserver(self,
selector: #selector(loginSuccess),
name: NSNotification.Name(rawValue: XQWBUserLoginSuccessNotification),
object: nil)
- 4.2 同样,要注销通知
deinit {
NotificationCenter.default.removeObserver(self)
}
- 4.3 实现通知的方法
@objc func loginSuccess(n: Notification) {
// print("登录成功")
// 当view = nil 的时候 会执行 loadView() -> ViewDidload()
// 清除nav左右按钮
navItem.leftBarButtonItem = nil
navItem.rightBarButtonItem = nil
view = nil
//注销通知 (避免ViewDidload()中重复注册)
NotificationCenter.default.removeObserver(self)
}
PS: 获取access_token
就告一段落了,下边我们就做一个启动页和欢迎页面
5.从服务器获取登录用户的信息,设置首页title
和欢迎页面的icon
-
5.1 封装请求用户信息的网络请求方法
func loadUserInfo(completion:@escaping (_ dict: [String:Any])->()) { guard let uid = userAccount.uid else { return } let urlStr = "https://api.weibo.com/2/users/show.json" let params = ["uid":uid] // 发起请求 tokenRequest(URLString: urlStr, parameters: params) { (json, isSuccess) in completion(json as? [String:Any] ?? [:]) } }
-
5.2 首页
title
按钮的文字在左边,图片在右边,我们可以封装一个单独的方法,来生成这样的按钮- 重载构造函数
// title 是nil ,显示首页,不是的话显示箭头和title init(title:String?) { super.init(frame: CGRect()) // 1. 判断title是否为nil if title == nil { setTitle("首页", for: .normal) }else { setTitle(title! + " ", for: .normal) // 设置图片 setImage(UIImage(named: "navigationbar_arrow_down"), for: .normal) setImage(UIImage(named: "navigationbar_arrow_up"), for: .selected) } // 2.设置字体颜色 titleLabel?.font = UIFont.boldSystemFont(ofSize: 17) setTitleColor(UIColor.darkGray, for: .normal) // 3.设置大小 sizeToFit() }
- 重新布局子视图
override func layoutSubviews() { super.layoutSubviews() guard let titleLabel = titleLabel, let imageView = imageView else { return } // 这个方法很关键 // 将 label 的 x 向左移动 imageView 的宽度 titleLabel.frame.origin.x = 0 // 将 imageView 的 x 向右移动 label 的宽度 imageView.frame.origin.x = titleLabel.bounds.width }
-
5.3 创建欢迎页面代码文件和xib文件
- 声明一个类方法,创建xib文件
class func welcomeView()-> XQWBWelcomeView { let nib = UINib(nibName: "XQWBWelcomeView", bundle: nil) let v = nib.instantiate(withOwner: nil, options: nil)[0] as! XQWBWelcomeView v.frame = UIScreen.main.bounds return v }
- 在
awakeFromNib()
中设置界面
override func awakeFromNib() { // 1. url guard let urlString = XQWBNetWorkManager.shared.userAccount.avatar_large, let url = URL(string: urlString) else { return } // 设置头衔 iconView.sd_setImage(with: url, placeholderImage: UIImage(named: "avatar_default_big")) // 设置圆角 iconView.layer.cornerRadius = iconView.bounds.width * 0.5 iconView.layer.masksToBounds = true }
- 在
didMoveToWindow()
中做头像的动画处理
// 视图被添加到window,做动画处理 override func didMoveToWindow() { super.didMoveToWindow() self.layoutIfNeeded() // 这里要先做一次更新约束,否则动画中我们会看到背景图片由小变大的bug bottomCons.constant = bounds.size.height - 200 UIView.animate(withDuration: 1.0, delay: 0.7, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: [], animations: { // 更新约束 self.layoutIfNeeded() }) { (_) in UIView.animate(withDuration: 1.0, animations: { self.tipLabel.alpha = 1 }, completion: { (_) in self.removeFromSuperview() }) } }
-
5.4 创建新特性页面代码文件和xib文件
- 声明一个类方法,创建xib文件
let nib = UINib(nibName: "XQWBNewFeatureView", bundle: nil) let v = nib.instantiate(withOwner: nil, options: nil)[0] as! XQWBNewFeatureView v.frame = UIScreen.main.bounds return v
- 在
awakeFromNib()
中设置界面
override func awakeFromNib() { // 如果使用自动布局设置的界面,从xib加载默认的大小是(600*600) // 添加4个图像视图 let count = 4 let rect = UIScreen.main.bounds for i in 0..<count { let imageName = "new_feature_\(i+1)" let iv = UIImageView(image: UIImage(named: imageName)) iv.frame = rect.offsetBy(dx: CGFloat(i) * rect.width, dy: 0) scrollView.addSubview(iv) } // 设置scrollView scrollView.contentSize = CGSize(width: CGFloat(count + 1) * rect.width, height: rect.height) scrollView.bounces = false scrollView.isPagingEnabled = true scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false scrollView.delegate = self // 隐藏按钮 enterButton.isHidden = true }
- 在
UIScrollViewDelegate
代理方法中,处理enterButton
和pageControl
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { // 1. scrollview 滚动到最后一屏幕,让视图删除 let page = Int(scrollView.contentOffset.x / scrollView.bounds.width) // 2. 判断是否是最后一页 if page == scrollView.subviews.count { removeFromSuperview() } // 3. 如果是倒数第二页,显示按钮 enterButton.isHidden = (page != scrollView.subviews.count - 1) } func scrollViewDidScroll(_ scrollView: UIScrollView) { // 0.滚动的时候隐藏按钮 enterButton.isHidden = true // 1. 计算当前偏移量 let page = Int(scrollView.contentOffset.x / scrollView.bounds.width + 0.5) print(page) // 2. 设置pageControl pageControl.currentPage = page // 3. pageControl 的隐藏 pageControl.isHidden = (page == scrollView.subviews.count) }
-
5.5 在
XQWBMainViewController
中获取版本号,如果是新的版本,就显示新特性,否则就显示欢迎页面fileprivate func setupNewFeatureViews() { // 0. 判断是否登录 if !XQWBNetWorkManager.shared.userLogon { return } // 1.检查版本 // 如果更新,显示新特性否则显示欢迎 let v = isNewVersion ? XQWBNewFeatureView.newFeatureView() : XQWBWelcomeView.welcomeView() view.addSubview(v) } // 在extension 中 不能定义存储属性,但是,可以声明计算型属性,因为计算型属性 private var isNewVersion: Bool { // 1.取版本号 //print(Bundle.main.infoDictionary) let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" // 2.取保存在Document() 中的版本号 let path:String = ("version" as NSString).cz_appendDocumentDir() let sandboxVersion = (try?String(contentsOfFile: path)) ?? "" // 3.将当前版本保存在沙盒 _ = try?currentVersion.write(toFile: path, atomically: true, encoding: .utf8) // 4.返回是否一致 return currentVersion == sandboxVersion } }