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 */; };
|
||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.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 */; };
|
||||
D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86825B7F2ED00DC75ED /* MainNavigationViewController.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -478,6 +488,7 @@
|
|||
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||
);
|
||||
path = "Data Sources";
|
||||
|
@ -522,6 +533,9 @@
|
|||
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||
D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */,
|
||||
D021A60925C36B32008A0C0D /* IdentityTableViewCell.swift */,
|
||||
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
||||
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
|
||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
|
||||
|
@ -570,6 +584,7 @@
|
|||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */,
|
||||
D087671525BAA8C0001FDD43 /* ExploreViewController.swift */,
|
||||
D021A5F525C34538008A0C0D /* IdentitiesViewController.swift */,
|
||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||
|
@ -851,6 +866,7 @@
|
|||
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||
D021A5F625C34538008A0C0D /* IdentitiesViewController.swift in Sources */,
|
||||
D07EC7DC25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */,
|
||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||
|
@ -881,6 +897,7 @@
|
|||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||
D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||
D0E9F9AA258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||
D021A60A25C36B32008A0C0D /* IdentityTableViewCell.swift in Sources */,
|
||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */,
|
||||
D0F4362D25C10B9600E4F896 /* AddIdentityViewController.swift in Sources */,
|
||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */,
|
||||
|
@ -889,6 +906,7 @@
|
|||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||
D07EC7E325B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||
D021A61A25C36C1A008A0C0D /* IdentityContentConfiguration.swift in Sources */,
|
||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||
|
@ -905,6 +923,7 @@
|
|||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||
D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */,
|
||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
|
||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
||||
|
@ -914,6 +933,7 @@
|
|||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||
D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */,
|
||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||
D01EF22425182B1F00650C6B /* AccountHeaderView.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
|
||||
|
||||
public final class IdentitiesViewModel: ObservableObject {
|
||||
public let currentIdentityId: Identity.Id
|
||||
@Published public private(set) var identities = [Identity]()
|
||||
@Published public var alertItem: AlertItem?
|
||||
public let identityContext: IdentityContext
|
||||
|
@ -14,10 +13,16 @@ public final class IdentitiesViewModel: ObservableObject {
|
|||
|
||||
public init(identityContext: IdentityContext) {
|
||||
self.identityContext = identityContext
|
||||
currentIdentityId = identityContext.identity.id
|
||||
|
||||
identityContext.service.identitiesPublisher()
|
||||
.receive(on: RunLoop.main)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.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 ViewModels
|
||||
|
||||
struct IdentitiesView: View {
|
||||
@StateObject var viewModel: IdentitiesViewModel
|
||||
struct IdentitiesView: UIViewControllerRepresentable {
|
||||
let viewModelClosure: () -> IdentitiesViewModel
|
||||
@EnvironmentObject var rootViewModel: RootViewModel
|
||||
@Environment(\.displayScale) var displayScale: CGFloat
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
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 makeUIViewController(context: Context) -> IdentitiesViewController {
|
||||
IdentitiesViewController(viewModel: viewModelClosure(), rootViewModel: rootViewModel)
|
||||
}
|
||||
|
||||
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)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarImageView.layer.cornerRadius = .barButtonItemDimension / 2
|
||||
avatarImageView.autoPlayAnimatedImage = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
||||
avatarImageView.contentMode = .scaleAspectFill
|
||||
avatarImageView.clipsToBounds = true
|
||||
|
||||
|
@ -67,6 +66,7 @@ private extension SecondaryNavigationTitleView {
|
|||
|
||||
func applyViewModel() {
|
||||
avatarImageView.kf.setImage(with: viewModel.identityContext.identity.image)
|
||||
avatarImageView.autoPlayAnimatedImage = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
||||
|
||||
if let displayName = viewModel.identityContext.identity.account?.displayName,
|
||||
!displayName.isEmpty {
|
||||
|
|
|
@ -24,7 +24,7 @@ struct SecondaryNavigationView: View {
|
|||
}
|
||||
}
|
||||
NavigationLink(
|
||||
destination: IdentitiesView(viewModel: .init(identityContext: viewModel.identityContext))
|
||||
destination: IdentitiesView { .init(identityContext: viewModel.identityContext) }
|
||||
.environmentObject(rootViewModel)) {
|
||||
Label("secondary-navigation.accounts", systemImage: "rectangle.stack.person.crop")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue