[Swift]原生第三方接入: 微信篇--集成/登录/分享/支付

文章涉及的demo在Github LQThirdParty, 欢迎Star | Fork

关于第三方登录/分享的接入, 很多时候使用的是友盟或者ShareSDK; 但并不是每次都想使用这些第三方的服务的, 这里作者整理了微信, QQ, 新浪微博原生第三方的接入:

[Swift]原生第三方接入: 微信篇--集成/登录/分享/支付
[Swift]原生第三方接入: QQ篇--集成/登录/分享
[Swift]原生第三方接入: 新浪微博篇--集成/登录/分享

一. 集成

1.1 新建应用

首先, 在微信-开放平台注册成为微信开发者, 然后新建APP, 获取相应的 AppIDAppSecret

1.2 集成SDK

微信官方集成文档

如果使用CocoaPods集成, 直接在Podfile文件添加:
pod 'WechatOpenSDK'

下载微信SDK: iOS开发工具包
解压后, 将文件内的以下三个文件拖入工程目录:

libWeChatSDK.a
WXApi.h
WXApiObject.h

添加系统依赖库

到Build Phases -> Link Binary With Libraries

SystemConfiguration.framework
Security.framework
CFNetwork.framework
CoreTelephony.framework
libz.dylib
libsqlite3.0.dylib
libc++.dylib
后面三个新版Xcode为:
libz.tbd
libsqlite3.0.tbd
libc++.tbd

添加 -Objc -all_load

来到Build Settings, 搜索other link 添加 -Objc -all_load

添加 -Objc -all_load
添加微信SDK文件路径

然后搜索search paths,添加微信SDK文件路径, 如果是在根目录下, 则不需要修改, 这里是根目录下:

设置路径
添加URL Scheme

来到Info-> URL Types, 点击左下角的 + 新加一个Scheme

scheme添加你的微信AppID即可

添加URL Scheme
适配iOS 9+ , 添加Scheme白名单
  • 方式一

在Info.plist文件内新加字段: LSApplicationQueriesSchemes, 类型为Array(数组)
然后添加内容, 类型为String(字符串)
微信需要添加以下字段:

wechat
weixin

  • 方式二

或者, 在Info.plist文件右键, Open as... -> Source Code, 可打开文件, 进行编辑, 在倒数第三行(即: </dict> 标签的上面)的空白处添加以下代码:

<key>LSApplicationQueriesSchemes</key>
<array>
        <string>wechat</string>
        <string>weixin</string>
</array>

如果还有其他平台的白名单需要添加, 例如QQ, 新浪微博, 只需要在<array></array>标签内添加对应的字段即可;

PS: Info.plist文件显示为Source Code后, 如果还想显示原来的列表格式, 可以: 邮件 -> Open as.. -> Property List 即可

适配iOS 9+, 网络请求
  • 方式一: 暂时回退到HTTP请求
    在Info.plist文件中添加字段: NSAppTransportSecurity, 类型为字典;
    然后添加一个Key:NSAllowsArbitraryLoads, 类型为Boolean, 值为 YES
回退到HTTP

或者以Source Code 打开Info.plist文件, 空白处添加以下代码:

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>
  • 方式二: 设置域

在项目的info.plist中添加一个Key:NSAppTransportSecurity,类型为字典类型。
然后给它添加一个值: NSExceptionDomains,类型为字典类型;
把需要的支持的域添加給NSExceptionDomains
其中域作为Key,类型为字典类型。
每个域下面需要设置3个属性:
NSIncludesSubdomains、
NSExceptionRequiresForwardSecrecy、NSExceptionAllowsInsecureHTTPLoads。

均为Boolean类型,值分别为YES、NO、YES

微信需要设置的域为:

QQ域

或者以Source Code 打开Info.plist文件, 空白处添加以下代码:

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>qq.com</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
            </dict>
        </dict>
    </dict>

PS: 这种方式需要对每个要以HTTP方式访问的域名进行设置, 比较麻烦, 建议使用第一种方式.

到此, 集成及适配结束...

PS: 在使用相关API的时候, 需要新建桥接头文件, 或者在已有桥接头文件内引用其头文件:

#import "WXApi.h"

二. 登录

微信的授权登录相比较于新浪微博和QQ授权复杂一些, 他需要我们调用相关的接口来获取相应的权限, 基本需要下面三个步骤:

  1. 获取 code
  1. 根据code, 获取accessToken
  2. 根据accessToken获取用户信息

在AppDelegate.swift中注册app:

WXApi.registerApp(wechatAppID)

在方法 func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool 中添加回调:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
  let urlKey: String = options[UIApplicationOpenURLOptionsKey.sourceApplication] as! String
   
  if urlKey == "com.tencent.xin" {
            // 微信 的回调
            return  WXApi.handleOpen(url, delegate: self)
        }
        
        return true
}

然后, 实现其代理方法:

func onReq(_ req: BaseReq!) {
        
    }
    
    func onResp(_ resp: BaseResp!) {
        // 这里是使用异步的方式来获取的
        let sendRes: SendAuthResp? = resp as? SendAuthResp
        let queue = DispatchQueue(label: "wechatLoginQueue")
        queue.async {
            
            print("async: \(Thread.current)")
            if let sd = sendRes {
                if sd.errCode == 0 {
                    
                    guard (sd.code) != nil else {
                        return
                    }
                    // 第一步: 获取到code, 根据code去请求accessToken
                    self.requestAccessToken((sd.code)!)
                } else {
                    
                    DispatchQueue.main.async {
                        // 授权失败
                    }
                }
            } else {
                
                DispatchQueue.main.async {
                   // 异常
                }
            }
        }
    }

根据第一步中获取到code来请求accessToken:

private func requestAccessToken(_ code: String) {
        // 第二步: 请求accessToken
        let urlStr = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=\(self.wechatAppID)&secret=\(self.wechatAppSecret)&code=\(code)&grant_type=authorization_code"
        
        let url = URL(string: urlStr)
        
        do {
            //                    let responseStr = try String.init(contentsOf: url!, encoding: String.Encoding.utf8)
            
            let responseData = try Data.init(contentsOf: url!, options: Data.ReadingOptions.alwaysMapped)
            
            let dic = try JSONSerialization.jsonObject(with: responseData, options: JSONSerialization.ReadingOptions.allowFragments) as? Dictionary<String, Any>
            
            guard dic != nil else {
                DispatchQueue.main.async {
                    // 获取授权信息异常
                }
                return
            }
            
            guard dic!["access_token"] != nil else {
                DispatchQueue.main.async {
                   获取授权信息异常
                }
                return
            }
            
            guard dic!["openid"] != nil else {
                DispatchQueue.main.async {
                    // 获取授权信息异常
                }
                return
            }
            // 根据获取到的accessToken来请求用户信息
            self.requestUserInfo(dic!["access_token"]! as! String, openID: dic!["openid"]! as! String)
        } catch {
            DispatchQueue.main.async {
                // 获取授权信息异常
            }
        }
    }

根据获取到的accessToken来请求用户信息:

private func requestUserInfo(_ accessToken: String, openID: String) {
        
        let urlStr = "https://api.weixin.qq.com/sns/userinfo?access_token=\(accessToken)&openid=\(openID)"
        
        let url = URL(string: urlStr)
        
        do {
            //                    let responseStr = try String.init(contentsOf: url!, encoding: String.Encoding.utf8)
            
            let responseData = try Data.init(contentsOf: url!, options: Data.ReadingOptions.alwaysMapped)
            
            let dic = try JSONSerialization.jsonObject(with: responseData, options: JSONSerialization.ReadingOptions.allowFragments) as? Dictionary<String, Any>
            
            guard dic != nil else {
                DispatchQueue.main.async {
                    // 获取授权信息异常
                }
                
                return
            }
            
            if let dic = dic {
                
                // 这个字典(dic)内包含了我们所请求回的相关用户信息
            }
        } catch {
            DispatchQueue.main.async {
               // 获取授权信息异常
            }
        }
    }

最后在需要发起登录的地方添加以下代码即可:

let req = SendAuthReq()
        req.scope = "snsapi_userinfo"
        req.state = "default_state"
        
        WXApi.send(req)

关于取消授权登录

微信授权成功后, 第三方的APP是无法主动取消授权的, 所谓的取消授权, 在第三方APP中表现即是重新吊起微信客户端进行授权登录, 知道了这些, 我们就可以自己做出一些取消授权的"假象". 是否吊起微信客户端, 在使用微信授权登录的时候, 重要的一个参数是scope, 即发起授权请求的 SendAuthReq 类的一个参数, 如果要想吊起微信客户端, 只需要将此值设置为 snsapi_userinfo, 如果在登录的有效期内, 不想每次都吊起微信客户端, 可不设置此参数, 或者置为空"", 则就不会吊起微信客户端, 或者设置一个参数来控制是否吊起微信客户端.

if isAlreadyAuthed {
            // 登录成功回调
        } else {
            let req = SendAuthReq()
            req.scope = "snsapi_userinfo"
            req.state = "default_state"
            
            WXApi.send(req)
        }

三. 分享

微信的分享API相对比较简单, 其官方的实例代码很清楚, 按照实例设置要分享的内容即可.
这里我使用Swift语言编写的如下:
以下代码中用到的 LDShareType 类型为我定义的一个枚举, 用来指定分享到哪里 :

enum LDShareType {
    case Session, Timeline, Favorite/*会话, 朋友圈, 收藏*/
}
分享文本
 func shareText(_ text: String, to scene: LDShareType) {
        
        let req = SendMessageToWXReq()
        req.text = text
        req.bText = true
        
        switch scene {
        case .Session:
            req.scene = Int32(WXSceneSession.rawValue)
        case .Timeline:
            req.scene = Int32(WXSceneTimeline.rawValue)
        case .Favorite:
            req.scene = Int32(WXSceneFavorite.rawValue)
        }
        
        WXApi.send(req)
    }
分享图片
func shareImage(_ data: Data, thumbImage: UIImage, title: String, description: String, to scene: LDShareType) {
        
        let message = WXMediaMessage()
        message.setThumbImage(thumbImage)
        message.title = title
        message.description = description
        
        let obj = WXImageObject()
        obj.imageData = data
        message.mediaObject = obj
        
        let req = SendMessageToWXReq()
        req.bText = false
        req.message = message
        
        switch scene {
        case .Session:
            req.scene = Int32(WXSceneSession.rawValue)
        case .Timeline:
            req.scene = Int32(WXSceneTimeline.rawValue)
        case .Favorite:
            req.scene = Int32(WXSceneFavorite.rawValue)
        }
        
        WXApi.send(req)
    }
分享音乐

这里我没有设置相应的数据, 只是示例代码, 根据自己的需求配置数据即可 :

func shareMusic(to scene: LDShareType) {
        let message = WXMediaMessage()
        message.title = "音乐标题"
        message.description = "音乐描述"
        message.setThumbImage(UIImage())
        
        let obj = WXMusicObject()
        obj.musicUrl = "音乐链接"
        obj.musicLowBandUrl = obj.musicUrl
        
        obj.musicDataUrl = "音乐数据链接地址"
        obj.musicLowBandDataUrl = obj.musicDataUrl
        message.mediaObject = obj
        
        let req = SendMessageToWXReq()
        req.bText = false
        req.message = message
        switch scene {
        case .Session:
            req.scene = Int32(WXSceneSession.rawValue)
        case .Timeline:
            req.scene = Int32(WXSceneTimeline.rawValue)
        case .Favorite:
            req.scene = Int32(WXSceneFavorite.rawValue)
        }
        
        WXApi.send(req)
    }
分享视频
func shareVideo(to scene: LDShareType) {
        
        let message = WXMediaMessage()
        message.title = "视频标题"
        message.description = "视频描述"
        message.setThumbImage(UIImage())
        
        let obj = WXVideoObject()
        obj.videoUrl = "视频链接地址"
        obj.videoLowBandUrl = "低分辨率视频地址"
        
        let req = SendMessageToWXReq()
        req.bText = false
        req.message = message
        
        switch scene {
        case .Session:
            req.scene = Int32(WXSceneSession.rawValue)
        case .Timeline:
            req.scene = Int32(WXSceneTimeline.rawValue)
        case .Favorite:
            req.scene = Int32(WXSceneFavorite.rawValue)
        }
        
        WXApi.send(req)
    }
分享URL
func shareURL(to scene: LDShareType) {
        
        let message = WXMediaMessage()
        message.title = "title"
        message.description = "description"
        message.setThumbImage(UIImage())
        
        let obj = WXWebpageObject()
        obj.webpageUrl = "http://www.baidu.com"
        message.mediaObject = obj
        
        let req = SendMessageToWXReq()
        req.bText = false
        req.message = message
        
        switch scene {
        case .Session:
            req.scene = Int32(WXSceneSession.rawValue)
        case .Timeline:
            req.scene = Int32(WXSceneTimeline.rawValue)
        case .Favorite:
            req.scene = Int32(WXSceneFavorite.rawValue)
        }
        
        WXApi.send(req)
    }

以上分享的结果回调同样是在 ** func onResp(_ resp: BaseResp!) ** 代理方法内获得, 通过判断resp的类型来区分是登录还是分享:

func onResp(_ resp: BaseResp!) {
        if resp is SendAuthResp {
            // 微信登录
           
        } else if resp is SendMessageToWXResp {
            let send = resp as? SendMessageToWXResp
            if let sm = send {
                if sm.errCode == 0 {
                    print("分享成功")
                } else {
                    print("分享失败")
                }
            }
        }
        
    }

四. 支付

微信支付详细接入流程, 请参考本人的另一篇文章: [iOS]微信支付接入详解, 大致过程和本文的 第一部分. 集成很想相似. 这里不再做过的介绍,只给出Swift的写法 :
发起微信支付

class func pay(to identifier: String, _ dic: [String: String], resultHandle: LDWechatShare_payResultHandle? = nil) {
        
        LDWechatShare.shared.payResultHandle = resultHandle
        LDWechatShare.shared.payIdentifier = identifier
        
        let req = PayReq()
        
        req.partnerId = dic["partnerid"]!
        req.prepayId = dic["prepayid"]!
        req.package = dic["package"]!
        req.nonceStr = dic["noncestr"]!
        req.timeStamp = UInt32(dic["timestamp"]!)!
        req.sign = dic["sign"]
        
        WXApi.send(req)
    }

这里吊起微信支付的参数(即PayReq的参数), 中需要注意的是sign, 这里没有做任何签名, 所有的都是后台完成的, 包括该值的二次签名, 一定要二次签名, 不然掉不起微信支付;

获取支付结果:

func onResp(_ resp: BaseResp!) {
        if resp is SendAuthResp {
            // 微信登录
        } else if resp is SendMessageToWXResp {
            // 分享
        } else if resp is PayResp { 
            // 支付
            var rs: LDWechatPayResult = .Failed
            
            switch resp.errCode {
            case WXSuccess.rawValue:
                rs = .Success
            case WXErrCodeUserCancel.rawValue:
                rs = .Cancel
            default:
                rs = .Failed
            }
            
            if let handle = self.payResultHandle {
                handle(rs, self.payIdentifier)
            }
        }
    }

这里我是已闭包的形式将支付结果回调的, 可能我设置一个标示符参数** identifier** 会使用不到, 这个是用来标识是哪个控制器发起的微信支付请求, 因为一个工程里, 可能发起支付的控制器会不止一个, 当有两个以上的控制器被创建时, 就有可能是多个控制器收到支付结果. 如果是使用通知来发送支付结果, 为避免多个控制器收到支付结果, 可能就会使用到这个标示符.

以上便是微信登录分享的所有内容, 如有不正确的地方, 还请评论指出, 或者私信;

文章涉及的demo在Github LQThirdParty, 欢迎Star | Fork

(完)

推荐阅读更多精彩内容