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

Draft
wants to merge 19 commits into
base: swift-6
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -15,7 +15,7 @@ open class DefaultChannelNameFormatter: ChannelNameFormatter {
public init() {}

/// Internal static property to add backwards compatibility to `Components.channelNamer`
internal static var channelNamer: (
nonisolated(unsafe) internal static var channelNamer: (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Review if MainActor is more approriate here

_ 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 {
MainActor.ensureIsolated { _default }
}
set {
MainActor.ensureIsolated { _default = newValue }
}
}

// Shared instance is mutated only on the main thread without explicit
// main actor annotation for easier SDK setup.
nonisolated(unsafe) private static var _default: Appearance = .init()
Comment on lines -42 to +53
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 was thinking about making it @MainActor, but this is used in multiple init methods meaning all of these must be @MainActor as well. Moreover, it might make the SDK initialization more complex as well since SDK users need to make sure Appearance is changed only from the MainActor.
a) we just make default nonisolated(unsafe)
b) we do what we have above which internally forcing main actor (for making sure SDK users do not corrupt the default by changing it from background threads)

}
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()
MainActor.ensureIsolated { [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
MainActor.ensureIsolated {
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()
MainActor.ensureIsolated {
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)
MainActor.ensureIsolated { [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)
MainActor.ensureIsolated { [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)
MainActor.ensureIsolated { [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)
MainActor.ensureIsolated { [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)
MainActor.ensureIsolated {
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>]
) {
MainActor.ensureIsolated {
_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>
) {
MainActor.ensureIsolated {
_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>
) {
MainActor.ensureIsolated {
_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) {
MainActor.ensureIsolated {
_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
)
MainActor.ensureIsolated {
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
MainActor.ensureIsolated { [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) {
MainActor.ensureIsolated {
collectionView.layoutIfNeeded()
}
}

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

if skipChannelUpdates {
skippedRendering = true
return
MainActor.ensureIsolated {
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) {
MainActor.ensureIsolated {
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StreamChat
import UIKit

/// The channel list search strategy. It is possible to search by messages or channels.
public struct ChannelListSearchStrategy {
public struct ChannelListSearchStrategy: Sendable {
/// The name of the strategy.
public var name: String
/// The type of search UI component.
Expand Down Expand Up @@ -34,7 +34,7 @@ public struct ChannelListSearchStrategy {
}

/// Creates the `UISearchController` for the Channel List depending on the current search strategy.
public func makeSearchController(
@MainActor public func makeSearchController(
with channelListVC: ChatChannelListVC
) -> UISearchController? {
if let messageSearchVC = searchVC.init() as? ChatMessageSearchVC {
Expand Down
Loading
Loading