Skip to content

Swift 6: complete concurrency checking for StreamChatUI #3660

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

Open
wants to merge 31 commits into
base: swift-6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3a2ecd2
Use SWIFT_STRICT_CONCURRENCY complete with Swift 5
laevandus Mar 26, 2025
1716b46
Use @preconcurrency @MainActor for backwards compatibility
laevandus Mar 26, 2025
2b9e30d
Merge branch 'swift-6' into swift-6-ui
laevandus Mar 27, 2025
2ccbc6d
Fix issues with invalid display scale
laevandus Mar 27, 2025
1e3641d
Merge branch 'swift-6' into swift-6-ui
laevandus May 2, 2025
1c7bbf8
Merge branch 'swift-6' into swift-6-ui
laevandus May 2, 2025
e0777cf
Cleanup imports and comments
laevandus May 5, 2025
31f663f
Force main thread for mutating Appearance.default and Components.defa…
laevandus May 5, 2025
4624c79
Merge branch 'swift-6' into swift-6-ui
laevandus May 5, 2025
abaffab
Merge branch 'swift-6' into swift-6-ui
laevandus May 5, 2025
09066fd
Merge branch 'swift-6' into swift-6-ui
laevandus May 6, 2025
1e64b77
Set Swift version to 6 for StreamChatUI target in the project
laevandus May 6, 2025
ab404ab
Use MainActor for constraint creation
laevandus May 6, 2025
f3f8e90
Merge branch 'swift-6' into swift-6-ui
laevandus May 7, 2025
1892c22
Merge branch 'swift-6' into swift-6-ui
laevandus May 7, 2025
0290926
Merge branch 'swift-6' into swift-6-ui
laevandus May 7, 2025
b2e3fd9
Merge branch 'swift-6' into swift-6-ui
laevandus May 8, 2025
a9b37ee
Merge branch 'swift-6' into swift-6-ui
laevandus May 8, 2025
d6dc7e4
Merge branch 'swift-6' into swift-6-ui
laevandus May 19, 2025
fe9dc14
Merge branch 'swift-6' into swift-6-ui
laevandus May 23, 2025
08dc3a5
Merge branch 'swift-6' into swift-6-ui
laevandus May 23, 2025
4d67d1a
Merge branch 'swift-6' into swift-6-ui
laevandus May 23, 2025
aa4dfbe
Rename MainActor.ensureIsolated to StreamConcurrency.onMain
laevandus May 23, 2025
58d2934
Use MainActor requirement in the channel name formatter
laevandus May 23, 2025
b109d9c
Fix warning in audio session generator
laevandus May 23, 2025
bf6d7f5
Revert Xcode 15 changes for PollsConfig
laevandus May 23, 2025
2692abc
Revert Xcode 15 required change
laevandus May 23, 2025
b7a35a0
Replace nonisolated with MainActor in Appearance
laevandus May 23, 2025
2c237df
Merge branch 'swift-6' into swift-6-ui
laevandus May 23, 2025
16e6ba9
Remove assumeIsolated because Swift 6 compiler does not require it
laevandus May 23, 2025
ab32ffc
Merge branch 'swift-6' into swift-6-ui
laevandus May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import StreamChat

/// A formatter that generates a name for the given channel.
public protocol ChannelNameFormatter {
func format(channel: ChatChannel, forCurrentUserId currentUserId: UserId?) -> String?
@preconcurrency @MainActor func format(channel: ChatChannel, forCurrentUserId currentUserId: UserId?) -> String?
}

/// The default channel name formatter.
open class DefaultChannelNameFormatter: ChannelNameFormatter {
public init() {}

/// Internal static property to add backwards compatibility to `Components.channelNamer`
internal static var channelNamer: (
@MainActor static var channelNamer: (
_ channel: ChatChannel,
_ currentUserId: UserId?
) -> String? = DefaultChatChannelNamer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ private extension UIFont {
@available(iOS 15.0, *)
private extension InlinePresentationIntent {
/// An intent that represents bold with italic presentation.
static var extremelyStronglyEmphasized = InlinePresentationIntent(rawValue: 3)
static var extremelyStronglyEmphasized: InlinePresentationIntent { InlinePresentationIntent(rawValue: 3) }
}

/// Configures the font style properties for base Markdown elements
Expand Down
17 changes: 14 additions & 3 deletions Sources/StreamChatUI/Appearance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
import StreamChat

/// An object containing visual configuration for whole application.
public struct Appearance {
public struct Appearance: @unchecked Sendable {
/// A color pallete to provide basic set of colors for the Views.
///
/// By providing different object or changing individual colors, you can change the look of the views.
Expand All @@ -29,7 +29,7 @@ public struct Appearance {
public var formatters = Formatters()

/// Provider for custom localization which is dependent on App Bundle.
public var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in
public var localizationProvider: @Sendable(_ key: String, _ table: String) -> String = { key, table in
Bundle.streamChatUI.localizedString(forKey: key, value: nil, table: table)
}

Expand All @@ -39,5 +39,16 @@ public struct Appearance {
// MARK: - Appearance + Default

public extension Appearance {
static var `default`: Appearance = .init()
static var `default`: Appearance {
get {
StreamConcurrency.onMain { _default }
}
set {
StreamConcurrency.onMain { _default = newValue }
}
}

// Shared instance is mutated only on the main thread without explicit
// main actor annotation for easier SDK setup.
@MainActor private static var _default: Appearance = .init()
}
2 changes: 1 addition & 1 deletion Sources/StreamChatUI/AppearanceProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UIKit

// MARK: - Protocol

public protocol AppearanceProvider: AnyObject {
@preconcurrency @MainActor public protocol AppearanceProvider: AnyObject {
/// Appearance object to change appearance of the existing views or to use default appearance of the SDK by custom components.
var appearance: Appearance { get set }

Expand Down
28 changes: 17 additions & 11 deletions Sources/StreamChatUI/ChatChannel/ChatChannelHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,41 +104,45 @@ open class ChatChannelHeaderView: _View,
withTimeInterval: statusUpdateInterval,
repeats: true
) { [weak self] _ in
self?.updateContentIfNeeded()
StreamConcurrency.onMain { [weak self] in
self?.updateContentIfNeeded()
}
}
}

// MARK: - ChatChannelControllerDelegate Implementation

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didUpdateChannel channel: EntityChange<ChatChannel>
) {
switch channel {
case .update, .create:
updateContent()
default:
break
StreamConcurrency.onMain {
switch channel {
case .update, .create:
updateContent()
default:
break
}
}
}

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
// By default the header view is not interested in typing events
// but this can be overridden by subclassing this component.
}

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didReceiveMemberEvent: MemberEvent
) {
// By default the header view is not interested in member events
// but this can be overridden by subclassing this component.
}

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
Expand All @@ -147,6 +151,8 @@ open class ChatChannelHeaderView: _View,
}

deinit {
timer?.invalidate()
StreamConcurrency.onMain {
timer?.invalidate()
}
}
}
87 changes: 66 additions & 21 deletions Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ open class ChatChannelVC: _ViewController,

channelController.delegate = self
channelController.synchronize { [weak self] error in
self?.didFinishSynchronizing(with: error)
StreamConcurrency.onMain { [weak self] in
self?.didFinishSynchronizing(with: error)
}
}

if channelController.channelQuery.pagination?.parameter == nil {
Expand Down Expand Up @@ -309,10 +311,12 @@ open class ChatChannelVC: _ViewController,
// MARK: - Loading previous and next messages state handling.

/// Called when the channel will load previous (older) messages.
open func loadPreviousMessages(completion: @escaping (Error?) -> Void) {
open func loadPreviousMessages(completion: @escaping @Sendable(Error?) -> Void) {
channelController.loadPreviousMessages { [weak self] error in
completion(error)
self?.didFinishLoadingPreviousMessages(with: error)
StreamConcurrency.onMain { [weak self] in
self?.didFinishLoadingPreviousMessages(with: error)
}
}
}

Expand All @@ -323,10 +327,12 @@ open class ChatChannelVC: _ViewController,
}

/// Called when the channel will load next (newer) messages.
open func loadNextMessages(completion: @escaping (Error?) -> Void) {
open func loadNextMessages(completion: @escaping @Sendable(Error?) -> Void) {
channelController.loadNextMessages { [weak self] error in
completion(error)
self?.didFinishLoadingNextMessages(with: error)
StreamConcurrency.onMain { [weak self] in
completion(error)
self?.didFinishLoadingNextMessages(with: error)
}
}
}

Expand Down Expand Up @@ -376,7 +382,7 @@ open class ChatChannelVC: _ViewController,
public func chatMessageListVC(
_ vc: ChatMessageListVC,
shouldLoadPageAroundMessageId messageId: MessageId,
_ completion: @escaping ((Error?) -> Void)
_ completion: @escaping @Sendable(Error?) -> Void
) {
if let message = channelController.dataStore.message(id: messageId),
let parentMessageId = getParentMessageId(forMessageInsideThread: message) {
Expand All @@ -386,8 +392,10 @@ open class ChatChannelVC: _ViewController,
}

channelController.loadPageAroundMessageId(messageId) { [weak self] error in
self?.updateJumpToUnreadRelatedComponents()
completion(error)
StreamConcurrency.onMain { [weak self] in
self?.updateJumpToUnreadRelatedComponents()
completion(error)
}
}
}

Expand Down Expand Up @@ -433,8 +441,10 @@ open class ChatChannelVC: _ViewController,
case is MarkUnreadActionItem:
dismiss(animated: true) { [weak self] in
self?.channelController.markUnread(from: message.id) { result in
if case let .success(channel) = result {
self?.updateAllUnreadMessagesRelatedComponents(channel: channel)
StreamConcurrency.onMain {
if case let .success(channel) = result {
self?.updateAllUnreadMessagesRelatedComponents(channel: channel)
}
}
}
}
Expand Down Expand Up @@ -506,7 +516,16 @@ open class ChatChannelVC: _ViewController,

// MARK: - ChatChannelControllerDelegate

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
StreamConcurrency.onMain {
_channelController(channelController, didUpdateMessages: changes)
}
}

private func _channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
Expand All @@ -531,7 +550,16 @@ open class ChatChannelVC: _ViewController,
viewPaginationHandler.updateElementsCount(with: channelController.messages.count)
}

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didUpdateChannel channel: EntityChange<ChatChannel>
) {
StreamConcurrency.onMain {
_channelController(channelController, didUpdateChannel: channel)
}
}

private func _channelController(
_ channelController: ChatChannelController,
didUpdateChannel channel: EntityChange<ChatChannel>
) {
Expand All @@ -545,7 +573,16 @@ open class ChatChannelVC: _ViewController,
channelAvatarView.content = (channelController.channel, client.currentUserId)
}

open func channelController(
nonisolated open func channelController(
_ channelController: ChatChannelController,
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
StreamConcurrency.onMain {
_channelController(channelController, didChangeTypingUsers: typingUsers)
}
}

private func _channelController(
_ channelController: ChatChannelController,
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
Expand All @@ -564,7 +601,13 @@ open class ChatChannelVC: _ViewController,

// MARK: - EventsControllerDelegate

open func eventsController(_ controller: EventsController, didReceiveEvent event: Event) {
nonisolated open func eventsController(_ controller: EventsController, didReceiveEvent event: Event) {
StreamConcurrency.onMain {
_eventsController(controller, didReceiveEvent: event)
}
}

private func _eventsController(_ controller: EventsController, didReceiveEvent event: Event) {
if let newMessagePendingEvent = event as? NewMessagePendingEvent {
let newMessage = newMessagePendingEvent.message
if !isFirstPageLoaded && newMessage.isSentByCurrentUser && !newMessage.isPartOfThread {
Expand Down Expand Up @@ -596,15 +639,17 @@ open class ChatChannelVC: _ViewController,

// MARK: - AudioQueuePlayerDatasource

open func audioQueuePlayerNextAssetURL(
nonisolated open func audioQueuePlayerNextAssetURL(
_ audioPlayer: AudioPlaying,
currentAssetURL: URL?
) -> URL? {
audioQueuePlayerNextItemProvider.findNextItem(
in: messages,
currentVoiceRecordingURL: currentAssetURL,
lookUpScope: .subsequentMessagesFromUser
)
StreamConcurrency.onMain {
audioQueuePlayerNextItemProvider.findNextItem(
in: messages,
currentVoiceRecordingURL: currentAssetURL,
lookUpScope: .subsequentMessagesFromUser
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import StreamChat
import SwiftUI

/// Protocol of `ChatChannelListItemView` wrapper for use in SwiftUI.
public protocol ChatChannelListItemViewSwiftUIView: View {
@preconcurrency @MainActor public protocol ChatChannelListItemViewSwiftUIView: View {
init(dataSource: ChatChannelListItemView.ObservedObject<Self>)
}

Expand Down
34 changes: 21 additions & 13 deletions Sources/StreamChatUI/ChatChannelList/ChatChannelListVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,9 @@ open class ChatChannelListVC: _ViewController,
isPaginatingChannels = true

controller.loadNextChannels { [weak self] _ in
self?.isPaginatingChannels = false
StreamConcurrency.onMain { [weak self] in
self?.isPaginatingChannels = false
}
}
}

Expand Down Expand Up @@ -403,28 +405,34 @@ open class ChatChannelListVC: _ViewController,

// MARK: - ChatChannelListControllerDelegate

open func controllerWillChangeChannels(_ controller: ChatChannelListController) {
collectionView.layoutIfNeeded()
nonisolated open func controllerWillChangeChannels(_ controller: ChatChannelListController) {
StreamConcurrency.onMain {
collectionView.layoutIfNeeded()
}
}

open func controller(
nonisolated open func controller(
_ controller: ChatChannelListController,
didChangeChannels changes: [ListChange<ChatChannel>]
) {
handleStateChanges(controller.state)

if skipChannelUpdates {
skippedRendering = true
return
StreamConcurrency.onMain {
handleStateChanges(controller.state)

if skipChannelUpdates {
skippedRendering = true
return
}

reloadChannels()
}

reloadChannels()
}

// MARK: - DataControllerStateDelegate

open func controller(_ controller: DataController, didChangeState state: DataController.State) {
handleStateChanges(state)
nonisolated open func controller(_ controller: DataController, didChangeState state: DataController.State) {
StreamConcurrency.onMain {
handleStateChanges(state)
}
}

/// Called whenever the channels data changes or the controller.state changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import StreamChat
import SwiftUI

/// Protocol of `ChatChannelUnreadCountView` wrapper for use in SwiftUI.
public protocol ChatChannelUnreadCountViewSwiftUIView: View {
@preconcurrency @MainActor public protocol ChatChannelUnreadCountViewSwiftUIView: View {
init(dataSource: ChatChannelUnreadCountView.ObservedObject<Self>)
}

Expand Down
Loading
Loading