diff --git a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift index 035136b4..b03e79e1 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift +++ b/Examples/apps/DestinationsExample/DestinationsExample/AppDelegate.swift @@ -24,22 +24,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { analytics = Analytics(configuration: configuration) // Add Adjust destination plugin - analytics?.add(plugin: AdjustDestination(name: "Adjust")) + analytics?.add(plugin: AdjustDestination()) // Add Amplitude session plugin - analytics?.add(plugin: AmplitudeSession(name: "Amplitude")) + analytics?.add(plugin: AmplitudeSession()) // Add Mixpanel destination plugin - analytics?.add(plugin: MixpanelDestination(name: "Mixpanel")) + analytics?.add(plugin: MixpanelDestination()) // Add Flurry destination plugin - analytics?.add(plugin: FlurryDestination(name: "Flurry")) + analytics?.add(plugin: FlurryDestination()) // Add the Firebase destination plugin - analytics?.add(plugin: FirebaseDestination(name: "Firebase")) + analytics?.add(plugin: FirebaseDestination()) //Add the AppsFlyer destination plugin - analytics?.add(plugin: AppsFlyerDestination(name: "AppsFlyer")) + analytics?.add(plugin: AppsFlyerDestination()) return true } diff --git a/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcodeproj/project.pbxproj b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcodeproj/project.pbxproj index 57cc4599..2b19b258 100644 --- a/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcodeproj/project.pbxproj +++ b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 46016C1326BB5BD700BCEE80 /* Segment in Frameworks */ = {isa = PBXBuildFile; productRef = 46016C1226BB5BD700BCEE80 /* Segment */; }; 9613E655265C2E350078A2BD /* SegmentSwiftUIExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9613E654265C2E350078A2BD /* SegmentSwiftUIExampleApp.swift */; }; 9613E657265C2E350078A2BD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9613E656265C2E350078A2BD /* ContentView.swift */; }; 9613E659265C2E360078A2BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9613E658265C2E360078A2BD /* Assets.xcassets */; }; 9613E65C265C2E360078A2BD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9613E65B265C2E360078A2BD /* Preview Assets.xcassets */; }; - 9613E665265C2E880078A2BD /* Segment in Frameworks */ = {isa = PBXBuildFile; productRef = 9613E664265C2E880078A2BD /* Segment */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -28,18 +28,26 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9613E665265C2E880078A2BD /* Segment in Frameworks */, + 46016C1326BB5BD700BCEE80 /* Segment in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 46016C1126BB5BD700BCEE80 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 9613E648265C2E350078A2BD = { isa = PBXGroup; children = ( 9613E653265C2E350078A2BD /* SegmentSwiftUIExample */, 9613E652265C2E350078A2BD /* Products */, + 46016C1126BB5BD700BCEE80 /* Frameworks */, ); sourceTree = ""; }; @@ -88,7 +96,7 @@ ); name = SegmentSwiftUIExample; packageProductDependencies = ( - 9613E664265C2E880078A2BD /* Segment */, + 46016C1226BB5BD700BCEE80 /* Segment */, ); productName = SegmentSwiftUIExample; productReference = 9613E651265C2E350078A2BD /* SegmentSwiftUIExample.app */; @@ -118,7 +126,6 @@ ); mainGroup = 9613E648265C2E350078A2BD; packageReferences = ( - 9613E663265C2E880078A2BD /* XCRemoteSwiftPackageReference "analytics-swift" */, ); productRefGroup = 9613E652265C2E350078A2BD /* Products */; projectDirPath = ""; @@ -335,21 +342,9 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 9613E663265C2E880078A2BD /* XCRemoteSwiftPackageReference "analytics-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "git@github.com:segmentio/analytics-swift.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - 9613E664265C2E880078A2BD /* Segment */ = { + 46016C1226BB5BD700BCEE80 /* Segment */ = { isa = XCSwiftPackageProductDependency; - package = 9613E663265C2E880078A2BD /* XCRemoteSwiftPackageReference "analytics-swift" */; productName = Segment; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/contents.xcworkspacedata b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..d2d73c4f --- /dev/null +++ b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Examples/apps/SegmentSwiftUIExample/SegmentSwiftUIExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Examples/apps/SegmentUIKitExample/SegmentUIKitExample/AppDelegate.swift b/Examples/apps/SegmentUIKitExample/SegmentUIKitExample/AppDelegate.swift index 92fe4a86..7d34eef2 100644 --- a/Examples/apps/SegmentUIKitExample/SegmentUIKitExample/AppDelegate.swift +++ b/Examples/apps/SegmentUIKitExample/SegmentUIKitExample/AppDelegate.swift @@ -14,12 +14,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // add console logging plugins to our multiple instances Analytics.main.add(plugin: ConsoleLogger(name: "main")) - Analytics.main.add(plugin: ConsentTracking(name: "consent")) - Analytics.main.add(plugin: IDFACollection(name: "idfa")) - Analytics.main.add(plugin: UIKitScreenTracking(name: "autoScreenTracking")) + Analytics.main.add(plugin: ConsentTracking()) + Analytics.main.add(plugin: IDFACollection()) + Analytics.main.add(plugin: UIKitScreenTracking()) Analytics.support.add(plugin: ConsoleLogger(name: "support")) - Analytics.support.add(plugin: ConsentTracking(name: "consent")) + Analytics.support.add(plugin: ConsentTracking()) Analytics.support.track(name: "test event") diff --git a/Examples/destination_plugins/AdjustDestination.swift b/Examples/destination_plugins/AdjustDestination.swift index 5429aef3..477e6467 100644 --- a/Examples/destination_plugins/AdjustDestination.swift +++ b/Examples/destination_plugins/AdjustDestination.swift @@ -47,20 +47,16 @@ internal struct AdjustSettings: Codable { } @objc -public class AdjustDestination: NSObject, DestinationPlugin, RemoteNotifications { - public let timeline: Timeline = Timeline() - public let type: PluginType = .destination - public let name: String - public var analytics: Analytics? = nil +class AdjustDestination: NSObject, DestinationPlugin, RemoteNotifications { + let timeline = Timeline() + let type = PluginType.destination + let key = "Adjust" + var analytics: Analytics? = nil internal var settings: AdjustSettings? = nil - required public init(name: String) { - self.name = name - } - public func update(settings: Settings) { - guard let settings: AdjustSettings = settings.integrationSettings(name: "Adjust") else { return } + guard let settings: AdjustSettings = settings.integrationSettings(forPlugin: self) else { return } self.settings = settings var environment = ADJEnvironmentSandbox diff --git a/Examples/destination_plugins/AmplitudeSession.swift b/Examples/destination_plugins/AmplitudeSession.swift index 0f134ebf..9ce3129e 100644 --- a/Examples/destination_plugins/AmplitudeSession.swift +++ b/Examples/destination_plugins/AmplitudeSession.swift @@ -42,18 +42,45 @@ import Foundation import Segment class AmplitudeSession: EventPlugin, iOSLifecycle { - - var type: PluginType - var name: String + var key = "Amplitude" + var type = PluginType.enrichment var analytics: Analytics? + var active = false + private var sessionTimer: Timer? private var sessionID: TimeInterval? private let fireTime = TimeInterval(300) - required init(name: String) { - self.name = name - self.type = .enrichment + func update(settings: Settings) { + if settings.isDestinationEnabled(key: key) { + active = true + } else { + active = false + } + } + + func execute(event: T?) -> T? { + if !active { + return event + } + + var result: T? = event + switch result { + case let r as IdentifyEvent: + result = self.identify(event: r) as? T + case let r as TrackEvent: + result = self.track(event: r) as? T + case let r as ScreenEvent: + result = self.screen(event: r) as? T + case let r as AliasEvent: + result = self.alias(event: r) as? T + case let r as GroupEvent: + result = self.group(event: r) as? T + default: + break + } + return result } func track(event: TrackEvent) -> TrackEvent? { @@ -101,15 +128,15 @@ class AmplitudeSession: EventPlugin, iOSLifecycle { } } + // MARK: - AmplitudeSession Helper Methods extension AmplitudeSession { - func insertSession(event: RawEvent) -> RawEvent { var returnEvent = event if var integrations = event.integrations?.dictionaryValue, let sessionID = sessionID { - integrations["Amplitude"] = ["session_id": (Int(sessionID) * 1000)] + integrations[key] = ["session_id": (Int(sessionID) * 1000)] returnEvent.integrations = try? JSON(integrations as Any) } return returnEvent diff --git a/Examples/destination_plugins/AppsFlyerDestination.swift b/Examples/destination_plugins/AppsFlyerDestination.swift index edc8e4a9..4e039f55 100644 --- a/Examples/destination_plugins/AppsFlyerDestination.swift +++ b/Examples/destination_plugins/AppsFlyerDestination.swift @@ -46,22 +46,17 @@ private struct AppsFlyerSettings: Codable { @objc class AppsFlyerDestination: UIResponder, DestinationPlugin, RemoteNotifications, iOSLifecycle { + let timeline = Timeline() + let type = PluginType.destination + let key = "AppsFlyer" - let timeline: Timeline = Timeline() - let type: PluginType = .destination - let name: String var analytics: Analytics? fileprivate var settings: AppsFlyerSettings? = nil - required init(name: String) { - self.name = name - analytics?.track(name: "AppsFlyer Loaded") - } - public func update(settings: Settings) { - guard let settings: AppsFlyerSettings = settings.integrationSettings(name: "AppsFlyer") else { return } + guard let settings: AppsFlyerSettings = settings.integrationSettings(forPlugin: self) else { return } self.settings = settings AppsFlyerLib.shared().appsFlyerDevKey = settings.appsFlyerDevKey diff --git a/Examples/destination_plugins/FirebaseDestination.swift b/Examples/destination_plugins/FirebaseDestination.swift index bdf3e0f4..bdbf93fb 100644 --- a/Examples/destination_plugins/FirebaseDestination.swift +++ b/Examples/destination_plugins/FirebaseDestination.swift @@ -43,19 +43,19 @@ import FirebaseAnalytics An implmentation of the Firebase Analytics device mode destination as a plugin. */ +struct FirebaseSettings: Codable { + let deepLinkURLScheme: String? +} + class FirebaseDestination: DestinationPlugin { - let timeline: Timeline = Timeline() - let type: PluginType = .destination - let name: String + let timeline = Timeline() + let type = PluginType.destination + let key = "Firebase" var analytics: Segment.Analytics? = nil - required init(name: String) { - self.name = name - } - func update(settings: Settings) { - guard let firebaseSettings = settings.integrationSettings(for: "Firebase") else { return } - if let deepLinkURLScheme = firebaseSettings["deepLinkURLScheme"] as? String { + guard let firebaseSettings: FirebaseSettings = settings.integrationSettings(forPlugin: self) else { return } + if let deepLinkURLScheme = firebaseSettings.deepLinkURLScheme { FirebaseOptions.defaultOptions()?.deepLinkURLScheme = deepLinkURLScheme analytics?.log(message: "Added deepLinkURLScheme: \(deepLinkURLScheme)") } diff --git a/Examples/destination_plugins/FlurryDestination.swift b/Examples/destination_plugins/FlurryDestination.swift index 27185128..35584f4c 100644 --- a/Examples/destination_plugins/FlurryDestination.swift +++ b/Examples/destination_plugins/FlurryDestination.swift @@ -42,33 +42,35 @@ import FlurryAnalytics An implmentation of the Flurry Analytics device mode destination as a plugin. */ +private struct FlurrySettings: Codable { + let apiKey: String + let sessionContinueSeconds: Int? + let screenTracksEvents: Bool? +} + + class FlurryDestination: DestinationPlugin { - let timeline: Timeline = Timeline() - let type: PluginType = .destination - let name: String + let timeline = Timeline() + let type = PluginType.destination + let key = "Flurry" var analytics: Analytics? = nil var screenTracksEvents = false - required init(name: String) { - self.name = name - } - func update(settings: Settings) { - guard let jsonSettings = settings.integrationSettings(for: "Flurry") else { return } - guard let flurryApiKey = jsonSettings["apiKey"] as? String else { return } + guard let flurrySettings: FlurrySettings = settings.integrationSettings(forPlugin: self) else { return } let builder = FlurrySessionBuilder() - if let sessionContinueSeconds = jsonSettings["sessionContinueSeconds"] as? Int { + if let sessionContinueSeconds = flurrySettings.sessionContinueSeconds { builder.withSessionContinueSeconds(sessionContinueSeconds) } - if let screenTracksEvents = jsonSettings["screenTracksEvents"] as? Bool { + if let screenTracksEvents = flurrySettings.screenTracksEvents { self.screenTracksEvents = screenTracksEvents } - Flurry.startSession(flurryApiKey, with: builder) + Flurry.startSession(flurrySettings.apiKey, with: builder) } func identify(event: IdentifyEvent) -> IdentifyEvent? { diff --git a/Examples/destination_plugins/MixpanelDestination.swift b/Examples/destination_plugins/MixpanelDestination.swift index 87900d00..1dd8d66e 100644 --- a/Examples/destination_plugins/MixpanelDestination.swift +++ b/Examples/destination_plugins/MixpanelDestination.swift @@ -39,27 +39,21 @@ import Mixpanel import Segment class MixpanelDestination: DestinationPlugin, RemoteNotifications { - - var type: PluginType - var name: String + let timeline = Timeline() + let type = PluginType.destination + let key = "Mixpanel" var analytics: Analytics? - var timeline: Timeline + private var mixpanel: MixpanelInstance? = nil private var settings: [String: Any]? = nil - required init(name: String) { - self.name = name - type = .destination - self.timeline = Timeline() - } - func update(settings: Settings) { // If we have a mixpanel instance, dump all the data first mixpanel?.flush() // TODO: Update the proper types - if let mixPanelSettings = settings.integrationSettings(for: "Mixpanel"), + if let mixPanelSettings = settings.integrationSettings(forKey: key), let token = mixPanelSettings["token"] as? String { self.settings = mixPanelSettings mixpanel = Mixpanel.initialize(token: token) diff --git a/Examples/other_plugins/ConsentTracking.swift b/Examples/other_plugins/ConsentTracking.swift index f8f171c9..ae5c89b9 100644 --- a/Examples/other_plugins/ConsentTracking.swift +++ b/Examples/other_plugins/ConsentTracking.swift @@ -45,8 +45,7 @@ import UIKit If consent is declined, all events are dropped immediately after entering the event timeline. */ class ConsentTracking: Plugin { - let type: PluginType = .before - let name: String + let type = PluginType.before var analytics: Analytics? = nil var queuedEvents = [RawEvent]() @@ -56,9 +55,7 @@ class ConsentTracking: Plugin { static var lock = NSLock() static var instances = [ConsentTracking]() - required init(name: String) { - self.name = name - + init() { Self.instances.append(self) // In our example, we'll be adding this plugin to multiple instances of Analytics. diff --git a/Examples/other_plugins/ConsoleLogger.swift b/Examples/other_plugins/ConsoleLogger.swift index 18ebe759..02f03d7f 100644 --- a/Examples/other_plugins/ConsoleLogger.swift +++ b/Examples/other_plugins/ConsoleLogger.swift @@ -40,7 +40,7 @@ import Segment run at the end of the event timeline, at which point it will print event data to the Xcode console window. */ class ConsoleLogger: Plugin { - let type: PluginType = .after + let type = PluginType.after let name: String var analytics: Analytics? = nil diff --git a/Examples/other_plugins/IDFACollection.swift b/Examples/other_plugins/IDFACollection.swift index 4aa8deee..cdcf437d 100644 --- a/Examples/other_plugins/IDFACollection.swift +++ b/Examples/other_plugins/IDFACollection.swift @@ -45,14 +45,9 @@ import AppTrackingTransparency Don't forget to add "NSUserTrackingUsageDescription" with a description to your Info.plist. */ class IDFACollection: Plugin { - let type: PluginType = .enrichment - let name: String + let type = PluginType.enrichment var analytics: Analytics? = nil - required init(name: String) { - self.name = name - } - func execute(event: T?) -> T? { let status = ATTrackingManager.trackingAuthorizationStatus if status == .notDetermined { diff --git a/Examples/other_plugins/UIKitScreenTracking.swift b/Examples/other_plugins/UIKitScreenTracking.swift index 61ce54f3..185c5dd5 100644 --- a/Examples/other_plugins/UIKitScreenTracking.swift +++ b/Examples/other_plugins/UIKitScreenTracking.swift @@ -50,12 +50,10 @@ class UIKitScreenTracking: UtilityPlugin { static let screenNameKey = "name" static let controllerKey = "controller" - let type: PluginType = .utility - let name: String + let type = PluginType.utility var analytics: Analytics? = nil - required init(name: String) { - self.name = name + init() { setupUIKitHooks() } diff --git a/Sources/Segment/Plugins.swift b/Sources/Segment/Plugins.swift index b8979bd0..fabd9bc9 100644 --- a/Sources/Segment/Plugins.swift +++ b/Sources/Segment/Plugins.swift @@ -25,10 +25,8 @@ public enum PluginType: Int, CaseIterable { public protocol Plugin: AnyObject { var type: PluginType { get } - var name: String { get } var analytics: Analytics? { get set } - init(name: String) func configure(analytics: Analytics) func update(settings: Settings) func execute(event: T?) -> T? @@ -46,24 +44,18 @@ public protocol EventPlugin: Plugin { } public protocol DestinationPlugin: EventPlugin { + var key: String { get } var timeline: Timeline { get } - func add(plugin: Plugin) -> String + func add(plugin: Plugin) -> Plugin func apply(closure: (Plugin) -> Void) - func remove(pluginName: String) + func remove(plugin: Plugin) } public protocol UtilityPlugin: EventPlugin { } // For internal platform-specific bits -internal protocol PlatformPlugin: Plugin { - static var specificName: String { get set } -} +internal protocol PlatformPlugin: Plugin { } -extension PlatformPlugin { - internal init() { - self.init(name: Self.specificName) - } -} // MARK: - Plugin instance helpers extension Plugin { @@ -101,12 +93,12 @@ extension DestinationPlugin { */ @discardableResult - public func add(plugin: Plugin) -> String { + public func add(plugin: Plugin) -> Plugin { if let analytics = self.analytics { plugin.configure(analytics: analytics) } timeline.add(plugin: plugin) - return plugin.name + return plugin } /** @@ -114,8 +106,8 @@ extension DestinationPlugin { - Parameter pluginName: An plugin name. */ - public func remove(pluginName: String) { - timeline.remove(pluginName: pluginName) + public func remove(plugin: Plugin) { + timeline.remove(plugin: plugin) } } @@ -140,15 +132,15 @@ extension Analytics { */ @discardableResult - public func add(plugin: Plugin) -> String { + public func add(plugin: Plugin) -> Plugin { plugin.configure(analytics: self) timeline.add(plugin: plugin) - if plugin is DestinationPlugin && !(plugin is SegmentDestination) { + if !(plugin is SegmentDestination), let destPlugin = plugin as? DestinationPlugin { // need to maintain the list of integrations to inject into payload - store.dispatch(action: System.AddIntegrationAction(pluginName: plugin.name)) + store.dispatch(action: System.AddIntegrationAction(key: destPlugin.key)) } - return plugin.name + return plugin } /** @@ -156,12 +148,14 @@ extension Analytics { - Parameter pluginName: An plugin name. */ - public func remove(pluginName: String) { - timeline.remove(pluginName: pluginName) - store.dispatch(action: System.RemoveIntegrationAction(pluginName: pluginName)) + public func remove(plugin: Plugin) { + timeline.remove(plugin: plugin) + if !(plugin is SegmentDestination), let destPlugin = plugin as? DestinationPlugin { + store.dispatch(action: System.RemoveIntegrationAction(key: destPlugin.key)) + } } - public func find(pluginName: String) -> Plugin? { - return timeline.find(pluginName: pluginName) + public func find(pluginType: T.Type) -> T? { + return timeline.find(pluginType: pluginType) } } diff --git a/Sources/Segment/Plugins/Context.swift b/Sources/Segment/Plugins/Context.swift index 105fb516..146a8f2b 100644 --- a/Sources/Segment/Plugins/Context.swift +++ b/Sources/Segment/Plugins/Context.swift @@ -8,19 +8,12 @@ import Foundation public class Context: PlatformPlugin { - static var specificName: String = "Segment_Context" - public let type: PluginType = .before - public let name: String = specificName public var analytics: Analytics? internal var staticContext = staticContextData() internal static var device = VendorSystem.current - public required init(name: String) { - // ignore name here; it's hardcoded above. - } - public func execute(event: T?) -> T? { guard var workingEvent = event else { return event } diff --git a/Sources/Segment/Plugins/DeviceToken.swift b/Sources/Segment/Plugins/DeviceToken.swift index e453ec6f..daa8bdd5 100644 --- a/Sources/Segment/Plugins/DeviceToken.swift +++ b/Sources/Segment/Plugins/DeviceToken.swift @@ -8,17 +8,12 @@ import Foundation public class DeviceToken: PlatformPlugin { - static var specificName = "Segment_DeviceToken" - - public let type: PluginType = .before - public let name: String = specificName + public let type = PluginType.before public var analytics: Analytics? public var token: String? = nil - public required init(name: String) { - // ignore `name` here, it's hard coded above. - } + public required init() { } public func execute(event: T?) -> T? { guard var workingEvent = event else { return event } @@ -36,10 +31,10 @@ public class DeviceToken: PlatformPlugin { extension Analytics { public func setDeviceToken(_ token: String) { - if let tokenPlugin = self.find(pluginName: DeviceToken.specificName) as? DeviceToken { + if let tokenPlugin = self.find(pluginType: DeviceToken.self) { tokenPlugin.token = token } else { - let tokenPlugin = DeviceToken(name: DeviceToken.specificName) + let tokenPlugin = DeviceToken() tokenPlugin.token = token add(plugin: tokenPlugin) } diff --git a/Sources/Segment/Plugins/Logger.swift b/Sources/Segment/Plugins/Logger.swift index 5f357977..a6ae0f92 100644 --- a/Sources/Segment/Plugins/Logger.swift +++ b/Sources/Segment/Plugins/Logger.swift @@ -14,19 +14,14 @@ public enum LogType: Int { } class Logger: UtilityPlugin { + public var filterType = LogType.info - public var filterType: LogType = .info - - let type: PluginType - let name: String + let type = PluginType.utility var analytics: Analytics? private var messages = [LogMessage]() - required init(name: String) { - self.name = name - self.type = .utility - } + required init() { } func log(type: LogType, message: String, event: RawEvent?) { print("\(type) -- Message: \(message)") diff --git a/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift index 41707241..12efff1c 100644 --- a/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/Linux/LinuxLifecycleMonitor.swift @@ -9,16 +9,7 @@ import Foundation #if os(Linux) class LinuxLifecycleMonitor: PlatformPlugin { - static var specificName = "Segment_LinuxLifecycleMonitor" - let type: PluginType - let name: String - + let type = PluginType.utility var analytics: Analytics? - - required init(name: String) { - self.type = .utility - self.name = name - } - } #endif diff --git a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift index 68ad78d7..3e34089f 100644 --- a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleMonitor.swift @@ -65,7 +65,7 @@ class macOSLifecycleMonitor: PlatformPlugin { NSApplication.willTerminateNotification, NSApplication.didChangeScreenParametersNotification] - required init(name: String) { + required init() { self.application = NSApplication.shared setupListeners() } diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift index b150bda4..9d100616 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift @@ -15,17 +15,9 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle { static var versionKey = "SEGVersionKey" static var buildKey = "SEGBuildKeyV2" - static var specificName: String = "Segment_iOSLifecycleEvents" - - let type: PluginType - let name: String + let type = PluginType.before var analytics: Analytics? - public required init(name: String) { - self.name = name - self.type = .before - } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { if analytics?.configuration.values.trackApplicationLifecycleEvents == false { return diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift index 8cb12d7e..9e0dcaab 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleMonitor.swift @@ -35,10 +35,7 @@ public extension iOSLifecycle { } class iOSLifecycleMonitor: PlatformPlugin { - static var specificName = "Segment_iOSLifecycleMonitor" - - let type: PluginType - let name: String + let type = PluginType.utility var analytics: Analytics? private var application: UIApplication @@ -52,11 +49,8 @@ class iOSLifecycleMonitor: PlatformPlugin { UIApplication.significantTimeChangeNotification, UIApplication.backgroundRefreshStatusDidChangeNotification] - required init(name: String) { - self.type = .utility - self.name = name + required init() { application = UIApplication.shared - setupListeners() } diff --git a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift index a8f93110..6946045c 100644 --- a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift @@ -14,17 +14,9 @@ class watchOSLifecycleEvents: PlatformPlugin, watchOSLifecycle { static var versionKey = "SEGVersionKey" static var buildKey = "SEGBuildKeyV2" - static var specificName: String = "Segment_watchOSLifecycleEvents" - - let type: PluginType - let name: String + let type = PluginType.before var analytics: Analytics? - public required init(name: String) { - self.name = name - self.type = .before - } - func applicationDidFinishLaunching(watchExtension: WKExtension) { if analytics?.configuration.values.trackApplicationLifecycleEvents == false { return diff --git a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift index c98057bc..c32b66f5 100644 --- a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift +++ b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleMonitor.swift @@ -28,10 +28,7 @@ public extension watchOSLifecycle { class watchOSLifecycleMonitor: PlatformPlugin { - static var specificName: String = "Segment_watchOSLifecycleMonitor" - - var type: PluginType - var name: String + var type = PluginType.utility var analytics: Analytics? var wasBackgrounded: Bool = false @@ -42,12 +39,8 @@ class watchOSLifecycleMonitor: PlatformPlugin { WKExtension.applicationDidBecomeActiveNotification, WKExtension.applicationWillResignActiveNotification] - required init(name: String) { - self.type = .utility - self.name = name - + required init() { watchExtension = WKExtension.shared() - setupListeners() } diff --git a/Sources/Segment/Plugins/SegmentDestination.swift b/Sources/Segment/Plugins/SegmentDestination.swift index dac75181..0ca7776d 100644 --- a/Sources/Segment/Plugins/SegmentDestination.swift +++ b/Sources/Segment/Plugins/SegmentDestination.swift @@ -16,8 +16,14 @@ import FoundationNetworking #endif public class SegmentDestination: DestinationPlugin { - public let type: PluginType = .destination - public let name: String + internal enum Constants: String { + case integrationName = "Segment.io" + case apiHost = "apiHost" + case apiKey = "apiKey" + } + + public let type = PluginType.destination + public let key: String = Constants.integrationName.rawValue public let timeline = Timeline() public var analytics: Analytics? { didSet { @@ -44,16 +50,6 @@ public class SegmentDestination: DestinationPlugin { @Atomic private var eventCount: Int = 0 internal var flushTimer: QueueTimer? = nil - internal enum Constants: String { - case integrationName = "Segment.io" - case apiHost = "apiHost" - case apiKey = "apiKey" - } - - required public init(name: String) { - self.name = name - } - internal func initialSetup() { guard let analytics = self.analytics else { return } storage = analytics.storage @@ -64,7 +60,7 @@ public class SegmentDestination: DestinationPlugin { } public func update(settings: Settings) { - let segmentInfo = settings.integrationSettings(for: Self.Constants.integrationName.rawValue) + let segmentInfo = settings.integrationSettings(forKey: self.key) apiKey = segmentInfo?[Self.Constants.apiKey.rawValue] as? String apiHost = segmentInfo?[Self.Constants.apiHost.rawValue] as? String if (apiHost != nil && apiKey != nil), let analytics = self.analytics { diff --git a/Sources/Segment/Plugins/StartupQueue.swift b/Sources/Segment/Plugins/StartupQueue.swift index 9ebf157e..5e3b39ce 100644 --- a/Sources/Segment/Plugins/StartupQueue.swift +++ b/Sources/Segment/Plugins/StartupQueue.swift @@ -9,13 +9,12 @@ import Foundation import Sovran internal class StartupQueue: Plugin, Subscriber { - static var specificName = "Segment_StartupQueue" static let maxSize = 1000 @Atomic var running: Bool = false let type: PluginType = .before - let name: String = specificName + var analytics: Analytics? = nil { didSet { analytics?.store.subscribe(self, handler: runningUpdate) @@ -24,9 +23,7 @@ internal class StartupQueue: Plugin, Subscriber { var queuedEvents = [RawEvent]() - required init(name: String) { - // ignore name; hardcoded above. - } + required init() { } func execute(event: T?) -> T? { if running == false, let e = event { diff --git a/Sources/Segment/Settings.swift b/Sources/Segment/Settings.swift index aa165a44..ef22470b 100644 --- a/Sources/Segment/Settings.swift +++ b/Sources/Segment/Settings.swift @@ -40,20 +40,32 @@ public struct Settings: Codable { * - Parameter for: The string name of the integration * - Returns: The dictionary representing the settings for this integration as supplied by Segment.com */ - public func integrationSettings(for name: String) -> [String: Any]? { + public func integrationSettings(forKey key: String) -> [String: Any]? { guard let settings = integrations?.dictionaryValue else { return nil } - let result = settings[name] as? [String: Any] + let result = settings[key] as? [String: Any] return result } - public func integrationSettings(name: String) -> T? { + public func integrationSettings(forKey key: String) -> T? { var result: T? = nil guard let settings = integrations?.dictionaryValue else { return nil } - if let dict = settings[name], let jsonData = try? JSONSerialization.data(withJSONObject: dict) { + if let dict = settings[key], let jsonData = try? JSONSerialization.data(withJSONObject: dict) { result = try? JSONDecoder().decode(T.self, from: jsonData) } return result } + + public func integrationSettings(forPlugin plugin: DestinationPlugin) -> T? { + return integrationSettings(forKey: plugin.key) + } + + public func isDestinationEnabled(key: String) -> Bool { + guard let settings = integrations?.dictionaryValue else { return false } + if settings.keys.contains(key) { + return true + } + return false + } } extension Settings: Equatable { diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index 95de8c9c..c1164d3f 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -11,21 +11,20 @@ import Sovran extension Analytics: Subscriber { internal func platformStartup() { - add(plugin: StartupQueue(name: StartupQueue.specificName)) + add(plugin: StartupQueue()) // add segment destination plugin unless // asked not to via configuration. if configuration.values.autoAddSegmentDestination { - let segmentDestination = SegmentDestination(name: "Segment.io") + let segmentDestination = SegmentDestination() segmentDestination.analytics = self add(plugin: segmentDestination) } // Setup platform specific plugins if let platformPlugins = platformPlugins() { - for pluginType in platformPlugins { - let prebuilt = pluginType.init() - add(plugin: prebuilt) + for plugin in platformPlugins { + add(plugin: plugin) } } @@ -35,26 +34,26 @@ extension Analytics: Subscriber { setupSettingsCheck() } - internal func platformPlugins() -> [PlatformPlugin.Type]? { - var plugins = [PlatformPlugin.Type]() + internal func platformPlugins() -> [PlatformPlugin]? { + var plugins = [PlatformPlugin]() // setup lifecycle if desired if configuration.values.trackApplicationLifecycleEvents { // add context plugin as well as it's platform specific internally. // this must come first. - plugins.append(Context.self) + plugins.append(Context()) #if os(iOS) || os(tvOS) - plugins += [iOSLifecycleMonitor.self, iOSLifecycleEvents.self, DeviceToken.self] + plugins += [iOSLifecycleMonitor(), iOSLifecycleEvents(), DeviceToken()] #endif #if os(watchOS) - plugins += [watchOSLifecycleMonitor.self, watchOSLifecycleEvents.self] + plugins += [watchOSLifecycleMonitor(), watchOSLifecycleEvents()] #endif #if os(macOS) - plugins += [macOSLifecycleMonitor.self, DeviceToken.self] + plugins += [macOSLifecycleMonitor(), DeviceToken()] #endif #if os(Linux) - plugins.append(LinuxLifecycleMonitor.self) + plugins.append(LinuxLifecycleMonitor()) #endif } diff --git a/Sources/Segment/State.swift b/Sources/Segment/State.swift index 40e39890..3a71be49 100644 --- a/Sources/Segment/State.swift +++ b/Sources/Segment/State.swift @@ -29,14 +29,14 @@ struct System: State { } struct AddIntegrationAction: Action { - let pluginName: String + let key: String func reduce(state: System) -> System { // we need to set any destination plugins to false in the // integrations payload. this prevents them from being sent // by segment.com once an event reaches segment. if var integrations = state.integrations?.dictionaryValue { - integrations[pluginName] = false + integrations[key] = false if let jsonIntegrations = try? JSON(integrations) { let result = System(configuration: state.configuration, integrations: jsonIntegrations, @@ -50,11 +50,11 @@ struct System: State { } struct RemoveIntegrationAction: Action { - let pluginName: String + let key: String func reduce(state: System) -> System { if var integrations = state.integrations?.dictionaryValue { - integrations.removeValue(forKey: pluginName) + integrations.removeValue(forKey: key) if let jsonIntegrations = try? JSON(integrations) { let result = System(configuration: state.configuration, integrations: jsonIntegrations, diff --git a/Sources/Segment/Timeline.swift b/Sources/Segment/Timeline.swift index ad1b0738..02239cb6 100644 --- a/Sources/Segment/Timeline.swift +++ b/Sources/Segment/Timeline.swift @@ -60,9 +60,9 @@ internal class Mediator { } } - internal func remove(pluginName: String) { - plugins.removeAll { (plugin) -> Bool in - return plugin.name == pluginName + internal func remove(plugin: Plugin) { + plugins.removeAll { (storedPlugin) -> Bool in + return plugin === storedPlugin } } @@ -103,31 +103,31 @@ extension Timeline { } } - internal func remove(pluginName: String) { + internal func remove(plugin: Plugin) { // remove all plugins with this name in every category for type in PluginType.allCases { if let mediator = plugins[type] { - let toRemove = mediator.plugins.filter { (plugin) -> Bool in - return plugin.name == pluginName + let toRemove = mediator.plugins.filter { (storedPlugin) -> Bool in + return plugin === storedPlugin } toRemove.forEach { (plugin) in plugin.shutdown() - mediator.remove(pluginName: pluginName) + mediator.remove(plugin: plugin) } } } } - internal func find(pluginName: String) -> Plugin? { + internal func find(pluginType: T.Type) -> T? { var found = [Plugin]() for type in PluginType.allCases { if let mediator = plugins[type] { found.append(contentsOf: mediator.plugins.filter { (plugin) -> Bool in - return plugin.name == pluginName + return plugin is T }) } } - return found.first + return found.first as? T } } @@ -209,6 +209,11 @@ extension DestinationPlugin { // This will process plugins (think destination middleware) that are tied // to this destination. + // For destination plugins, we will always have some kind of `settings`, + // and if we don't, it means this destination hasn't been setup on app.segment.com, + // which in turn ALSO means that we shouldn't be sending events to it. + + // apply .before and .enrichment types first ... let beforeResult = timeline.applyPlugins(type: .before, event: incomingEvent) let enrichmentResult = timeline.applyPlugins(type: .enrichment, event: beforeResult) diff --git a/Tests/Segment-Tests/Analytics_Tests.swift b/Tests/Segment-Tests/Analytics_Tests.swift index a251e954..7d3e3a7d 100644 --- a/Tests/Segment-Tests/Analytics_Tests.swift +++ b/Tests/Segment-Tests/Analytics_Tests.swift @@ -5,10 +5,10 @@ final class Analytics_Tests: XCTestCase { func testBaseEventCreation() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let myDestination = MyDestination(name: "fakeDestination") - myDestination.add(plugin: GooberPlugin(name: "booya")) + let myDestination = MyDestination() + myDestination.add(plugin: GooberPlugin()) - analytics.add(plugin: ZiggyPlugin(name: "crikey")) + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: myDestination) let traits = MyTraits(email: "brandon@redf.net") @@ -17,9 +17,9 @@ final class Analytics_Tests: XCTestCase { func testPluginConfigure() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let ziggy = ZiggyPlugin(name: "crikey") - let myDestination = MyDestination(name: "fakeDestination") - let goober = GooberPlugin(name: "booya") + let ziggy = ZiggyPlugin() + let myDestination = MyDestination() + let goober = GooberPlugin() myDestination.add(plugin: goober) analytics.add(plugin: ziggy) @@ -32,11 +32,11 @@ final class Analytics_Tests: XCTestCase { func testPluginRemove() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let myDestination = MyDestination(name: "fakeDestination") - myDestination.add(plugin: GooberPlugin(name: "booya")) + let myDestination = MyDestination() + myDestination.add(plugin: GooberPlugin()) let expectation = XCTestExpectation(description: "Ziggy Expectation") - let ziggy = ZiggyPlugin(name: "crikey") + let ziggy = ZiggyPlugin() ziggy.completion = { expectation.fulfill() } @@ -45,7 +45,7 @@ final class Analytics_Tests: XCTestCase { let traits = MyTraits(email: "brandon@redf.net") analytics.identify(userId: "brandon", traits: traits) - analytics.remove(pluginName: "crikey") + analytics.remove(plugin: ziggy) wait(for: [expectation], timeout: 1.0) } @@ -60,7 +60,7 @@ final class Analytics_Tests: XCTestCase { func testDeviceToken() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -77,7 +77,7 @@ final class Analytics_Tests: XCTestCase { #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) func testDeviceTokenData() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -95,7 +95,7 @@ final class Analytics_Tests: XCTestCase { func testTrack() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -109,7 +109,7 @@ final class Analytics_Tests: XCTestCase { func testIdentify() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -124,7 +124,7 @@ final class Analytics_Tests: XCTestCase { func testUserIdAndTraitsPersistCorrectly() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -150,7 +150,7 @@ final class Analytics_Tests: XCTestCase { func testScreen() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -172,7 +172,7 @@ final class Analytics_Tests: XCTestCase { func testGroup() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -192,7 +192,7 @@ final class Analytics_Tests: XCTestCase { func testReset() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) @@ -236,7 +236,7 @@ final class Analytics_Tests: XCTestCase { func testVersion() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let outputReader = OutputReaderPlugin(name: "outputReader") + let outputReader = OutputReaderPlugin() analytics.add(plugin: outputReader) waitUntilStarted(analytics: analytics) diff --git a/Tests/Segment-Tests/Logger_Tests.swift b/Tests/Segment-Tests/Logger_Tests.swift index b0b30796..4e3e7c8e 100644 --- a/Tests/Segment-Tests/Logger_Tests.swift +++ b/Tests/Segment-Tests/Logger_Tests.swift @@ -32,7 +32,7 @@ final class Logger_Tests: XCTestCase { let expectation = XCTestExpectation(description: "Called") - let mockLogger = LoggerMock(name: "Blah") + let mockLogger = LoggerMock() mockLogger.logClosure = { (type, message) in expectation.fulfill() diff --git a/Tests/Segment-Tests/Metrics_Tests.swift b/Tests/Segment-Tests/Metrics_Tests.swift index 4c47b6c0..30494773 100644 --- a/Tests/Segment-Tests/Metrics_Tests.swift +++ b/Tests/Segment-Tests/Metrics_Tests.swift @@ -14,10 +14,10 @@ final class Metrics_Tests: XCTestCase { func testBaseEventCreation() { let analytics = Analytics(configuration: Configuration(writeKey: "test")) - let myDestination = MyDestination(name: "fakeDestination") - myDestination.add(plugin: GooberPlugin(name: "booya")) + let myDestination = MyDestination() + myDestination.add(plugin: GooberPlugin()) - analytics.add(plugin: ZiggyPlugin(name: "crikey")) + analytics.add(plugin: ZiggyPlugin()) analytics.add(plugin: myDestination) let traits = MyTraits(email: "brandon@redf.net") diff --git a/Tests/Segment-Tests/Support/TestUtilities.swift b/Tests/Segment-Tests/Support/TestUtilities.swift index ed6c4aa5..3b3e0a24 100644 --- a/Tests/Segment-Tests/Support/TestUtilities.swift +++ b/Tests/Segment-Tests/Support/TestUtilities.swift @@ -25,11 +25,9 @@ struct MyTraits: Codable { class GooberPlugin: EventPlugin { let type: PluginType - let name: String var analytics: Analytics? - required init(name: String) { - self.name = name + init() { self.type = .enrichment } @@ -50,13 +48,11 @@ class GooberPlugin: EventPlugin { class ZiggyPlugin: EventPlugin { let type: PluginType - let name: String var analytics: Analytics? var completion: (() -> Void)? - required init(name: String) { - self.name = name + required init() { self.type = .enrichment } @@ -75,11 +71,11 @@ class ZiggyPlugin: EventPlugin { class MyDestination: DestinationPlugin { var timeline: Timeline let type: PluginType - let name: String + let key: String var analytics: Analytics? - required init(name: String) { - self.name = name + init() { + self.key = "MyDestination" self.type = .destination self.timeline = Timeline() } @@ -92,14 +88,12 @@ class MyDestination: DestinationPlugin { class OutputReaderPlugin: Plugin { let type: PluginType - let name: String var analytics: Analytics? var lastEvent: RawEvent? = nil - required init(name: String) { + init() { self.type = .after - self.name = name } func execute(event: T?) -> T? where T : RawEvent { @@ -114,7 +108,7 @@ class OutputReaderPlugin: Plugin { func waitUntilStarted(analytics: Analytics?) { guard let analytics = analytics else { return } // wait until the startup queue has emptied it's events. - if let startupQueue = analytics.find(pluginName: StartupQueue.specificName) as? StartupQueue { + if let startupQueue = analytics.find(pluginType: StartupQueue.self) { while startupQueue.running != true { RunLoop.main.run(until: Date.distantPast) }