如何构造 Swift 框架(四)

本贴最后更新于 3046 天前,其中的信息可能已经水流花落

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

推送服务

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
/// 如果你希望更改权限开启按钮的英文,就需要自己配置本地化文件
/// 参考这里 https://github.com/nickoneill/PermissionScope/pull/12#issuecomment-96428580
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):

1
2
3
4
5
6
7
let permissionTypes = [
INSPermissionType.notification(nil, "打开推送服务"),
INSPermissionType.camera("打开相机服务"),
INSPermissionType.photos("希望使用照片")
]
 
Permission.requestPermission(permissionTypes)

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

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

等会儿先看个东西:

好、继续写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final public class Push {
public static let `default` = Push()
 
public func DeviceToken(_ deviceToken: Data) {
 
}
 
public func ReceivedPushMessage (_ userInfo: [AnyHashable : Any]) {
 
}
 
private init() {
 
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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()获取当前的捕获器,在自己的捕获成功之后也让别人的捕获成功。啊好累啊,这还不是完整的奔溃捕获,于是我们接着写代码(写代码到时无妨,主要是这里有一坑爹的事情,需要自己去查看解决,说明:无法把方法传入这些捕获方法也附上气前一个链接中的代码):

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/// 设置异常捕获
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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

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

1
2
3
4
// TODO: 上报
AnalyticsManager.track("CRASH", ["info": crashLog])
// 程序奔溃需要调用标记未上传
AnalyticsManager.setNeedsRestoreItems()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//存储引擎
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) {
 
}
}

包括模型的基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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的语言熟悉,不作为框架教学。

  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    36 引用 • 37 回帖 • 541 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    21 引用 • 31 回帖
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 101 关注
  • Eclipse

    Eclipse 是一个开放源代码的、基于 Java 的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。

    75 引用 • 258 回帖 • 631 关注
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖 • 1 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 731 关注
  • 书籍

    宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”

    77 引用 • 389 回帖
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 215 关注
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    12 引用 • 54 回帖 • 30 关注
  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    85 引用 • 165 回帖 • 1 关注
  • 音乐

    你听到信仰的声音了么?

    61 引用 • 512 回帖
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    223 引用 • 474 回帖 • 1 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    289 引用 • 4492 回帖 • 657 关注
  • FreeMarker

    FreeMarker 是一款好用且功能强大的 Java 模版引擎。

    23 引用 • 20 回帖 • 457 关注
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 28 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    26 引用 • 196 回帖 • 25 关注
  • App

    App(应用程序,Application 的缩写)一般指手机软件。

    91 引用 • 384 回帖
  • IPFS

    IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。请浏览 IPFS 入门笔记了解更多细节。

    21 引用 • 245 回帖 • 240 关注
  • Follow
    4 引用 • 12 回帖
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 213 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 68 关注
  • 快应用

    快应用 是基于手机硬件平台的新型应用形态;标准是由主流手机厂商组成的快应用联盟联合制定;快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台;以平台化的生态模式对个人开发者和企业开发者全品类开放。

    15 引用 • 127 回帖 • 1 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • Webswing

    Webswing 是一个能将任何 Swing 应用通过纯 HTML5 运行在浏览器中的 Web 服务器,详细介绍请看 将 Java Swing 应用变成 Web 应用

    1 引用 • 15 回帖 • 635 关注
  • RemNote
    2 引用 • 16 回帖 • 10 关注
  • Visio
    1 引用 • 2 回帖 • 1 关注
  • RIP

    愿逝者安息!

    8 引用 • 92 回帖 • 379 关注
  • 电影

    这是一个不能说的秘密。

    121 引用 • 606 回帖