How to manage OneSignal push notification in iOS

Issue #377

OneSignal is an alternative for Parse for push notifications but the sdk has many extra stuff and assumptions and lots of swizzling.

We can just use Rest to make API calls. From https://github.com/onmyway133/Dust

Every official push notification SDK can do many things

  • Register device token. This is crucial for the notification to get from your backend -> APNS -> device
  • Manage player id, user id, arn, …This is used to associate with device token
  • Manager tag, topic, subscription, segments, …This is used to group a set of device tokens
  • Do swizzling, update your application badge number, change your user notification settings, … without your knowing about that
  • Some other fancy stuffs
  • Dust does only one thing, which is push notification handling. The rest is under your control

OneSignal

1
2
3
4
5
6
7
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
OneSignal.appID = ""
}

func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
OneSignal.handleDeviceToken(deviceToken)
}

Here is the implementation

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
import UIKit

struct Utils {

static func parse(deviceToken data: NSData) -> String {
let buffer = UnsafePointer<CChar>(data.bytes)
var string = ""

for i in 0..<data.length {
string += String(format: "%02.2hhx", arguments: [buffer[i]])
}

return string
}

static func deviceModel() -> String {
var systemInfo = utsname()
uname(&systemInfo)
var v = systemInfo.machine

var deviceModel = ""
let _ = withUnsafePointer(&v) {
deviceModel = String(UTF8String: UnsafePointer($0)) ?? ""
}

return deviceModel
}

static func systemVersion() -> String {
let version = NSProcessInfo.processInfo().operatingSystemVersion

return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
}

static func language() -> String {
return NSLocale.preferredLanguages().first!
}

static func timezone() -> Int {
return NSTimeZone.localTimeZone().secondsFromGMT
}

static func soundFiles() -> [String] {
guard let resourcePath = NSBundle.mainBundle().resourcePath
else { return [] }

let files = try? NSFileManager.defaultManager()
.contentsOfDirectoryAtPath(resourcePath)
.filter {
return $0.hasSuffix(".wav") || $0.hasSuffix(".mp3")
}

return files ?? []
}

static func versionNumber() -> String? {
return NSBundle.mainBundle().infoDictionary?["CFBundleShortVersionString"] as? String
}

static func buildNumber() -> String? {
return NSBundle.mainBundle().infoDictionary?["CFBundleVersionString"] as? String
}

static func netType() -> Int {
// Reachability
return 0
}
}
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
import Foundation

public struct UserDefaults {

struct Key {
static let playerID: String = "Dust-OneSignal-Player-ID-Key"
static let deviceToken: String = "Dust-OneSignal-Device-Token-Key"
static let subscribed: String = "Dust-OneSignal-Disable-Subscribed-Key"
}

public static var playerID: String? {
get {
return NSUserDefaults.standardUserDefaults().stringForKey(Key.playerID)
}

set {
NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: Key.playerID)
NSUserDefaults.standardUserDefaults().synchronize()
}
}

public static var deviceToken: String? {
get {
return NSUserDefaults.standardUserDefaults().stringForKey(Key.deviceToken)
}

set {
NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: Key.deviceToken)
NSUserDefaults.standardUserDefaults().synchronize()
}
}

public static var subscribed: Bool {
get {
return NSUserDefaults.standardUserDefaults().boolForKey(Key.subscribed)
}

set {
NSUserDefaults.standardUserDefaults().setBool(newValue, forKey: Key.subscribed)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
}
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
130
131
132
133
134
135
136
137
138
139
140
141
import Foundation
import Alamofire

public struct OneSignal {

static var appID: String = ""
static let version = "020115"
static let baseURL = NSURL(string: "https://onesignal.com/api/v1")!

enum NotificationType: Int {
case subscribed = 7
case unsubscribed = -2

static func value() -> Int {
return UserDefaults.subscribed
? NotificationType.subscribed.rawValue : NotificationType.unsubscribed.rawValue
}
}

enum Provisioning: Int {
case development = 1
}

public static func setup(appID appID: String) {
NSUserDefaults.standardUserDefaults().registerDefaults([
UserDefaults.Key.subscribed: true
])

OneSignal.appID = appID
}

public static func registerOrUpdateSession(completion: ((String?) -> Void)? = nil) {
guard let bundleID = NSBundle.mainBundle().bundleIdentifier,
let deviceToken = UserDefaults.deviceToken
else {
return
}

var params: [String: AnyObject] = [
"app_id" : appID,
"device_model" : Utils.deviceModel(),
"device_os" : Utils.systemVersion(),
"language" : Utils.language(),
"timezone" : NSNumber(integer: Utils.timezone()),
"device_type" : NSNumber(integer : 0),
"sounds" : Utils.soundFiles(),
"sdk" : version,
"identifier" : deviceToken,
"net_type" : NSNumber(integer: Utils.netType()),
"rooted": NSNumber(bool: false),
"as_id": "OptedOut",
"sdk_type": "native",
"ios_bundle": bundleID,
"game_version": Utils.versionNumber() ?? "",
"notification_types": NotificationType.value(),
]

#if DEBUG
params["test_type"] = Provisioning.development.rawValue
#endif

let url: NSURL

if let playerID = UserDefaults.playerID {
url = baseURL.URLByAppendingPathComponent("players/\(playerID)/on_session")
} else {
url = baseURL.URLByAppendingPathComponent("players")
}

Alamofire
.request(.POST, url, parameters: params)
.responseJSON { response in
guard let json = response.result.value as? [String: AnyObject]
else {
completion?(nil)
return
}

if let id = json["id"] as? String {
UserDefaults.playerID = id
completion?(id)
} else if let value = json["success"] as? Int,
playerID = UserDefaults.playerID where value == 1 {
completion?(playerID)
} else {
completion?(nil)
}
}
}

public static func handle(deviceToken data: NSData) {
UserDefaults.deviceToken = Utils.parse(deviceToken: data)
registerOrUpdateSession()
}

public static func update(subscription subscribed: Bool) {
guard let playerID = UserDefaults.playerID else { return }
UserDefaults.subscribed = subscribed

let url = baseURL.URLByAppendingPathComponent("players/\(playerID)")
let params: [String: AnyObject] = [
"app_id": appID,
"notification_types": NotificationType.value()
]

Alamofire
.request(.PUT, url, parameters: params)
.responseJSON { response in
print(response)
}
}

public static func update(badge count: Int) {
guard let playerID = UserDefaults.playerID else { return }

let url = baseURL.URLByAppendingPathComponent("players/\(playerID)")
let params: [String: AnyObject] = [
"app_id": appID,
"badge_count": count
]

Alamofire
.request(.PUT, url, parameters: params)
.responseJSON { response in

}
}

public static func getPlayerID(completion: String -> Void) {
if let playerID = UserDefaults.playerID {
completion(playerID)
return
}

registerOrUpdateSession { playerID in
if let playerID = playerID {
completion(playerID)
}
}
}
}

Comments