From 66af30da2a3ea4fd28c02fe14d103abb3a2b79de Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 13 Sep 2021 19:14:26 +0800 Subject: [PATCH] feat: [WIP] add account list for multiple account switch --- Mastodon.xcodeproj/project.pbxproj | 20 ++++ .../xcschemes/xcschememanagement.plist | 14 +-- .../xcshareddata/swiftpm/Package.resolved | 4 +- Mastodon/Coordinator/SceneCoordinator.swift | 4 + .../Account/AccountListTableViewCell.swift | 34 +++++++ .../Scene/Account/AccountListViewModel.swift | 80 +++++++++++++++ .../Scene/Account/AccountViewController.swift | 98 +++++++++++++++++++ ...meTimelineViewController+DebugAction.swift | 68 ++++++++----- .../MastodonPickServerViewController.swift | 3 +- Podfile.lock | 2 +- 10 files changed, 292 insertions(+), 35 deletions(-) create mode 100644 Mastodon/Scene/Account/AccountListTableViewCell.swift create mode 100644 Mastodon/Scene/Account/AccountListViewModel.swift create mode 100644 Mastodon/Scene/Account/AccountViewController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c935acb21..fac9174ca 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -415,6 +415,9 @@ DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; }; DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D7C20269824B80054B3DF /* APIService+Filter.swift */; }; DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; + DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */; }; + DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */; }; + DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */; }; DBA088DF26958164003EB4B2 /* UserFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA1DB7F268F84F80052DB59 /* NotificationType.swift */; }; @@ -1184,6 +1187,9 @@ DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; DB9D7C20269824B80054B3DF /* APIService+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Filter.swift"; sourceTree = ""; }; DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = ""; }; + DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; + DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListViewModel.swift; sourceTree = ""; }; + DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListTableViewCell.swift; sourceTree = ""; }; DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchedResultsController.swift; sourceTree = ""; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; DBA1DB7F268F84F80052DB59 /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = ""; }; @@ -2601,6 +2607,7 @@ DB6180E426391A500018D199 /* Transition */, DB8AF54E25C13703002E6C99 /* MainTab */, DB01409B25C40BB600F9F3CF /* Onboarding */, + DB9F58ED26EF435800E7BBE9 /* Account */, 2D38F1D325CD463600561493 /* HomeTimeline */, 2D76316325C14BAC00929FB9 /* PublicTimeline */, 5B24BBD6262DB14800A9381B /* Report */, @@ -2775,6 +2782,16 @@ path = ViewModel; sourceTree = ""; }; + DB9F58ED26EF435800E7BBE9 /* Account */ = { + isa = PBXGroup; + children = ( + DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */, + DB9F58EE26EF491E00E7BBE9 /* AccountListViewModel.swift */, + DB9F58F026EF512300E7BBE9 /* AccountListTableViewCell.swift */, + ); + path = Account; + sourceTree = ""; + }; DBA5E7A6263BD298004598BB /* ContextMenu */ = { isa = PBXGroup; children = ( @@ -3960,6 +3977,7 @@ 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, + DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */, DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */, DB0E91EA26A9675100BD2ACC /* MetaLabel.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, @@ -4017,6 +4035,7 @@ DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */, DB482A45261335BA008AE74C /* UserTimelineViewController+Provider.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, + DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, @@ -4105,6 +4124,7 @@ 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, + DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 9d41c6857..62193f0d7 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,17 +7,17 @@ AppShared.xcscheme_^#shared#^_ orderHint - 60 + 24 CoreDataStack.xcscheme_^#shared#^_ orderHint - 62 + 23 Mastodon - ASDK.xcscheme_^#shared#^_ orderHint - 11 + 2 Mastodon - RTL.xcscheme_^#shared#^_ @@ -27,7 +27,7 @@ Mastodon - Release.xcscheme_^#shared#^_ orderHint - 10 + 1 Mastodon - ar.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 56 + 25 MastodonIntents.xcscheme_^#shared#^_ @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 13 + 3 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 58 + 22 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0dc724ded..5d8511197 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "0959ba76a1d4a98fd11163aa83fd49c25b93bfae", - "version": "0.0.5" + "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", + "version": "0.0.7" } }, { diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 04804dde3..43a51fcda 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -87,6 +87,7 @@ extension SceneCoordinator { case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) #if DEBUG + case accountList case publicTimeline #endif @@ -321,6 +322,9 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController #if DEBUG + case .accountList: + let _viewController = AccountListViewController() + viewController = _viewController case .publicTimeline: let _viewController = PublicTimelineViewController() _viewController.viewModel = PublicTimelineViewModel(context: appContext) diff --git a/Mastodon/Scene/Account/AccountListTableViewCell.swift b/Mastodon/Scene/Account/AccountListTableViewCell.swift new file mode 100644 index 000000000..c98e94ec1 --- /dev/null +++ b/Mastodon/Scene/Account/AccountListTableViewCell.swift @@ -0,0 +1,34 @@ +// +// AccountListTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-13. +// + + +#if DEBUG +import UIKit + +final class AccountListTableViewCell: UITableViewCell { + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension AccountListTableViewCell { + + private func _init() { + + } + +} + +#endif diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift new file mode 100644 index 000000000..3f56134b0 --- /dev/null +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -0,0 +1,80 @@ +// +// AccountListViewModel.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-13. +// + +#if DEBUG +import UIKit +import Combine +import CoreData +import CoreDataStack + +final class AccountListViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + + // output + let authentications = CurrentValueSubject<[Item], Never>([]) + var diffableDataSource: UITableViewDiffableDataSource! + + init(context: AppContext) { + self.context = context + + context.authenticationService.mastodonAuthentications + .map { authentications in + return authentications.map { + Item.authentication(objectID: $0.objectID) + } + } + .assign(to: \.value, on: authentications) + .store(in: &disposeBag) + + authentications + .receive(on: DispatchQueue.main) + .sink { [weak self] authentications in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(authentications, toSection: .main) + + diffableDataSource.apply(snapshot) + } + .store(in: &disposeBag) + } + +} + +extension AccountListViewModel { + enum Section: Hashable { + case main + } + + enum Item: Hashable { + case authentication(objectID: NSManagedObjectID) + } + + func setupDiffableDataSource( + tableView: UITableView, + managedObjectContext: NSManagedObjectContext + ) { + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in + switch item { + case .authentication(let objectID): + let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication + let user = authentication.user + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell + cell.textLabel?.text = user.acctWithDomain + return cell + } + } + } +} + +#endif diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift new file mode 100644 index 000000000..65c41620d --- /dev/null +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -0,0 +1,98 @@ +// +// AccountViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-13. +// + +#if DEBUG + +import os.log +import UIKit +import Combine +import CoreDataStack + +final class AccountListViewController: UIViewController, NeedsDependency { + + let logger = Logger(subsystem: "AccountListViewController", category: "UI") + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + private(set) lazy var viewModel = AccountListViewModel(context: context) + + private(set) lazy var addBarButtonItem: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem( + image: UIImage(systemName: "plus"), + style: .plain, + target: self, + action: #selector(AccountListViewController.addBarButtonItem(_:)) + ) + return barButtonItem + }() + + private(set) lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.register(AccountListTableViewCell.self, forCellReuseIdentifier: String(describing: AccountListTableViewCell.self)) + return tableView + }() + +} + +extension AccountListViewController { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + navigationItem.rightBarButtonItem = addBarButtonItem + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + tableView: tableView, + managedObjectContext: context.managedObjectContext + ) + } +} + +extension AccountListViewController { + + @objc private func addBarButtonItem(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + } + +} + +// MARK: - UITableViewDelegate +extension AccountListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard case let .authentication(objectID) = diffableDataSource.itemIdentifier(for: indexPath) else { return } + assert(Thread.isMainThread) + + let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication + context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + self.coordinator.setup() + } + .store(in: &disposeBag) + + } +} + + +#endif diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index a55f1ebf6..4d9bb470f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -23,20 +23,9 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ - UIAction(title: "Show FLEX", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.showFLEXAction(action) - }), + showMenu, moveMenu, dropMenu, - UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showWelcomeAction(action) - }, - UIAction(title: "Show Confirm Email", image: UIImage(systemName: "envelope"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showConfirmEmail(action) - }, UIAction(title: "Toggle EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in guard let self = self else { return } if self.emptyView.superview != nil { @@ -45,18 +34,6 @@ extension HomeTimelineViewController { self.showEmptyView() } }, - UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showPublicTimelineAction(action) - }, - UIAction(title: "Show Profile", image: UIImage(systemName: "person.crop.circle"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showProfileAction(action) - }, - UIAction(title: "Show Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showThreadAction(action) - }, UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in guard let self = self else { return } self.showSettings(action) @@ -69,6 +46,45 @@ extension HomeTimelineViewController { ) return menu } + + var showMenu: UIMenu { + return UIMenu( + title: "Show…", + image: UIImage(systemName: "plus.rectangle.on.rectangle"), + identifier: nil, + options: [], + children: [ + UIAction(title: "FLEX", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.showFLEXAction(action) + }), + UIAction(title: "Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showWelcomeAction(action) + }, + UIAction(title: "Confirm Email", image: UIImage(systemName: "envelope"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showConfirmEmail(action) + }, + UIAction(title: "Account List", image: UIImage(systemName: "person"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showAccountList(action) + }, + UIAction(title: "Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showPublicTimelineAction(action) + }, + UIAction(title: "Profile", image: UIImage(systemName: "person.crop.circle"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showProfileAction(action) + }, + UIAction(title: "Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showThreadAction(action) + }, + ] + ) + } var moveMenu: UIMenu { return UIMenu( @@ -321,6 +337,10 @@ extension HomeTimelineViewController { let mastodonConfirmEmailViewModel = MastodonConfirmEmailViewModel() coordinator.present(scene: .mastodonConfirmEmail(viewModel: mastodonConfirmEmailViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } + + @objc private func showAccountList(_ sender: UIAction) { + coordinator.present(scene: .accountList, from: self, transition: .modal(animated: true, completion: nil)) + } @objc private func showPublicTimelineAction(_ sender: UIAction) { coordinator.present(scene: .publicTimeline, from: self, transition: .show) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 70c1067f9..604e23825 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -222,7 +222,8 @@ extension MastodonPickServerViewController { assertionFailure(error.localizedDescription) case .success(let isActived): assert(isActived) - self.dismiss(animated: true, completion: nil) + // self.dismiss(animated: true, completion: nil) + self.coordinator.setup() } } .store(in: &disposeBag) diff --git a/Podfile.lock b/Podfile.lock index b2c540abf..7c64e59b9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -80,4 +80,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 4db0bdf969729c5758bd923e33d9e097cb892086 -COCOAPODS: 1.10.2 +COCOAPODS: 1.11.0