From f5890b68a2305cf4202a75c47171046406114cbe Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 25 May 2022 13:28:19 +0300 Subject: [PATCH] vector-im/element-x-ios/issues/53 - Adopt MainActors, dispatch heavy operations do detached tasks and ensure combine publisher call back on the right queue --- ElementX/Sources/Other/Coordinator.swift | 1 + .../NavigationRouterStoreProtocol.swift | 1 + .../Other/Routers/NavigationRouterType.swift | 1 + .../Sources/Other/Routers/Presentable.swift | 1 + .../Other/Routers/RootRouterType.swift | 1 + .../ViewModel/StateStoreViewModel.swift | 2 + .../HomeScreen/HomeScreenCoordinator.swift | 8 +-- .../HomeScreen/HomeScreenViewModel.swift | 6 +- .../HomeScreenViewModelProtocol.swift | 1 + .../LoginScreenViewModelProtocol.swift | 1 + .../Screens/RoomScreen/RoomScreenModels.swift | 2 +- .../RoomScreen/RoomScreenViewModel.swift | 13 +---- .../RoomScreenViewModelProtocol.swift | 1 + .../AuthenticationCoordinator.swift | 1 + .../Services/Authentication/UserSession.swift | 25 +++++---- .../Services/Media/MediaProvider.swift | 48 ++++++++-------- .../Media/MediaProviderProtocol.swift | 1 + .../Members/MemberDetailProviderManager.swift | 1 + .../MemberDetailsProviderProtocol.swift | 1 + .../Sources/Services/Room/RoomProxy.swift | 56 +++++++++---------- .../EventBriefFactoryProtocol.swift | 1 + .../Room/RoomSummary/RoomSummary.swift | 6 +- .../RoomSummary/RoomSummaryProtocol.swift | 1 + .../Timeline/RoomTimelineController.swift | 5 +- .../RoomTimelineControllerProtocol.swift | 1 + .../RoomTimelineProviderProtocol.swift | 1 + .../RoomTimelineItemFactory.swift | 1 + .../RoomTimelineViewFactory.swift | 1 + ...emplateSimpleScreenViewModelProtocol.swift | 1 + 29 files changed, 100 insertions(+), 90 deletions(-) diff --git a/ElementX/Sources/Other/Coordinator.swift b/ElementX/Sources/Other/Coordinator.swift index 3345c2f94..3ea806b04 100755 --- a/ElementX/Sources/Other/Coordinator.swift +++ b/ElementX/Sources/Other/Coordinator.swift @@ -19,6 +19,7 @@ import UIKit /// Protocol describing a [Coordinator](http://khanlou.com/2015/10/coordinators-redux/). /// Coordinators are the objects which control the navigation flow of the application. /// It helps to isolate and reuse view controllers and pass dependencies down the navigation hierarchy. +@MainActor protocol Coordinator: AnyObject { /// Starts job of the coordinator. diff --git a/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift b/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift index 112f4b95b..ad5fb663f 100644 --- a/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift +++ b/ElementX/Sources/Other/Routers/NavigationRouterStoreProtocol.swift @@ -17,6 +17,7 @@ import UIKit /// `NavigationRouterStoreProtocol` describes a structure that enables to get a NavigationRouter from a UINavigationController instance. +@MainActor protocol NavigationRouterStoreProtocol { /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. diff --git a/ElementX/Sources/Other/Routers/NavigationRouterType.swift b/ElementX/Sources/Other/Routers/NavigationRouterType.swift index 484590088..1d1e39a90 100755 --- a/ElementX/Sources/Other/Routers/NavigationRouterType.swift +++ b/ElementX/Sources/Other/Routers/NavigationRouterType.swift @@ -18,6 +18,7 @@ import UIKit /// Protocol describing a router that wraps a UINavigationController and add convenient completion handlers. Completions are called when a Presentable is removed. /// Routers are used to be passed between coordinators. They handles only `physical` navigation. +@MainActor protocol NavigationRouterType: AnyObject, Presentable { /// Present modally a view controller on the navigation controller diff --git a/ElementX/Sources/Other/Routers/Presentable.swift b/ElementX/Sources/Other/Routers/Presentable.swift index e9f760393..2d6e4048e 100755 --- a/ElementX/Sources/Other/Routers/Presentable.swift +++ b/ElementX/Sources/Other/Routers/Presentable.swift @@ -17,6 +17,7 @@ import UIKit /// Protocol used to pass UIViewControllers to routers +@MainActor protocol Presentable { func toPresentable() -> UIViewController } diff --git a/ElementX/Sources/Other/Routers/RootRouterType.swift b/ElementX/Sources/Other/Routers/RootRouterType.swift index 633da610c..4d956acee 100755 --- a/ElementX/Sources/Other/Routers/RootRouterType.swift +++ b/ElementX/Sources/Other/Routers/RootRouterType.swift @@ -18,6 +18,7 @@ import UIKit /// Protocol describing a router that wraps the root navigation of the application. /// Routers are used to be passed between coordinators. They handles only `physical` navigation. +@MainActor protocol RootRouterType: AnyObject { /// Update the root view controller diff --git a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift index fc10cd0b3..24bed91aa 100644 --- a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift +++ b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift @@ -30,6 +30,7 @@ import Combine /// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks /// can't be made into the `ViewModel`. @dynamicMemberLookup +@MainActor class ViewModelContext: ObservableObject { // MARK: - Properties @@ -70,6 +71,7 @@ class ViewModelContext: ObservableObject { /// a specific portion of state that can be safely bound to. /// If we decide to add more features to our state management (like doing state processing off the main thread) /// we can do it in this centralised place. +@MainActor class StateStoreViewModel { typealias Context = ViewModelContext diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 5e750c965..6d6c49036 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -85,16 +85,12 @@ final class HomeScreenCoordinator: Coordinator, Presentable { Task { if case let .success(userAvatarURL) = await parameters.userSession.loadUserAvatarURL() { if case let .success(avatar) = await parameters.mediaProvider.loadImageFromURL(userAvatarURL) { - await MainActor.run { - self.viewModel.updateWithUserAvatar(avatar) - } + self.viewModel.updateWithUserAvatar(avatar) } } if case let .success(userDisplayName) = await parameters.userSession.loadUserDisplayName() { - await MainActor.run { - self.viewModel.updateWithUserDisplayName(userDisplayName) - } + self.viewModel.updateWithUserDisplayName(userDisplayName) } } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 1498466ea..c28c59926 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -47,11 +47,11 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol override func process(viewAction: HomeScreenViewAction) { switch viewAction { case .logout: - self.completion?(.logout) + completion?(.logout) case .loadRoomData(let roomIdentifier): - self.loadRoomDataForIdentifier(roomIdentifier) + loadRoomDataForIdentifier(roomIdentifier) case .selectRoom(let roomIdentifier): - self.completion?(.selectRoom(roomIdentifier: roomIdentifier)) + completion?(.selectRoom(roomIdentifier: roomIdentifier)) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift index 3e3e95d9a..a7526f3b0 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift @@ -17,6 +17,7 @@ import Foundation import UIKit +@MainActor protocol HomeScreenViewModelProtocol { var completion: ((HomeScreenViewModelResult) -> Void)? { get set } diff --git a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift b/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift index 9e6d82518..dab024124 100644 --- a/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/LoginScreen/LoginScreenViewModelProtocol.swift @@ -16,6 +16,7 @@ import Foundation +@MainActor protocol LoginScreenViewModelProtocol { var completion: ((LoginScreenViewModelResult) -> Void)? { get set } var context: LoginScreenViewModelType.Context { get } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 49431f7d2..179f91429 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -39,7 +39,7 @@ struct RoomScreenViewState: BindableState { var isBackPaginating = false var bindings: RoomScreenViewStateBindings - var contextMenuBuilder: ((_ itemId: String) -> TimelineItemContextMenu)? + var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)? var sendButtonDisabled: Bool { bindings.composerText.count == 0 diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index d863b7f8a..05592c3f0 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -67,15 +67,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { switch viewAction { case .loadPreviousPage: - await MainActor.run { - state.isBackPaginating = true - } + state.isBackPaginating = true switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) { default: - await MainActor.run { - state.isBackPaginating = false - } + state.isBackPaginating = false } case .itemAppeared(let id): @@ -90,10 +86,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } await timelineController.sendMessage(state.bindings.composerText) - - await MainActor.run { - state.bindings.composerText = "" - } + state.bindings.composerText = "" } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift index f33e5181e..73874fc8e 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift @@ -16,6 +16,7 @@ import Foundation +@MainActor protocol RoomScreenViewModelProtocol { var context: RoomScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift index cdfe69832..d06d4aa9d 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationCoordinator.swift @@ -14,6 +14,7 @@ enum AuthenticationCoordinatorError: Error { case failedSettingUpSession } +@MainActor protocol AuthenticationCoordinatorDelegate: AnyObject { func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) diff --git a/ElementX/Sources/Services/Authentication/UserSession.swift b/ElementX/Sources/Services/Authentication/UserSession.swift index 258dc17f1..526af3906 100644 --- a/ElementX/Sources/Services/Authentication/UserSession.swift +++ b/ElementX/Sources/Services/Authentication/UserSession.swift @@ -27,12 +27,13 @@ private class WeakUserSessionWrapper: ClientDelegate { self.userSession = userSession } - func didReceiveSyncUpdate() { + @MainActor func didReceiveSyncUpdate() { self.userSession?.didReceiveSyncUpdate() } } -class UserSession: ClientDelegate { +@MainActor +class UserSession { private let client: Client @@ -74,26 +75,26 @@ class UserSession: ClientDelegate { } func loadUserDisplayName() async -> Result { - await withCheckedContinuation { continuation in + await Task.detached { () -> Result in do { let displayName = try self.client.displayName() - continuation.resume(returning: .success(displayName)) + return .success(displayName) } catch { - continuation.resume(returning: .failure(.failedRetrievingDisplayName)) + return .failure(.failedRetrievingDisplayName) } - } + }.value } func loadUserAvatarURL() async -> Result { - await withCheckedContinuation { continuation in + await Task.detached { () -> Result in do { let avatarURL = try self.client.avatarUrl() - continuation.resume(returning: .success(avatarURL)) + return .success(avatarURL) } catch { - continuation.resume(returning: .failure(.failedRetrievingDisplayName)) + return .failure(.failedRetrievingDisplayName) } - } + }.value } // MARK: ClientDelegate @@ -101,8 +102,8 @@ class UserSession: ClientDelegate { func didReceiveSyncUpdate() { Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update") - Task { - await updateRooms() + Task.detached { + await self.updateRooms() } } diff --git a/ElementX/Sources/Services/Media/MediaProvider.swift b/ElementX/Sources/Services/Media/MediaProvider.swift index 6f0c91d52..c2d30889d 100644 --- a/ElementX/Sources/Services/Media/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/MediaProvider.swift @@ -46,33 +46,33 @@ struct MediaProvider: MediaProviderProtocol { return .success(image) } - return await withCheckedContinuation { continuation in + let cachedImageLoadResult = await withCheckedContinuation({ continuation in imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in - if case let .success(cacheResult) = result, - let image = cacheResult.image { - continuation.resume(returning: .success(image)) - return + continuation.resume(returning: result) + } + }) + + if case let .success(cacheResult) = cachedImageLoadResult, + let image = cacheResult.image { + return .success(image) + } + + return await Task.detached { () -> Result in + do { + let imageData = try client.getMediaContent(source: source.underlyingSource) + + guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else { + MXLog.error("Invalid image data") + return .failure(.invalidImageData) } - processingQueue.async { - do { - let imageData = try client.getMediaContent(source: source.underlyingSource) - - guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else { - MXLog.error("Invalid image data") - continuation.resume(returning: .failure(.invalidImageData)) - return - } - - imageCache.store(image, forKey: source.underlyingSource.url()) - - continuation.resume(returning: .success(image)) - } catch { - MXLog.error("Failed retrieving image with error: \(error)") - continuation.resume(returning: .failure(.failedRetrievingImage)) - } - } + imageCache.store(image, forKey: source.underlyingSource.url()) + + return .success(image) + } catch { + MXLog.error("Failed retrieving image with error: \(error)") + return .failure(.failedRetrievingImage) } - } + }.value } } diff --git a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift index 13dbd2689..ed9b66d99 100644 --- a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift +++ b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift @@ -14,6 +14,7 @@ enum MediaProviderError: Error { case invalidImageData } +@MainActor protocol MediaProviderProtocol { func imageFromSource(_ source: MediaSource?) -> UIImage? diff --git a/ElementX/Sources/Services/Room/Members/MemberDetailProviderManager.swift b/ElementX/Sources/Services/Room/Members/MemberDetailProviderManager.swift index c3a7218e1..86802363c 100644 --- a/ElementX/Sources/Services/Room/Members/MemberDetailProviderManager.swift +++ b/ElementX/Sources/Services/Room/Members/MemberDetailProviderManager.swift @@ -8,6 +8,7 @@ import Foundation +@MainActor class MemberDetailProviderManager { private var memberDetailProviders: [String: MemberDetailProviderProtocol] = [:] diff --git a/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift b/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift index 0905107c7..911a4f051 100644 --- a/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift @@ -14,6 +14,7 @@ enum MemberDetailProviderError: Error { case failedRetrievingUserDisplayName } +@MainActor protocol MemberDetailProviderProtocol { func avatarURLForUserId(_ userId: String) -> String? func loadAvatarURLForUserId(_ userId: String) async -> Result diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 616597a80..45d197bff 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -44,9 +44,7 @@ class RoomProxy: RoomProxyProtocol { room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self)) - Task { - backwardStream = room.startLiveEventListener() - } + backwardStream = room.startLiveEventListener() } var id: String { @@ -86,78 +84,76 @@ class RoomProxy: RoomProxyProtocol { } func loadAvatarURLForUserId(_ userId: String) async -> Result { - await withCheckedContinuation({ continuation in + await Task.detached { () -> Result in do { let avatarURL = try self.room.memberAvatarUrl(userId: userId) - continuation.resume(returning: .success(avatarURL)) + return .success(avatarURL) } catch { - continuation.resume(returning: .failure(.failedRetrievingMemberAvatarURL)) + return .failure(.failedRetrievingMemberAvatarURL) } - }) + }.value } func loadDisplayNameForUserId(_ userId: String) async -> Result { - await withCheckedContinuation({ continuation in + await Task.detached { () -> Result in do { let displayName = try self.room.memberDisplayName(userId: userId) - continuation.resume(returning: .success(displayName)) + return .success(displayName) } catch { - continuation.resume(returning: .failure(.failedRetrievingMemberDisplayName)) + return .failure(.failedRetrievingMemberDisplayName) } - }) + }.value } func loadDisplayName() async -> Result { - await withCheckedContinuation({ continuation in - if let displayName = displayName { - continuation.resume(returning: .success(displayName)) - return + await Task.detached { () -> Result in + if let displayName = self.displayName { + return .success(displayName) } do { let displayName = try self.room.displayName() self.displayName = displayName - continuation.resume(returning: .success(displayName)) + return .success(displayName) } catch { - continuation.resume(returning: .failure(.failedRetrievingDisplayName)) + return .failure(.failedRetrievingDisplayName) } - }) + }.value } func paginateBackwards(count: UInt) async -> Result { - await withCheckedContinuation { continuation in + await Task.detached { () -> Result in guard let backwardStream = self.backwardStream else { - continuation.resume(returning: .failure(.backwardStreamNotAvailable)) - return + return .failure(RoomProxyError.backwardStreamNotAvailable) } - + Benchmark.startTrackingForIdentifier("BackPagination \(self.id)", message: "Backpaginating \(count) message(s) in room \(self.id)") let sdkMessages = backwardStream.paginateBackwards(count: UInt64(count)) Benchmark.endTrackingForIdentifier("BackPagination \(self.id)", message: "Finished backpaginating \(count) message(s) in room \(self.id)") - + let messages = sdkMessages.map { message in self.messageFactory.buildRoomMessageFrom(message) }.reversed() - + self.messages.insert(contentsOf: messages, at: 0) - continuation.resume(returning: .success(())) - } + return .success(()) + }.value } func sendMessage(_ message: String) async -> Result { let messageContent = messageEventContentFromMarkdown(md: message) let transactionId = genTransactionId() - return await withCheckedContinuation { continuation in + return await Task(priority: .high) { () -> Result in do { try self.room.send(msg: messageContent, txnId: transactionId) - continuation.resume(returning: .success(())) + return .success(()) } catch { - continuation.resume(returning: .failure(.failedSendingMessage)) + return .failure(.failedSendingMessage) } - } + }.value } // MARK: - Private diff --git a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift index 806bcf1d4..c22358b56 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift @@ -8,6 +8,7 @@ import Foundation +@MainActor protocol EventBriefFactoryProtocol { func eventBriefForMessage(_ message: RoomMessageProtocol?) async -> EventBrief? } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index e7afcb579..2aeb8b2e2 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -98,13 +98,13 @@ class RoomSummary: RoomSummaryProtocol { } await withTaskGroup(of: Void.self) { group in - group.addTask { + group.addTask(priority: .medium) { await self.loadDisplayName() } - group.addTask { + group.addTask(priority: .medium) { await self.loadAvatar() } - group.addTask { + group.addTask(priority: .medium) { await self.loadLastMessage() } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift index 5bc3dd7dc..cba73691c 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift @@ -13,6 +13,7 @@ enum RoomSummaryCallback { case updatedData } +@MainActor protocol RoomSummaryProtocol { var id: String { get } var name: String? { get } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index ddc3feca7..1b705f324 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -31,7 +31,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { self.mediaProvider = mediaProvider self.memberDetailProvider = memberDetailProvider - self.timelineProvider.callbacks.sink { [weak self] callback in + self.timelineProvider + .callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in guard let self = self else { return } switch callback { diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index 45fa0c66f..4db9e68b3 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -18,6 +18,7 @@ enum RoomTimelineControllerError: Error { case generic } +@MainActor protocol RoomTimelineControllerProtocol { var timelineItems: [RoomTimelineItemProtocol] { get } var callbacks: PassthroughSubject { get } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift index cd61cbc5d..67b04ff63 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift @@ -18,6 +18,7 @@ enum RoomTimelineProviderError: Error { case generic } +@MainActor protocol RoomTimelineProviderProtocol { var callbacks: PassthroughSubject { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index 546c15260..c08d95d20 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -9,6 +9,7 @@ import Foundation import UIKit +@MainActor struct RoomTimelineItemFactory { private let mediaProvider: MediaProviderProtocol private let memberDetailProvider: MemberDetailProviderProtocol diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift index a51e927b5..c1caaf6b6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewFactory.swift @@ -8,6 +8,7 @@ import Foundation +@MainActor struct RoomTimelineViewFactory { func buildTimelineViewFor(_ timelineItem: RoomTimelineItemProtocol) -> RoomTimelineViewProvider { switch timelineItem { diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateSimpleScreenViewModelProtocol.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateSimpleScreenViewModelProtocol.swift index d32b2596a..02b3c2ae1 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateSimpleScreenViewModelProtocol.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateSimpleScreenViewModelProtocol.swift @@ -16,6 +16,7 @@ import Foundation +@MainActor protocol TemplateSimpleScreenViewModelProtocol { var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? { get set } var context: TemplateSimpleScreenViewModelType.Context { get }