chore: update UI/UX of suggestion account

This commit is contained in:
sunxiaojian 2021-04-22 19:58:42 +08:00
parent 64b4247706
commit e664722b13
11 changed files with 370 additions and 82 deletions

View File

@ -74,6 +74,9 @@
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */; };
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */; };
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */; };
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; };
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; };
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; };
@ -489,6 +492,9 @@
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountCollectionViewCell.swift; sourceTree = "<group>"; };
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountSection.swift; sourceTree = "<group>"; };
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedAccountItem.swift; sourceTree = "<group>"; };
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = "<group>"; };
2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = "<group>"; };
2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = "<group>"; };
@ -1020,6 +1026,14 @@
path = Button;
sourceTree = "<group>";
};
2D4AD89A2631659400613EFC /* CollectionViewCell */ = {
isa = PBXGroup;
children = (
2D4AD89B263165B500613EFC /* SuggestionAccountCollectionViewCell.swift */,
);
path = CollectionViewCell;
sourceTree = "<group>";
};
2D59819925E4A55C000FB903 /* ConfirmEmail */ = {
isa = PBXGroup;
children = (
@ -1116,6 +1130,7 @@
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
2D35237926256D920031AF25 /* NotificationSection.swift */,
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
@ -1170,6 +1185,7 @@
children = (
2D7631B225C159F700929FB9 /* Item.swift */,
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
2D7867182625B77500211898 /* NotificationItem.swift */,
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
DB1E347725F519300079D7DF /* PickServerItem.swift */,
@ -1193,6 +1209,7 @@
children = (
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */,
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */,
2D4AD89A2631659400613EFC /* CollectionViewCell */,
2DAC9E43262FC9DE0062E1A6 /* TableViewCell */,
);
path = SuggestionAccount;
@ -2427,6 +2444,7 @@
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
@ -2554,6 +2572,7 @@
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
@ -2618,6 +2637,7 @@
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */,
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,

View File

@ -0,0 +1,38 @@
//
// SelectedAccountItem.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/22.
//
import Foundation
import CoreData
enum SelectedAccountItem {
case accountObjectID(accountObjectID: NSManagedObjectID)
case placeHolder(uuid: UUID)
}
extension SelectedAccountItem: Equatable {
static func == (lhs: SelectedAccountItem, rhs: SelectedAccountItem) -> Bool {
switch (lhs, rhs) {
case (.accountObjectID(let idLeft), .accountObjectID(let idRight)):
return idLeft == idRight
case (.placeHolder(let uuidLeft), .placeHolder(let uuidRight)):
return uuidLeft == uuidRight
default:
return false
}
}
}
extension SelectedAccountItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .accountObjectID(let id):
hasher.combine(id)
case .placeHolder(let id):
hasher.combine(id.uuidString)
}
}
}

View File

@ -0,0 +1,35 @@
//
// SelectedAccountSection.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/22.
//
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
enum SelectedAccountSection: Equatable, Hashable {
case main
}
extension SelectedAccountSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
managedObjectContext: NSManagedObjectContext
) -> UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self), for: indexPath) as! SuggestionAccountCollectionViewCell
switch item {
case .accountObjectID(let objectID):
let user = managedObjectContext.object(with: objectID) as! MastodonUser
cell.config(with: user)
case .placeHolder( _):
cell.configAsPlaceHolder()
}
return cell
}
}
}

View File

@ -69,6 +69,7 @@ internal enum Asset {
internal static let highlight = ColorAsset(name: "Colors/Label/highlight")
internal static let primary = ColorAsset(name: "Colors/Label/primary")
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
internal static let tertiary = ColorAsset(name: "Colors/Label/tertiary")
}
internal enum Notification {
internal static let favourite = ColorAsset(name: "Colors/Notification/favourite")

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.300",
"blue" : "67",
"green" : "60",
"red" : "60"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -125,7 +125,6 @@ final class HomeTimelineViewModel: NSObject {
.store(in: &disposeBag)
homeTimelineNeedRefresh
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self)
}

View File

@ -9,7 +9,7 @@ import UIKit
final class ProfileRelationshipActionButton: RoundedEdgesButton {
let actvityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.color = .white
return activityIndicatorView
@ -31,15 +31,15 @@ extension ProfileRelationshipActionButton {
private func _init() {
titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
actvityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
addSubview(actvityIndicatorView)
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
addSubview(activityIndicatorView)
NSLayoutConstraint.activate([
actvityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
actvityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
actvityIndicatorView.hidesWhenStopped = true
actvityIndicatorView.stopAnimating()
activityIndicatorView.hidesWhenStopped = true
activityIndicatorView.stopAnimating()
}
}
@ -52,13 +52,13 @@ extension ProfileRelationshipActionButton {
setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
actvityIndicatorView.stopAnimating()
activityIndicatorView.stopAnimating()
if let option = actionOptionSet.highPriorityAction(except: .editOptions), option == .blocked || option == .suspended {
isEnabled = false
} else if actionOptionSet.contains(.updating) {
isEnabled = false
actvityIndicatorView.startAnimating()
activityIndicatorView.startAnimating()
} else {
isEnabled = true
}

View File

@ -0,0 +1,60 @@
//
// SuggestionAccountCollectionViewCell.swift
// Mastodon
//
// Created by sxiaojian on 2021/4/22.
//
import Foundation
import UIKit
import CoreDataStack
class SuggestionAccountCollectionViewCell: UICollectionViewCell {
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.tintColor = Asset.Colors.Label.tertiary.color
imageView.layer.cornerRadius = 4
imageView.clipsToBounds = true
imageView.image = UIImage.placeholder(color: .systemFill)
return imageView
}()
func configAsPlaceHolder() {
imageView.tintColor = Asset.Colors.Label.tertiary.color
imageView.image = UIImage.placeholder(color: .systemFill)
}
func config(with mastodonUser: MastodonUser) {
imageView.af.setImage(
withURL: URL(string: mastodonUser.avatar)!,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
override func prepareForReuse() {
super.prepareForReuse()
}
override init(frame: CGRect) {
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
}
extension SuggestionAccountCollectionViewCell {
private func configure() {
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
}

View File

@ -46,13 +46,16 @@ class SuggestionAccountViewController: UIViewController, NeedsDependency {
return label
}()
let avatarStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.alignment = .center
stackView.spacing = 15
return stackView
let selectedCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
view.register(SuggestionAccountCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SuggestionAccountCollectionViewCell.self))
view.backgroundColor = .clear
view.showsHorizontalScrollIndicator = false
view.showsVerticalScrollIndicator = false
view.layer.masksToBounds = false
return view
}()
deinit {
@ -70,6 +73,7 @@ extension SuggestionAccountViewController {
target: self,
action: #selector(SuggestionAccountViewController.doneButtonDidClick(_:)))
tableView.delegate = self
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
@ -85,6 +89,8 @@ extension SuggestionAccountViewController {
delegate: self
)
viewModel.collectionDiffableDataSource = SelectedAccountSection.collectionViewDiffableDataSource(for: selectedCollectionView, managedObjectContext: context.managedObjectContext)
viewModel.accounts
.receive(on: DispatchQueue.main)
.sink { [weak self] accounts in
@ -94,6 +100,17 @@ extension SuggestionAccountViewController {
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let avatarImageViewHeight: Double = 56
let avatarImageViewCount = Int(floor((Double(view.frame.width) - 20) / (avatarImageViewHeight + 15)))
viewModel.headerPlaceholderCount = avatarImageViewCount
viewModel.applySelectedCollectionViewDataSource(accounts: [])
}
func setupHeader(accounts: [NSManagedObjectID]) {
if accounts.isEmpty {
return
@ -106,56 +123,89 @@ extension SuggestionAccountViewController {
tableHeader.trailingAnchor.constraint(equalTo: followExplainLabel.trailingAnchor, constant: 20),
])
avatarStackView.translatesAutoresizingMaskIntoConstraints = false
tableHeader.addSubview(avatarStackView)
selectedCollectionView.translatesAutoresizingMaskIntoConstraints = false
tableHeader.addSubview(selectedCollectionView)
NSLayoutConstraint.activate([
avatarStackView.topAnchor.constraint(equalTo: followExplainLabel.topAnchor, constant: 20),
avatarStackView.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
avatarStackView.trailingAnchor.constraint(equalTo: tableHeader.trailingAnchor),
avatarStackView.bottomAnchor.constraint(equalTo: tableHeader.bottomAnchor),
selectedCollectionView.frameLayoutGuide.topAnchor.constraint(equalTo: followExplainLabel.topAnchor, constant: 20),
selectedCollectionView.frameLayoutGuide.leadingAnchor.constraint(equalTo: tableHeader.leadingAnchor, constant: 20),
selectedCollectionView.frameLayoutGuide.trailingAnchor.constraint(equalTo: tableHeader.trailingAnchor),
selectedCollectionView.frameLayoutGuide.bottomAnchor.constraint(equalTo: tableHeader.bottomAnchor),
])
let avatarImageViewHeight: Double = 56
let avatarImageViewCount = Int(floor((Double(tableView.frame.width) - 20) / (avatarImageViewHeight + 15)))
let count = min(avatarImageViewCount, accounts.count)
for i in 0 ..< count {
let account = context.managedObjectContext.object(with: accounts[i]) as! MastodonUser
let imageView = UIImageView()
imageView.layer.cornerRadius = 6
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: CGFloat(avatarImageViewHeight)),
imageView.heightAnchor.constraint(equalToConstant: CGFloat(avatarImageViewHeight)),
])
if let url = account.avatarImageURL() {
imageView.af.setImage(
withURL: url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
avatarStackView.addArrangedSubview(imageView)
}
selectedCollectionView.delegate = self
tableView.tableHeaderView = tableHeader
}
}
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
func accountButtonPressed(objectID: NSManagedObjectID, sender: UIButton) {
let selected = !sender.isSelected
sender.isSelected = !sender.isSelected
if selected {
viewModel.selectedAccounts.append(objectID)
} else {
viewModel.selectedAccounts.removeAll { $0 == objectID }
extension SuggestionAccountViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 15
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 56, height: 56)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.collectionDiffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .accountObjectID(let accountObjectID):
let mastodonUser = context.managedObjectContext.object(with: accountObjectID) as! MastodonUser
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
default:
break
}
}
}
extension SuggestionAccountViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let objectID = diffableDataSource.itemIdentifier(for: indexPath) else { return }
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: mastodonUser)
DispatchQueue.main.async {
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
}
}
}
extension SuggestionAccountViewController: SuggestionAccountTableViewCellDelegate {
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell) {
let selected = !viewModel.selectedAccounts.contains(objectID)
cell.startAnimating()
viewModel.followAction(objectID: objectID)?
.sink(receiveCompletion: { [weak self] completion in
guard let self = self else { return }
cell.stopAnimating()
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
case .finished:
if selected {
self.viewModel.selectedAccounts.append(objectID)
} else {
self.viewModel.selectedAccounts.removeAll { $0 == objectID }
}
cell.button.isSelected = selected
self.viewModel.selectedAccountsDidChange.send()
}
}, receiveValue: { relationShip in
})
.store(in: &disposeBag)
}
}
extension SuggestionAccountViewController {
@objc func doneButtonDidClick(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
viewModel.followAction()
if viewModel.selectedAccounts.count > 0 {
viewModel.delegate?.homeTimelineNeedRefresh.send()
}
}
}

View File

@ -27,16 +27,20 @@ final class SuggestionAccountViewModel: NSObject {
// output
let accounts = CurrentValueSubject<[NSManagedObjectID], Never>([])
var selectedAccounts = [NSManagedObjectID]()
let selectedAccountsDidChange = PassthroughSubject<Void, Never>()
var headerPlaceholderCount: Int?
var suggestionAccountsFallback = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>? {
didSet(value) {
if !accounts.value.isEmpty {
applyDataSource(accounts: accounts.value)
applyTableViewDataSource(accounts: accounts.value)
}
}
}
var collectionDiffableDataSource: UICollectionViewDiffableDataSource<SelectedAccountSection, SelectedAccountItem>?
init(context: AppContext, accounts: [NSManagedObjectID]? = nil) {
self.context = context
@ -45,7 +49,8 @@ final class SuggestionAccountViewModel: NSObject {
self.accounts
.receive(on: DispatchQueue.main)
.sink { [weak self] accounts in
self?.applyDataSource(accounts: accounts)
self?.applyTableViewDataSource(accounts: accounts)
self?.applySelectedCollectionViewDataSource(accounts: [])
}
.store(in: &disposeBag)
@ -53,6 +58,13 @@ final class SuggestionAccountViewModel: NSObject {
self.accounts.value = accounts
}
selectedAccountsDidChange
.sink { [weak self] _ in
if let selectedAccout = self?.selectedAccounts {
self?.applySelectedCollectionViewDataSource(accounts: selectedAccout)
}
}
.store(in: &disposeBag)
if accounts == nil || (accounts ?? []).isEmpty {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
@ -102,7 +114,7 @@ final class SuggestionAccountViewModel: NSObject {
.store(in: &disposeBag)
}
func applyDataSource(accounts: [NSManagedObjectID]) {
func applyTableViewDataSource(accounts: [NSManagedObjectID]) {
guard let dataSource = diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<RecommendAccountSection, NSManagedObjectID>()
snapshot.appendSections([.main])
@ -110,6 +122,23 @@ final class SuggestionAccountViewModel: NSObject {
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
func applySelectedCollectionViewDataSource(accounts: [NSManagedObjectID]) {
guard let count = headerPlaceholderCount else { return }
guard let dataSource = collectionDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<SelectedAccountSection, SelectedAccountItem>()
snapshot.appendSections([.main])
let placeholderCount = count - accounts.count
let accountItems = accounts.map { SelectedAccountItem.accountObjectID(accountObjectID: $0) }
snapshot.appendItems(accountItems, toSection: .main)
if placeholderCount > 0 {
for _ in 0 ..< placeholderCount {
snapshot.appendItems([SelectedAccountItem.placeHolder(uuid: UUID())], toSection: .main)
}
}
dataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
func receiveAccounts(ids: [String]) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
@ -135,25 +164,14 @@ final class SuggestionAccountViewModel: NSObject {
}
}
func followAction() {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
for objectID in selectedAccounts {
func followAction(objectID: NSManagedObjectID) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>? {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
context.apiService.toggleFollow(
return context.apiService.toggleFollow(
for: mastodonUser,
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
needFeedback: false
)
.sink { completion in
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: follow failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
case .finished:
self.delegate?.homeTimelineNeedRefresh.send()
}
} receiveValue: { _ in
}
.store(in: &disposeBag)
}
}
}

View File

@ -13,7 +13,7 @@ import MastodonSDK
import UIKit
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
func accountButtonPressed(objectID: NSManagedObjectID, sender: UIButton)
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
}
final class SuggestionAccountTableViewCell: UITableViewCell {
@ -43,7 +43,13 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
return label
}()
lazy var button: HighlightDimmableButton = {
let buttonContainer: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
let button: HighlightDimmableButton = {
let button = HighlightDimmableButton(type: .custom)
if let plusImage = UIImage(systemName: "plus.circle", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .regular))?.withRenderingMode(.alwaysTemplate) {
button.setImage(plusImage, for: .normal)
@ -54,6 +60,13 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
return button
}()
let activityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.color = .white
activityIndicatorView.hidesWhenStopped = true
return activityIndicatorView
}()
override func prepareForReuse() {
super.prepareForReuse()
_imageView.af.cancelImageRequest()
@ -112,8 +125,25 @@ extension SuggestionAccountTableViewCell {
containerStackView.addArrangedSubview(textStackView)
textStackView.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
buttonContainer.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(buttonContainer)
NSLayoutConstraint.activate([
buttonContainer.widthAnchor.constraint(equalToConstant: 24).priority(.required - 1),
buttonContainer.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1),
])
buttonContainer.setContentHuggingPriority(.required - 1, for: .horizontal)
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
containerStackView.addArrangedSubview(button)
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
buttonContainer.addSubview(button)
buttonContainer.addSubview(activityIndicatorView)
NSLayoutConstraint.activate([
buttonContainer.centerXAnchor.constraint(equalTo: activityIndicatorView.centerXAnchor),
buttonContainer.centerYAnchor.constraint(equalTo: activityIndicatorView.centerYAnchor),
buttonContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor),
buttonContainer.centerYAnchor.constraint(equalTo: button.centerYAnchor),
])
}
func config(with account: MastodonUser, isSelected: Bool) {
@ -130,7 +160,7 @@ extension SuggestionAccountTableViewCell {
button.publisher(for: .touchUpInside)
.sink { [weak self] _ in
guard let self = self else { return }
self.delegate?.accountButtonPressed(objectID: account.objectID, sender: self.button)
self.delegate?.accountButtonPressed(objectID: account.objectID, cell: self)
}
.store(in: &disposeBag)
button.publisher(for: \.isSelected)
@ -141,6 +171,23 @@ extension SuggestionAccountTableViewCell {
self?.button.tintColor = Asset.Colors.Label.secondary.color
}
}
.store(in: &self.disposeBag)
.store(in: &disposeBag)
activityIndicatorView.publisher(for: \.isHidden)
.receive(on: DispatchQueue.main)
.sink { [weak self] isHidden in
self?.button.isHidden = !isHidden
}
.store(in: &disposeBag)
}
func startAnimating() {
activityIndicatorView.isHidden = false
activityIndicatorView.startAnimating()
}
func stopAnimating() {
activityIndicatorView.stopAnimating()
activityIndicatorView.isHidden = true
}
}