一 显示弹出菜单
- 点击用户名弹出菜单
// MARK: - 监听点击事件
@objc private func titleBtnClick(titleBtn: XMGTitleButton)
{
// 1.修改标题按钮箭头的方向
titleBtn.selected = !titleBtn.selected
// 2.创建菜单
let sb = UIStoryboard(name: "PopoverViewController", bundle: nil)
// 设置转场保证 modol 出一个新view时,不会盖住后面的 view
if let menuVC = sb.instantiateInitialViewController()
{
// 2.1设置负责自定义转场的代理
menuVC.transitioningDelegate = self
// 2.2设置转场的样式
menuVC.modalPresentationStyle = UIModalPresentationStyle.Custom
// 3.弹出菜单
presentViewController(menuVC, animated: true, completion: nil)
}
}
- 转场菜单展示内容(自定义类XMGPresentationController)
class XMGPresentationController: UIPresentationController {
/*
第一个参数:presentedViewController: 被展现的对象
第二个参数:presentingViewController: 发起转场的对象(在Xcode6中系统传入的是nil, 在Xcode7中系统传入的是野指针)
*/
override init(presentedViewController: UIViewController, presentingViewController: UIViewController) {
super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController)
NJLog(presentedViewController)
// NJLog(presentingViewController)
}
/*
containerView: 所有被展现的内容都放在containerView上
presentedView(): 通过该方法就可以拿到被展现的视图
*/
override func containerViewWillLayoutSubviews()
{
super.containerViewWillLayoutSubviews()
// 1.添加蒙版
containerView?.insertSubview(cover, atIndex: 0)
// 2.设置蒙版frame
cover.frame = containerView!.bounds
// 3.调整被展现视图的大小
presentedView()?.frame = CGRect(x: 100, y: 56, width: 200, height: 200)
}
}
- 点击菜单外区域,菜单消失
- 添加一个大的蒙版
// MARK: - 内部控制方法(消失菜单)
@objc private func coverClick()
{
presentedViewController.dismissViewControllerAnimated(true, completion: nil)
}
// MARK: -懒加载
private lazy var cover: UIButton = {
let customView = UIButton()
customView.backgroundColor = UIColor(white: 0.5, alpha: 0.2)
customView.addTarget(self, action: Selector("coverClick"), forControlEvents: UIControlEvents.TouchUpInside)
return customView
}()
- 转场动画代理方法
extension HomeTableViewController: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning
{
// MARK: - UIViewControllerTransitioningDelegate
/**
该方法用于返回负责转场的对象
*/
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController?
{
return XMGPresentationController(presentedViewController: presented, presentingViewController: presenting)
}
/**
告诉系统谁来负责转场如何出现
*/
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
isPesented = true
return self
}
/**
告诉系统谁来负责转场如何消失
*/
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
isPesented = false
return self
}
// MARK: - UIViewControllerAnimatedTransitioning
/// 告诉系统转场动画出现和消失需要多长时间
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
{
return 0.5
}
/// 无论转场动画出现还是消失都会调用这个方法, 我们需要在这个方法中自定义转场动画的样式
// transitionContext: 上下文, 该上下文中就包含了我们需要的所有数据
func animateTransition(transitionContext: UIViewControllerContextTransitioning)
{
let duration = transitionDuration(transitionContext)
if isPesented
{
NJLog("展现")
// 1.拿到被展现的视图
// 如果是展现, 那么我们需要修改toVC, 如果是消失我们需要修改formVC
// 被展现的View
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// 注意: 一旦自定义转场这时系统就不会帮我们做任何操作, 包括将需要展示的view添加到containerview上也不会帮我们添加
transitionContext.containerView()?.addSubview(toView)
// 2.控制被展现的视图如何显示和消失
// _ 忽略该参数
toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
toView.transform = CGAffineTransformMakeScale(1.0, 0.0)
UIView.animateWithDuration(duration, animations: { () -> Void in
toView.transform = CGAffineTransformIdentity
}) { (_) -> Void in
// 注意: 如果是自定义转场, 一定要在动画执行完毕之后告诉系统动画已经执行完毕, 否则有可能引发一些未知的错误
transitionContext.completeTransition(true)
}
}else
{
NJLog("消失")
let formView = transitionContext.viewForKey(UITransitionContextFromViewKey)
// 注意: 消失动画一下子就不见了的原因是因为CGFloat是不准确的
// 想解决这个问题, 只需要将y的CGFloat的值改为一个非常小得值即可
UIView.animateWithDuration(duration, animations: { () -> Void in
formView?.transform = CGAffineTransformMakeScale(1.0, 0.0001)
}, completion: { (_) -> Void in
// 注意: 如果是自定义转场, 一定要在动画执行完毕之后告诉系统动画已经执行完毕, 否则有可能引发一些未知的错误
transitionContext.completeTransition(true)
})
}
}
二 二维码布局搭建
- 定义首页右侧导航条
@objc private func rigthBtnClick()
{
// 1.创建二维码控制器
let sb = UIStoryboard(name: "QRCodeViewController", bundle: nil)
let QRCodeVC = sb.instantiateInitialViewController()!
// 2.展现二维码控制器
presentViewController(QRCodeVC, animated: true, completion: nil)
}
- 默认选中二维码 TabBar
// MARK: - 生命周期方法
override func viewDidLoad() {
super.viewDidLoad()
// 1.设置默认选中
customTabbar.selectedItem = customTabbar.items![0]
customTabbar.delegate = self
}
- 冲击波实现
- 设置约束冲击波和边框顶部对齐,修改冲击波顶部约束可以实现扫描功能
/// 自定义UITabBar
@IBOutlet weak var customTabbar: UITabBar!
/// 冲击波顶部约束
@IBOutlet weak var scanLineTopCons: NSLayoutConstraint!
/// 容器高度
@IBOutlet weak var containerHeightCons: NSLayoutConstraint!
/// 冲击波
@IBOutlet weak var scanLineView: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
startAnimation()
}
// MARK: - 内部控制方法
/**
开始冲击波动画
*/
private func startAnimation()
{
// 1.初始化冲击波位置
scanLineTopCons.constant = -containerHeightCons.constant
view.layoutIfNeeded()
// 2.执行动画
UIView.animateWithDuration(1.0, animations: { () -> Void in
// 告诉系统该动画需要重复
UIView.setAnimationRepeatCount(MAXFLOAT)
self.scanLineTopCons.constant = self.containerHeightCons.constant
self.view.layoutIfNeeded()
})
}
}
- 二维码和条形码切换
extension QRCodeViewController: UITabBarDelegate
{
func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
// NJLog(item.tag)
containerHeightCons.constant = (item.tag == 0) ? 300 : 150
view.layoutIfNeeded()
// 先移除图层上所有的动画
scanLineView.layer.removeAllAnimations()
// 再重新开启动画
startAnimation()
}
}
三 扫描二维码
// MARK: - 懒加载
/// 创建输入
private lazy var deviceInput: AVCaptureDeviceInput? = {
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
do{
let input = try AVCaptureDeviceInput(device: device)
return input
}catch
{
return nil
}
}()
/// 创建输出
private lazy var output: AVCaptureMetadataOutput = AVCaptureMetadataOutput()
/// 创建会话
private lazy var session: AVCaptureSession = AVCaptureSession()
/// 创建预览图层
private lazy var previewLayer: AVCaptureVideoPreviewLayer = {
let layer = AVCaptureVideoPreviewLayer(session: self.session)
layer.frame = self.view.bounds
return layer
}()
- 开始扫描
// MARK: - 内部控制方法
private func startScan()
{
// 1.判断输入是否可以添加到会话中
if !session.canAddInput(deviceInput)
{
return
}
// 2.判断输出是否可以添加到会话中
if !session.canAddOutput(output)
{
return
}
// 3.添加输入和输出
session.addInput(deviceInput)
session.addOutput(output)
// 4.设置输出可以解析的数据类型
// 注意点: 设置输出对象能够解析的数据类型, 必须在输出对象添加到会话之后设置
output.metadataObjectTypes = output.availableMetadataObjectTypes
output.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
// 5.添加预览图层
view.layer.insertSublayer(previewLayer, atIndex: 0)
// 5.开始扫描
session.startRunning()
}
- 实现代理方法
extension QRCodeViewController: AVCaptureMetadataOutputObjectsDelegate
{
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)
{
if metadataObjects.count > 0
{
NJLog((metadataObjects.last as! AVMetadataMachineReadableCodeObject).stringValue)
}
}
}
四 二维码描边
找到二维码4个点,连线描边
- bounds
- corners
坐标转换
// MARK: - 扫描数据代理
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
for object in metadataObjects {
let dataObject = previewLayer.transformedMetadataObjectForMetadataObject(object as! AVMetadataObject) as! AVMetadataMachineReadableCodeObject
print(dataObject)
}
}
- 转换结果
# 转换前
<AVMetadataMachineReadableCodeObject: 0x170220720,
type="org.iso.QRCode",
bounds={ 0.4,0.4 0.1x0.2 }>
corners { 0.4,0.6 0.5,0.6 0.5,0.4 0.4,0.4 },
time 155921691680958,
stringValue "http://weibo.cn/qr/userinfo?uid=5365823342"
# 转换后
<AVMetadataMachineReadableCodeObject: 0x170622cc0,
type="org.iso.QRCode",
bounds={ 116.6,224.9 79.5x80.0 }>
corners { 116.6,226.1 117.2,304.4 196.1,304.9 195.7,224.9 },
time 155921691680958,
stringValue "http://weibo.cn/qr/userinfo?uid=5365823342"
转换的目的是将采集到的坐标转换成能够识别的坐标数值
- 绘制图层
/// 绘制图层
lazy var drawLayer: CALayer = {
return CALayer()
}()
- 添加图层
/// 设置图层
func setupLayers() {
drawLayer.frame = view.bounds
view.layer.insertSublayer(drawLayer, atIndex: 0)
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
previewLayer.frame = view.bounds
view.layer.insertSublayer(previewLayer, atIndex: 0)
}
注意:一定要用
insertSublayer
,否则会遮挡住TabBar
- 创建路径 & 绘制条码形状
/// 绘制条码形状
private func drawCornersShape(dataObject: AVMetadataMachineReadableCodeObject) {
// 判断数组是否为空
if dataObject.corners.isEmpty {
return
}
let layer = CAShapeLayer()
layer.lineWidth = 4
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.clearColor().CGColor
layer.path = cornersPath(dataObject.corners)
// 添加到绘图图层
drawLayer.addSublayer(layer)
}
/// 创建边线路径
///
/// -parameter corners: 边角顶点数组
private func cornersPath(corners: NSArray) -> CGPathRef {
let path = UIBezierPath()
var point = CGPoint()
// 1. 移动到第一个点
var index = 0
CGPointMakeWithDictionaryRepresentation((corners[index++] as! CFDictionaryRef), &point)
path.moveToPoint(point)
// 2. 遍历剩余的点
while index < corners.count {
CGPointMakeWithDictionaryRepresentation((corners[index++] as! CFDictionaryRef), &point)
path.addLineToPoint(point)
}
// 3. 关闭路径
path.closePath()
return path.CGPath
}
注意
*corners
是保存CFDictionary
对象的数组
* 一定要判断corners
是否包含数据,否则会崩溃
- 清空绘图图层
/// 清空绘图图层
private func clearDrawLayer() {
if drawLayer.sublayers == nil {
return
}
for layer in drawLayer.sublayers! {
layer.removeFromSuperlayer()
}
}
注意:一定要判断
subLayers
否则会崩溃
- 调整后的代码
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
clearDrawLayer()
for object in metadataObjects {
if object is AVMetadataMachineReadableCodeObject {
let dataObject = previewLayer.transformedMetadataObjectForMetadataObject(object as! AVMetadataObject) as! AVMetadataMachineReadableCodeObject
drawCornersShape(dataObject)
print(dataObject.stringValue)
}
}
}
一定要判断一下 object 的类型,否则遇到非
CodeObject
会直接崩溃
- 二维码的扫描范围
/// 创建输出
private lazy var output: AVCaptureMetadataOutput = {
let output = AVCaptureMetadataOutput()
let containerFrame = self.containerView.frame
let size = UIScreen.mainScreen().bounds.size
// 注意:
// 1.rectOfInterest 接收的都是比例
// 2.rectOfInterest是按照横屏的左上角为原点
output.rectOfInterest = CGRect(x: containerFrame.origin.y / size.height, y: containerFrame.origin.x / size.width, width: containerFrame.size.height / size.height, height: containerFrame.size.width / size.width)
return output
}()
- 生成二维码
/// 生成二维码
private func generateQRCodeImage() -> UIImage {
// 1. 生成二维码
let qrFilter = CIFilter(name: "CIQRCodeGenerator")!
qrFilter.setDefaults()
qrFilter.setValue("极客江南".dataUsingEncoding(NSUTF8StringEncoding), forKey: "inputMessage")
let ciImage = qrFilter.outputImage
// 2. 缩放处理
let transform = CGAffineTransformMakeScale(10, 10)
let transformImage = ciImage.imageByApplyingTransform(transform)
// 3. 颜色滤镜
let colorFilter = CIFilter(name: "CIFalseColor")!
colorFilter.setDefaults()
colorFilter.setValue(transformImage, forKey: "inputImage")
// 前景色
colorFilter.setValue(CIColor(color: UIColor.blackColor()), forKey: "inputColor0")
// 背景色
colorFilter.setValue(CIColor(color: UIColor.whiteColor()), forKey: "inputColor1")
let outputImage = colorFilter.outputImage
return insertAvatarImage(UIImage(CIImage: outputImage), avatar: UIImage(named: "avatar")!)
}
- 插入头像
func insertAvatarImage(qrimage: UIImage, avatar: UIImage) -> UIImage {
UIGraphicsBeginImageContext(qrimage.size)
let rect = CGRect(origin: CGPointZero, size: qrimage.size)
qrimage.drawInRect(rect)
let w = rect.width * 0.2
let x = (rect.width - w) * 0.5
let y = (rect.height - w) * 0.5
avatar.drawInRect(CGRect(x: x, y: y, width: w, height: w))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
五 OAuth授权
OAuth授权
- 可以让第三方客户端在不知道用户的账号密码的前提下授权
新浪OAuth授权步骤
- 1>成为新浪开发者
- open.weibo.com → 注册一个账号
- 2>在新浪开发者平台创建一个应用程序
- http://open.weibo.com/apps/new?sort=mobile
- 拿到App Key和App Secret
3>获取授权
3.1 获取未授权的RequestToken(获取登录界面)
注意点:
- appkey不能写错 → (error:invalid_client)
- redirect_uri不能写错 →(error:redirect_uri_mismatch)
- 整个URL中不能出现空格 →(error:invalid_request)
3.2 获取已经授权的RequestToken(让用户登录)
- 只要用户登录成功, 就会自动跳转到回调页面
- 在回调界面地址的后面会跟上一个code= , code=后面的字符串就是已经授权的RequestToken
- 如果取消授权也会调整到回调界面, 但是URL后面没有code=
- 只要用户登录成功, 就会自动跳转到回调页面
3.3利用已经授权的RequestToken换取AccessToekn
加载授权页面
- 准备工作
- 新建
OAuth
文件夹 - 新建
OAuthViewController.swift
继承自UIViewController
- 新建
加载 OAuth 视图控制器
修改
BaseTableViewController
中用户登录部分代码/// 用户登录 func visitorLoginButtonClicked() { let oauth = OAuthViewController() let nav = UINavigationController(rootViewController: oauth) presentViewController(nav, animated: true, completion: nil) }
在
OAuthViewController
中添加以下代码lazy var webView: UIWebView = { return UIWebView() }() override func loadView() { view = webView title = "XXX微博" navigationItem.rightBarButtonItem = UIBarButtonItem(title: "关闭", style: UIBarButtonItemStyle.Plain, target: self, action: "close") } /// 关闭 func close() { dismissViewControllerAnimated(true, completion: nil) }
- 准备工作
获取已经授权的RequestToken
/*
webView的该代理方法用于控制是否允许发起请求
*/
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
/*
授权成功: http://www.520it.com/?code=7fd52d029ec1578775d3abec426e878a
授权失败: http://www.520it.com/?error_uri=%2Foauth2%2Fauthorize&error=access_denied&error_description=user%20denied%20your%20request.&error_code=21330
其它界面: https://api.weibo.com/oauth2/ 开头
*/
NJLog(request.URL!)
// 1.判断是否是授权回调页面
guard let urlStr = request.URL?.absoluteString where urlStr.hasPrefix("http://www.520it.com") else
{
return true
}
// 2.判断是否授权成功
guard !urlStr.containsString("?error_uri=") else
{
SVProgressHUD.showErrorWithStatus("授权失败", maskType: SVProgressHUDMaskType.Black)
return false
}
// 3.授权成功, 截取code=后面的内容
if let str = request.URL?.query
{
// 3.1截取code=后面的字符串
// let code = (str as NSString).substringFromIndex("code=".characters.count)
let code = str.substringFromIndex("code=".endIndex)
NJLog(code)
}
return false
}
- 获取AccessToken
/**
根据Code换取AccessToken
- parameter code: 已经授权的RequestToken
*/
private func loadAccessToken(code: String)
{
let path = "oauth2/access_token"
// 1.拼接参数
let parameters = ["client_id": WB_App_Key, "client_secret": WB_App_Secret, "grant_type": "authorization_code", "code": code, "redirect_uri": WB_Redirect_URI]
// 2.发送请求
NetworkTools.shareInstance.POST(path, parameters: parameters, success: { (_, dict) -> Void in
// "access_token" = "2.00SqDL_C04G7Sw87a2e20ec9819trD";
NJLog(dict)
}) { (_, error) -> Void in
NJLog(error)
}
}
- 保存AccessToken,UserAccountModel
class UserAccountModel: NSObject, NSCoding {
var access_token: String?
var expires_in: NSNumber?
var uid: String?
// MAKR: - 生命周期方法
init(dict: [String: AnyObject])
{
super.init()
setValuesForKeysWithDictionary(dict)
}
override func setValue(value: AnyObject?, forUndefinedKey key: String) {
}
override var description: String {
let property = ["access_token", "expires_in", "uid"]
let dict = dictionaryWithValuesForKeys(property)
return "\(dict)"
}
// MARK: - 外部控制方法
func saveUserAccount() -> Bool
{
let docPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!
let filePath = (docPath as NSString).stringByAppendingPathComponent("useraccount.plist")
NJLog("filePath = \(filePath)")
return NSKeyedArchiver.archiveRootObject(self, toFile: filePath)
}
- 归档
// MARK: - NSCoding
// 将对象写入到文件中
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
}
// 从文件中读取对象
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeObject(expires_in, forKey: "expires_in")
aCoder.encodeObject(uid, forKey: "uid")
}