[iOS] Mark DisplayLinkManager.shared and init() @MainActor (#189815)
Now that DisplayLinkManager is injected in nearly all use-cases and the
`.shared` singleton is used at exactly 3 remaining call-sites, marks
`.shared` and `init()` as `@MainActor` now that it's safe to do so.
`DisplayLinkManager.init()` has always required betin called on the main
thread since it reads `UIScreen.main`/`Bundle.main`, but that was only
enforced by a runtime assert. which compiles out in most release
configruations. The Swift compiler can now enforce a static, checkable
contract instead. Actor-isolation checking is enforced in Swift 5.5+
regardless of strict-concurrency level or language mode, so this takes
should take effect immediately given our current Swift 5 language
mode/default concurrency checking not gated behind a migration.
I've left the manual `assert(Thread.isMainThread, ...)` in place for now
given the existing Obj-C use in `FlutterViewController` and
`FlutterMetalLayer`. If we do eventually remove it, we should ensure
that the runtime itself is enforcing this runtime behaviour first --
this is dependent on SDK version.
I've also left the internal testing initializer non-isolated: it doesn't
touch `UIScreen`/`Bundle.main`, so it has no main-thread requirement,
and leaving it alone keeps the two tests that use it compiling
unchanged.
`DisplayLinkManagerTest.swift`'s `testSharedInstanceReturnsAValidValue`
and `testDisplayConfigurationNotificationsAreHandledWithoutCrashing`
both call `.shared` directly, so we needed to mark the test class
`@MainActor`. XCTest already runs these on the main thread, so this is a
no-op but makes the compiler (which doesn't know about XCTest's
implementation) happy.
No semantic changes to tests because there's no semantic change to the
logic, and this is enforced by the complier itself.
Issue: https://github.com/flutter/flutter/issues/175879
Issue: https://github.com/flutter/flutter/issues/181684
## Pre-launch Checklist
- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift
index afafd5e..15d8e48 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManager.swift
@@ -36,7 +36,9 @@
/// The shared DisplayLinkManager.
///
/// The first access performs a one-time read of `UIScreen.main`, and must happen on the main
- /// thread; this is enforced by an assertion in `init()`.
+ /// thread; `@MainActor` isolation enforces this for Swift callers at compile time. Objective-C
+ /// callers remain responsible for calling from the main thread themselves.
+ @MainActor
@objc
public static let shared = DisplayLinkManager()
@@ -90,6 +92,7 @@
///
/// Queries the system plist and main screen properties on the main thread, then starts observing
/// for changes that can affect the cached refresh rate.
+ @MainActor
private override init() {
assert(
Thread.isMainThread,
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManagerTest.swift b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManagerTest.swift
index ad12b26..4f693b0 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManagerTest.swift
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/DisplayLinkManagerTest.swift
@@ -6,6 +6,7 @@
@testable import InternalFlutterSwift
+@MainActor
class DisplayLinkManagerTest: XCTestCase {
func testDisplayLinkManagerCanBeInstantiatedWithMockValues() {