Re-write identity management in UIKit
This commit is contained in:
parent
1151fc7563
commit
1c295a1f5a
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
enum IdentitiesSection: Hashable {
|
||||||
|
case add
|
||||||
|
case identities(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentitiesItem: Hashable {
|
||||||
|
case add
|
||||||
|
case identitiy(Identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class IdentitiesDataSource: UITableViewDiffableDataSource<IdentitiesSection, IdentitiesItem> {
|
||||||
|
private let updateQueue =
|
||||||
|
DispatchQueue(label: "com.metabolist.metatext.identities-data-source.update-queue")
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let deleteAction: (Identity) -> Void
|
||||||
|
|
||||||
|
init(tableView: UITableView,
|
||||||
|
publisher: AnyPublisher<[Identity], Never>,
|
||||||
|
viewModelProvider: @escaping (Identity) -> IdentityViewModel,
|
||||||
|
deleteAction: @escaping (Identity) -> Void) {
|
||||||
|
self.deleteAction = deleteAction
|
||||||
|
|
||||||
|
tableView.register(UITableViewCell.self,
|
||||||
|
forCellReuseIdentifier: String(describing: UITableViewCell.self))
|
||||||
|
tableView.register(IdentityTableViewCell.self,
|
||||||
|
forCellReuseIdentifier: String(describing: IdentityTableViewCell.self))
|
||||||
|
|
||||||
|
super.init(tableView: tableView) { tableView, indexPath, item in
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: item.cellReuseIdentifier,
|
||||||
|
for: indexPath)
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .add:
|
||||||
|
var configuration = cell.defaultContentConfiguration()
|
||||||
|
|
||||||
|
configuration.text = NSLocalizedString("add", comment: "")
|
||||||
|
configuration.image = UIImage(systemName: "plus.circle.fill")
|
||||||
|
cell.contentConfiguration = configuration
|
||||||
|
case let .identitiy(identity):
|
||||||
|
let viewModel = viewModelProvider(identity)
|
||||||
|
|
||||||
|
(cell as? IdentityTableViewCell)?.viewModel = viewModel
|
||||||
|
cell.accessoryType = identity.id == viewModel.identityContext.identity.id ? .checkmark : .none
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
publisher
|
||||||
|
.sink { [weak self] in self?.update(identities: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<IdentitiesSection, IdentitiesItem>,
|
||||||
|
animatingDifferences: Bool = true,
|
||||||
|
completion: (() -> Void)? = nil) {
|
||||||
|
updateQueue.async {
|
||||||
|
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
|
let currentSnapshot = snapshot()
|
||||||
|
let section = currentSnapshot.sectionIdentifiers[section]
|
||||||
|
|
||||||
|
if currentSnapshot.numberOfItems(inSection: section) > 0, case let .identities(title) = section {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
itemIdentifier(for: indexPath) != .add
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView,
|
||||||
|
commit editingStyle: UITableViewCell.EditingStyle,
|
||||||
|
forRowAt indexPath: IndexPath) {
|
||||||
|
guard editingStyle == .delete,
|
||||||
|
case let .identitiy(identity) = itemIdentifier(for: indexPath)
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
deleteAction(identity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentitiesDataSource {
|
||||||
|
private func update(identities: [Identity]) {
|
||||||
|
var newSnapshot = NSDiffableDataSourceSnapshot<IdentitiesSection, IdentitiesItem>()
|
||||||
|
let sections = [
|
||||||
|
(section: IdentitiesSection.identities(NSLocalizedString("identities.accounts", comment: "")),
|
||||||
|
identities: identities.filter { $0.authenticated && !$0.pending }.map(IdentitiesItem.identitiy)),
|
||||||
|
(section: IdentitiesSection.identities(NSLocalizedString("identities.browsing", comment: "")),
|
||||||
|
identities: identities.filter { !$0.authenticated && !$0.pending }.map(IdentitiesItem.identitiy)),
|
||||||
|
(section: IdentitiesSection.identities(NSLocalizedString("identities.pending", comment: "")),
|
||||||
|
identities: identities.filter { $0.pending }.map(IdentitiesItem.identitiy))
|
||||||
|
]
|
||||||
|
.filter { !$0.identities.isEmpty }
|
||||||
|
|
||||||
|
newSnapshot.appendSections([.add] + sections.map(\.section))
|
||||||
|
newSnapshot.appendItems([.add], toSection: .add)
|
||||||
|
|
||||||
|
for section in sections {
|
||||||
|
newSnapshot.appendItems(section.identities, toSection: section.section)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(newSnapshot, animatingDifferences: !snapshot().sectionIdentifiers.filter { $0 != .add }.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentitiesItem {
|
||||||
|
var cellReuseIdentifier: String {
|
||||||
|
switch self {
|
||||||
|
case .add:
|
||||||
|
return String(describing: UITableViewCell.self)
|
||||||
|
case .identitiy:
|
||||||
|
return String(describing: IdentityTableViewCell.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,11 @@
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; };
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; };
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
||||||
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
|
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
|
||||||
|
D021A5F625C34538008A0C0D /* IdentitiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021A5F525C34538008A0C0D /* IdentitiesViewController.swift */; };
|
||||||
|
D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */; };
|
||||||
|
D021A60A25C36B32008A0C0D /* IdentityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */; };
|
||||||
|
D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021A61325C36BFB008A0C0D /* IdentityView.swift */; };
|
||||||
|
D021A61A25C36C1A008A0C0D /* IdentityContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */; };
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
|
||||||
D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */; };
|
D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */; };
|
||||||
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; };
|
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; };
|
||||||
|
@ -213,6 +218,11 @@
|
||||||
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
|
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
|
||||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
|
||||||
D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = "<group>"; };
|
D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = "<group>"; };
|
||||||
|
D021A5F525C34538008A0C0D /* IdentitiesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitiesDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D021A61325C36BFB008A0C0D /* IdentityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityView.swift; sourceTree = "<group>"; };
|
||||||
|
D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||||
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = "<group>"; };
|
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = "<group>"; };
|
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -478,6 +488,7 @@
|
||||||
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||||
);
|
);
|
||||||
path = "Data Sources";
|
path = "Data Sources";
|
||||||
|
@ -522,6 +533,9 @@
|
||||||
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
|
D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */,
|
||||||
|
D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */,
|
||||||
|
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
||||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||||
|
@ -570,6 +584,7 @@
|
||||||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
|
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
|
||||||
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */,
|
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */,
|
||||||
|
D021A5F525C34538008A0C0D /* IdentitiesViewController.swift */,
|
||||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||||
|
@ -851,6 +866,7 @@
|
||||||
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||||
|
D021A5F625C34538008A0C0D /* IdentitiesViewController.swift in Sources */,
|
||||||
D07EC7DC25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */,
|
D07EC7DC25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */,
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
|
@ -881,6 +897,7 @@
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||||
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||||
|
D021A60A25C36B32008A0C0D /* IdentityTableViewCell.swift in Sources */,
|
||||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */,
|
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */,
|
||||||
D0F4362D25C10B9600E4F896 /* AddIdentityViewController.swift in Sources */,
|
D0F4362D25C10B9600E4F896 /* AddIdentityViewController.swift in Sources */,
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
|
||||||
|
@ -889,6 +906,7 @@
|
||||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||||
D07EC7E325B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
D07EC7E325B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||||
|
D021A61A25C36C1A008A0C0D /* IdentityContentConfiguration.swift in Sources */,
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||||
|
@ -905,6 +923,7 @@
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
|
D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */,
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
|
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
|
||||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
||||||
|
@ -914,6 +933,7 @@
|
||||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
|
D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */,
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class IdentitiesViewController: UITableViewController {
|
||||||
|
private let viewModel: IdentitiesViewModel
|
||||||
|
private let rootViewModel: RootViewModel
|
||||||
|
|
||||||
|
private lazy var dataSource: IdentitiesDataSource = {
|
||||||
|
.init(tableView: tableView,
|
||||||
|
publisher: viewModel.$identities.eraseToAnyPublisher(),
|
||||||
|
viewModelProvider: viewModel.viewModel(identity:),
|
||||||
|
deleteAction: { [weak self] in self?.rootViewModel.deleteIdentity(id: $0.id) })
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(viewModel: IdentitiesViewModel, rootViewModel: RootViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.rootViewModel = rootViewModel
|
||||||
|
|
||||||
|
super.init(style: .insetGrouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.dataSource = dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
|
parent?.navigationItem.title = NSLocalizedString("secondary-navigation.accounts", comment: "")
|
||||||
|
parent?.navigationItem.rightBarButtonItem = editButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
if case let .identitiy(identityViewModel) = dataSource.itemIdentifier(for: indexPath) {
|
||||||
|
return identityViewModel.id != viewModel.identityContext.identity.id
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .add:
|
||||||
|
let addIdentityViewModel = rootViewModel.addIdentityViewModel()
|
||||||
|
let addIdentityView = AddIdentityView(viewModelClosure: { addIdentityViewModel }, displayWelcome: false)
|
||||||
|
.environmentObject(rootViewModel)
|
||||||
|
let addIdentityViewController = UIHostingController(rootView: addIdentityView)
|
||||||
|
|
||||||
|
show(addIdentityViewController, sender: self)
|
||||||
|
case let .identitiy(identityViewModel):
|
||||||
|
rootViewModel.identitySelected(id: identityViewModel.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import Foundation
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class IdentitiesViewModel: ObservableObject {
|
public final class IdentitiesViewModel: ObservableObject {
|
||||||
public let currentIdentityId: Identity.Id
|
|
||||||
@Published public private(set) var identities = [Identity]()
|
@Published public private(set) var identities = [Identity]()
|
||||||
@Published public var alertItem: AlertItem?
|
@Published public var alertItem: AlertItem?
|
||||||
public let identityContext: IdentityContext
|
public let identityContext: IdentityContext
|
||||||
|
@ -14,10 +13,16 @@ public final class IdentitiesViewModel: ObservableObject {
|
||||||
|
|
||||||
public init(identityContext: IdentityContext) {
|
public init(identityContext: IdentityContext) {
|
||||||
self.identityContext = identityContext
|
self.identityContext = identityContext
|
||||||
currentIdentityId = identityContext.identity.id
|
|
||||||
|
|
||||||
identityContext.service.identitiesPublisher()
|
identityContext.service.identitiesPublisher()
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
.assign(to: &$identities)
|
.assign(to: &$identities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension IdentitiesViewModel {
|
||||||
|
func viewModel(identity: Identity) -> IdentityViewModel {
|
||||||
|
.init(identity: identity, identityContext: identityContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class IdentityViewModel: ObservableObject {
|
||||||
|
public let identity: Identity
|
||||||
|
public let identityContext: IdentityContext
|
||||||
|
|
||||||
|
init(identity: Identity, identityContext: IdentityContext) {
|
||||||
|
self.identity = identity
|
||||||
|
self.identityContext = identityContext
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,91 +4,15 @@ import Kingfisher
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
struct IdentitiesView: View {
|
struct IdentitiesView: UIViewControllerRepresentable {
|
||||||
@StateObject var viewModel: IdentitiesViewModel
|
let viewModelClosure: () -> IdentitiesViewModel
|
||||||
@EnvironmentObject var rootViewModel: RootViewModel
|
@EnvironmentObject var rootViewModel: RootViewModel
|
||||||
@Environment(\.displayScale) var displayScale: CGFloat
|
|
||||||
|
|
||||||
var body: some View {
|
func makeUIViewController(context: Context) -> IdentitiesViewController {
|
||||||
Form {
|
IdentitiesViewController(viewModel: viewModelClosure(), rootViewModel: rootViewModel)
|
||||||
Section {
|
|
||||||
NavigationLink(
|
|
||||||
destination: AddIdentityView(
|
|
||||||
viewModelClosure: { rootViewModel.addIdentityViewModel() },
|
|
||||||
displayWelcome: false),
|
|
||||||
label: {
|
|
||||||
Label("add", systemImage: "plus.circle")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
section(title: "identities.accounts",
|
|
||||||
identities: viewModel.identities.filter { $0.authenticated && !$0.pending })
|
|
||||||
section(title: "identities.browsing",
|
|
||||||
identities: viewModel.identities.filter { !$0.authenticated && !$0.pending })
|
|
||||||
section(title: "identities.pending",
|
|
||||||
identities: viewModel.identities.filter { $0.pending })
|
|
||||||
}
|
|
||||||
.navigationTitle(Text("secondary-navigation.accounts"))
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
|
|
||||||
EditButton()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: IdentitiesViewController, context: Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentitiesView {
|
|
||||||
@ViewBuilder
|
|
||||||
func section(title: LocalizedStringKey, identities: [Identity]) -> some View {
|
|
||||||
if identities.isEmpty {
|
|
||||||
EmptyView()
|
|
||||||
} else {
|
|
||||||
Section(header: Text(title)) {
|
|
||||||
List {
|
|
||||||
ForEach(identities) { identity in
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
rootViewModel.identitySelected(id: identity.id)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
row(identity: identity)
|
|
||||||
}
|
|
||||||
.disabled(identity.id == viewModel.currentIdentityId)
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
.onDelete {
|
|
||||||
guard let index = $0.first else { return }
|
|
||||||
|
|
||||||
rootViewModel.deleteIdentity(id: identities[index].id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
func row(identity: Identity) -> some View {
|
|
||||||
HStack {
|
|
||||||
Label {
|
|
||||||
Text(verbatim: identity.handle)
|
|
||||||
} icon: {
|
|
||||||
KFImage(identity.image)
|
|
||||||
.downsampled(dimension: .barButtonItemDimension, scaleFactor: displayScale)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if identity.id == viewModel.currentIdentityId {
|
|
||||||
Image(systemName: "checkmark.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import PreviewViewModels
|
|
||||||
|
|
||||||
struct IdentitiesView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
IdentitiesView(viewModel: .init(identityContext: .preview))
|
|
||||||
.environmentObject(RootViewModel.preview)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct IdentityContentConfiguration {
|
||||||
|
let viewModel: IdentityViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IdentityContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
IdentityView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> IdentityContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class IdentityTableViewCell: UITableViewCell {
|
||||||
|
var viewModel: IdentityViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = IdentityContentConfiguration(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class IdentityView: UIView {
|
||||||
|
let imageView = AnimatedImageView()
|
||||||
|
let nameLabel = UILabel()
|
||||||
|
let secondaryLabel = UILabel()
|
||||||
|
|
||||||
|
private var identityConfiguration: IdentityContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: IdentityContentConfiguration) {
|
||||||
|
identityConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyIdentityConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IdentityView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { identityConfiguration }
|
||||||
|
set {
|
||||||
|
guard let identityConfiguration = newValue as? IdentityContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.identityConfiguration = identityConfiguration
|
||||||
|
|
||||||
|
applyIdentityConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension IdentityView {
|
||||||
|
func initialSetup() {
|
||||||
|
addSubview(imageView)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.layer.cornerRadius = .avatarDimension / 2
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.spacing = .compactSpacing
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(nameLabel)
|
||||||
|
nameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
nameLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
nameLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(secondaryLabel)
|
||||||
|
secondaryLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
secondaryLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
secondaryLabel.numberOfLines = 0
|
||||||
|
secondaryLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: .avatarDimension)
|
||||||
|
|
||||||
|
imageViewHeightConstraint.priority = .justBelowMax
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
|
||||||
|
imageViewHeightConstraint,
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
imageView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: .defaultSpacing),
|
||||||
|
stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyIdentityConfiguration() {
|
||||||
|
let viewModel = identityConfiguration.viewModel
|
||||||
|
|
||||||
|
imageView.kf.setImage(with: viewModel.identity.image)
|
||||||
|
imageView.autoPlayAnimatedImage = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
||||||
|
|
||||||
|
if let displayName = viewModel.identity.account?.displayName,
|
||||||
|
!displayName.isEmpty {
|
||||||
|
let mutableName = NSMutableAttributedString(string: displayName)
|
||||||
|
|
||||||
|
if let emojis = viewModel.identity.account?.emojis {
|
||||||
|
mutableName.insert(emojis: emojis, view: nameLabel)
|
||||||
|
mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLabel.attributedText = mutableName
|
||||||
|
} else {
|
||||||
|
nameLabel.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryLabel.text = viewModel.identity.handle
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,6 @@ private extension SecondaryNavigationTitleView {
|
||||||
addSubview(avatarImageView)
|
addSubview(avatarImageView)
|
||||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
avatarImageView.layer.cornerRadius = .barButtonItemDimension / 2
|
avatarImageView.layer.cornerRadius = .barButtonItemDimension / 2
|
||||||
avatarImageView.autoPlayAnimatedImage = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
|
||||||
avatarImageView.contentMode = .scaleAspectFill
|
avatarImageView.contentMode = .scaleAspectFill
|
||||||
avatarImageView.clipsToBounds = true
|
avatarImageView.clipsToBounds = true
|
||||||
|
|
||||||
|
@ -67,6 +66,7 @@ private extension SecondaryNavigationTitleView {
|
||||||
|
|
||||||
func applyViewModel() {
|
func applyViewModel() {
|
||||||
avatarImageView.kf.setImage(with: viewModel.identityContext.identity.image)
|
avatarImageView.kf.setImage(with: viewModel.identityContext.identity.image)
|
||||||
|
avatarImageView.autoPlayAnimatedImage = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
||||||
|
|
||||||
if let displayName = viewModel.identityContext.identity.account?.displayName,
|
if let displayName = viewModel.identityContext.identity.account?.displayName,
|
||||||
!displayName.isEmpty {
|
!displayName.isEmpty {
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct SecondaryNavigationView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
destination: IdentitiesView(viewModel: .init(identityContext: viewModel.identityContext))
|
destination: IdentitiesView { .init(identityContext: viewModel.identityContext) }
|
||||||
.environmentObject(rootViewModel)) {
|
.environmentObject(rootViewModel)) {
|
||||||
Label("secondary-navigation.accounts", systemImage: "rectangle.stack.person.crop")
|
Label("secondary-navigation.accounts", systemImage: "rectangle.stack.person.crop")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue