forked from zelo72/mastodon-ios
chore: update UI/UX of suggestion account
This commit is contained in:
parent
64b4247706
commit
e664722b13
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue