Skip to content
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

IOS-10545 Migrating to Swift 6.0 with strict concurrency #405

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

alejandroruizponce
Copy link
Contributor

🎟️ Jira ticket

https://jira.tid.es/browse/IOS-10545

🥅 What's the goal?

In a previous PR we made changes to make Mistica work in XCode 16 without using Swift 6. Now in this PR Swift 6 is enabled and many new concurrency issues arise. You can check more information here:
https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/incrementaladoption

🚧 How do we do it?

  • Package has been updated to support Swift 6.0 and the SnapshotTesting library has been configured to the latest version to support strict concurrency testing.
  • Two elements closely related to all our Mistica logic such as UIView and View are now executed in @MainActor which implies that all components related to these two classes must also be isolated in the main thread.
  • Classes like FontStyle or MisticaConfig that are quite important in Mistica's logic have been refactored as @unchecked Sendable and concurrency will be handled manually through queues. This way we do not isolate these classes which would mean that any place where they are used would also have to be isolated and that would be a very big refactor.
  • All tests have been refactored with the changes in the latest version of SnapshotTesting, the most important change is:

Before in old version:

    override func setUp() {
        super.setUp()
        isRecording = false
    }

Now in latest version:

    override func invokeTest() {
        withSnapshotTesting(record: .never) {
            super.invokeTest()
        }
    }

*This change implies changing our screenshots action since now the isRecording variable does not exist and must be configured in the new function. In fact this function now allows several values: .all, .failed, .missing, .never.

The screenshots record has been set to run with .failed in order to re-record only those screenshots that fail to be regenerated.

🧪 How can I verify this?

A version of Catalog has been generated to verify that everything works as before.

.forEach(buttonsView.addArrangedSubview(_:))
Task { @MainActor in
try [primaryButton, secondaryButton].compactMap { $0 }
.forEach(buttonsView.addArrangedSubview(_:))
Copy link
Contributor Author

@alejandroruizponce alejandroruizponce Oct 25, 2024

Choose a reason for hiding this comment

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

Now addArrangedSubview is isolated:
@MainActor open class UIStackView : UIView { .. }

@@ -365,7 +367,7 @@ private extension FeedbackView {
}

// Prepare
views.forEach(prepare(view:))
try? views.forEach(prepare(view:))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now forEach method has a rethrow to be handled:
@inlinable public func forEach(_ body: (Element) throws -> Void) rethrows

Task { @MainActor in
self.feedbackGenerator?.notificationOccurred(hapticFeedbackStyle)
self.feedbackGenerator = nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now UINotificationFeedbackGenerator is isolated:
@MainActor open class UINotificationFeedbackGenerator : UIFeedbackGenerator

@@ -15,7 +15,7 @@ import Lottie
**/

public extension Lottie.LottieConfiguration {
static var current: LottieConfiguration = {
nonisolated(unsafe) static var current: LottieConfiguration = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lottie is not updated yet with strict concurrency.

find . -type f -name "*.swift" -exec sed -i '' 's/isRecording = false/isRecording = true/' {} +
find . -type f -name "*.swift" -exec sed -i '' 's/record: .never/record: .failed/' {} +
Copy link
Contributor

@salavert salavert Oct 28, 2024

Choose a reason for hiding this comment

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

What we do at Latch is 1 test plan with 3 configurations so we can specify them on xcodebuild -testPlan LatchSnapshots -only-test-configuration "Record all"

Screenshot 2024-10-28 at 11 28 50

https://github.com/Telefonica/path2-ios/blob/master/LatchSnapshotTests/Helpers/LatchSnapshotTest.swift

Copy link
Contributor

Choose a reason for hiding this comment

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

if you define this env variable, if only CI executes this and you don't want a test plan to do it locally, you should be able to provide it in the makefile and set it before executing the xcodebuild call

export RECORD_SCREENSHOTS=all
xcodebuild -project .... test

Copy link
Contributor

Choose a reason for hiding this comment

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

In teory yes, tried it but didn't work, maybe I'm doing something wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it would be a good idea to investigate this in a separate ticket.

@@ -11,7 +11,7 @@ import UIKit
final class BottomSheetInteractiveDismissalTransition: NSObject {
private enum Constants {
static let maxBouncingHeight: CGFloat = 250
static let animationDuration: CGFloat = UIView.defaultAnimationDuration
static let animationDuration: CGFloat = 0.25
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missing fix, now it's fixed!

@@ -21,7 +21,7 @@ public extension EnvironmentValues {
@available(iOSApplicationExtension, unavailable)
private struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
UIApplication.shared.keyWindow?.safeAreaInsets.swiftUiInsets ?? EdgeInsets()
EdgeInsets()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change right? I'm not sure we always want to return a new instance of EdgeInsets().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first I did not see how to fix it, but now I do through preconcurrency.

Copy link
Contributor

@DavidMarinCalleja DavidMarinCalleja left a comment

Choose a reason for hiding this comment

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

set {
concurrentQueue.async { _currentColors = newValue }
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

seems that this pattern is being replicated in multiple places, as @salavert said, it would be nice to have a property wrapper for this kind of var

Copy link

github-actions bot commented Nov 12, 2024

Screenshot tests report

✔️ All passing

@alejandroruizponce alejandroruizponce force-pushed the IOS-10545-Migrating-to-Swift-6 branch from 93b9001 to cd7d14d Compare November 19, 2024 12:38
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.

7 participants