如何构造Swift框架(4)推送服务

[中文教程]
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看165 回复回复1 收藏收藏2 分享淘帖 转播转播 分享分享 微信
查看: 165|回复: 1
收起左侧

[中文教程] 如何构造Swift框架(4)推送服务

[复制链接]
Caesar 发表于 2016-10-14 10:22:44 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑
本帖最后由 Caesar 于 2016-10-14 11:14 编辑

上期提到了使用Moya作为网络基础模块,但是涉及到了一个sampleData的问题,我们也是即时的提交了一个issue来质问这样的默认Response data为什么类型竟然是Optional的。Moya的开发者举例:可以将上一次获取到的数据在需要的时候(网络请求失败)传入这里,所以进而给出建议:将var sampleData改为var cachePolicy进行缓存控制即可,缓存过期的时间由Server端使用Cache-control或Expires决定,目前有的回复是,作者觉得这个建议很棒,说不定有机会为Moya加入缓存机制。接下来继续我们的开发计划:




推送服务

应当明确的是,每家公司用的推送第三方都是不同的(大部分是阿里云、极光、个推),所以继承第三方SDK这个事情不应该出现在框架中。框架仅仅负责申请推送能力即可。测试:在测试之前,Info.plist中所需要申请权限的Key需要自己手动配置。Xcode 8 后打开推送需要在程序中打开选项:

如何构造Swift框架(4)推送服务 1

如何构造Swift框架(4)推送服务 - 敏捷大拇指 - 如何构造Swift框架(4)推送服务 1


这样,如果是单单写权限的话,直接用之前我们引入的PermissionScope就可以搞定了,Push的class可以写为open的,因为每个项目对Push的需求不同,所以在Push中我们顺便截获一下信息然后提供给用户,也很简单。所以获得推送权限的需求我们放到Permission.swift中。由于屏幕限制,所以Permission也最多允许大家同时打开3个权限。改写之前的Permission.swift:

[Swift] 纯文本查看 复制代码
import Foundation
import PermissionScope
public enum INSPermissionType {
  case notification(Set<UIUserNotificationCategory>?, String)
  case locationAlways(String)
  case locationWhenInUse(String)
  case contact(String)
  case event(String)
  case microphone(String)
  case camera(String)
  case photos(String)
  case reminders(String)
  case bluetooth(String)
  case motion(String)
}
open class Permission {
  open static let `default` = Permission()
  static let pscope: PermissionScope = {
    let permissionScope = PermissionScope()
    // Default customs
    permissionScope.headerLabel.text = "嗨,你好!"
    permissionScope.bodyLabel.text = "在使用我们的应用之前\n我们需要你做一些事情:"
    permissionScope.closeButtonTextColor = UIColor.clear
    permissionScope.permissionButtonΒorderWidth = 0.5
    permissionScope.permissionButtonCornerRadius = 2
    /// 如果你希望更改权限开启按钮的英文,就需要自己配置本地化文件
    /// 参考这里 [url=https://github.com/nickoneill/PermissionScope/pull/12#issuecomment-96428580]https://github.com/nickoneill/Pe ... suecomment-96428580[/url]
    return permissionScope
  }()
  
  open class func requestPermission(_ permissionTypes: [INSPermissionType], _ authChange: authClosureType? = nil, cancelled: cancelClosureType? = nil) {
    for item in permissionTypes {
      switch item {
      case .notification(let categories, let message):
        pscope.addPermission(NotificationsPermission(notificationCategories: categories), message: message)
        continue
      case .locationAlways(let message):
        pscope.addPermission(LocationWhileInUsePermission(), message: message)
        continue
      case .locationWhenInUse(let message):
        pscope.addPermission(LocationWhileInUsePermission(), message: message)
        continue
      case .contact(let message):
        pscope.addPermission(ContactsPermission(), message: message)
        continue
      case .event(let message):
        pscope.addPermission(EventsPermission(), message: message)
        continue
      case .microphone(let message):
        pscope.addPermission(MicrophonePermission(), message: message)
        continue
      case .camera(let message):
        pscope.addPermission(CameraPermission(), message: message)
        continue
      case .photos(let message):
        pscope.addPermission(PhotosPermission(), message: message)
        continue
      case .reminders(let message):
        pscope.addPermission(RemindersPermission(), message: message)
        continue
      case .bluetooth(let message):
        pscope.addPermission(BluetoothPermission(), message: message)
        continue
      case .motion(let message):
        pscope.addPermission(MotionPermission(), message: message)
        continue
      default:
        continue
      }
    }
    
    pscope.show(authChange, cancelled: cancelled)
  }
}


然后测试效果(别忘记在Info.plist中添加相关的请求权限的Key-Desc):

[Swift] 纯文本查看 复制代码
let permissionTypes = [
  INSPermissionType.notification(nil, "打开推送服务"),
  INSPermissionType.camera("打开相机服务"),
  INSPermissionType.photos("希望使用照片")
]
    
Permission.requestPermission(permissionTypes)


如何构造Swift框架(4)推送服务 2

如何构造Swift框架(4)推送服务 - 敏捷大拇指 - 如何构造Swift框架(4)推送服务 2


原本计划是要写Push.swift进行截获数据的,但是总感觉这样做貌似不太合理。所以索性我们止只统计一下用户收到推送好了,在Push.swift中仅提供一个方法入口,把推送的内容传进来供我们内部处理。不要干涉AppDelegate处理推送了,而且在iOS10之后要做版本兼容,使用UNUserNotificationCenterDelegate来处理推送,而且大多数第三方SDK都会有自己的处理方式。

所以在我们的Push.swift中,我们先预留一些代码:

等会儿先看个东西:

如何构造Swift框架(4)推送服务 3

如何构造Swift框架(4)推送服务 - 敏捷大拇指 - 如何构造Swift框架(4)推送服务 3


好、继续写代码:

[Swift] 纯文本查看 复制代码
final public class Push {
  public static let `default` = Push()
  
  public func DeviceToken(_ deviceToken: Data) {
    
  }
  
  public func ReceivedPushMessage (_ userInfo: [AnyHashable : Any]) {
    
  }
  
  private init() {
    
  }
}


简单的预留一些方法入口即可,不急着写,接着往下写日志上报(直接改造之前的Logger类):

[Swift] 纯文本查看 复制代码
final public class INSLogger {
  /// 默认为输出全部日志
  public static let `default` = INSLogger()
  
  /// 日志级别
  public var level: LogLevel = .all
  /// 是否上报崩溃
  public var crashCollect: Bool = true
  
  /// 日志输出
  ///
  /// - parameter lev: 日志级别
  /// - parameter content: 日志内容
  public func printLog(_ lev: LogLevel, _ details: String, _ items: Any) {
    guard level == .all || level == lev, ModeSwitcher.currentMode == .develope else {
      return
    }
    
    print(lev.rawValue, details, "\n", items)
  }
  private var exception: NSException? = nil
  public func setUncaughtException() {
    NSSetUncaughtExceptionHandler {
      let exception = $0
      let name = exception.name
      let reason = exception.reason ?? "Without system crash version."
      let callStack = exception.callStackSymbols
      let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
      
      // TODO: 上报
    }
  }
}


获取到崩溃的信息后,我们在这里加一个TODO标签。 这里需要注意的是:框架外部如果也需要做日志捕获,那么需要先使用NSGetUncaughtExceptionHandler()获取当前的捕获器,在自己的捕获成功之后也让别人的捕获成功。啊好累啊,这还不是完整的奔溃捕获,于是我们接着写代码(写代码到时无妨,主要是这里有一坑爹的事情,需要自己去查看解决,说明:无法把方法传入这些捕获方法,也附上气前一个链接中的代码):

如何构造Swift框架(4)推送服务 4

如何构造Swift框架(4)推送服务 - 敏捷大拇指 - 如何构造Swift框架(4)推送服务 4


慵懒的完善了signal后(上边提到的不能使用C方法的问题自己去解决把,这里仅仅是展示,所以不写那么详细了):

[Swift] 纯文本查看 复制代码
/// 设置异常捕获
  public func setUncaughtException() {
    
    NSSetUncaughtExceptionHandler {
      let exception = $0
      let name = exception.name
      let reason = exception.reason ?? "Without system crash version."
      let callStack = exception.callStackSymbols
      let crashLog = "name:\(name)\nreason:\(reason)\ncallStack:\(callStack.joined(separator: "\n"))"
      
      exception.raise()
      // TODO: 上报
    }
    
    signal(SIGILL) {
      let crashLog = "SignalRaisedException(\($0)): Illegal instruction (not reset when caught)"
      // TODO: 上报
    }
    signal(SIGABRT)  {
      let crashLog = "SignalRaisedException(\($0)): Abort, abort()"
      // TODO: 上报
    }
    signal(SIGFPE)  {
      let crashLog = "SignalRaisedException(\($0)): Floating point exception"
      // TODO: 上报
    }
    signal(SIGBUS)  {
      let crashLog = "SignalRaisedException(\($0)): Bus Error"
      // TODO: 上报
    }
    signal(SIGSEGV)  {
      let crashLog = "SignalRaisedException(\($0)): segmentation violation"
      // TODO: 上报
    }
    signal(SIGSYS)  {
      let crashLog = "SignalRaisedException(\($0)): Bad argument to system call"
      // TODO: 上报
    }
    signal(SIGPIPE)  {
      let crashLog = "SignalRaisedException(\($0)): Write on a pipe with no one to read it"
      // TODO: 上报
    }
  }
  
  public func unSetUncaughtException() {
    NSSetUncaughtExceptionHandler(nil)
    signal(SIGILL, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGSYS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
  }


这里只捕获了一部分signal,点进去自己看了解下,我之前也写过一篇关于日志捕获的文章,可以去找找。继续往下写:信息收集,新建swift文件Analytics.swift,这里我只给出一部分思路(完整的Analytics又是一个独立的框架,建议参考的是开源的ZhugeIO):

[Swift] 纯文本查看 复制代码
import Foundation
import LKDBHelper
let AnalyticsManagerFlushedFlagKey = "AnalyticsManagerFlushedFlagKey"
class AnalyticsItem: NSObject {
  /// 事件名称
  var eventName: String
  /// 事件数据
  var parameters: [String: Any]?
  
  func toAnalytice() ->[String: String] {
    return [eventName ?? "": "\(parameters ?? ["": ""])"]
  }
  
  init(_ eventName: String, _ parameters: [String: Any]? = nil) {
    self.eventName = eventName
    self.parameters = parameters
  }
  
  override static func getTableName() -> String {
    return "AnalyticsItem"
  }
}
final public class Analytics {
  public static let `default` = Analytics()
  public typealias FlushHandler = (_ info: [String], _ analyticsData: [[String: String]])->()
  
  /// 设备唯一标识,默认是UUID
  public var deviceIdentifier: String
  /// 设备用户标识,以设备标识为准
  public var userIdentifier: String
  /// 上报间隔,会调用上报的方法,外部控制网络请求
  public var flushInterval: Int = 10
  /// 上报的回调方法
  public var flushHandler: FlushHandler?
  
  /// 存储准备上报的数组
  private var analyticsItems: [AnalyticsItem] = []
  /// 是否已经上报,通过检查本地值来确定
  private var flushed: Bool
  private var timer: Timer? = nil
  
  /// 追踪事件
  public func track(_ eventName: String, _ parameters: [String: Any]? = nil) {
    if analyticsItems.count == 0 {
      startTimer()
    }
    analyticsItems.append(AnalyticsItem(eventName, parameters))
  }
  
  /// 主动上报到服务器
  public func flush() {
    guard analyticsItems.count > 0, let handler = flushHandler else {
      return
    }
    handler([deviceIdentifier, userIdentifier], analyticsItems.map { return $0.toAnalytice() })
    stopTimer()
    flushed = true
  }
  
  private func startTimer() {
    stopTimer()
    timer = Timer.init(timeInterval: TimeInterval(flushInterval), target: self, selector: "flush", userInfo: nil, repeats: true)
    RunLoop.current.add(timer!, forMode: .commonModes)
  }
  
  private func stopTimer() {
    timer?.invalidate()
    timer = nil
    analyticsItems.removeAll()
  }
  
  private func getLocalAnalyticsItem() {
    AnalyticsItem.search(withWhere: nil).forEach {
      [unowned self] in
      self.analyticsItems.append($0 as! AnalyticsItem)
    }
    let dbHelper = AnalyticsItem.getUsingLKDBHelper()!
    dbHelper.dropTable(with: AnalyticsItem.self)
  }
  
  public func setNeedsRestoreItems() {
    analyticsItems.forEach { $0.saveToDB() }
    analyticsItems.removeAll()
    UserDefaults.standard.set(false, forKey: AnalyticsManagerFlushedFlagKey)
    UserDefaults.standard.synchronize()
  }
  
  public func restoreItems() {
    if flushed == false {
      getLocalAnalyticsItem()
    }
  }
  
  private func UIApplicationDidEnterBackground() {
    setNeedsRestoreItems()
  }
  
  private func UIApplicationDidBecomeActive() {
    self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
    restoreItems()
  }
  
  private func addListener() {
    NotificationCenter.default.addObserver(self, selector: "UIApplicationDidEnterBackground", name: .UIApplicationDidEnterBackground, object: nil)
    NotificationCenter.default.addObserver(self, selector: "UIApplicationDidBecomeActive", name: .UIApplicationDidBecomeActive, object: nil)
  }
  
  private func removeListener() {
    stopTimer()
    NotificationCenter.default.removeObserver(self)
  }
  
  private init() {
    self.deviceIdentifier = UUID().uuidString
    self.userIdentifier = "iOS Device"
    self.flushed = UserDefaults.standard.bool(forKey: AnalyticsManagerFlushedFlagKey)
    restoreItems()
    addListener()
  }
  
  deinit {
    removeListener()
  }
}
public let AnalyticsManager = Analytics.default


完成之前的奔溃时日志上报

[Swift] 纯文本查看 复制代码
// TODO: 上报
AnalyticsManager.track("CRASH", ["info": crashLog])
// 程序奔溃需要调用标记未上传
AnalyticsManager.setNeedsRestoreItems()


写到这里,框架其实只有30%,只有结合业务才能做出与业务相匹配的框架,接下来就是Cache,我只给出代码框架:

[Swift] 纯文本查看 复制代码
//存储引擎
public enum IDPStorageType {
  case disk
  case sql
}
//缓存策略
public enum IDPCacheStoragePolicy {
  case memory
  case disk
  case memoryAndDisk
}
open class INSCache {
  open static let `default` = INSCache()
  
  open var _nameSpace: String = "INSCache"
  open var _cacheStoragePolicy: IDPCacheStoragePolicy = .memoryAndDisk
  open var _memoryCapacity: Float = 0
  open var _memoryTotalCost: Float = 0
  open var _diskExpiredTime: Int = 0
  
  open func existCacheForKey(_ key: String) ->Bool {
    return false
  }
  
  open func clearMemory() {
    
  }
  
  open func existCacheForKeyInMemory(_ key: String) ->Bool {
    return false
  }
  
  open func existCacheForKeyOnDisk(_ key: String) ->Bool {
    return false
  }
  
  open func setObject(_ data: AnyObject, for key: String) {
    
  }
  
  open func getObject(for key: String) ->AnyObject? {
    return nil
  }
  
  open func objectForKeyOnlyInMemory(_ key: String) ->AnyObject? {
    return nil
  }
  
  open func asyncObject(forKey key: String, _ handler: (AnyObject)->()) {
    
  }
  
  open func removeObjcet(for key: String) {
    
  }
  
  open func removeObjcetForKeyOnlyInMemory(_ key: String) {
    
  }
  
  open func removeAll () {
    
  }
  
  open func removeAllInMemory() {
    
  }
  
  open func removeAllInDisk() {
    
  }
  
  open class func removeNameSpace(_ spaceName: String) {
    
  }
}


包括模型的基类:

[Swift] 纯文本查看 复制代码
open class Model: NSObject {
  open var _ModelIdentifier: String?
  
  open var _ModelUpdatedAt: Date?
  open var _ModelCreatedAt: Date?
  open var _ModelExpiredAt: Date?
  open var _ModelNeedsCache: Bool?
  
  open var _CurrentPage: Int = 0
  open var _PageSize: Int = 10
  open var _TotalCount: Int = 0
  open var _StartAt: Int = 0
  
  open func load() { }
  open func refresh() { }
  open func cancel() { }
  
  open func goNextPage() { }
  open func goPrevPage() { }
  
  open func hasPrev() ->Bool{ return false }
  open func hasNext() ->Bool{ return false }
  
  public override init() {
    
  }
}


框架到这里就不说了,接下来有时间就会实际的在使用中一步步的优化框架,让框架适应业务。最近有点忙,开了算法课程,所以框架上边大部分东西都是懒得写,但是使用到的第三方库都建议大家去阅读源码(除ASDK以外)。希望会有所提升。




相关内容

如何构造Swift框架(1)知识储备

如何构造Swift框架(2)功能编写

如何构造Swift框架(3)数据模块

如何构造Swift框架(4)推送服务

作者:dylan

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

智能科技 发表于 2016-10-14 19:06:56 | 显示全部楼层
系列不错~
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表