Skip to content

Use iOS 15 and Swift 6.2 APIs now that they're the floor - #536

Merged
yusuftor merged 3 commits into
chore/min-ios15-swift62from
chore/ios15-api-cleanups
Oct 7, 2026
Merged

yusuftor merged 3 commits into
chore/min-ios15-swift62from
chore/ios15-api-cleanups

Conversation

@yusuftor

@yusuftor yusuftor commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #532, stacked on it. Now that the SDK needs iOS 15 and Swift 6.2, this swaps a few hand-rolled workarounds for the built-in APIs.

Changes

  • Core Data: CoreDataStack.count and the last-placement lookup use iOS 15's async NSManagedObjectContext.perform and return their value directly, removing five continuation wrappers in CoreDataManager. The last-placement lookup now returns the placement's createdAt read inside perform. Before, it handed the managed object back and read the date on another thread, which Core Data doesn't allow.
  • Dev server probe: DevServerLocator.loadWithURLSession uses URLSession.data(for:) instead of wrapping dataTask.
  • Window lookup: activeWindow uses UIWindowScene.keyWindow. The test mode purchase and restore alerts use the existing UIViewController.topMostViewController instead of their own copy built on the deprecated UIApplication.windows.
  • Landscape check: UIWindow.isLandscape reads the active scene instead of the first window in UIApplication.windows, which iOS 15 deprecated.
  • Review request: when the paywall has no window of its own, it asks for a review in the active scene instead of calling the scene-less SKStoreReviewController.requestReview(), which iOS 14 deprecated.
  • Superscript: imported with internal import, so it's no longer part of SuperwallKit's interface.
  • Version reporting: removed the Swift and compiler version branches below 6.2, which can no longer compile.
  • Added import Combine to two loading listeners that used Published.Publisher without importing it (this was already a warning).

Tests

The tests went in first, in their own commit, and passed against the old code before any change:

  • CoreDataManagerComputedPropertyTests: getComputedPropertySincePlacement had no tests. It now covers no placement, latest placement, other names, and skipping the placement currently being evaluated.
  • DevServerLocatorURLSessionTests: runs the real URLSession loader against a stub URLProtocol that only answers a made-up host. Covers success, a 404 still coming back as a response, an empty body, and a connection error throwing.
  • DeviceHelperTests.swiftAndCompilerVersions_matchTheToolchainFloor pins the values the version functions can still return.

The existing ~20 countPlacement/countAudienceOccurrences tests cover the count path. The window lookups, landscape check, review request and internal import can't be unit tested because the test target has no host app, so for those the build is the check.

Full suite: 1,319 of 1,320 passed. The one failure was ProductsFetcherSK2Tests' non-existent-product test, which calls the real App Store; it passed on rerun.

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

No changelog entry: nothing here changes behavior for apps.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The PR should not merge until test-mode purchases and restores reliably select the presenting window in multi-scene apps.

Findings

  1. P1 Wrong window for test drawers ▶
Fix with agent prompt
### Issue 1
Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift:28
If an app has multiple connected scenes, the first foreground-active scene can have a window but no key window while another scene has the key window. The new controller lookup uses that first scene’s window instead of the key window the old lookup would have used. The purchase drawer-or the restore drawer, which uses the same lookup-can then appear in the wrong scene or fail to appear.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR replaces continuation-based Core Data and URLSession operations with async APIs, updates window and test-mode controller lookup, narrows Superscript imports, simplifies version reporting, and adds regression tests. The test-mode controller replacement needs attention in multi-scene apps.

Reviews (1) · Last reviewed commit: "Use iOS 15 and Swift 6.2 APIs now that t..."

yusuftor and others added 2 commits September 29, 2026 16:56
Pins current behaviour before moving these onto iOS 15 and Swift 6.2 APIs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Core Data reads use the async perform, and a computed property reads the
  placement's date on the context's queue instead of after it.
- The dev server probe uses URLSession's async data(for:).
- Window lookups use UIWindowScene.keyWindow, and test mode reuses the
  existing top view controller helper instead of the deprecated windows list.
- Superscript is imported internally so it stays out of the SDK's interface.
- Drop Swift and compiler version branches below 6.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No critical issues. One loose test assertion is flagged inline.

Reviewed changes

I reviewed both commits: the tests-first commit and the switch to iOS 15 / Swift 6.2 APIs.

  • Async Core Data reads: CoreDataStack.count(for:) and the new getLastSavedPlacementDate(name:before:) return their values from the async NSManagedObjectContext.perform. createdAt is now read inside perform instead of on the caller's thread, which fixes a real threading bug.
  • Dev server probe: loadWithURLSession now calls URLSession.data(for:) and behaves the same as before (a 404 still comes back as a response, an ATS failure still has a readable NSError.code). It's also internal now so the tests can call it, and cancelling the task now cancels the request.
  • Window lookup: activeWindow uses UIWindowScene.keyWindow. The test mode alerts use topMostViewController, which still finds the presentation window that GetPresenter makes key.
  • internal import Superscript: the only two source files that import it are internal-only and nothing public or @inlinable exposes Superscript types.
  • Version reporting: removed the branches below 6.2. In the Swift 5 language mode, currentSwiftVersion still returns "5.10", the same value the old chain gave.
  • New tests: added computed-property tests on the in-memory stack, URLProtocol stub tests for the loader, and a version floor test.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

withExtendedLifetime(dependencyContainer) {
#expect(["5.10", "6.2"].contains(deviceHelper.currentSwiftVersion()))
let compilerVersion = deviceHelper.currentCompilerVersion()
#expect(compilerVersion.compare("6.2", options: .numeric) != .orderedAscending)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This assertion passes even when the function falls through to "Unknown". With .numeric, only runs of digits are compared as numbers, so "Unknown".compare("6.2") compares 'U' with '6' and returns .orderedDescending. So the test can't catch the #else branch it's meant to guard against. Also check that the value starts with a digit.

Suggested change
#expect(compilerVersion.compare("6.2", options: .numeric) != .orderedAscending)
#expect(compilerVersion.first?.isNumber == true)
#expect(compilerVersion.compare("6.2", options: .numeric) != .orderedAscending)

purchaseSource: PurchaseSource
) async -> PurchaseResult {
guard let viewController = topViewController() else {
guard let viewController = UIViewController.topMostViewController else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Wrong window for test drawers

If an app has multiple connected scenes, the first foreground-active scene can have a window but no key window while another scene has the key window. The new controller lookup uses that first scene’s window instead of the key window the old lookup would have used. The purchase drawer—or the restore drawer, which uses the same lookup—can then appear in the wrong scene or fail to appear.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/TestMode/TestModeTransactionHandler.swift
Line: 28

Comment:
**Wrong window for test drawers**

If an app has multiple connected scenes, the first foreground-active scene can have a window but no key window while another scene has the key window. The new controller lookup uses that first scene’s window instead of the key window the old lookup would have used. The purchase drawer—or the restore drawer, which uses the same lookup—can then appear in the wrong scene or fail to appear.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

The landscape check reads the active scene instead of UIApplication.windows,
and a review request with no window of its own falls back to the active scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No new issues in this commit. The loose compiler-version assertion flagged in the last review is still open.

Reviewed changes

I reviewed dd25776, which moves the last two call sites off the app-wide window list and the scene-less review API.

  • Landscape check: UIWindow.isLandscape now reads activeWindowScene?.interfaceOrientation instead of the first window in the deprecated UIApplication.windows. activeWindowScene is ordered foreground active, then foreground inactive, then any scene, so it's at least as accurate as picking an arbitrary first window. Its only caller is ShimmerView.
  • Review request: if the paywall has no window, requestReview(.inApp) now uses activeWindowScene instead of calling SKStoreReviewController.requestReview() with no scene. It's also one branch for both platforms now instead of two copies behind #if os(visionOS). If there's no scene at all, nothing is requested but trackReviewRequest still fires. visionOS already behaved this way, and a foregrounded app always has a connected scene.

Pullfrog  | Fix it ➔ | View workflow run | Using Claude Opus | 𝕏

@yusuftor
yusuftor merged commit dd25776 into chore/min-ios15-swift62 Oct 7, 2026
5 checks passed
@yusuftor
yusuftor deleted the chore/ios15-api-cleanups branch October 7, 2026 12:03
@yusuftor yusuftor mentioned this pull request Oct 7, 2026
4 of 10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant