Skip to content

[Swift 6] Add Swift 6 testing for Sessions #14599

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

Merged
merged 12 commits into from
May 15, 2025
Merged
9 changes: 9 additions & 0 deletions .github/workflows/sessions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ jobs:
- os: macos-14
xcode: Xcode_16.2
tests:
swift_version: 5.9
# Flaky tests on CI
- os: macos-15
xcode: Xcode_16.3
tests: --skip-tests
swift_version: 5.9
# Flaky tests on CI
- os: macos-15
xcode: Xcode_16.2
tests: --skip-tests
swift_version: 6.0
runs-on: ${{ matrix.build-env.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -51,6 +58,8 @@ jobs:
run: scripts/setup_bundler.sh
- name: Xcode
run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer
- name: Set Swift swift_version
run: sed -i "" "s/s.swift_version[[:space:]]*=[[:space:]]*'5.9'/s.swift_version = '${{ matrix.build-env.swift_version }}'/" FirebaseSessions.podspec
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3
with:
timeout_minutes: 120
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ typedef void (^FIRInstallationsTokenHandler)(
* as the ability to delete it. A Firebase Installation is unique by `FirebaseApp.name` and
* `FirebaseApp.options.googleAppID` .
*/
NS_SWIFT_NAME(Installations)
@interface FIRInstallations : NSObject
NS_SWIFT_NAME(Installations) NS_SWIFT_SENDABLE @interface FIRInstallations : NSObject

- (instancetype)init NS_UNAVAILABLE;

Expand Down
9 changes: 6 additions & 3 deletions FirebaseSessions/Sources/ApplicationInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum DevEnvironment: String {
case autopush // Autopush environment
}

protocol ApplicationInfoProtocol {
protocol ApplicationInfoProtocol: Sendable {
/// Google App ID / GMP App ID
var appID: String { get }

Expand Down Expand Up @@ -62,12 +62,15 @@ protocol ApplicationInfoProtocol {
var osDisplayVersion: String { get }
}

class ApplicationInfo: ApplicationInfoProtocol {
final class ApplicationInfo: ApplicationInfoProtocol {
let appID: String

private let networkInformation: NetworkInfoProtocol
private let envParams: [String: String]
private let infoDict: [String: Any]?

// Used to hold bundle info, so the `Any` params should also
// be Sendable.
private nonisolated(unsafe) let infoDict: [String: Any]?

init(appID: String, networkInfo: NetworkInfoProtocol = NetworkInfo(),
envParams: [String: String] = ProcessInfo.processInfo.environment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Foundation
import FirebaseSessionsObjC
#endif // SWIFT_PACKAGE

class DevEventConsoleLogger: EventGDTLoggerProtocol {
final class DevEventConsoleLogger: EventGDTLoggerProtocol {
private let commandLineArgument = "-FIRSessionsDebugEvents"

func logEvent(event: SessionStartEvent, completion: @escaping (Result<Void, Error>) -> Void) {
Expand Down
4 changes: 2 additions & 2 deletions FirebaseSessions/Sources/EventGDTLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation

internal import GoogleDataTransport

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

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

Expand Down
34 changes: 28 additions & 6 deletions FirebaseSessions/Sources/FirebaseSessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ private enum GoogleDataTransportConfig {

// Initializes the SDK and top-level classes
required convenience init(appID: String, installations: InstallationsProtocol) {
let googleDataTransport = GDTCORTransport(
let googleDataTransport = GoogleDataTransporter(
mappingID: GoogleDataTransportConfig.sessionsLogSource,
transformers: nil,
target: GoogleDataTransportConfig.sessionsTarget
)

let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport!)
let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport)

let appInfo = ApplicationInfo(appID: appID)
let settings = SessionsSettings(
Expand Down Expand Up @@ -135,10 +135,10 @@ private enum GoogleDataTransportConfig {
}

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

self.sessionGenerator = sessionGenerator
Expand Down Expand Up @@ -247,18 +247,40 @@ private enum GoogleDataTransportConfig {
return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
}

// This type is not actually sendable, but works around an issue below.
// It's safe only if executed on the main actor.
private struct MainActorNotificationCallback: @unchecked Sendable {
private let callback: (Notification) -> Void

init(_ callback: @escaping (Notification) -> Void) {
self.callback = callback
}

func invoke(notification: Notification) {
dispatchPrecondition(condition: .onQueue(.main))
callback(notification)
}
}

func register(subscriber: SessionsSubscriber) {
Logger
.logDebug(
"Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
)

// TODO(Firebase 12): After bumping to iOS 13, this hack should be replaced
// with `Task { @MainActor in }`.
let callback = MainActorNotificationCallback { notification in
subscriber.onSessionChanged(self.currentSessionDetails)
}

// Guaranteed to execute its callback on the main queue because of the queue parameter.
notificationCenter.addObserver(
forName: Sessions.SessionIDChangedNotificationName,
object: nil,
queue: nil
queue: OperationQueue.main
) { notification in
subscriber.onSessionChanged(self.currentSessionDetails)
callback.invoke(notification: notification)
}
// Immediately call the callback because the Sessions SDK starts
// before subscribers, so subscribers will miss the first Notification
Expand Down
2 changes: 1 addition & 1 deletion FirebaseSessions/Sources/FirebaseSessionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// Contains the list of errors that are localized for Firebase Sessions Library
enum FirebaseSessionsError: Error {
enum FirebaseSessionsError: Error, Sendable {
/// Event sampling related error
case SessionSamplingError
/// Firebase Installation ID related error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,31 @@

import Foundation

internal import GoogleDataTransport
@preconcurrency internal import GoogleDataTransport

enum GoogleDataTransportProtocolErrors: Error {
case writeFailure
}

protocol GoogleDataTransportProtocol {
protocol GoogleDataTransportProtocol: Sendable {
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void)
func eventForTransport() -> GDTCOREvent
}

extension GDTCORTransport: GoogleDataTransportProtocol {
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void) {
sendDataEvent(event) { wasWritten, error in
/// Workaround in combo with preconcurrency import of GDT. When GDT's
/// `GDTCORTransport`type conforms to Sendable within the GDT module,
/// this can be removed.
final class GoogleDataTransporter: GoogleDataTransportProtocol {
private let transporter: GDTCORTransport

init(mappingID: String,
transformers: [any GDTCOREventTransformer]?,
target: GDTCORTarget) {
transporter = GDTCORTransport(mappingID: mappingID, transformers: transformers, target: target)!
}

func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, any Error>) -> Void) {
transporter.sendDataEvent(event) { wasWritten, error in
if let error {
completion(.failure(error))
} else if !wasWritten {
Expand All @@ -38,4 +49,8 @@ extension GDTCORTransport: GoogleDataTransportProtocol {
}
}
}

func eventForTransport() -> GDTCOREvent {
transporter.eventForTransport()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation

internal import FirebaseInstallations

protocol InstallationsProtocol {
protocol InstallationsProtocol: Sendable {
var installationsWaitTimeInSecond: Int { get }

/// Override Installation function for testing
Expand Down
4 changes: 2 additions & 2 deletions FirebaseSessions/Sources/NetworkInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ import Foundation
internal import GoogleUtilities
#endif // SWIFT_PACKAGE

protocol NetworkInfoProtocol {
protocol NetworkInfoProtocol: Sendable {
var networkType: GULNetworkType { get }

var mobileSubtype: String { get }
}

class NetworkInfo: NetworkInfoProtocol {
final class NetworkInfo: NetworkInfoProtocol {
var networkType: GULNetworkType {
return GULNetworkInfo.getNetworkType()
}
Expand Down
2 changes: 1 addition & 1 deletion FirebaseSessions/Sources/Public/SessionsSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Foundation
/// Sessions Subscriber is an interface that dependent SDKs
/// must implement.
@objc(FIRSessionsSubscriber)
public protocol SessionsSubscriber {
public protocol SessionsSubscriber: Sendable {
func onSessionChanged(_ session: SessionDetails)
var isDataCollectionEnabled: Bool { get }
var sessionsSubscriberName: SessionsSubscriberName { get }
Expand Down
5 changes: 3 additions & 2 deletions FirebaseSessions/Sources/SessionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import Foundation

protocol SessionCoordinatorProtocol {
protocol SessionCoordinatorProtocol: Sendable {
func attemptLoggingSessionStart(event: SessionStartEvent,
callback: @escaping (Result<Void, FirebaseSessionsError>) -> Void)
}
Expand All @@ -23,8 +23,9 @@ protocol SessionCoordinatorProtocol {
/// SessionCoordinator is responsible for coordinating the systems in this SDK
/// involved with sending a Session Start event.
///
class SessionCoordinator: SessionCoordinatorProtocol {
final class SessionCoordinator: SessionCoordinatorProtocol {
let installations: InstallationsProtocol

let fireLogger: EventGDTLoggerProtocol

init(installations: InstallationsProtocol,
Expand Down
6 changes: 3 additions & 3 deletions FirebaseSessions/Sources/SessionInitiator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ import Foundation
///
class SessionInitiator {
let currentTime: () -> Date
var settings: SettingsProtocol
var backgroundTime = Date.distantFuture
var initiateSessionStart: () -> Void = {}
let settings: SettingsProtocol
private var backgroundTime = Date.distantFuture
private var initiateSessionStart: () -> Void = {}

init(settings: SettingsProtocol, currentTimeProvider: @escaping () -> Date = Date.init) {
currentTime = currentTimeProvider
Expand Down
Loading
Loading