No3 彈出菜單和二維碼

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/ab20514/article/details/50434298

一 顯示彈出菜單

  • 點擊用戶名彈出菜單
// 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(獲取登錄界面)

        注意點:

        1. appkey不能寫錯 → (error:invalid_client)
        2. redirect_uri不能寫錯 →(error:redirect_uri_mismatch)
        3. 整個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")
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章