From 583c796a4c4b13801b21ef6a8c86a4c52659f3f3 Mon Sep 17 00:00:00 2001 From: shannon Date: Wed, 23 Oct 2024 09:59:18 -0400 Subject: [PATCH] Some defense against ProfileViewController.authContext.getter Forced Unwrapping Crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the viewModel of the ProfileViewController optional rather than force unwrapping it. If the necessary information is not available, Profile page should show blank rather than crashing, and hopefully will have the expected info soon or the next time it is opened. There is still a crash danger inherent in the use of TabBarPager, which requires a non-optional for several protocol methods were we can’t guarantee to have one to return. This dependency should be removed in the future. --- .../Protocol/PagerTabStripNavigateable.swift | 3 +- .../Paging/ProfilePagingViewController.swift | 13 +- .../Scene/Profile/ProfileViewController.swift | 217 +++++++++++------- .../Root/MainTab/MainTabBarController.swift | 10 + .../Persistence/FileManager+Account.swift | 3 +- 5 files changed, 155 insertions(+), 91 deletions(-) diff --git a/Mastodon/Protocol/PagerTabStripNavigateable.swift b/Mastodon/Protocol/PagerTabStripNavigateable.swift index 14ef8bbe6..0df428a1f 100644 --- a/Mastodon/Protocol/PagerTabStripNavigateable.swift +++ b/Mastodon/Protocol/PagerTabStripNavigateable.swift @@ -12,7 +12,7 @@ import MastodonLocalization typealias PagerTabStripNavigateable = PagerTabStripNavigateableCore & PagerTabStripNavigateableRelay protocol PagerTabStripNavigateableCore: AnyObject { - var navigateablePageViewController: PagerTabStripViewController { get } + var navigateablePageViewController: PagerTabStripViewController? { get } var pagerTabStripNavigateKeyCommands: [UIKeyCommand] { get } func pagerTabStripNavigateKeyCommandHandler(_ sender: UIKeyCommand) @@ -82,6 +82,7 @@ extension PagerTabStripNavigateableCore where Self: PagerTabStripNavigateableRel extension PagerTabStripNavigateableCore { func navigate(direction: PagerTabStripNavigationDirection) { + guard let navigateablePageViewController = navigateablePageViewController else { return } let index = navigateablePageViewController.currentIndex let targetIndex: Int diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift index 9b0e067f9..41a46b753 100644 --- a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift @@ -23,7 +23,7 @@ final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, T weak var pagingDelegate: ProfilePagingViewControllerDelegate? var disposeBag = Set() - var viewModel: ProfilePagingViewModel! + var viewModel: ProfilePagingViewModel? let buttonBarShadowView = UIView() private var buttonBarShadowAlpha: CGFloat = 0.0 @@ -31,7 +31,7 @@ final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, T // MARK: - TabBarPageViewController var currentPage: TabBarPage? { - return viewModel.viewControllers[currentIndex] + return viewModel?.viewControllers[currentIndex] } var currentPageIndex: Int? { @@ -41,13 +41,13 @@ final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, T // MARK: - ButtonBarPagerTabStripViewController override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { - return viewModel.viewControllers + return viewModel?.viewControllers ?? [] } override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) { super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged) - guard indexWasChanged else { return } + guard indexWasChanged, let viewModel = viewModel else { return } let page = viewModel.viewControllers[toIndex] tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex) } @@ -88,7 +88,7 @@ extension ProfilePagingViewController { buttonBarView.backgroundColor = .systemBackground buttonBarShadowView.pinTo(to: buttonBarView) - viewModel.$needsSetupBottomShadow + viewModel?.$needsSetupBottomShadow .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupBottomShadow in guard let self = self else { return } @@ -145,7 +145,7 @@ extension ProfilePagingViewController { } func setupBottomShadow() { - guard viewModel.needsSetupBottomShadow else { + guard let viewModel = viewModel, viewModel.needsSetupBottomShadow else { buttonBarShadowView.layer.shadowColor = nil buttonBarShadowView.layer.shadowRadius = 0 return @@ -176,6 +176,7 @@ extension ProfilePagingViewController { extension ProfilePagingViewController { var currentViewController: (UIViewController & TabBarPage)? { + guard let viewModel = viewModel else { return nil } guard !viewModel.viewControllers.isEmpty, currentIndex < viewModel.viewControllers.count else { return nil } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 9c5920efa..f06b8bc13 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -31,15 +31,21 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - //TODO: Replace with something better than ! - var viewModel: ProfileViewModel! { + + var viewModel: ProfileViewModel? { didSet { if isViewLoaded { - bindViewModel() - + guard let viewModel = viewModel else { return } viewModel.isEditing = false + + if profileHeaderViewController == nil { + createSupplementaryViews(withViewModel: viewModel) + } + bindToViewModel(viewModel) + + guard let profileHeaderViewController = profileHeaderViewController else { return } profileHeaderViewController.viewModel.isEditing = false - profilePagingViewController.viewModel.profileAboutViewController.viewModel.isEditing = false + profilePagingViewController?.viewModel?.profileAboutViewController.viewModel?.isEditing = false viewModel.profileAboutViewModel.isEditing = false } } @@ -130,12 +136,21 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi private(set) lazy var tabBarPagerController = TabBarPagerController() - private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = { - let viewController = ProfileHeaderViewController(context: context, authContext: authContext, coordinator: coordinator, profileViewModel: viewModel) - return viewController - }() + private(set) var profileHeaderViewController: ProfileHeaderViewController? - private(set) lazy var profilePagingViewController: ProfilePagingViewController = { + private func createSupplementaryViews(withViewModel viewModel: ProfileViewModel) { + profileHeaderViewController = createProfileHeaderViewController(viewModel: viewModel) + profilePagingViewController = createProfilePagingViewController(viewModel: viewModel) + } + + private func createProfileHeaderViewController(viewModel: ProfileViewModel) -> ProfileHeaderViewController { + let viewController = ProfileHeaderViewController(context: context, authContext: viewModel.authContext, coordinator: coordinator, profileViewModel: viewModel) + return viewController + } + + private(set) var profilePagingViewController: ProfilePagingViewController? + + private func createProfilePagingViewController(viewModel: ProfileViewModel) -> ProfilePagingViewController { let profilePagingViewController = ProfilePagingViewController() profilePagingViewController.viewModel = { let profilePagingViewModel = ProfilePagingViewModel( @@ -153,11 +168,11 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi return profilePagingViewModel }() return profilePagingViewController - }() + } // title view nested in header - var titleView: DoubleTitleLabelNavigationBarTitleView { - profileHeaderViewController.titleView + var titleView: DoubleTitleLabelNavigationBarTitleView? { + profileHeaderViewController?.titleView } @@ -172,7 +187,7 @@ extension ProfileViewController { override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() - profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets) + profileHeaderViewController?.updateHeaderContainerSafeAreaInset(view.safeAreaInsets) } override func viewDidLoad() { @@ -198,16 +213,22 @@ extension ProfileViewController { view.addSubview(tabBarPagerController.view) tabBarPagerController.didMove(toParent: self) tabBarPagerController.view.pinToParent() - - tabBarPagerController.delegate = self - tabBarPagerController.dataSource = self tabBarPagerController.relayScrollView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged) + if let viewModel = viewModel { + setUpSupplementaryViews(viewModel: viewModel) + } + } + + private func setUpSupplementaryViews(viewModel: ProfileViewModel) { // setup delegate - profileHeaderViewController.delegate = self - profilePagingViewController.viewModel.profileAboutViewController.delegate = self + if profileHeaderViewController == nil { + createSupplementaryViews(withViewModel: viewModel) + } + profileHeaderViewController?.delegate = self + profilePagingViewController?.viewModel?.profileAboutViewController.delegate = self } override func viewWillAppear(_ animated: Bool) { @@ -215,15 +236,14 @@ extension ProfileViewController { navigationController?.navigationBar.prefersLargeTitles = false - bindViewModel() - bindTitleView() - bindMoreBarButtonItem() - bindPager() + if let viewModel = viewModel { + bindToViewModel(viewModel) + } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.send() + viewModel?.viewDidAppear.send() setNeedsStatusBarAppearanceUpdate() } @@ -232,9 +252,19 @@ extension ProfileViewController { extension ProfileViewController { - private func bindViewModel() { + private func bindToViewModel(_ viewModel: ProfileViewModel) { + guard let profileHeaderViewController = profileHeaderViewController, let profilePagingViewController = profilePagingViewController else { return } + bindViewModel(viewModel, toHeaderViewController: profileHeaderViewController) + bindTitleView(profileHeaderViewController.titleView, headerView: profileHeaderViewController.profileHeaderView) + bindMoreBarButtonItem(viewModel: viewModel) + bindPager(pagingViewController: profilePagingViewController) + tabBarPagerController.delegate = self + tabBarPagerController.dataSource = self + } + + private func bindViewModel(_ viewModel: ProfileViewModel, toHeaderViewController headerViewController: ProfileHeaderViewController) { // header - let headerViewModel = profileHeaderViewController.viewModel + let headerViewModel = headerViewController.viewModel viewModel.$account .assign(to: \.account, on: headerViewModel) .store(in: &disposeBag) @@ -308,13 +338,13 @@ extension ProfileViewController { // build items Publishers.CombineLatest4( viewModel.$relationship, - profileHeaderViewController.viewModel.$isTitleViewDisplaying, + headerViewController.viewModel.$isTitleViewDisplaying, editingAndUpdatingPublisher, barButtonItemHiddenPublisher ) .receive(on: DispatchQueue.main) .sink { [weak self] account, isTitleViewDisplaying, tuple1, tuple2 in - guard let self else { return } + guard let self, let viewModel = self.viewModel else { return } let (isEditing, _) = tuple1 let (isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden) = tuple2 @@ -327,7 +357,7 @@ extension ProfileViewController { } } - if let suspended = self.viewModel.account.suspended, suspended == true { + if let suspended = viewModel.account.suspended, suspended == true { return } @@ -383,32 +413,32 @@ extension ProfileViewController { } - private func bindTitleView() { + private func bindTitleView(_ titleView: DoubleTitleLabelNavigationBarTitleView, headerView: ProfileHeaderView) { Publishers.CombineLatest3( - profileHeaderViewController.profileHeaderView.viewModel.$name, - profileHeaderViewController.profileHeaderView.viewModel.$emojiMeta, - profileHeaderViewController.profileHeaderView.viewModel.$statusesCount + headerView.viewModel.$name, + headerView.viewModel.$emojiMeta, + headerView.viewModel.$statusesCount ) .receive(on: DispatchQueue.main) .sink { [weak self] name, emojiMeta, statusesCount in guard let self = self else { return } guard let title = name, let statusesCount = statusesCount, let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { - self.titleView.isHidden = true + titleView.isHidden = true return } - self.titleView.isHidden = false + titleView.isHidden = false let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) do { let metaContent = try MastodonMetaContent.convert(document: mastodonContent) - self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) + titleView.update(titleMetaContent: metaContent, subtitle: subtitle) } catch { } } .store(in: &disposeBag) - profileHeaderViewController.profileHeaderView.viewModel.$name + headerView.viewModel.$name .receive(on: DispatchQueue.main) .sink { [weak self] name in guard let self = self, self.isModal == false else { return } @@ -418,7 +448,7 @@ extension ProfileViewController { } // This More-button is only visible for other users, but not myself - private func bindMoreBarButtonItem() { + private func bindMoreBarButtonItem(viewModel: ProfileViewModel) { Publishers.CombineLatest3( viewModel.$account, viewModel.$me, @@ -485,13 +515,14 @@ extension ProfileViewController { .store(in: &disposeBag) } - private func bindPager() { + private func bindPager(pagingViewController: ProfilePagingViewController) { + guard let viewModel = viewModel else { return } viewModel.$isPagingEnabled .receive(on: DispatchQueue.main) .sink { [weak self] isPagingEnabled in guard let self else { return } - self.profilePagingViewController.containerView.isScrollEnabled = isPagingEnabled - self.profilePagingViewController.buttonBarView.isUserInteractionEnabled = isPagingEnabled + pagingViewController.containerView.isScrollEnabled = isPagingEnabled + pagingViewController.buttonBarView.isUserInteractionEnabled = isPagingEnabled } .store(in: &disposeBag) @@ -502,17 +533,17 @@ extension ProfileViewController { // set first responder for key command if !isEditing { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.profilePagingViewController.becomeFirstResponder() + pagingViewController.becomeFirstResponder() } // dismiss keyboard if needs self.view.endEditing(true) } if isEditing, - let index = self.profilePagingViewController.viewControllers.firstIndex(where: { type(of: $0) is ProfileAboutViewController.Type }), - self.profilePagingViewController.canMoveTo(index: index) + let index = pagingViewController.viewControllers.firstIndex(where: { type(of: $0) is ProfileAboutViewController.Type }), + pagingViewController.canMoveTo(index: index) { - self.profilePagingViewController.moveToViewController(at: index) + pagingViewController.moveToViewController(at: index) } } .store(in: &disposeBag) @@ -528,6 +559,7 @@ extension ProfileViewController { let url = URL(string: href) else { return } _ = coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .hashtag(_, let hashtag, _): + guard let viewModel = viewModel else { break } let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: hashtag) _ = coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) case .email, .emoji: @@ -550,6 +582,8 @@ extension ProfileViewController { } @objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) { + guard let viewModel = viewModel else { return } + let activityViewController = DataSourceFacade.createActivityViewController( dependency: self, account: viewModel.account @@ -566,17 +600,22 @@ extension ProfileViewController { } @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { + guard let viewModel = viewModel else { return } + let favoriteViewModel = FavoriteViewModel(context: context, authContext: viewModel.authContext) _ = coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) } @objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) { + guard let viewModel = viewModel else { return } + let bookmarkViewModel = BookmarkViewModel(context: context, authContext: viewModel.authContext) _ = coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { - + guard let viewModel = viewModel else { return } + let mention = "@" + viewModel.account.acct UITextChecker.learnWord(mention) let composeViewModel = ComposeViewModel( @@ -590,29 +629,33 @@ extension ProfileViewController { } @objc private func followedTagsItemPressed(_ sender: UIBarButtonItem) { + guard let viewModel = viewModel else { return } + let followedTagsViewModel = FollowedTagsViewModel(context: context, authContext: viewModel.authContext) _ = coordinator.present(scene: .followedTags(viewModel: followedTagsViewModel), from: self, transition: .show) } @objc private func refreshControlValueChanged(_ sender: RefreshControl) { - if let userTimelineViewController = profilePagingViewController.currentViewController as? UserTimelineViewController { + if let userTimelineViewController = profilePagingViewController?.currentViewController as? UserTimelineViewController { userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) } Task { + guard let viewModel = viewModel else { return } + let account = viewModel.account if let domain = account.domain, - let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox), - let updatedRelationship = try? await context.apiService.relationship(forAccounts: [updatedAccount], authenticationBox: authContext.mastodonAuthenticationBox).value.first + let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authContext.mastodonAuthenticationBox), + let updatedRelationship = try? await context.apiService.relationship(forAccounts: [updatedAccount], authenticationBox: viewModel.authContext.mastodonAuthenticationBox).value.first { viewModel.account = updatedAccount viewModel.relationship = updatedRelationship viewModel.profileAboutViewModel.fields = updatedAccount.mastodonFields } - if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: viewModel.authContext.mastodonAuthenticationBox).value { viewModel.me = updatedMe - FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + FileManager.default.store(account: updatedMe, forUserID: viewModel.authContext.mastodonAuthenticationBox.authentication.userIdentifier()) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { @@ -631,7 +674,7 @@ extension ProfileViewController: TabBarPagerDelegate { } func resetPageContentOffset(_ tabBarPagerController: TabBarPagerController) { - for viewController in profilePagingViewController.viewModel.viewControllers { + for viewController in profilePagingViewController?.viewModel?.viewControllers ?? [] { viewController.pageScrollView.contentOffset = .zero } } @@ -649,6 +692,7 @@ extension ProfileViewController: TabBarPagerDelegate { // """ // ) + guard let profileHeaderViewController = profileHeaderViewController else { return } // elastically banner @@ -700,7 +744,7 @@ extension ProfileViewController: TabBarPagerDelegate { profileHeaderViewController.updateHeaderScrollProgress(progress, throttle: throttle) // setup buttonBar shadow - profilePagingViewController.updateButtonBarShadow(progress: progress) + profilePagingViewController?.updateButtonBarShadow(progress: progress) } } @@ -709,17 +753,17 @@ extension ProfileViewController: TabBarPagerDelegate { // MARK: - TabBarPagerDataSource extension ProfileViewController: TabBarPagerDataSource { func headerViewController() -> UIViewController & TabBarPagerHeader { - return profileHeaderViewController + return profileHeaderViewController! // no good way around this force unwrap given the requirement that the return value be non-optional } func pageViewController() -> UIViewController & TabBarPageViewController { - return profilePagingViewController + return profilePagingViewController! // no good way around this force unwrap given the requirement that the return value be non-optional } } // MARK: - AuthContextProvider extension ProfileViewController: AuthContextProvider { - var authContext: AuthContext { viewModel.authContext } + var authContext: AuthContext { viewModel!.authContext } } // MARK: - ProfileHeaderViewControllerDelegate @@ -729,6 +773,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { + guard let viewModel = viewModel else { return } if viewModel.me == viewModel.account { editProfile() } else { @@ -739,12 +784,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { private func editProfile() { // do nothing when updating + guard let viewModel = viewModel, let profileHeaderViewModel = profileHeaderViewController?.viewModel else { return } guard viewModel.isUpdating == false else { return } - - let profileHeaderViewModel = profileHeaderViewController.viewModel - guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } + + guard let profileAboutViewModel = profilePagingViewController?.viewModel?.profileAboutViewController.viewModel else { return } let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited @@ -758,11 +803,11 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { headerProfileInfo: profileHeaderViewModel.profileInfoEditing, aboutProfileInfo: profileAboutViewModel.profileInfoEditing ).value - self.viewModel.isEditing = false - self.profileHeaderViewController.viewModel.isEditing = false + viewModel.isEditing = false + self.profileHeaderViewController?.viewModel.isEditing = false profileAboutViewModel.isEditing = false - self.viewModel.account = updatedAccount - self.viewModel.profileAboutViewModel.fields = updatedAccount.mastodonFields + viewModel.account = updatedAccount + viewModel.profileAboutViewModel.fields = updatedAccount.mastodonFields } catch { let alertController = UIAlertController( @@ -776,20 +821,20 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { } // finish updating - self.viewModel.isUpdating = false + viewModel.isUpdating = false } } else if viewModel.isEditing == false { // set `updating` then toggle `edit` state viewModel.isUpdating = true - profileHeaderViewController.viewModel.isUpdating = true + profileHeaderViewController?.viewModel.isUpdating = true viewModel.fetchEditProfileInfo() .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self else { return } defer { // finish updating - self.viewModel.isUpdating = false - self.profileHeaderViewController.viewModel.isUpdating = false + viewModel.isUpdating = false + self.profileHeaderViewController?.viewModel.isUpdating = false } switch completion { case .failure(let error): @@ -803,15 +848,15 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) case .finished: // enter editing mode - self.viewModel.isEditing = true - self.profileHeaderViewController.viewModel.isEditing = true + viewModel.isEditing = true + self.profileHeaderViewController?.viewModel.isEditing = true profileAboutViewModel.isEditing = true } } receiveValue: { [weak self] response in guard let self else { return } - self.profileHeaderViewController.viewModel.setProfileInfo(accountForEdit: response.value) - self.viewModel.accountForEdit = response.value + self.profileHeaderViewController?.viewModel.setProfileInfo(accountForEdit: response.value) + viewModel.accountForEdit = response.value } .store(in: &disposeBag) } else if isEdited == false { @@ -820,14 +865,15 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { } private func cancelEditing() { + guard let viewModel = viewModel else { return } viewModel.isEditing = false - profileHeaderViewController.viewModel.isEditing = false - profilePagingViewController.viewModel.profileAboutViewController.viewModel.isEditing = false + profileHeaderViewController?.viewModel.isEditing = false + profilePagingViewController?.viewModel?.profileAboutViewController.viewModel.isEditing = false viewModel.profileAboutViewModel.isEditing = false } private func editRelationship() { - guard let relationship = viewModel.relationship, viewModel.isUpdating == false else { + guard let viewModel = viewModel, let relationship = viewModel.relationship, viewModel.isUpdating == false else { return } @@ -866,13 +912,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Actions.unblockDomain(domain), style: .default) { [weak self] _ in - guard let self else { return } + guard let self, let viewModel = self.viewModel else { return } Task { _ = try await DataSourceFacade.responseToDomainBlockAction(dependency: self, account: account) - guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first else { return } + guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: viewModel.authContext.mastodonAuthenticationBox).value.first else { return } - self.viewModel.isUpdating = false + viewModel.isUpdating = false // we need to trigger this here as domain block doesn't return a relationship let userInfo = [ @@ -943,6 +989,7 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { // MARK: - MastodonMenuDelegate extension ProfileViewController: MastodonMenuDelegate { func menuAction(_ action: MastodonMenu.Action) { + guard let viewModel = viewModel else { return } switch action { case .muteUser(_), .blockUser(_), .blockDomain(_), .hideReblogs(_), .reportUser(_), .shareUser(_), .openUserInBrowser(_), .copyProfileLink(_), .followUser(_): Task { @@ -972,6 +1019,7 @@ extension ProfileViewController: ScrollViewContainer { extension ProfileViewController { override var keyCommands: [UIKeyCommand]? { + guard let viewModel = viewModel else { return nil } if !viewModel.isEditing { return pagerTabStripNavigateKeyCommands } @@ -984,7 +1032,7 @@ extension ProfileViewController { // MARK: - PagerTabStripNavigateable extension ProfileViewController: PagerTabStripNavigateable { - var navigateablePageViewController: PagerTabStripViewController { + var navigateablePageViewController: PagerTabStripViewController? { return profilePagingViewController } @@ -1011,6 +1059,7 @@ extension ProfileViewController: DataSourceProvider { } func updateViewModelsWithDataControllers(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) { + guard let viewModel = viewModel else { return } viewModel.postsUserTimelineViewModel.dataController.update(status: status, intent: intent) viewModel.repliesUserTimelineViewModel.dataController.update(status: status, intent: intent) viewModel.mediaUserTimelineViewModel.dataController.update(status: status, intent: intent) @@ -1026,6 +1075,8 @@ extension ProfileViewController { guard let userInfo = notification.userInfo, let relationship = userInfo[UserInfoKey.relationship] as? Mastodon.Entity.Relationship else { return } + + guard let viewModel = viewModel else { return } viewModel.isUpdating = true if viewModel.account.id == relationship.id { @@ -1033,12 +1084,12 @@ extension ProfileViewController { Task { let account = viewModel.account if let domain = account.domain, - let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) { + let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) { viewModel.account = updatedAccount viewModel.relationship = relationship - self.profileHeaderViewController.viewModel.relationship = relationship - self.profileHeaderViewController.profileHeaderView.viewModel.relationship = relationship + self.profileHeaderViewController?.viewModel.relationship = relationship + self.profileHeaderViewController?.profileHeaderView.viewModel.relationship = relationship } viewModel.isUpdating = false @@ -1046,10 +1097,10 @@ extension ProfileViewController { } else if viewModel.account == viewModel.me { // update my profile Task { - if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: viewModel.authContext.mastodonAuthenticationBox).value { viewModel.me = updatedMe viewModel.account = updatedMe - FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + FileManager.default.store(account: updatedMe, forUserID: viewModel.authContext.mastodonAuthenticationBox.authentication.userIdentifier()) } viewModel.isUpdating = false diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 4db099b75..2cfad1a6c 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -108,6 +108,16 @@ extension MainTabBarController { return selectedViewController } + override var selectedViewController: UIViewController? { + willSet { + if let profileView = (newValue as? UINavigationController)?.topViewController as? ProfileViewController{ + guard let authContext = authContext, + let account = authContext.mastodonAuthenticationBox.authentication.account() else { return } + profileView.viewModel = ProfileViewModel(context: self.context, authContext: authContext, account: account, relationship: nil, me: account) + } + } + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift index cf72c53af..2da26738f 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift @@ -17,7 +17,7 @@ public extension FileManager { } func accounts(for userId: UserIdentifier) -> [Mastodon.Entity.Account] { - guard let sharedDirectory else { return [] } + guard let sharedDirectory else { assert(false); return [] } let accountPath = Persistence.accounts(userId).filepath(baseURL: sharedDirectory) @@ -28,6 +28,7 @@ public extension FileManager { do { let accounts = try jsonDecoder.decode([Mastodon.Entity.Account].self, from: data) + assert(accounts.count > 0) return accounts } catch { return []