Skip to content

Commit 491d037

Browse files
[Swift 6] Add Swift 6 testing for Sessions (#14599)
Co-authored-by: Morgan Chen <morganchen12@gmail.com>
1 parent 7b9899b commit 491d037

30 files changed

+252
-219
lines changed

.github/workflows/sessions.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,17 @@ jobs:
3939
- os: macos-14
4040
xcode: Xcode_16.2
4141
tests:
42+
swift_version: 5.9
4243
# Flaky tests on CI
4344
- os: macos-15
4445
xcode: Xcode_16.3
4546
tests: --skip-tests
47+
swift_version: 5.9
48+
# Flaky tests on CI
49+
- os: macos-15
50+
xcode: Xcode_16.2
51+
tests: --skip-tests
52+
swift_version: 6.0
4653
runs-on: ${{ matrix.build-env.os }}
4754
steps:
4855
- uses: actions/checkout@v4
@@ -51,6 +58,8 @@ jobs:
5158
run: scripts/setup_bundler.sh
5259
- name: Xcode
5360
run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer
61+
- name: Set Swift swift_version
62+
run: sed -i "" "s/s.swift_version[[:space:]]*=[[:space:]]*'5.9'/s.swift_version = '${{ matrix.build-env.swift_version }}'/" FirebaseSessions.podspec
5463
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3
5564
with:
5665
timeout_minutes: 120

FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FIRInstallations.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ typedef void (^FIRInstallationsTokenHandler)(
5757
* as the ability to delete it. A Firebase Installation is unique by `FirebaseApp.name` and
5858
* `FirebaseApp.options.googleAppID` .
5959
*/
60-
NS_SWIFT_NAME(Installations)
61-
@interface FIRInstallations : NSObject
60+
NS_SWIFT_NAME(Installations) NS_SWIFT_SENDABLE @interface FIRInstallations : NSObject
6261

6362
- (instancetype)init NS_UNAVAILABLE;
6463

FirebaseSessions/Sources/ApplicationInfo.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ enum DevEnvironment: String {
3434
case autopush // Autopush environment
3535
}
3636

37-
protocol ApplicationInfoProtocol {
37+
protocol ApplicationInfoProtocol: Sendable {
3838
/// Google App ID / GMP App ID
3939
var appID: String { get }
4040

@@ -62,12 +62,15 @@ protocol ApplicationInfoProtocol {
6262
var osDisplayVersion: String { get }
6363
}
6464

65-
class ApplicationInfo: ApplicationInfoProtocol {
65+
final class ApplicationInfo: ApplicationInfoProtocol {
6666
let appID: String
6767

6868
private let networkInformation: NetworkInfoProtocol
6969
private let envParams: [String: String]
70-
private let infoDict: [String: Any]?
70+
71+
// Used to hold bundle info, so the `Any` params should also
72+
// be Sendable.
73+
private nonisolated(unsafe) let infoDict: [String: Any]?
7174

7275
init(appID: String, networkInfo: NetworkInfoProtocol = NetworkInfo(),
7376
envParams: [String: String] = ProcessInfo.processInfo.environment,

FirebaseSessions/Sources/Development/DevEventConsoleLogger.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Foundation
1919
import FirebaseSessionsObjC
2020
#endif // SWIFT_PACKAGE
2121

22-
class DevEventConsoleLogger: EventGDTLoggerProtocol {
22+
final class DevEventConsoleLogger: EventGDTLoggerProtocol {
2323
private let commandLineArgument = "-FIRSessionsDebugEvents"
2424

2525
func logEvent(event: SessionStartEvent, completion: @escaping (Result<Void, Error>) -> Void) {

FirebaseSessions/Sources/EventGDTLogger.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Foundation
1717

1818
internal import GoogleDataTransport
1919

20-
protocol EventGDTLoggerProtocol {
20+
protocol EventGDTLoggerProtocol: Sendable {
2121
func logEvent(event: SessionStartEvent, completion: @escaping (Result<Void, Error>) -> Void)
2222
}
2323

@@ -26,7 +26,7 @@ protocol EventGDTLoggerProtocol {
2626
/// 1) Creating GDT Events and logging them to the GoogleDataTransport SDK
2727
/// 2) Handling debugging situations (eg. running in Simulator or printing the event to console)
2828
///
29-
class EventGDTLogger: EventGDTLoggerProtocol {
29+
final class EventGDTLogger: EventGDTLoggerProtocol {
3030
let googleDataTransport: GoogleDataTransportProtocol
3131
let devEventConsoleLogger: EventGDTLoggerProtocol
3232

FirebaseSessions/Sources/FirebaseSessions.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ private enum GoogleDataTransportConfig {
6262

6363
// Initializes the SDK and top-level classes
6464
required convenience init(appID: String, installations: InstallationsProtocol) {
65-
let googleDataTransport = GDTCORTransport(
65+
let googleDataTransport = GoogleDataTransporter(
6666
mappingID: GoogleDataTransportConfig.sessionsLogSource,
6767
transformers: nil,
6868
target: GoogleDataTransportConfig.sessionsTarget
6969
)
7070

71-
let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport!)
71+
let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport)
7272

7373
let appInfo = ApplicationInfo(appID: appID)
7474
let settings = SessionsSettings(
@@ -135,10 +135,10 @@ private enum GoogleDataTransportConfig {
135135
}
136136

137137
// Initializes the SDK and begins the process of listening for lifecycle events and logging
138-
// events
138+
// events. `logEventCallback` is invoked on a global background queue.
139139
init(appID: String, sessionGenerator: SessionGenerator, coordinator: SessionCoordinatorProtocol,
140140
initiator: SessionInitiator, appInfo: ApplicationInfoProtocol, settings: SettingsProtocol,
141-
loggedEventCallback: @escaping (Result<Void, FirebaseSessionsError>) -> Void) {
141+
loggedEventCallback: @escaping @Sendable (Result<Void, FirebaseSessionsError>) -> Void) {
142142
self.appID = appID
143143

144144
self.sessionGenerator = sessionGenerator
@@ -247,18 +247,40 @@ private enum GoogleDataTransportConfig {
247247
return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
248248
}
249249

250+
// This type is not actually sendable, but works around an issue below.
251+
// It's safe only if executed on the main actor.
252+
private struct MainActorNotificationCallback: @unchecked Sendable {
253+
private let callback: (Notification) -> Void
254+
255+
init(_ callback: @escaping (Notification) -> Void) {
256+
self.callback = callback
257+
}
258+
259+
func invoke(notification: Notification) {
260+
dispatchPrecondition(condition: .onQueue(.main))
261+
callback(notification)
262+
}
263+
}
264+
250265
func register(subscriber: SessionsSubscriber) {
251266
Logger
252267
.logDebug(
253268
"Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
254269
)
255270

271+
// TODO(Firebase 12): After bumping to iOS 13, this hack should be replaced
272+
// with `Task { @MainActor in }`.
273+
let callback = MainActorNotificationCallback { notification in
274+
subscriber.onSessionChanged(self.currentSessionDetails)
275+
}
276+
277+
// Guaranteed to execute its callback on the main queue because of the queue parameter.
256278
notificationCenter.addObserver(
257279
forName: Sessions.SessionIDChangedNotificationName,
258280
object: nil,
259-
queue: nil
281+
queue: OperationQueue.main
260282
) { notification in
261-
subscriber.onSessionChanged(self.currentSessionDetails)
283+
callback.invoke(notification: notification)
262284
}
263285
// Immediately call the callback because the Sessions SDK starts
264286
// before subscribers, so subscribers will miss the first Notification

FirebaseSessions/Sources/FirebaseSessionsError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import Foundation
1616

1717
/// Contains the list of errors that are localized for Firebase Sessions Library
18-
enum FirebaseSessionsError: Error {
18+
enum FirebaseSessionsError: Error, Sendable {
1919
/// Event sampling related error
2020
case SessionSamplingError
2121
/// Firebase Installation ID related error

FirebaseSessions/Sources/GoogleDataTransport+GoogleDataTransportProtocol.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,31 @@
1515

1616
import Foundation
1717

18-
internal import GoogleDataTransport
18+
@preconcurrency internal import GoogleDataTransport
1919

2020
enum GoogleDataTransportProtocolErrors: Error {
2121
case writeFailure
2222
}
2323

24-
protocol GoogleDataTransportProtocol {
24+
protocol GoogleDataTransportProtocol: Sendable {
2525
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void)
2626
func eventForTransport() -> GDTCOREvent
2727
}
2828

29-
extension GDTCORTransport: GoogleDataTransportProtocol {
30-
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void) {
31-
sendDataEvent(event) { wasWritten, error in
29+
/// Workaround in combo with preconcurrency import of GDT. When GDT's
30+
/// `GDTCORTransport`type conforms to Sendable within the GDT module,
31+
/// this can be removed.
32+
final class GoogleDataTransporter: GoogleDataTransportProtocol {
33+
private let transporter: GDTCORTransport
34+
35+
init(mappingID: String,
36+
transformers: [any GDTCOREventTransformer]?,
37+
target: GDTCORTarget) {
38+
transporter = GDTCORTransport(mappingID: mappingID, transformers: transformers, target: target)!
39+
}
40+
41+
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, any Error>) -> Void) {
42+
transporter.sendDataEvent(event) { wasWritten, error in
3243
if let error {
3344
completion(.failure(error))
3445
} else if !wasWritten {
@@ -38,4 +49,8 @@ extension GDTCORTransport: GoogleDataTransportProtocol {
3849
}
3950
}
4051
}
52+
53+
func eventForTransport() -> GDTCOREvent {
54+
transporter.eventForTransport()
55+
}
4156
}

FirebaseSessions/Sources/Installations+InstallationsProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Foundation
1717

1818
internal import FirebaseInstallations
1919

20-
protocol InstallationsProtocol {
20+
protocol InstallationsProtocol: Sendable {
2121
var installationsWaitTimeInSecond: Int { get }
2222

2323
/// Override Installation function for testing

FirebaseSessions/Sources/NetworkInfo.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ import Foundation
2525
internal import GoogleUtilities
2626
#endif // SWIFT_PACKAGE
2727

28-
protocol NetworkInfoProtocol {
28+
protocol NetworkInfoProtocol: Sendable {
2929
var networkType: GULNetworkType { get }
3030

3131
var mobileSubtype: String { get }
3232
}
3333

34-
class NetworkInfo: NetworkInfoProtocol {
34+
final class NetworkInfo: NetworkInfoProtocol {
3535
var networkType: GULNetworkType {
3636
return GULNetworkInfo.getNetworkType()
3737
}

FirebaseSessions/Sources/Public/SessionsSubscriber.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Foundation
1818
/// Sessions Subscriber is an interface that dependent SDKs
1919
/// must implement.
2020
@objc(FIRSessionsSubscriber)
21-
public protocol SessionsSubscriber {
21+
public protocol SessionsSubscriber: Sendable {
2222
func onSessionChanged(_ session: SessionDetails)
2323
var isDataCollectionEnabled: Bool { get }
2424
var sessionsSubscriberName: SessionsSubscriberName { get }
@@ -38,7 +38,7 @@ public class SessionDetails: NSObject {
3838

3939
/// Session Subscriber Names are used for identifying subscribers
4040
@objc(FIRSessionsSubscriberName)
41-
public enum SessionsSubscriberName: Int, CustomStringConvertible {
41+
public enum SessionsSubscriberName: Int, CustomStringConvertible, Sendable {
4242
case Unknown
4343
case Crashlytics
4444
case Performance

FirebaseSessions/Sources/SessionCoordinator.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import Foundation
1616

17-
protocol SessionCoordinatorProtocol {
17+
protocol SessionCoordinatorProtocol: Sendable {
1818
func attemptLoggingSessionStart(event: SessionStartEvent,
1919
callback: @escaping (Result<Void, FirebaseSessionsError>) -> Void)
2020
}
@@ -23,8 +23,9 @@ protocol SessionCoordinatorProtocol {
2323
/// SessionCoordinator is responsible for coordinating the systems in this SDK
2424
/// involved with sending a Session Start event.
2525
///
26-
class SessionCoordinator: SessionCoordinatorProtocol {
26+
final class SessionCoordinator: SessionCoordinatorProtocol {
2727
let installations: InstallationsProtocol
28+
2829
let fireLogger: EventGDTLoggerProtocol
2930

3031
init(installations: InstallationsProtocol,

FirebaseSessions/Sources/SessionInitiator.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ import Foundation
4141
///
4242
class SessionInitiator {
4343
let currentTime: () -> Date
44-
var settings: SettingsProtocol
45-
var backgroundTime = Date.distantFuture
46-
var initiateSessionStart: () -> Void = {}
44+
let settings: SettingsProtocol
45+
private var backgroundTime = Date.distantFuture
46+
private var initiateSessionStart: () -> Void = {}
4747

4848
init(settings: SettingsProtocol, currentTimeProvider: @escaping () -> Date = Date.init) {
4949
currentTime = currentTimeProvider

0 commit comments

Comments
 (0)