chore: remove many calls to ZMUserSession.shared() - WPB-23738#4373
chore: remove many calls to ZMUserSession.shared() - WPB-23738#4373samwyndham wants to merge 38 commits intodevelopfrom
ZMUserSession.shared() - WPB-23738#4373Conversation
…rSession through init methods
…tionView, ConnectRequestCell
Test Results1 872 tests 1 845 ✅ 2m 7s ⏱️ Results for commit 91802bd. ♻️ This comment has been updated with latest results. Summary: workflow run #22569268487 |
# Conflicts: # wire-ios/Wire-iOS Tests/MessageReplyPreviewViewTests.swift
|
There was a problem hiding this comment.
Pull request overview
This PR continues the app-wide effort to reduce implicit session dependencies by removing many direct calls to the deprecated ZMUserSession.shared() and instead passing a UserSession (and casting to ZMUserSession only where unavoidable).
Changes:
- Reworked many UI/controller initializers to accept
UserSessionand removedZMUserSession.shared()defaults. - Threaded
UserSessioninto media picking/confirmation, message preview/forwarding, and call-related UI flows. - Updated Settings / Developer tools to operate on an injected session rather than global state.
Reviewed changes
Copilot reviewed 150 out of 150 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/Views/SecurityLevelView.swift | Remove shared-session default provider |
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift | Use injected session for directory |
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewControllerViewModel.swift | Remove shared-session default provider |
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewController.swift | Derive classification provider from session |
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/Details/ProfileDetailsViewController.swift | Pass session into content controller |
| wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/Details/ProfileDetailsContentController.swift | Store session; replace shared calls |
| wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift | Inject session into sections |
| wire-ios/Wire-iOS/Sources/UserInterface/StartUI/Common/SectionControllers/TopPeopleLine/TopPeopleSectionController.swift | Pass session to inner controller |
| wire-ios/Wire-iOS/Sources/UserInterface/StartUI/Common/SectionControllers/TopPeopleLine/TopPeopleLineCollectionViewController.swift | Carry session to cells |
| wire-ios/Wire-iOS/Sources/UserInterface/StartUI/Common/SectionControllers/Suggestions/DirectorySectionController.swift | Require session; cast when needed |
| wire-ios/Wire-iOS/Sources/UserInterface/StartUI/Common/Cells/TopPeopleCell.swift | Expose session to avatar view |
| wire-ios/Wire-iOS/Sources/UserInterface/Sketchpad/CanvasViewController.swift | Require session; remove shared usage |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsViewControllerBuilder.swift | Thread session into Settings VCs |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/SettingsTableViewController.swift | Store session; remove shared observers |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/DeviceManagement/SettingsClientViewController.swift | Pass session into removal observer |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/DeviceManagement/ClientListViewController.swift | Make session non-optional; update observers |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift | Accept session explicitly in actions |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/DatabaseStatisticsController.swift | Require session; remove shared lookup |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/ChangingDetails/ChangeHandleViewController.swift | Pull profile from injected session |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsSignOutCellDescriptor.swift | Build logout helper with session |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsShareDatabaseCellDescriptor.swift | Use captured session vs shared |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory.swift | Carry session through descriptor tree |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+SoundAlert.swift | Pass session into group descriptors |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Options.swift | Pass session into option groups |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Developer.swift | Use injected session for debug actions |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+DataUsagePermissions.swift | Pass session into group descriptor |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Advanced.swift | Pass session; remove shared push token call |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptorFactory+Account.swift | Pass session into account flows |
| wire-ios/Wire-iOS/Sources/UserInterface/Settings/CellDescriptors/SettingsCellDescriptor.swift | Store session on group descriptor |
| wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift | Create picker/factory with session |
| wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController+SettingsChangeAlert.swift | Use injected session for perform block |
| wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/AccountViews/PersonalAccountView.swift | Add session dependency for observers |
| wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/AccountViews/AccountViewBuilder.swift | Thread session into account view |
| wire-ios/Wire-iOS/Sources/UserInterface/Overlay/CallTopOverlayController.swift | Require session for call observers |
| wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift | Inject session into proximity/settings |
| wire-ios/Wire-iOS/Sources/UserInterface/MainController/DefaultSettingsPropertyFactoryDelegate.swift | Pass session into passcode flow |
| wire-ios/Wire-iOS/Sources/UserInterface/LegalHoldDetails/Sections/LegalHoldParticipantsSectionController.swift | Add session dependency for observers |
| wire-ios/Wire-iOS/Sources/UserInterface/LegalHoldDetails/LegalHoldDetailsViewController.swift | Pass session into section controller |
| wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/Sections/ReceiptOptionsSectionController.swift | Use stored session instead of shared |
| wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/GroupDetailsViewController.swift | Replace shared session calls with injected |
| wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/ConversationActions/ConversationCallController.swift | Require session; remove shared leave |
| wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/ConversationActions/ConversationActionController.swift | Use injected session cast |
| wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/ConversationActions/ConversationActionController+Folders.swift | Get directory from injected session |
| wire-ios/Wire-iOS/Sources/UserInterface/DeviceView/DeviceDetailsViewActionsHandler.swift | Pass session into removal observer |
| wire-ios/Wire-iOS/Sources/UserInterface/CustomAppLock/Setup/PasscodeSetupViewController.swift | Require session; inject into presenter |
| wire-ios/Wire-iOS/Sources/UserInterface/CustomAppLock/Setup/PasscodeSetupPresenter.swift | Inject session into interactor |
| wire-ios/Wire-iOS/Sources/UserInterface/CustomAppLock/Setup/PasscodeSetupInteractor.swift | Use injected session for app lock |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ViewModel/ConversationListViewModel.swift | Make session non-optional; remove shared enqueue |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConversationListContentController/ConversationListContentController.swift | Pass session into connect/call flows |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ListContent/ConnectRequestsCell.swift | Configure cell with injected session |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ViewModel/ConversationListViewControllerViewModel.swift | Use injected session for observers |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ViewModel/ConversationListViewControllerViewModel+ZMConversationListObserver.swift | Use injected session for list observers |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController.swift | Require session for network status VC |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/Container/ConversationListViewController+EmptyState.swift | Remove optional session usage |
| wire-ios/Wire-iOS/Sources/UserInterface/ConversationList/ArchivedList/ArchivedListViewController.swift | Pass session into call controller |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ReplyComposingView.swift | Require session for message preview/obs |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController/ConversationInputBarViewController.swift | Init restriction manager with session |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift | Reuse restriction manager |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+FilesPopover.swift | Use session for upload limit |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+DragAndDrop.swift | Pass session into confirmation VC |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift | Pass session into camera/confirm flows |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Audio.swift | Remove shared guard; use session contextProvider |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/CameraKeyboard/CameraKeyboardViewController.swift | Require session; remove shared limits |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecorder.swift | Require session for call-state gating |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift | Inject session into recorder |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordKeyboardViewController.swift | Inject session into recorder |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController.swift | Pass session into call controller; remove shared didClose |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ViewLifeCycle.swift | Remove shared didOpen |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationRootViewController.swift | Use session for network status + classification |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/MessagePresenter.swift | Require session; remove shared enqueue |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController.swift | Create presenter with session |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+Reply.swift | Pass session into reply composing view |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+MessageAction.swift | Use session perform; pass session to canvas |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+Header.swift | Cast injected session where required |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/ConversationContentViewController+Forward.swift | Update Shareable forwarding to use session |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/FileTransfer/AudioMessageView.swift | Add session setter; remove shared observer |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageSectionController.swift | Pass session into cell descriptions |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationCollapsedMessageCell.swift | Inject session into avatar/observers |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Components/ConversationSystemMessageCellDescription.swift | Pass session to sender cell |
| wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Components/ConversationSenderMessageDetailsCell.swift | Inject session into avatar/observers |
| wire-ios/Wire-iOS/Sources/UserInterface/ConnectRequests/UserConnectionViewController.swift | Make userSession required; pass to views |
| wire-ios/Wire-iOS/Sources/UserInterface/ConnectRequests/UserConnectionView.swift | Require session; remove shared image session |
| wire-ios/Wire-iOS/Sources/UserInterface/ConnectRequests/IncomingConnectionView.swift | Require session; remove shared image session |
| wire-ios/Wire-iOS/Sources/UserInterface/ConnectRequests/ConnectRequestsViewController.swift | Configure cell with user+session |
| wire-ios/Wire-iOS/Sources/UserInterface/ConnectRequests/ConnectRequestCell.swift | Configure via injected session (no shared) |
| wire-ios/Wire-iOS/Sources/UserInterface/Components/NetworkStatus/NetworkStatusViewController.swift | Require session; remove shared lookups |
| wire-ios/Wire-iOS/Sources/UserInterface/Components/MessagePreviewView.swift | Require session for previews/observers |
| wire-ios/Wire-iOS/Sources/UserInterface/Components/Controllers/ImagePickerConfirmationController.swift | Require session for confirmation VC |
| wire-ios/Wire-iOS/Sources/UserInterface/Components/Controllers/ConfirmAssetViewController/ConfirmAssetViewController.swift | Require session; pass to canvas |
| wire-ios/Wire-iOS/Sources/UserInterface/Collections/ConversationImagesViewController.swift | Use injected session for restrictions |
| wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionsViewController.swift | Create presenter with session; set audio session |
| wire-ios/Wire-iOS/Sources/UserInterface/Collections/CollectionAudioCell.swift | Forward session into audio view |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallViewController/CallViewController.swift | Pass session into call grid VC |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallQualityController/CallQualityController.swift | Observe calls via injected session |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallGridView/CallParticipantViews/CallParticipantView.swift | Pass session through participant view |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallGridView/CallParticipantViews/BaseCallParticipantView.swift | Store injected session (no shared) |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallGridView/CallGridViewController.swift | Store injected session; pass to views |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallController/CallController.swift | Use injected session instead of shared |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/CallController/ActiveCallRouter.swift | Pass session to call controller/quality/top overlay |
| wire-ios/Wire-iOS/Sources/UserInterface/Calling/BottomSheetContainerViewController/CallingBottomSheetViewController.swift | Use injected session for priority call |
| wire-ios/Wire-iOS/Sources/Managers/ProximityMonitorManager.swift | Require session; remove shared callCenter |
| wire-ios/Wire-iOS/Sources/Managers/Image/ProfileImagePickerManager.swift | Use injected session for profile update |
| wire-ios/Wire-iOS/Sources/Managers/Image/ImagePickerManager.swift | Require session; remove shared restrictions |
| wire-ios/Wire-iOS/Sources/Helpers/syncengine/MessageKeyPathObserver.swift | Require session for message observing |
| wire-ios/Wire-iOS/Sources/Helpers/syncengine/ClientRemovalObserver.swift | Store injected session; remove shared usage |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/Journal/JournalViewModel.swift | Use injected session for CoreData context |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift | Use active session from router |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperE2ei/DeveloperE2eiViewModel.swift | Inject session instead of shared |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperE2ei/DeveloperE2eiView.swift | Update preview initializer |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift | Inject session; logout uses helper(session) |
| wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsView.swift | Update preview initializer |
| wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController.swift | Shareable now requires session |
| wire-ios/Wire-iOS/Sources/Components/ShareViewController/ShareViewController+Views.swift | Pass session to preview generation |
| wire-ios/Wire-iOS/Sources/Components/Settings+Account.swift | lastViewedConversation now takes session |
| wire-ios/Wire-iOS/Sources/Authentication/Helpers/LogOutHelper.swift | Require session; remove shared logout |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| func setupConnectRequestsCell(userSession: UserSession) { | ||
| self.userSession = userSession | ||
|
|
||
| clipsToBounds = true | ||
| addSubview(itemView) | ||
| updateAppearance() | ||
|
|
||
| if let userSession = ZMUserSession.shared() { | ||
| if let userSession = userSession as? ZMUserSession { | ||
| conversationListObserverToken = ConversationListChangeInfo.add( |
There was a problem hiding this comment.
setupConnectRequestsCell(userSession:) is not idempotent: it re-adds itemView and re-registers ConversationListChangeInfo every time it's called. Since the collection view calls this during cell reuse, this can lead to lost constraints (because hasCreatedInitialConstraints prevents re-adding them after reattaching the view) and multiple active observers (the previous token is overwritten without being removed). Consider moving the one-time view setup/constraints/observer registration back into init, and keep a separate lightweight configure(userSession:) that only updates stored session + appearance, or guard so setup/observer registration runs only once (and explicitly unregister/replace observers when session changes).
| cancelAction: { [weak self] in | ||
| guard let userSession = ZMUserSession.shared() else { return } | ||
| self?.conversation.voiceChannel?.leave(userSession: userSession, completion: nil) | ||
| guard let self, let userSession = userSession as? ZMUserSession else { return } | ||
| conversation.voiceChannel?.leave(userSession: userSession, completion: nil) | ||
|
|
There was a problem hiding this comment.
In cancelAction, let userSession = userSession as? ZMUserSession relies on implicit member lookup inside an escaping closure and also shadows the stored userSession property name, which is easy to misread and can trip Swift's explicit-self rules. Rename the unwrapped variable (e.g. zmUserSession) and reference self.userSession explicitly to make capture semantics unambiguous.
| func startRemoval() { | ||
| delegate?.setIsLoadingViewVisible(self, isVisible: true) | ||
| ZMUserSession.shared()?.deleteClient(userClientToDelete, credentials: credentials) | ||
| (userSession as? ZMUserSession)?.deleteClient(userClientToDelete, credentials: credentials) | ||
| } |
There was a problem hiding this comment.
startRemoval() turns on the loading UI and then calls deleteClient only if userSession can be cast to ZMUserSession. If a non-ZMUserSession implementation is ever passed in (e.g. in tests/mocks), the deletion is skipped and the loading UI can remain stuck indefinitely with no completion. Either require ZMUserSession in the initializer (since this type needs deleteClient/addClientUpdateObserver anyway) or handle the cast failure by hiding the loading UI and calling completion with an error.
| func lastViewedConversation(for account: Account, in session: UserSession) -> ZMConversation? { | ||
| guard let conversationID: String = value(for: .lastViewedConversation, in: account) else { | ||
| return nil | ||
| } | ||
|
|
||
| let conversationURI = URL(string: conversationID) | ||
| let session = ZMUserSession.shared() | ||
| let session = session as? ZMUserSession | ||
| let objectID = ZMManagedObject.objectID(forURIRepresentation: conversationURI, inUserSession: session) | ||
| return ZMConversation.existingObject(with: objectID, inUserSession: session) |
There was a problem hiding this comment.
The parameter session is immediately shadowed by let session = session as? ZMUserSession, which makes the code harder to follow and easier to misuse (especially when passing session into other APIs below). Consider renaming the casted value (e.g. zmSession) or using a separate guard let zmSession = session as? ZMUserSession else { return nil } to avoid shadowing.
netbe
left a comment
There was a problem hiding this comment.
this is quite a large PR so hard to be thorough. I left some comments but looking at the complexity it looks fine to me
|
|
||
| guard let userSession = ZMUserSession.shared() else { | ||
| // This is a hack for testing as a real ZMUserSession is not available | ||
| guard userSession is ZMUserSession else { |
There was a problem hiding this comment.
suggestion: change zmlog as WireLogger
| guard let userSession = ZMUserSession.shared() else { | ||
| // This is a hack for testing as a real ZMUserSession is not available | ||
| guard userSession is ZMUserSession else { | ||
| zmLog.error("UserSession not available when initializing \(type(of: self))") |
There was a problem hiding this comment.
suggestion: change zmlog as WireLogger
| } | ||
| } | ||
|
|
||
| func setUserSession(userSession: UserSession) { |
There was a problem hiding this comment.
question: where is this method called?
| func setUserSession(userSession: UserSession) { | ||
| self.userSession = userSession | ||
|
|
||
| if callStateObserverToken == nil, let userSession = userSession as? ZMUserSession { |
There was a problem hiding this comment.
question: I notice the check for callStateObserverToken == nil compared to before, is it needed?



Issue
This PR removes many calls to the deprecated
ZMUserSession.shared().I hope this will help lay down some work towards simplifying session management by removing the implicit dependency of SessionManager from many components. (
ZMUserSession.shared()usesSessionManager.shared) internally.I used Claude code to help me here which generally worked well but also made some funny choices that I changed. It also took me longer than I had hoped.
In general I've tried to keep changes to a minimum and to avoid behavior changes.
ZMUserSessionis has generally be passed throughinitmethods as aUserSessionand calls toZMUserSession.shared()have often been changed toUserSession as? ZMUserSession.I noticed that a lot of our tests depend on not using a real
ZMUserSessionwhich I haven't attempted to fix.While this PR does get rid of a lot of calls to
ZMUserSession.shared()that call is still used by for exampleZMUser.selfUser()so may take quite some effort to completely get rid of it.I recommend checking this PR commit by commit. There are some overly large commits but many are small
Testing
Changes can't really be tested outside a full regression test at least the following should be tested before merge as it
Repeat the following with call kit enabled & disabled and in the foreground, background, and app quit
(6 states).
Checklist
[WPB-XXX].