Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BuildSettingsKit v2 #24233

Merged
merged 10 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Modules/Sources/BuildSettingsKit/BuildSettings+Live.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

extension BuildSettings {
static let live = BuildSettings(bundle: .app)

init(bundle: Bundle) {
pushNotificationAppID = bundle.infoValue(forKey: "WPPushNotificationAppID")
appGroupName = bundle.infoValue(forKey: "WPAppGroupName")
appKeychainAccessGroup = bundle.infoValue(forKey: "WPAppKeychainAccessGroup")
}
}
Comment on lines +3 to +11
Copy link
Contributor

@AliSoftware AliSoftware Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought about an idea. Not sure if it's worth it to implement, thought that could make it easier to add new values to the struct when we add new settings in Info.plist (cf @mokagio 's comment):

  • Use an enum BuildSettingsKeys: String to declare the list of property names <-> Info.plist keys we want to track
  • Use @dynamicMemberLookup on the struct BuildSettings with subscript<T>(dynamicMember: KeyPath<BuildSettingsKey, T>) -> T to call the bundle,infoValue method with the right key (rawValue of the enum) when .live, dummy value when .preview

This would avoid having to declare each public var in the struct BuildSettings declaration (well you'd have to declare them as case of the BuildSettingsKeys enum instead though) but also avoid having to duplicate the init implementation here and for the .preview case, automating it all instead using dynamic lookup.

That way, adding a new key would just be a matter of adding a new case to the enum BuildSettingsKeys and the rest will be automatic: no need to add a new public var on the struct BuildSettings, no need to update the init(bundle: Bundle) implementation here, no need to update the implementation for static var preview = BuildSettings(…) either…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't considered @dynamicMemberLookup. It might be a good fit here. I'm yet to use it anywhere tbh.

The other two potentials options are:

  • Property wrappers (something inspired by swift-dependencies/Dependency.swift)
  • Codable and PropertyListDecoder (assuming we manage to pull these outside of the Info.plist so we could drop the WP* prefix for keys and use automatic keys from Codable).

having to declare each public var in the struct BuildSettings declaration

These properties also work as a cache for the settings. Reading a value from s stored property is as fast as it gets.

easier to add new values to the struct when we add new settings

This is something that you need to do very rarely, so probably not something to optimize for. I'd stop where we are right now as the main pain points when adding keys are already gone thanks to Bundle.app, and there isn't much else to improve upon without introducing more complexity and/or indirection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to get some inspiration from https://github.com/sindresorhus/Defaults

Copy link
Contributor

@AliSoftware AliSoftware Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kean I just tested this in Xcode Playground:

// MARK: Bundle extension for typed Info.plist key access

extension Bundle {
  func infoValue<T>(forKey key: String) -> T where T: LosslessStringConvertible {
    guard let object = Bundle.app.object(forInfoDictionaryKey: key) else {
      fatalError("Missing value for key \(key))")
    }
    switch object {
    case let value as T: return value
    case let string as String:
      guard let value = T(string) else { fallthrough }
      return value
    default:
      fatalError("Unexpected value \(object) for key: \(key))")
    }
  }
}




// MARK: BuildSettings type, declaring its different environments

enum BuildSettings {
  case live
  case preview

  static let current: BuildSettings = {
#if DEBUG
    let processInfo = ProcessInfo.processInfo
    if processInfo.isXcodePreview {
      return .preview
    }
    if processInfo.isTesting {
      fatalError("BuildSettings are unavailable when running unit tests. Make sure to inject the values manually in system under test.")
    }
#endif
    return .live
  }()
}



// MARK: Magic to make it possible to access BuildSettings via subscript

extension BuildSettings {
  // We cannot use `Key` as the container for keys because of "Static stored properties not supported in generic types".
  // To workaround, we use a type-erased container as the parent class and the generic-annotated Key<T> as subclass of it.
  // That way, in `BuildSettings.current[.foo]`, Swift will still find the `static let foo` declared in parent class Keys
  // even if the subscript<T>(key: Key<T>) expects an instance of the subclass
  class Keys {
    let infoPlistKey: String
    init(infoPlistKey: String) {
      self.infoPlistKey = infoPlistKey
    }
  }

  final class Key<T>: Keys {
    let xcpreviewValue: T

    init(_ infoPlistKey: String, xcpreviewValue: T) {
      self.xcpreviewValue = xcpreviewValue
      super.init(infoPlistKey: infoPlistKey)
    }
  }

  subscript<T>(key: Key<T>) -> T where T: LosslessStringConvertible {
    switch self {
    case .live:
      return Bundle.app.infoValue(forKey: key.infoPlistKey)
    case .preview:
      return key.xcpreviewValue
    }
  }
}

extension BuildSettings.Key<String> {
  convenience init(_ infoPlistKey: String) {
    self.init(infoPlistKey, xcpreviewValue: "XCPreview-\(infoPlistKey)")
  }
}

Usage:

// MARK: Declare your keys

extension BuildSettings.Keys {
  static let pushNotificationAppID = BuildSettings.Key<String>("WPPushNotificationAppID")
  static let appGroupName = BuildSettings.Key<String>("WPAppGroupName")
  static let appKeychainAccessGroup = BuildSettings.Key<String>("WPAppKeychainAccessGroup")
  static let myFeatureFlag = BuildSettings.Key<Bool>("WPMyFeatureFlag", xcpreviewValue: true)
}

// MARK: Access your keys

let appId = BuildSettings.current[.pushNotificationAppID]
print("appID = \(appId)") // Value of Info.plist

let appIdPreview = BuildSettings.preview[.pushNotificationAppID]
print("appID (XCPreview) = \(appIdPreview)") // "XCPreview-WPPushNotificationAppID"

let flag = BuildSettings.current[.myFeatureFlag]
print("flag = \(flag)") // Boolean value from Info.plist

let flagPreview = BuildSettings.preview[.myFeatureFlag]
print("flag (XCPreview) = \(flagPreview)") // true

This has the huge benefit that the declaration of new keys only consists of adding them under extension BuildSettings.Keys, with their <Type>, infoPlistKey and xcpreviewValue all declared at once.

Copy link
Contributor

@AliSoftware AliSoftware Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These properties also work as a cache for the settings. Reading a value from s stored property is as fast as it gets.

As for that part, you can always implement a Cache layer either via property wrappers, or by storing the data in an in-memory Dictionary before trying to hit Bundle.app.object(forInfoDictionaryKey:).

That being said, I wouldn't bother, because I wouldn't be surprised that the value of Bundle.infoDictionary is only read from disk once then cached in-memory, making Bundle.object(forInfoDictionaryKey:)'s own implementation already including a cache layer implicitly (It's just like UserDefaults's implementation do for avoiding to access the .plist files every time)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks fairly similar in terms of how much information you need to provide.

// Without property wrappers
pushNotificationAppID = bundle.infoValue(forKey: "WPPushNotificationAppID")        

// With property wrappers
static let pushNotificationAppID = BuildSettings.Key<String>("WPPushNotificationAppID") 

I'm happy to improve upon it, but I'm also pretty happy with the current setup – it's stupid simple, which is perfect. There isn't anything that bothers me except for the fast that we have to rely on the ability to read app's Info.plist values from app extensions, which is also fine.

That being said, I wouldn't bother, because I wouldn't be surprised that the value of Bundle.infoDictionary is only read from disk once then cached in-memory

It's likely true, and reading the values upfront is probably not the best idea either. I doubt it has any measurable impact on performance either way.


private extension Bundle {
func infoValue<T>(forKey key: String) -> T where T: LosslessStringConvertible {
guard let object = object(forInfoDictionaryKey: key) else {
fatalError("missing value for key: \(key)")
}
switch object {
case let value as T:
return value
case let string as String:
guard let value = T(string) else { fallthrough }
return value
default:
fatalError("unexpected value: \(object) for key: \(key)")
}
}
}

private extension Bundle {
/// Returns the `Bundle` for the host `.app`.
///
/// - If this is called from code already located in the main app's bundle or from a Pod/Framework,
/// this will return the same as `Bundle.main`, aka the bundle of the app itself.
/// - If this is called from an App Extension (Widget, ShareExtension, etc), this will return the bundle of the
/// main app hosting said App Extension (while `Bundle.main` would return the App Extension itself)
static let app: Bundle = {
var url = Bundle.main.bundleURL
while url.pathExtension != "app" && url.lastPathComponent != "/" {
Copy link
Contributor Author

@kean kean Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only non-stellar part of the PR, but we already use the same approach for other purposes, and it seems to be completely safe to access the app's contents from extensions.

Example:

(lldb) po Bundle.app
NSBundle </Users/kean/Library/Developer/CoreSimulator/Devices/1B783B63-75EA-4E96-98C6-83A5FE39A9DC/data/Containers/Bundle/Application/E1AF6C2A-0052-4A39-B9F4-34E1CB4A8D17/Jetpack.app> (not yet loaded)
(lldb) po Bundle.main
NSBundle </Users/kean/Library/Developer/CoreSimulator/Devices/1B783B63-75EA-4E96-98C6-83A5FE39A9DC/data/Containers/Bundle/Application/E1AF6C2A-0052-4A39-B9F4-34E1CB4A8D17/Jetpack.app/PlugIns/JetpackShareExtension.appex> (loaded)

url.deleteLastPathComponent()
}
guard let appBundle = Bundle(url: url) else { fatalError("Unable to find the parent app bundle") }
return appBundle
}()
}
25 changes: 25 additions & 0 deletions Modules/Sources/BuildSettingsKit/BuildSettings+Preview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

/// The container for Xcode previews.
extension BuildSettings {
nonisolated(unsafe) static var preview = BuildSettings(
pushNotificationAppID: "xcpreview_push_notification_id",
appGroupName: "xcpreview_app_group_name",
appKeychainAccessGroup: "xcpreview_app_keychain_access_group"
)
}

extension BuildSettings {
/// Updates the preview settings for the lifetime of the given closure.
/// Reverts to the original settings when done.
@MainActor
public static func withSettings<T>(_ configure: (inout BuildSettings) -> Void, perform closure: () -> T) -> T {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildSettings.preview isn't thread-safe, but we can live with that for the purposes of main-thread confined Xcode Previews.

var container = BuildSettings.preview
let original = container
configure(&container)
BuildSettings.preview = container
let value = closure()
BuildSettings.preview = original
return value
}
}
48 changes: 19 additions & 29 deletions Modules/Sources/BuildSettingsKit/BuildSettings.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
import Foundation

/// Provides convenient access for values defined in Info.plist files for
/// apps and app extensions.
/// Manages global build settings.
///
/// - warning: Most of these values exist only in Info.plist files for apps as
/// app extensions only need a tiny subset of these settings.
public enum BuildSettings {
public static var pushNotificationAppID: String {
infoPlistValue(forKey: "WPPushNotificationAppID")
}

public static var appGroupName: String {
infoPlistValue(forKey: "WPAppGroupName")
}

public static var appKeychainAccessGroup: String {
infoPlistValue(forKey: "WPAppKeychainAccessGroup")
}
}
/// The build settings work differently depending on the environment:
///
/// - **Live** – the code runs as part of an app or app extensions with build
/// settings configured using the `Info.plist` file.
/// - **Preview** – the code runs as part of the SwiftPM or Xcode target. In this
/// environment, the build settings have predefined values that can also be
/// changed at runtime.
/// - **Test** – `BuildSettings` are not available when running unit tests as
/// they are incompatible with parallelized tests and are generally not recommended.
public struct BuildSettings: Sendable {
public var pushNotificationAppID: String
public var appGroupName: String
public var appKeychainAccessGroup: String

private func infoPlistValue<T>(forKey key: String) -> T where T: LosslessStringConvertible {
guard let object = Bundle.main.object(forInfoDictionaryKey: key) else {
fatalError("missing value for key: \(key)")
}
switch object {
case let value as T:
return value
case let string as String:
guard let value = T(string) else { fallthrough }
return value
default:
fatalError("unexpected value: \(object) for key: \(key)")
public static var current: BuildSettings {
switch BuildSettingsEnvironment.current {
case .live: .live
case .preview: .preview
}
}
}
39 changes: 39 additions & 0 deletions Modules/Sources/BuildSettingsKit/BuildSettingsEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

enum BuildSettingsEnvironment {
case live
case preview

static let current: BuildSettingsEnvironment = {
#if DEBUG
let processInfo = ProcessInfo.processInfo
if processInfo.isXcodePreview {
return .preview
}
if processInfo.isTesting {
fatalError("BuildSettings are unavailable when running unit tests. Make sure to inject the values manually in system under test.")
}
#endif
return .live
}()
}

private extension ProcessInfo {
var isXcodePreview: Bool {
environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}

var isTesting: Bool {
if environment.keys.contains("XCTestBundlePath") { return true }
if environment.keys.contains("XCTestConfigurationFilePath") { return true }
if environment.keys.contains("XCTestSessionIdentifier") { return true }

return arguments.contains { argument in
let path = URL(fileURLWithPath: argument)
return path.lastPathComponent == "swiftpm-testing-helper"
|| argument == "--testing-library"
|| path.lastPathComponent == "xctest"
|| path.pathExtension == "xctest"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class NotificationSettingsService {

notificationsServiceRemote?.registerDeviceForPushNotifications(
token,
pushNotificationAppId: BuildSettings.pushNotificationAppID,
pushNotificationAppId: BuildSettings.current.pushNotificationAppID,
success: success,
failure: failure
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ open class NotificationSupportService: NSObject {
private let appKeychainAccessGroup: String

@objc convenience override init() {
self.init(appKeychainAccessGroup: BuildSettings.appKeychainAccessGroup)
self.init(appKeychainAccessGroup: BuildSettings.current.appKeychainAccessGroup)
}

init(appKeychainAccessGroup: String) {
Expand Down
4 changes: 2 additions & 2 deletions WordPress/Classes/Services/ShareExtensionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ open class ShareExtensionService: NSObject {

@objc public convenience override init() {
self.init(
appGroupName: BuildSettings.appGroupName,
appKeychainAccessGroup: BuildSettings.appKeychainAccessGroup
appGroupName: BuildSettings.current.appGroupName,
appKeychainAccessGroup: BuildSettings.current.appKeychainAccessGroup
)
}

Expand Down
114 changes: 79 additions & 35 deletions WordPress/Classes/Stores/StatsWidgetsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class StatsWidgetsStore {
private let appKeychainAccessGroup: String

init(coreDataStack: CoreDataStack = ContextManager.shared,
appGroupName: String = BuildSettings.appGroupName,
appKeychainAccessGroup: String = BuildSettings.appKeychainAccessGroup) {
appGroupName: String = BuildSettings.current.appGroupName,
appKeychainAccessGroup: String = BuildSettings.current.appKeychainAccessGroup) {
self.coreDataStack = coreDataStack
self.appGroupName = appGroupName
self.appKeychainAccessGroup = appKeychainAccessGroup
Expand All @@ -27,17 +27,17 @@ class StatsWidgetsStore {
initializeStatsWidgetsIfNeeded()

if let newTodayData = refreshStats(type: HomeWidgetTodayData.self) {
HomeWidgetTodayData.write(items: newTodayData)
setCachedItems(newTodayData)
WidgetCenter.shared.reloadTodayTimelines()
}

if let newAllTimeData = refreshStats(type: HomeWidgetAllTimeData.self) {
HomeWidgetAllTimeData.write(items: newAllTimeData)
setCachedItems(newAllTimeData)
WidgetCenter.shared.reloadAllTimeTimelines()
}

if let newThisWeekData = refreshStats(type: HomeWidgetThisWeekData.self) {
HomeWidgetThisWeekData.write(items: newThisWeekData)
setCachedItems(newThisWeekData)
WidgetCenter.shared.reloadThisWeekTimelines()
}
}
Expand All @@ -51,21 +51,21 @@ class StatsWidgetsStore {

var isReloadRequired = false

if !HomeWidgetTodayData.cacheDataExists() {
if !hasCachedItems(for: HomeWidgetTodayData.self) {
DDLogInfo("StatsWidgets: Writing initialization data into HomeWidgetTodayData.plist")
HomeWidgetTodayData.write(items: initializeHomeWidgetData(type: HomeWidgetTodayData.self))
setCachedItems(initializeHomeWidgetData(type: HomeWidgetTodayData.self))
isReloadRequired = true
}

if !HomeWidgetThisWeekData.cacheDataExists() {
if !hasCachedItems(for: HomeWidgetThisWeekData.self) {
DDLogInfo("StatsWidgets: Writing initialization data into HomeWidgetThisWeekData.plist")
HomeWidgetThisWeekData.write(items: initializeHomeWidgetData(type: HomeWidgetThisWeekData.self))
setCachedItems(initializeHomeWidgetData(type: HomeWidgetThisWeekData.self))
isReloadRequired = true
}

if !HomeWidgetAllTimeData.cacheDataExists() {
if !hasCachedItems(for: HomeWidgetAllTimeData.self) {
DDLogInfo("StatsWidgets: Writing initialization data into HomeWidgetAllTimeData.plist")
HomeWidgetAllTimeData.write(items: initializeHomeWidgetData(type: HomeWidgetAllTimeData.self))
setCachedItems(initializeHomeWidgetData(type: HomeWidgetAllTimeData.self))
isReloadRequired = true
}

Expand All @@ -83,7 +83,7 @@ class StatsWidgetsStore {
return
}

var homeWidgetCache = T.read() ?? initializeHomeWidgetData(type: widgetType)
var homeWidgetCache = getCachedItems(for: T.self) ?? initializeHomeWidgetData(type: widgetType)
guard let oldData = homeWidgetCache[siteID.intValue] else {
DDLogError("StatsWidgets: Failed to find a matching site")
return
Expand All @@ -93,7 +93,7 @@ class StatsWidgetsStore {
DDLogError("StatsWidgets: the site does not exist anymore")
// if for any reason that site does not exist anymore, remove it from the cache.
homeWidgetCache.removeValue(forKey: siteID.intValue)
T.write(items: homeWidgetCache)
setCachedItems(homeWidgetCache)
return
}

Expand All @@ -102,37 +102,81 @@ class StatsWidgetsStore {
if widgetType == HomeWidgetTodayData.self, let stats = stats as? TodayWidgetStats {
widgetReload = WidgetCenter.shared.reloadTodayTimelines

homeWidgetCache[siteID.intValue] = HomeWidgetTodayData(siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats) as? T
homeWidgetCache[siteID.intValue] = HomeWidgetTodayData(
siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats
) as? T

} else if widgetType == HomeWidgetAllTimeData.self, let stats = stats as? AllTimeWidgetStats {
widgetReload = WidgetCenter.shared.reloadAllTimeTimelines

homeWidgetCache[siteID.intValue] = HomeWidgetAllTimeData(siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats) as? T
homeWidgetCache[siteID.intValue] = HomeWidgetAllTimeData(
siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats
) as? T

} else if widgetType == HomeWidgetThisWeekData.self, let stats = stats as? ThisWeekWidgetStats {
widgetReload = WidgetCenter.shared.reloadThisWeekTimelines

homeWidgetCache[siteID.intValue] = HomeWidgetThisWeekData(siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats) as? T
homeWidgetCache[siteID.intValue] = HomeWidgetThisWeekData(
siteID: siteID.intValue,
siteName: blog.title ?? oldData.siteName,
url: blog.url ?? oldData.url,
timeZone: blog.timeZone ?? TimeZone.current,
date: Date(),
stats: stats
) as? T
}

T.write(items: homeWidgetCache)
setCachedItems(homeWidgetCache)
widgetReload?()
}

// MARK: HomeWidgetCache (Helpers)

private func getCachedItems<T: HomeWidgetData>(for type: T.Type) -> [Int: T]? {
do {
return try makeCache(for: type).read()
} catch {
DDLogError("HomeWidgetCache: failed to read items: \(error)")
return nil
}
}

private func hasCachedItems<T: HomeWidgetData>(for type: T.Type) -> Bool {
guard let items = getCachedItems(for: type) else {
return false
}
return !items.isEmpty
}

private func deleteCachedItems<T: HomeWidgetData>(for type: T.Type) {
do {
try makeCache(for: T.self).delete()
} catch {
DDLogError("HomeWidgetCache: failed to delete items: \(error)")
}
}

private func setCachedItems<T: HomeWidgetData>(_ items: [Int: T]) {
do {
try makeCache(for: T.self).write(items: items)
} catch {
DDLogError("HomeWidgetCache: failed to write items: \(error)")
}
}

private func makeCache<T: HomeWidgetData>(for type: T.Type) -> HomeWidgetCache<T> {
HomeWidgetCache<T>(appGroup: appGroupName)
}
}

// MARK: - Helper methods
Expand Down Expand Up @@ -283,9 +327,9 @@ private extension StatsWidgetsStore {

guard !isLoggedIn else { return }

HomeWidgetTodayData.delete()
HomeWidgetThisWeekData.delete()
HomeWidgetAllTimeData.delete()
deleteCachedItems(for: HomeWidgetTodayData.self)
deleteCachedItems(for: HomeWidgetThisWeekData.self)
deleteCachedItems(for: HomeWidgetAllTimeData.self)

userDefaults?.setValue(nil, forKey: WidgetStatsConfiguration.userDefaultsSiteIdKey)

Expand Down
2 changes: 1 addition & 1 deletion WordPress/Classes/System/WordPressAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
// 21-Oct-2017: We are only handling background URLSessions initiated by the share extension so there
// is no need to inspect the identifier beyond the simple check here.
let appGroupName = BuildSettings.appGroupName
let appGroupName = BuildSettings.current.appGroupName
if identifier.contains(appGroupName) {
let manager = ShareExtensionSessionManager(appGroup: appGroupName, backgroundSessionIdentifier: identifier)
manager.backgroundSessionCompletionBlock = completionHandler
Expand Down
Loading