-
Notifications
You must be signed in to change notification settings - Fork 105
Swift 6: complete concurrency checking #825
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
Changes from 17 commits
ddf78cc
b73c9c5
2f64d75
87292cf
ab7356d
391b156
26dd6aa
ef3bbc0
bb0ed90
87fcb5a
994196f
4c94da4
6b78288
8eae0dd
6e1e8c9
f10725c
c0b9111
79ef1c1
e28503d
f8d9504
fa2872a
4b305c5
1ab09e2
1170a52
7a0cfe3
7d0237c
9769ff4
365f9f6
9087e48
55d666c
db9e461
ad036e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,9 @@ import StreamChatSwiftUI | |
import SwiftUI | ||
|
||
struct ChooseChannelQueryView: View { | ||
static let queryIdentifiers = ChannelListQueryIdentifier.allCases.sorted(using: KeyPathComparator(\.title)) | ||
static var queryIdentifiers: [ChannelListQueryIdentifier] { | ||
ChannelListQueryIdentifier.allCases.sorted(by: { $0.title < $1.title }) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was giving Sendable related error which did not feel right. Just switched to another sorting method. |
||
|
||
var body: some View { | ||
ForEach(Self.queryIdentifiers) { queryIdentifier in | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,7 +42,7 @@ class LoginViewModel: ObservableObject { | |
return | ||
} | ||
|
||
DispatchQueue.main.async { [weak self] in | ||
Task { @MainActor [weak self] in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DispatchQueue gives Sendable error for self There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, we should use Task going forward |
||
withAnimation { | ||
self?.loading = false | ||
UnsecureRepository.shared.save(user: credentials) | ||
|
@@ -64,7 +64,7 @@ class LoginViewModel: ObservableObject { | |
return | ||
} | ||
|
||
DispatchQueue.main.async { [weak self] in | ||
Task { @MainActor [weak self] in | ||
withAnimation { | ||
self?.loading = false | ||
AppState.shared.userState = .loggedIn | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// swift-tools-version:5.9 | ||
// swift-tools-version:6.0 | ||
|
||
import Foundation | ||
import PackageDescription | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,11 +33,6 @@ do | |
replaceDeclaration 'signpost(log' 'signpost(nukeLog' $f | ||
replaceDeclaration ' Cache(' ' NukeCache(' $f | ||
replaceDeclaration ' Cache<' ' NukeCache<' $f | ||
replaceDeclaration ' Image?' ' NukeImage?' $f | ||
replaceDeclaration ' Image(' ' NukeImage(' $f | ||
replaceDeclaration 'struct Image:' 'struct NukeImage:' $f | ||
replaceDeclaration 'extension Image {' 'extension NukeImage {' $f | ||
replaceDeclaration 'Content == Image' 'Content == NukeImage' $f | ||
Comment on lines
-36
to
-40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nuke was upgraded to 12.8 (supports Swift 6), some of the types are not there anymore |
||
replaceDeclaration ' VideoPlayerView' ' NukeVideoPlayerView' $f | ||
replaceDeclaration 'typealias Color' 'typealias NukeColor' $f | ||
replaceDeclaration 'extension Color' 'extension NukeColor' $f | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,20 +21,21 @@ public class Appearance { | |
} | ||
|
||
/// Provider for custom localization which is dependent on App Bundle. | ||
public static var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in | ||
nonisolated(unsafe) | ||
public static var localizationProvider: @Sendable(_ key: String, _ table: String) -> String = { key, table in | ||
Bundle.streamChatUI.localizedString(forKey: key, value: nil, table: table) | ||
} | ||
Comment on lines
+24
to
27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternative could be making it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is fine |
||
} | ||
|
||
// MARK: - Appearance + Default | ||
|
||
public extension Appearance { | ||
static var `default`: Appearance = .init() | ||
nonisolated(unsafe) static var `default`: Appearance = .init() | ||
Comment on lines
-32
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also fine to keep it like this, it anyway contains only UI stuff - shouldn't be called from a bg thread |
||
} | ||
|
||
/// Provides the default value of the `Appearance` class. | ||
public struct AppearanceKey: EnvironmentKey { | ||
public static let defaultValue: Appearance = Appearance() | ||
public static var defaultValue: Appearance { .init() } | ||
} | ||
|
||
extension EnvironmentValues { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ import StreamChat | |
import SwiftUI | ||
|
||
/// View model for the `AddUsersView`. | ||
class AddUsersViewModel: ObservableObject { | ||
@MainActor class AddUsersViewModel: ObservableObject { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since view model factory is MainActor and view models are used from view, then it made more than sense to make all the view models main actor. |
||
|
||
@Injected(\.chatClient) private var chatClient | ||
|
||
|
@@ -46,9 +46,11 @@ class AddUsersViewModel: ObservableObject { | |
if !loadingNextUsers { | ||
loadingNextUsers = true | ||
searchController.loadNextUsers { [weak self] _ in | ||
guard let self = self else { return } | ||
self.users = self.searchController.userArray | ||
self.loadingNextUsers = false | ||
MainActor.ensureIsolated { [weak self] in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Controllers do not ensure on the compilation level which actor is used. We can use any queue for controller completion handlers although the default is main. Therefore, we need to make sure that main is used. Applies to all the completion handlers and controller delegates. |
||
guard let self = self else { return } | ||
self.users = self.searchController.userArray | ||
self.loadingNextUsers = false | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -57,16 +59,20 @@ class AddUsersViewModel: ObservableObject { | |
let filter: Filter<UserListFilterScope> = .notIn(.id, values: loadedUserIds) | ||
let query = UserListQuery(filter: filter) | ||
searchController.search(query: query) { [weak self] error in | ||
guard let self = self, error == nil else { return } | ||
self.users = self.searchController.userArray | ||
MainActor.ensureIsolated { [weak self] in | ||
guard let self = self, error == nil else { return } | ||
self.users = self.searchController.userArray | ||
} | ||
} | ||
} | ||
|
||
private func searchUsers(term: String) { | ||
searchController.search(term: searchText) { [weak self] error in | ||
guard let self = self, error == nil else { return } | ||
self.users = self.searchController.userArray.filter { user in | ||
!self.loadedUserIds.contains(user.id) | ||
MainActor.ensureIsolated { [weak self] in | ||
guard let self = self, error == nil else { return } | ||
self.users = self.searchController.userArray.filter { user in | ||
!self.loadedUserIds.contains(user.id) | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ import StreamChat | |
import SwiftUI | ||
|
||
// View model for the `ChatChannelInfoView`. | ||
public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDelegate { | ||
@MainActor public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDelegate { | ||
|
||
@Injected(\.chatClient) private var chatClient | ||
|
||
|
@@ -172,7 +172,7 @@ public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDe | |
loadAdditionalUsers() | ||
} | ||
|
||
public func leaveConversationTapped(completion: @escaping () -> Void) { | ||
public func leaveConversationTapped(completion: @escaping @MainActor() -> Void) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For avoiding actor isolation in the view. Better force main on the view model level. |
||
if !channel.isDirectMessageChannel { | ||
removeUserFromConversation(completion: completion) | ||
} else { | ||
|
@@ -195,20 +195,22 @@ public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDe | |
) | ||
} | ||
|
||
public func channelController( | ||
nonisolated public func channelController( | ||
_ channelController: ChatChannelController, | ||
didUpdateChannel channel: EntityChange<ChatChannel> | ||
) { | ||
if let channel = channelController.channel { | ||
self.channel = channel | ||
if self.channel.lastActiveMembers.count > participants.count { | ||
participants = channel.lastActiveMembers.map { member in | ||
ParticipantInfo( | ||
chatUser: member, | ||
displayName: member.name ?? member.id, | ||
onlineInfoText: onlineInfo(for: member), | ||
isDeactivated: member.isDeactivated | ||
) | ||
MainActor.ensureIsolated { | ||
if let channel = channelController.channel { | ||
self.channel = channel | ||
if self.channel.lastActiveMembers.count > participants.count { | ||
participants = channel.lastActiveMembers.map { member in | ||
ParticipantInfo( | ||
chatUser: member, | ||
displayName: member.name ?? member.id, | ||
onlineInfoText: onlineInfo(for: member), | ||
isDeactivated: member.isDeactivated | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -221,23 +223,27 @@ public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDe | |
|
||
// MARK: - private | ||
|
||
private func removeUserFromConversation(completion: @escaping () -> Void) { | ||
private func removeUserFromConversation(completion: @escaping @MainActor() -> Void) { | ||
guard let userId = chatClient.currentUserId else { return } | ||
channelController.removeMembers(userIds: [userId]) { [weak self] error in | ||
if error != nil { | ||
self?.errorShown = true | ||
} else { | ||
completion() | ||
MainActor.ensureIsolated { [weak self] in | ||
if error != nil { | ||
self?.errorShown = true | ||
} else { | ||
completion() | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func deleteChannel(completion: @escaping () -> Void) { | ||
private func deleteChannel(completion: @escaping @MainActor() -> Void) { | ||
channelController.deleteChannel { [weak self] error in | ||
if error != nil { | ||
self?.errorShown = true | ||
} else { | ||
completion() | ||
MainActor.ensureIsolated { [weak self] in | ||
if error != nil { | ||
self?.errorShown = true | ||
} else { | ||
completion() | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -249,19 +255,21 @@ public class ChatChannelInfoViewModel: ObservableObject, ChatChannelControllerDe | |
|
||
loadingUsers = true | ||
memberListController.loadNextMembers { [weak self] error in | ||
guard let self = self else { return } | ||
self.loadingUsers = false | ||
if error == nil { | ||
let newMembers = self.memberListController.members.map { member in | ||
ParticipantInfo( | ||
chatUser: member, | ||
displayName: member.name ?? member.id, | ||
onlineInfoText: self.onlineInfo(for: member), | ||
isDeactivated: member.isDeactivated | ||
) | ||
} | ||
if newMembers.count > self.participants.count { | ||
self.participants = newMembers | ||
MainActor.ensureIsolated { [weak self] in | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this crashes if it's not on the main actor, right? Do we really need that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a custom addition (maybe we should rename it to be more clear about it) which makes sure the closure runs on the main thread. Note that one can set any queue to be the one used for completion handers (default is main). This custom addition just checks if we are currently on the main, if yes, just call the closure, otherwise jump on the main thread (since UI updates and MainActor view models require main). The difference between: DispatchQueue.global().async {
print("Simulate API calls and DB updates")
DispatchQueue.main.async {
print(Thread.isMainThread)
Task { @MainActor in
print("Task main actor")
}
MainActor.ensureIsolated {
print("Ensure isolated")
}
}
} If this snippet runs, ensure isolated is called first. I did some micro benchmarking just for fun and the difference is insignificant: we are talking about 1-10 microseconds. Therefore, I guess it makes sense to simplify this and get rid of the custom ensureIsolated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I renamed the custom addition to StreamConcurrency.onMain. I tried to get rid of it, but loads of unit-tests will break because using a Task introduces slight delay. |
||
guard let self = self else { return } | ||
self.loadingUsers = false | ||
if error == nil { | ||
let newMembers = self.memberListController.members.map { member in | ||
ParticipantInfo( | ||
chatUser: member, | ||
displayName: member.name ?? member.id, | ||
onlineInfoText: self.onlineInfo(for: member), | ||
isDeactivated: member.isDeactivated | ||
) | ||
} | ||
if newMembers.count > self.participants.count { | ||
self.participants = newMembers | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Xcode 15 builds are going away, disabling it for now and a cleanup happens separately