From 435274879c296c8b70036156fda0ffa372814501 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Thu, 1 Oct 2026 19:06:29 -0400 Subject: [PATCH] chore!: require Xcode 26 (swift-tools-version 6.2) Collapse Package.swift and Package@swift-5.9.swift into a single swift-tools-version 6.2 manifest that keeps Swift 5 language mode, and drop the compiler-version fallbacks that a Swift 6 floor makes dead. CI's minimum-toolchain, Pods-example and CocoaPods publish jobs move to Xcode 26.0.1. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/v4-swift-tools-6.md | 5 + .github/workflows/build-examples.yml | 6 +- .github/workflows/build.yml | 11 +- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 4 +- AGENTS.md | 4 +- CONTRIBUTING.md | 4 +- Makefile | 7 +- Package.swift | 12 +- Package@swift-5.9.swift | 110 ------------------ .../Autocapture/SwiftUITapAutocapture.swift | 18 ++- .../PostHogCrashReportProcessor.swift | 7 +- ...gErrorTrackingAutoCaptureIntegration.swift | 7 +- PostHog/PostHogSDK.swift | 16 +-- .../Plugins/Network/URLSessionSwizzler.swift | 7 +- PostHog/Utils/BoxedBeforeSend.swift | 6 +- PostHog/Utils/UIImage+WebP.swift | 7 +- 17 files changed, 50 insertions(+), 183 deletions(-) create mode 100644 .changeset/v4-swift-tools-6.md delete mode 100644 Package@swift-5.9.swift diff --git a/.changeset/v4-swift-tools-6.md b/.changeset/v4-swift-tools-6.md new file mode 100644 index 000000000..32b8ec3d6 --- /dev/null +++ b/.changeset/v4-swift-tools-6.md @@ -0,0 +1,5 @@ +--- +'posthog-ios': major +--- + +**Breaking:** require Xcode 26 or later (Swift 6.2 toolchain, `swift-tools-version` 6.2) to build PostHog, the same Xcode the App Store already requires for uploads; the SDK still compiles in Swift 5 language mode and its API is unchanged. diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 25bcae1a2..daa999543 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -65,10 +65,10 @@ jobs: target: buildExamplePodsStaticLib runner: macos-15 xcode-version: latest-stable - - name: Static Lib (Xcode 16.2) + - name: Static Lib (Xcode 26.0.1) target: buildExamplePodsStaticLib - runner: macos-14-xlarge - xcode-version: '16.2' + runner: macos-15-xlarge + xcode-version: '26.0.1' - name: Static Framework target: buildExamplePodsStaticFramework runner: macos-15 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe91b4355..710e6d0a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,10 +65,10 @@ jobs: - name: Check public API snapshot run: make apiCheck - build-sdk-swift53-compat: + build-sdk-min-toolchain: needs: detect-markdown-only - runs-on: macos-14-xlarge - name: Build SDK (Swift 5 compatibility on Xcode 15.4 with CocoaPods) + runs-on: macos-15-xlarge + name: Build SDK (minimum toolchain, Xcode 26.0.1) steps: - name: Complete markdown-only PR check if: needs.detect-markdown-only.outputs.markdown_only == 'true' @@ -78,12 +78,15 @@ jobs: - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 if: needs.detect-markdown-only.outputs.markdown_only != 'true' with: - xcode-version: '15.4' + xcode-version: '26.0.1' - name: Print toolchain versions if: needs.detect-markdown-only.outputs.markdown_only != 'true' run: | xcodebuild -version xcrun swift --version + - name: Build SDK via Swift Package Manager + if: needs.detect-markdown-only.outputs.markdown_only != 'true' + run: make buildSdkSpm - name: Install CocoaPods if: needs.detect-markdown-only.outputs.markdown_only != 'true' run: gem install cocoapods diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2471199f0..fa9f3dff5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,7 +9,7 @@ on: jobs: analyze: name: Analyze (${{ matrix.language }}) - runs-on: macos-14-xlarge + runs-on: macos-15-xlarge permissions: security-events: write # for uploading CodeQL results packages: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73c86fc6b..003e3115a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -297,7 +297,7 @@ jobs: publish-cocoapods: name: Publish to CocoaPods needs: [create-github-release, notify-approval-needed] - runs-on: macos-14 + runs-on: macos-15 if: always() && needs.create-github-release.result == 'success' permissions: contents: read @@ -312,7 +312,7 @@ jobs: - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: - xcode-version: '16.2' + xcode-version: '26.0.1' - name: Publish to CocoaPods env: diff --git a/AGENTS.md b/AGENTS.md index 94755de5a..46820cb42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,8 @@ - SDK entry point: [PostHog/PostHogSDK.swift](PostHog/PostHogSDK.swift); tests: `PostHogTests/`; replay/privacy: `PostHog/Replay/` and `PostHog/Resources/`. ## Repository invariants -- Maintain Swift 5 SDK language compatibility. Tests require Swift 6+ with Swift Testing (Xcode 16+); prefer Swift Testing for new tests. Use `PostHogTests/TestUtils/MockPostHogServer.swift` for HTTP stubbing. -- Preserve deployment minima: iOS 15, tvOS 13, macOS 10.15, watchOS 6, visionOS 1. Keep platform guards and graceful degradation, especially for session replay. Availability annotations/checks do not require adding platform support or raising targets. Intentional support changes require checking both `Package.swift` and `Package@swift-5.9.swift`, plus CocoaPods/Xcode packaging/build configuration; visionOS declarations belong in the Swift 5.9 manifest. +- Maintain Swift 5 SDK language mode. The package needs swift-tools-version 6.2 (Xcode 26+); prefer Swift Testing for new tests. Use `PostHogTests/TestUtils/MockPostHogServer.swift` for HTTP stubbing. +- Preserve deployment minima: iOS 15, tvOS 13, macOS 10.15, watchOS 6, visionOS 1. Keep platform guards and graceful degradation, especially for session replay. Availability annotations/checks do not require adding platform support or raising targets. Intentional support changes require checking `Package.swift` plus CocoaPods/Xcode packaging/build configuration. - Public APIs must remain thread-safe and callable from any thread. Preserve offline operation and queue-based event batching; never assume connectivity. `PostHogSDK.shared` is the default singleton; independent instances use `PostHogSDK.with(_:)` with `PostHogConfig`. - Preserve replay masking/privacy behavior and privacy-safe error logging; do not expose sensitive user data. - Prefer no new dependencies. libwebp is embedded; PHPLCrashReporter is vendored, prefixed PLCrashReporter for native crash reporting on iOS/macOS/tvOS, excluded on watchOS/visionOS. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d754d5b2..c7a1becef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ If you would like to contribute code to `posthog-ios` you can do so through GitH Use the repository's `make` wrappers rather than invoking `swift` or `xcodebuild` directly; [Makefile](Makefile) owns the commands and options. -For initial setup, install Xcode and, if needed, use `make bootstrap` to install CocoaPods, xcpretty, SwiftLint, SwiftFormat and Periphery. Agents should report missing prerequisites rather than install tools without authorization. Tests import `Testing` and need a Swift 6+ toolchain with Swift Testing (Xcode 16+); use current stable Xcode as CI does. This is separate from the SDK's Swift 5 language compatibility. +For initial setup, install Xcode 26 or later and, if needed, use `make bootstrap` to install CocoaPods, xcpretty, SwiftLint, SwiftFormat and Periphery. Agents should report missing prerequisites rather than install tools without authorization. The package uses swift-tools-version 6.2 and compiles in Swift 5 language mode; use current stable Xcode as CI does. Iterate with focused checks, then run the core CI-aligned checks before submitting **SDK changes**: @@ -41,7 +41,7 @@ Markdown-only changes need documentation/link checks and `git diff --check`, not | CocoaPods integration modes | `make buildExamplePodsStaticLib`, `make buildExamplePodsStaticFramework`, `make buildExamplePodsDynamicFramework` | | XCFramework integration | `make buildExampleXCFramework` | -Use relevant example/package checks for integration or packaging changes, and smoke test changed behavior in an example where applicable. `make buildExamples` runs all example builds; `make build` combines that with `make buildSdk`. These broad targets include CocoaPods installation and XCFramework generation, so they are not the default iteration loop. For package/support changes, also consult the package lint and Swift 5 compatibility lanes in [build CI](.github/workflows/build.yml) and [example CI](.github/workflows/build-examples.yml). +Use relevant example/package checks for integration or packaging changes, and smoke test changed behavior in an example where applicable. `make buildExamples` runs all example builds; `make build` combines that with `make buildSdk`. These broad targets include CocoaPods installation and XCFramework generation, so they are not the default iteration loop. For package/support changes, also consult the package lint and minimum-toolchain lanes in [build CI](.github/workflows/build.yml) and [example CI](.github/workflows/build-examples.yml). ### Simulator, UI and masking prerequisites diff --git a/Makefile b/Makefile index e825b9c93..0413819bf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: testSurveyUI build buildSdk buildExamples format swiftLint swiftFormat swiftLintCheck swiftFormatCheck installSwiftLint installSwiftFormat test testUploadSymbols recordEventShapeSnapshots testDowngradeCompatibility testOniOSSimulator testOnMacSimulator maskSnapshots recordMaskSnapshots checkMaskSnapshotRuntime lint bootstrap releaseCocoaPods api apiCheck apiUpdate buildIOS +.PHONY: testSurveyUI build buildSdk buildExamples format swiftLint swiftFormat swiftLintCheck swiftFormatCheck installSwiftLint installSwiftFormat test testUploadSymbols recordEventShapeSnapshots testDowngradeCompatibility testOniOSSimulator testOnMacSimulator maskSnapshots recordMaskSnapshots checkMaskSnapshotRuntime lint bootstrap releaseCocoaPods api apiCheck apiUpdate buildIOS buildSdkSpm build: buildSdk buildExamples @@ -7,13 +7,16 @@ buildIOS: buildSdk: set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=ios | xcpretty #ios - set -o pipefail && xcrun swift build --arch arm64 #macOS + $(MAKE) buildSdkSpm #macOS set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=macos | xcpretty #macOS set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination 'platform=macOS,variant=Mac Catalyst' | xcpretty #Mac Catalyst set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=tvos | xcpretty #tvOS set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=watchos | xcpretty #watchOS set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=xros | xcpretty #visionOS +buildSdkSpm: + set -o pipefail && xcrun swift build --arch arm64 + buildExamples: \ buildExamplesPlatforms \ buildExampleXCFramework \ diff --git a/Package.swift b/Package.swift index 40d71e29f..ec5b884c5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,10 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.2 import PackageDescription let package = Package( name: "PostHog", platforms: [ - // visionOS is supported via Package@swift-5.9.swift for Swift 5.9+ users - .macOS(.v10_15), .iOS(.v15), .tvOS(.v13), .watchOS(.v6), + .macOS(.v10_15), .iOS(.v15), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1), ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. @@ -28,7 +27,7 @@ let package = Package( dependencies: [ "PostHogObjCExceptionSupport", "phlibwebp", - .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .tvOS])), + .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])), ], path: "PostHog", exclude: [ @@ -100,12 +99,13 @@ let package = Package( "OHHTTPStubs", .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), // The crash-report processor tests import this directly to build a PHPLCrashReport. - .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .tvOS])), + .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])), ], path: "PostHogTests", resources: [ .process("Resources"), ] ), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift deleted file mode 100644 index 592cb7029..000000000 --- a/Package@swift-5.9.swift +++ /dev/null @@ -1,110 +0,0 @@ -// swift-tools-version:5.9 -import PackageDescription - -let package = Package( - name: "PostHog", - platforms: [ - .macOS(.v10_15), .iOS(.v15), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1), - ], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "PostHog", - targets: ["PostHog"] - ), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Quick/Quick.git", from: "6.0.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "12.0.0"), - .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", from: "9.0.0"), - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "PostHog", - dependencies: [ - "PostHogObjCExceptionSupport", - "phlibwebp", - .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])), - ], - path: "PostHog", - exclude: [ - "ObjCExceptionSupport", - ], - resources: [ - .copy("Resources/PrivacyInfo.xcprivacy"), - ] - ), - .target( - name: "PostHogObjCExceptionSupport", - path: "PostHog/ObjCExceptionSupport", - publicHeadersPath: "." - ), - .target( - name: "PostHogTestsObjC", - path: "PostHogTestsObjC", - publicHeadersPath: "." - ), - .target( - name: "phlibwebp", - path: "vendor/libwebp", - publicHeadersPath: ".", - cSettings: [ - .define("PLCR_PRIVATE"), - .headerSearchPath("."), - ] - ), - .target( - name: "PHPLCrashReporter", - path: "vendor/PHPLCrashReporter", - exclude: [ - "Source/dwarf_opstream.hpp", - "Source/dwarf_stack.hpp", - "Source/PLCrashAsyncDwarfCFAState.hpp", - "Source/PLCrashAsyncDwarfCIE.hpp", - "Source/PLCrashAsyncDwarfEncoding.hpp", - "Source/PLCrashAsyncDwarfExpression.hpp", - "Source/PLCrashAsyncDwarfFDE.hpp", - "Source/PLCrashAsyncDwarfPrimitives.hpp", - "Source/PLCrashAsyncLinkedList.hpp", - "Source/PLCrashReport.proto", - "Dependencies/protobuf-c/generate-pb-c.sh", - "LICENSE", - ], - sources: [ - "Source", - ], - resources: [.process("Resources/PrivacyInfo.xcprivacy")], - publicHeadersPath: "include", - cSettings: [ - .define("PLCR_PRIVATE"), - .define("PLCF_RELEASE_BUILD"), - .define("PLCRASHREPORTER_PREFIX", to: "PH"), - .define("SWIFT_PACKAGE"), - .headerSearchPath("Source"), - ], - linkerSettings: [ - .linkedFramework("Foundation"), - ] - ), - .testTarget( - name: "PostHogTests", - dependencies: [ - "PostHog", - "PostHogTestsObjC", - "Quick", - "Nimble", - "OHHTTPStubs", - .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), - // The crash-report processor tests import this directly to build a PHPLCrashReport. - .target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])), - ], - path: "PostHogTests", - resources: [ - .process("Resources"), - ] - ), - ] -) diff --git a/PostHog/Autocapture/SwiftUITapAutocapture.swift b/PostHog/Autocapture/SwiftUITapAutocapture.swift index f7e00a43f..8de862ac8 100644 --- a/PostHog/Autocapture/SwiftUITapAutocapture.swift +++ b/PostHog/Autocapture/SwiftUITapAutocapture.swift @@ -262,16 +262,14 @@ } } } - #if compiler(>=6.0) - if #available(iOS 18.0, *), - let element = root.accessibilityHitTest(point, event: nil) as? NSObject, - element !== root - { - visit(element, depth: 0, path: "") - // Indexed traversal must still assign a path to an unlabeled hit-test leaf. - visited.removeAll(keepingCapacity: true) - } - #endif + if #available(iOS 18.0, *), + let element = root.accessibilityHitTest(point, event: nil) as? NSObject, + element !== root + { + visit(element, depth: 0, path: "") + // Indexed traversal must still assign a path to an unlabeled hit-test leaf. + visited.removeAll(keepingCapacity: true) + } visit(root, depth: 0, path: "") return AccessibilityTarget(identifier: best ?? fallback, excluded: excluded, traits: traits, developerIdentifier: best) } diff --git a/PostHog/ErrorTracking/PostHogCrashReportProcessor.swift b/PostHog/ErrorTracking/PostHogCrashReportProcessor.swift index 22517ac9a..a771c8823 100644 --- a/PostHog/ErrorTracking/PostHogCrashReportProcessor.swift +++ b/PostHog/ErrorTracking/PostHogCrashReportProcessor.swift @@ -8,12 +8,7 @@ import Foundation #if os(iOS) || os(macOS) || os(tvOS) - #if compiler(>=6.0) - internal import PHPLCrashReporter - #else - // swiftlint:disable:next duplicate_imports - @_implementationOnly import PHPLCrashReporter - #endif + internal import PHPLCrashReporter enum PostHogCrashReportProcessor { /// Process a PHPLCrashReport and convert it to PostHog $exception event properties diff --git a/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift b/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift index 165e6ff35..3b9a299cc 100644 --- a/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift +++ b/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift @@ -8,12 +8,7 @@ import Foundation #if os(iOS) || os(macOS) || os(tvOS) - #if compiler(>=6.0) - internal import PHPLCrashReporter - #else - // swiftlint:disable:next duplicate_imports - @_implementationOnly import PHPLCrashReporter - #endif + internal import PHPLCrashReporter class PostHogErrorTrackingAutoCaptureIntegration: PostHogIntegration { private static let integrationInstallState = PostHogIntegrationInstallState() diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 8b05116dc..be6a49dac 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -133,18 +133,10 @@ let maxRetryDelay = 30.0 private weak var surveysIntegration: PostHogSurveyIntegration? #endif - // nonisolated(unsafe) is introduced in Swift 5.10 - #if swift(>=5.10) - /// Shared singleton SDK instance used by most applications. - /// - /// Call `setup(_:)` once with a `PostHogConfig` before using capture APIs. - @objc public nonisolated(unsafe) static let shared: PostHogSDK = .init(PostHogConfig(projectToken: "")) - #else - /// Shared singleton SDK instance used by most applications. - /// - /// Call `setup(_:)` once with a `PostHogConfig` before using capture APIs. - @objc public static let shared: PostHogSDK = .init(PostHogConfig(projectToken: "")) - #endif + /// Shared singleton SDK instance used by most applications. + /// + /// Call `setup(_:)` once with a `PostHogConfig` before using capture APIs. + @objc public nonisolated(unsafe) static let shared: PostHogSDK = .init(PostHogConfig(projectToken: "")) deinit { #if !os(watchOS) diff --git a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift index 279e9865e..8c68b4d48 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift @@ -9,12 +9,7 @@ #if os(iOS) import Foundation - #if compiler(>=6.0) - internal import PostHogObjCExceptionSupport - #else - // swiftlint:disable:next duplicate_imports - @_implementationOnly import PostHogObjCExceptionSupport - #endif + internal import PostHogObjCExceptionSupport final class URLSessionInstrumentation { typealias RequestModifier = (URLRequest) -> URLRequest diff --git a/PostHog/Utils/BoxedBeforeSend.swift b/PostHog/Utils/BoxedBeforeSend.swift index 15e36f39c..f67fbfe1c 100644 --- a/PostHog/Utils/BoxedBeforeSend.swift +++ b/PostHog/Utils/BoxedBeforeSend.swift @@ -4,11 +4,7 @@ // import Foundation -#if compiler(>=6.0) - internal import PostHogObjCExceptionSupport -#else - @_implementationOnly import PostHogObjCExceptionSupport -#endif +internal import PostHogObjCExceptionSupport /// ObjC wrappers for the Swift function-typed `beforeSend` chains: Swift /// function types aren't `@objc`-bridgeable, and `@objc` classes can't be diff --git a/PostHog/Utils/UIImage+WebP.swift b/PostHog/Utils/UIImage+WebP.swift index b0c366932..fca34b6cc 100644 --- a/PostHog/Utils/UIImage+WebP.swift +++ b/PostHog/Utils/UIImage+WebP.swift @@ -10,12 +10,7 @@ import Accelerate import CoreGraphics import Foundation - #if compiler(>=6.0) - internal import phlibwebp - #else - // swiftlint:disable:next duplicate_imports - @_implementationOnly import phlibwebp - #endif + internal import phlibwebp import UIKit extension UIImage {