Merge pull request #1137 from mastodon/remove_coredata/followers

IOS-174: Don't persist followers
This commit is contained in:
Nathan Mattes 2023-11-10 10:32:12 +01:00 committed by GitHub
commit 093be8bbc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 218 additions and 156 deletions

View File

@ -317,7 +317,6 @@
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; }; DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; }; DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; };
DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; }; DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; };
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */; };
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; }; DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; };
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; };
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; };
@ -1021,7 +1020,6 @@
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; }; DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; }; DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = "<group>"; }; DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = "<group>"; };
DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = "<group>"; }; DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = "<group>"; };
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; }; DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
@ -2494,7 +2492,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */, DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */,
DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */,
DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */, DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */,
DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */, DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */,
DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */, DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */,
@ -3738,7 +3735,6 @@
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */, 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */,
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */,
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */, DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,

View File

@ -222,43 +222,35 @@ extension SceneCoordinator {
func setup() { func setup() {
let rootViewController: UIViewController let rootViewController: UIViewController
do {
let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
self.authContext = _authContext
switch UIDevice.current.userInterfaceIdiom {
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
}
sceneDelegate.window?.rootViewController = rootViewController // base: main
self.rootViewController = rootViewController
if _authContext == nil { // entry #1: welcome let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
DispatchQueue.main.async { let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
_ = self.present( self.authContext = _authContext
scene: .welcome,
from: self.sceneDelegate.window?.rootViewController, switch UIDevice.current.userInterfaceIdiom {
transition: .modal(animated: true, completion: nil) case .phone:
) let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
} self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
}
sceneDelegate.window?.rootViewController = rootViewController // base: main
self.rootViewController = rootViewController
if _authContext == nil { // entry #1: welcome
DispatchQueue.main.async {
_ = self.present(
scene: .welcome,
from: self.sceneDelegate.window?.rootViewController,
transition: .modal(animated: true, completion: nil)
)
} }
} catch {
assertionFailure(error.localizedDescription)
Task {
try? await Task.sleep(nanoseconds: .second * 2)
setup() // entry #2: retry
} // end Task
} }
} }
@ -464,9 +456,8 @@ private extension SceneCoordinator {
_viewController.viewModel = viewModel _viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .follower(let viewModel): case .follower(let viewModel):
let _viewController = FollowerListViewController() let followerListViewController = FollowerListViewController(viewModel: viewModel, coordinator: self, context: appContext)
_viewController.viewModel = viewModel viewController = followerListViewController
viewController = _viewController
case .following(let viewModel): case .following(let viewModel):
let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext) let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext)
viewController = followingListViewController viewController = followingListViewController

View File

@ -49,11 +49,12 @@ private extension DataSourceFacade {
authenticationBox: provider.authContext.mastodonAuthenticationBox authenticationBox: provider.authContext.mastodonAuthenticationBox
).value ).value
guard let content = value.content else { if value.content != nil {
return value
} else {
return nil return nil
} }
return value
} catch { } catch {
throw TranslationFailure.emptyOrInvalidResponse throw TranslationFailure.emptyOrInvalidResponse
} }

View File

@ -150,7 +150,7 @@ extension MastodonRegisterViewController {
return "en" return "en"
} }
let fallbackLanguageCode: String = { let fallbackLanguageCode: String = {
let code = Locale.current.languageCode ?? "en" let code = Locale.current.language.languageCode?.identifier ?? "en"
guard localCode[code] != nil else { return "en" } guard localCode[code] != nil else { return "en" }
return code return code
}() }()
@ -161,7 +161,7 @@ extension MastodonRegisterViewController {
} }
// prepare languageCode and validate then return fallback if needs // prepare languageCode and validate then return fallback if needs
let local = Locale(identifier: identifier) let local = Locale(identifier: identifier)
guard let languageCode = local.languageCode, guard let languageCode = local.language.languageCode?.identifier,
localCode[languageCode] != nil localCode[languageCode] != nil
else { else {
return fallbackLanguageCode return fallbackLanguageCode
@ -170,10 +170,10 @@ extension MastodonRegisterViewController {
let extendCodes: [String] = { let extendCodes: [String] = {
let locales = Locale.preferredLanguages.map { Locale(identifier: $0) } let locales = Locale.preferredLanguages.map { Locale(identifier: $0) }
return locales.compactMap { locale in return locales.compactMap { locale in
guard let languageCode = locale.languageCode, guard let languageCode = locale.language.languageCode?.identifier,
let regionCode = locale.regionCode let regionIdentifier = locale.region?.identifier
else { return nil } else { return nil }
return languageCode + "-" + regionCode return languageCode + "-" + regionIdentifier
} }
}() }()
let _firstMatchExtendCode = extendCodes.first { code in let _firstMatchExtendCode = extendCodes.first { code in

View File

@ -1,34 +0,0 @@
//
// FollowerListViewController+DataSourceProvider.swift
// Mastodon
//
// Created by MainasuK on 2022-1-20.
//
import UIKit
extension FollowerListViewController: DataSourceProvider {
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
var _indexPath = source.indexPath
if _indexPath == nil, let cell = source.tableViewCell {
_indexPath = await self.indexPath(for: cell)
}
guard let indexPath = _indexPath else { return nil }
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
return nil
}
switch item {
case .user(let record):
return .user(record: record)
default:
return nil
}
}
@MainActor
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
return tableView.indexPath(for: cell)
}
}

View File

@ -11,28 +11,53 @@ import Combine
import MastodonCore import MastodonCore
import MastodonUI import MastodonUI
import MastodonLocalization import MastodonLocalization
import CoreDataStack
final class FollowerListViewController: UIViewController, NeedsDependency { final class FollowerListViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var context: AppContext!
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator!
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var viewModel: FollowerListViewModel! var viewModel: FollowerListViewModel
lazy var tableView: UITableView = { let tableView: UITableView
let tableView = UITableView() let refreshControl: UIRefreshControl
init(viewModel: FollowerListViewModel, coordinator: SceneCoordinator, context: AppContext) {
self.context = context
self.coordinator = coordinator
self.viewModel = viewModel
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self)) tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self))
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
return tableView
}() refreshControl = UIRefreshControl()
tableView.refreshControl = refreshControl
super.init(nibName: nil, bundle: nil)
title = L10n.Scene.Follower.title
view.backgroundColor = .secondarySystemBackground
view.addSubview(tableView)
tableView.pinToParent()
tableView.delegate = self
tableView.refreshControl?.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged)
viewModel.tableView = tableView
refreshControl.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
} }
extension FollowerListViewController { extension FollowerListViewController {
@ -40,23 +65,13 @@ extension FollowerListViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = L10n.Scene.Follower.title
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.pinToParent()
tableView.delegate = self
viewModel.setupDiffableDataSource( viewModel.setupDiffableDataSource(
tableView: tableView, tableView: tableView,
userTableViewCellDelegate: self userTableViewCellDelegate: self
) )
// setup batch fetch // setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView) viewModel.shouldFetch
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
@ -75,6 +90,8 @@ extension FollowerListViewController {
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
tableView.refreshControl = UIRefreshControl()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -82,7 +99,13 @@ extension FollowerListViewController {
tableView.deselectRow(with: transitionCoordinator, animated: animated) tableView.deselectRow(with: transitionCoordinator, animated: animated)
} }
//MARK: - Actions
@objc
func refresh(_ sender: UIRefreshControl) {
viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
}
} }
// MARK: - AuthContextProvider // MARK: - AuthContextProvider
@ -107,3 +130,52 @@ extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableView
// MARK: - UserTableViewCellDelegate // MARK: - UserTableViewCellDelegate
extension FollowerListViewController: UserTableViewCellDelegate {} extension FollowerListViewController: UserTableViewCellDelegate {}
// MARK: - DataSourceProvider
extension FollowerListViewController: DataSourceProvider {
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
var _indexPath = source.indexPath
if _indexPath == nil, let cell = source.tableViewCell {
_indexPath = await self.indexPath(for: cell)
}
guard let indexPath = _indexPath else { return nil }
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
return nil
}
switch item {
case .account(let account, let relationship):
return .account(account: account, relationship: relationship)
default:
return nil
}
}
@MainActor
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
return tableView.indexPath(for: cell)
}
}
//MARK: - UIScrollViewDelegate
extension FollowerListViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isDragging || scrollView.isTracking { return }
let frame = scrollView.frame
let contentOffset = scrollView.contentOffset
let contentSize = scrollView.contentSize
let visibleBottomY = contentOffset.y + frame.height
let offset = 2 * frame.height
let fetchThrottleOffsetY = contentSize.height - offset
if visibleBottomY > fetchThrottleOffsetY {
viewModel.shouldFetch.send()
}
}
}

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import MastodonSDK
extension FollowerListViewModel { extension FollowerListViewModel {
func setupDiffableDataSource( func setupDiffableDataSource(
@ -27,34 +28,43 @@ extension FollowerListViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems([.bottomLoader], toSection: .main) snapshot.appendItems([.bottomLoader], toSection: .main)
diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil) diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
userFetchedResultsController.$records $accounts
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] records in .sink { [weak self] accounts in
guard let self = self else { return } guard let self else { return }
guard let diffableDataSource = self.diffableDataSource else { return } guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>() var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
let items = records.map { UserItem.user(record: $0) }
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
return (account: account, relationship: relationship)
}
let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) }
snapshot.appendItems(items, toSection: .main) snapshot.appendItems(items, toSection: .main)
if let currentState = self.stateMachine.currentState { if let currentState = self.stateMachine.currentState {
switch currentState { switch currentState {
case is State.Idle, is State.Loading, is State.Fail: case is State.Loading:
snapshot.appendItems([.bottomLoader], toSection: .main) snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore: case is State.NoMore:
guard let userID = self.userID, guard let userID = self.userID,
userID != self.authContext.mastodonAuthenticationBox.userID userID != self.authContext.mastodonAuthenticationBox.userID
else { break } else { break }
// display hint footer exclude self // display footer exclude self
let text = L10n.Scene.Follower.footer let text = L10n.Scene.Following.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default: case is State.Idle, is State.Fail:
break break
default:
break
} }
} }
diffableDataSource.apply(snapshot, animatingDifferences: false) diffableDataSource.apply(snapshot, animatingDifferences: false)
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -9,7 +9,6 @@ import Foundation
import GameplayKit import GameplayKit
import MastodonSDK import MastodonSDK
import MastodonCore import MastodonCore
import CoreDataStack
extension FollowerListViewModel { extension FollowerListViewModel {
class State: GKState { class State: GKState {
@ -61,8 +60,9 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
// reset // reset
viewModel.userFetchedResultsController.userIDs = [] viewModel.accounts = []
viewModel.relationships = []
stateMachine.enter(Loading.self) stateMachine.enter(Loading.self)
} }
} }
@ -97,6 +97,12 @@ extension FollowerListViewModel.State {
return false return false
} }
} }
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
viewModel?.tableView?.refreshControl?.endRefreshing()
}
} }
class Loading: FollowerListViewModel.State { class Loading: FollowerListViewModel.State {
@ -123,47 +129,66 @@ extension FollowerListViewModel.State {
maxID = nil maxID = nil
} }
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let viewModel, let stateMachine else { return }
guard let userID = viewModel.userID, !userID.isEmpty else { guard let userID = viewModel.userID, userID.isEmpty == false else {
stateMachine.enter(Fail.self) stateMachine.enter(Fail.self)
return return
} }
Task { Task {
do { do {
let response = try await viewModel.context.apiService.followers( let accountResponse = try await viewModel.context.apiService.followers(
userID: userID, userID: userID,
maxID: maxID, maxID: maxID,
authenticationBox: viewModel.authContext.mastodonAuthenticationBox authenticationBox: viewModel.authContext.mastodonAuthenticationBox
) )
if accountResponse.value.isEmpty {
await enter(state: NoMore.self)
viewModel.accounts = []
viewModel.relationships = []
return
}
var hasNewAppend = false var hasNewAppend = false
var userIDs = viewModel.userFetchedResultsController.userIDs
for user in response.value { let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
guard !userIDs.contains(user.id) else { continue }
userIDs.append(user.id) var accounts = viewModel.accounts
for user in accountResponse.value {
guard accounts.contains(user) == false else { continue }
accounts.append(user)
hasNewAppend = true hasNewAppend = true
} }
let maxID = response.link?.maxID var relationships = viewModel.relationships
if hasNewAppend && maxID != nil { for relationship in newRelationships.value {
guard relationships.contains(relationship) == false else { continue }
relationships.append(relationship)
}
let maxID = accountResponse.link?.maxID
if hasNewAppend, maxID != nil {
await enter(state: Idle.self) await enter(state: Idle.self)
} else { } else {
await enter(state: NoMore.self) await enter(state: NoMore.self)
} }
viewModel.accounts = accounts
viewModel.relationships = relationships
self.maxID = maxID self.maxID = maxID
viewModel.userFetchedResultsController.userIDs = userIDs
} catch { } catch {
await enter(state: Fail.self) await enter(state: Fail.self)
} }
} // end Task }
} // end func didEnter }
} }
class NoMore: FollowerListViewModel.State { class NoMore: FollowerListViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool { override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass { switch stateClass {
@ -176,6 +201,8 @@ extension FollowerListViewModel.State {
override func didEnter(from previousState: GKState?) { override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState) super.didEnter(from: previousState)
viewModel?.tableView?.refreshControl?.endRefreshing()
} }
} }
} }

View File

@ -19,12 +19,16 @@ final class FollowerListViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController @Published var accounts: [Mastodon.Entity.Account]
let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var relationships: [Mastodon.Entity.Relationship]
@Published var domain: String? @Published var domain: String?
@Published var userID: String? @Published var userID: String?
let shouldFetch = PassthroughSubject<Void, Never>()
var tableView: UITableView?
// output // output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>? var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
private(set) lazy var stateMachine: GKStateMachine = { private(set) lazy var stateMachine: GKStateMachine = {
@ -48,13 +52,9 @@ final class FollowerListViewModel {
) { ) {
self.context = context self.context = context
self.authContext = authContext self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalPredicate: nil
)
self.domain = domain self.domain = domain
self.userID = userID self.userID = userID
// end init self.accounts = []
self.relationships = []
} }
} }

View File

@ -173,7 +173,6 @@ extension FollowingListViewController: UIScrollViewDelegate {
if visibleBottomY > fetchThrottleOffsetY { if visibleBottomY > fetchThrottleOffsetY {
viewModel.shouldFetch.send() viewModel.shouldFetch.send()
} }
} }
} }