Skip to content

Commit

Permalink
refactor!: singleton API only way to use SDK now (customerio#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed Nov 17, 2022
1 parent 398bd80 commit 72b7477
Show file tree
Hide file tree
Showing 83 changed files with 1,437 additions and 1,986 deletions.
10 changes: 4 additions & 6 deletions .swiftlint.yml
@@ -1,13 +1,11 @@
# Rules: https://realm.github.io/SwiftLint/rule-directory.html
# Config file learn more: https://github.com/realm/SwiftLint#configuration=
# Config file learn more: https://github.com/realm/SwiftLint#configuration

disabled_rules:
- line_length # The swiftformat tool formats the swift code for us to not have super long lines. This rule after swiftformat has become more of an annoyance then a value where most errors are error message strings being too long.

excluded: # paths or files to ignore during linting. Takes precedence over `included`.
- .build
- Package.swift

line_length:
ignores_comments: true
ignores_interpolated_strings: true
ignores_urls: true

reporter: "emoji" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
32 changes: 3 additions & 29 deletions README.md
Expand Up @@ -44,11 +44,7 @@ We recommend that you set the *Dependency Rule* to *Up to Next Major Version*. W

Before you can use the Customer.io SDK, you need to initialize it. Any calls that you make to the SDK before you initialize it are ignored.

There are two ways to initialize the SDK. The method you use depends on how you want to use the SDK:

1. Singleton, shared instance (the quick and easy way):

When you use the shared instance, you don't need to manage your own instances of Customer.io SDK classes. To get started, initialize the SDK in the `AppDelegate` `application(_ application: didFinishLaunchingWithOptions)` function:
To get started, initialize the SDK in the `AppDelegate` `application(_ application: didFinishLaunchingWithOptions)` function:

```swift
import CioTracking
Expand All @@ -61,7 +57,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY")

// You can optionally provide a Region to set the Region for your Workspace:
CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU)
CustomerIO.initialize(siteId: "YOUR SITE ID", apiKey: "YOUR API KEY", region: Region.EU)

return true
}
Expand All @@ -71,32 +67,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
Then, when you want to use any of the SDK features, you use the shared instance of the class:

```swift
CustomerIO.shared.track(...)
MessagingPush.shared.application(...)
```

2. Create your own instances (better for projects using automated tests):

We recommend that you create your own instances of SDK classes if your project has automated tests. We designed our SDK with first-class support for dependency injection and mocking, which makes it easier to write automated tests. See [testing](#Testing) for more information.

> **Note**: Code samples in this readme use the singleton, shared instance method to call the SDK. However, all samples will also work with your own instances of SDK classes.
```swift
import CioTracking

let customerIO = CustomerIO(siteId: "XXX", apiKey: "YYY")

// You can optionally provide a Region to set the Region for your Workspace:
let customerIO = CustomerIO(siteId: "XXX", apiKey: "YYY", region: Region.EU)
```

Then, when you want to use any of the SDK features, you use the shared instance of the class:

```swift
let messagingPush = MessagingPush(customerIO: customerIO)

messagingPush.application(...)
```

# More information

See our complete SDK documentation at [https://customer.io/docs/sdk/ios/](https://customer.io/docs/sdk/ios/)
Expand Down
10 changes: 5 additions & 5 deletions Sources/Common/Background Queue/Queue.swift
Expand Up @@ -94,12 +94,12 @@ public class CioQueue: Queue {
private let runRequest: QueueRunRequest
private let jsonAdapter: JsonAdapter
private let logger: Logger
private let sdkConfigStore: SdkConfigStore
private let sdkConfig: SdkConfig
private let queueTimer: SingleScheduleTimer
private let dateUtil: DateUtil

private var numberSecondsToScheduleTimer: Seconds {
sdkConfigStore.config.backgroundQueueSecondsDelay
sdkConfig.backgroundQueueSecondsDelay
}

init(
Expand All @@ -108,7 +108,7 @@ public class CioQueue: Queue {
runRequest: QueueRunRequest,
jsonAdapter: JsonAdapter,
logger: Logger,
sdkConfigStore: SdkConfigStore,
sdkConfig: SdkConfig,
queueTimer: SingleScheduleTimer,
dateUtil: DateUtil
) {
Expand All @@ -117,7 +117,7 @@ public class CioQueue: Queue {
self.runRequest = runRequest
self.jsonAdapter = jsonAdapter
self.logger = logger
self.sdkConfigStore = sdkConfigStore
self.sdkConfig = sdkConfig
self.queueTimer = queueTimer
self.dateUtil = dateUtil
}
Expand Down Expand Up @@ -178,7 +178,7 @@ public class CioQueue: Queue {
/// the device battery life so we try to do that when we can.
private func processQueueStatus(_ status: QueueStatus) {
logger.debug("processing queue status \(status).")
let isManyTasksInQueue = status.numTasksInQueue >= sdkConfigStore.config.backgroundQueueMinNumberOfTasks
let isManyTasksInQueue = status.numTasksInQueue >= sdkConfig.backgroundQueueMinNumberOfTasks

if isManyTasksInQueue {
logger.info("queue met criteria to run automatically")
Expand Down
8 changes: 4 additions & 4 deletions Sources/Common/Background Queue/QueueStorage.swift
Expand Up @@ -40,7 +40,7 @@ public class FileManagerQueueStorage: QueueStorage {
private let fileStorage: FileStorage
private let jsonAdapter: JsonAdapter
private let siteId: SiteId
private let sdkConfigStore: SdkConfigStore
private let sdkConfig: SdkConfig
private let logger: Logger
private let dateUtil: DateUtil

Expand All @@ -51,14 +51,14 @@ public class FileManagerQueueStorage: QueueStorage {
fileStorage: FileStorage,
jsonAdapter: JsonAdapter,
lockManager: LockManager,
sdkConfigStore: SdkConfigStore,
sdkConfig: SdkConfig,
logger: Logger,
dateUtil: DateUtil
) {
self.siteId = siteId
self.fileStorage = fileStorage
self.jsonAdapter = jsonAdapter
self.sdkConfigStore = sdkConfigStore
self.sdkConfig = sdkConfig
self.logger = logger
self.dateUtil = dateUtil
self.lock = lockManager.getLock(id: .queueStorage)
Expand Down Expand Up @@ -178,7 +178,7 @@ public class FileManagerQueueStorage: QueueStorage {
logger.debug("deleting expired tasks from the queue")

var tasksToDelete: Set<QueueTaskMetadata> = Set()
let queueTaskExpiredThreshold = Date().subtract(sdkConfigStore.config.backgroundQueueExpiredSeconds, .second)
let queueTaskExpiredThreshold = Date().subtract(sdkConfig.backgroundQueueExpiredSeconds, .second)
logger.debug("""
deleting tasks older then \(queueTaskExpiredThreshold.string(format: .iso8601noMilliseconds)),
current time is: \(Date().string(format: .iso8601noMilliseconds))
Expand Down
32 changes: 8 additions & 24 deletions Sources/Common/DIGraph.swift
@@ -1,34 +1,18 @@
import Foundation

public class DIGraph {
public var overrides: [String: Any] = [:]
public var singletons: [String: Any] = [:]

public let siteId: SiteId
internal init(siteId: String) {
self.siteId = siteId
}
public let apiKey: String
public let sdkConfig: SdkConfig

class Store {
var instances: [String: DIGraph] = [:]
func getInstance(siteId: String) -> DIGraph {
if let existingInstance = instances[siteId] {
return existingInstance
}
let newInstance = DIGraph(siteId: siteId)
instances[siteId] = newInstance
return newInstance
}
}

@Atomic internal static var store = Store()
public static func getInstance(siteId: String) -> DIGraph {
Self.store.getInstance(siteId: siteId)
public init(siteId: SiteId, apiKey: String, sdkConfig: SdkConfig) {
self.siteId = siteId
self.apiKey = apiKey
self.sdkConfig = sdkConfig
}

public static func getAllWorkspacesSharedInstance() -> DIGraph {
Self.store.getInstance(siteId: "shared")
}
public var overrides: [String: Any] = [:]
public var singletons: [String: Any] = [:]

/**
Designed to be used only in test classes to override dependencies.
Expand Down
3 changes: 2 additions & 1 deletion Sources/Common/Extensions/UIApplicationExtensions.swift
Expand Up @@ -3,7 +3,8 @@ import Foundation
import UIKit

public extension UIApplication {
@available(iOSApplicationExtension, unavailable) // since some extension functions may be able to run in iOS app extensions, only disable for this 1 function
// since some extension functions may be able to run in iOS app extensions, only disable for this 1 function
@available(iOSApplicationExtension, unavailable)
func open(url: URL) {
open(url, options: [:]) { _ in }
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/Common/Service/HttpClient.swift
Expand Up @@ -27,8 +27,8 @@ public class CIOHttpClient: HttpClient {

init(
siteId: SiteId,
sdkCredentialsStore: SdkCredentialsStore,
configStore: SdkConfigStore,
apiKey: ApiKey,
sdkConfig: SdkConfig,
jsonAdapter: JsonAdapter,
httpRequestRunner: HttpRequestRunner,
globalDataStore: GlobalDataStore,
Expand All @@ -40,11 +40,11 @@ public class CIOHttpClient: HttpClient {
self.httpRequestRunner = httpRequestRunner
self.session = Self.getSession(
siteId: siteId,
apiKey: sdkCredentialsStore.credentials.apiKey,
apiKey: apiKey,
deviceInfo: deviceInfo,
sdkWrapperConfig: configStore.config._sdkWrapperConfig
sdkWrapperConfig: sdkConfig._sdkWrapperConfig
)
self.baseUrls = configStore.config.httpBaseUrls
self.baseUrls = sdkConfig.httpBaseUrls
self.jsonAdapter = jsonAdapter
self.globalDataStore = globalDataStore
self.logger = logger
Expand Down
35 changes: 0 additions & 35 deletions Sources/Common/Store/ActiveWorkspaces.swift

This file was deleted.

50 changes: 4 additions & 46 deletions Sources/Common/Store/GlobalDataStore.swift
Expand Up @@ -2,11 +2,6 @@ import Foundation

/// SDK data that is common between all site ids.
public protocol GlobalDataStore: AutoMockable {
// site id used for the singleton instance of the SDK.
var sharedInstanceSiteId: String? { get set }
func appendSiteId(_ siteId: String)
// all site ids that have ever been registered with the SDK
var siteIds: [String] { get }
// APN or FCM device token
var pushDeviceToken: String? { get set }
// HTTP requests can be paused to avoid spamming the API too hard.
Expand All @@ -16,23 +11,7 @@ public protocol GlobalDataStore: AutoMockable {

// sourcery: InjectRegister = "GlobalDataStore"
public class CioGlobalDataStore: GlobalDataStore {
private var diGraph: DIGraph {
// Used *only* for information that needs to be global between all site ids!
DIGraph.getAllWorkspacesSharedInstance()
}

internal var keyValueStorage: KeyValueStorage {
diGraph.keyValueStorage
}

public var sharedInstanceSiteId: String? {
get {
keyValueStorage.string(.sharedInstanceSiteId)
}
set {
keyValueStorage.setString(newValue, forKey: .sharedInstanceSiteId)
}
}
private let keyValueStorage: KeyValueStorage

public var pushDeviceToken: String? {
get {
Expand All @@ -52,30 +31,9 @@ public class CioGlobalDataStore: GlobalDataStore {
}
}

public init() {}

/**
Save all of the site ids given to the SDK. We are storing this because we may need to iterate all of the
site ids in the future so let's capture them now so we have them.
*/
public func appendSiteId(_ siteId: String) {
let existingSiteIds = keyValueStorage.string(.allSiteIds) ?? ""

// Must convert String.Substring to String which is why we map()
var allSiteIds = Set(existingSiteIds.split(separator: ",").map { String($0) })

allSiteIds.insert(siteId)

let newSiteIds = allSiteIds.joined(separator: ",")

keyValueStorage.setString(newSiteIds, forKey: .allSiteIds)
}

public var siteIds: [String] {
guard let allSiteIds = keyValueStorage.string(.allSiteIds) else {
return []
}
public init(keyValueStorage: KeyValueStorage) {
self.keyValueStorage = keyValueStorage

return allSiteIds.split(separator: ",").map { String($0) }
self.keyValueStorage.switchToGlobalDataStore()
}
}
21 changes: 10 additions & 11 deletions Sources/Common/Store/SdkConfig.swift
Expand Up @@ -5,12 +5,20 @@ import Foundation
See `CustomerIO.config()` to configurate the SDK.
*/
public struct SdkConfig {
// Used to create new instance of SdkConfig when the SDK is initialized.
// Then, each property of the SdkConfig object can be modified by the user.
public enum Factory {
public static func create(region: Region) -> SdkConfig {
SdkConfig(trackingApiUrl: region.productionTrackingUrl)
}
}

/**
Base URL to use for the Customer.io track API. You will more then likely not modify this value.
If you override this value, `Region` set when initializing the SDK will be ignored.
*/
public var trackingApiUrl: String = ""
public var trackingApiUrl: String

/**
Automatic tracking of push events will automatically generate `opened` and `delivered` metrics
Expand Down Expand Up @@ -55,6 +63,7 @@ public struct SdkConfig {
operating system, device locale, device model, app version etc
*/
public var autoTrackDeviceAttributes: Bool = true

internal var httpBaseUrls: HttpBaseUrls {
HttpBaseUrls(trackingApi: trackingApiUrl)
}
Expand All @@ -66,13 +75,3 @@ public struct SdkConfig {
*/
public var _sdkWrapperConfig: SdkWrapperConfig? // swiftlint:disable:this identifier_name
}

public protocol SdkConfigStore: AutoMockable {
var config: SdkConfig { get set }
}

// sourcery: InjectRegister = "SdkConfigStore"
// sourcery: InjectSingleton
public class InMemorySdkConfigStore: SdkConfigStore {
@Atomic public var config = SdkConfig()
}

0 comments on commit 72b7477

Please sign in to comment.