diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f66c14c40..cfe6a3ed0 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -96,6 +96,12 @@ DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; + DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; }; + DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; }; + DB1FD44A25F26CD7004CFCFC /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */; }; + DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; + DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; }; + DB1FD46025F278AF004CFCFC /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; @@ -310,6 +316,12 @@ DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; + DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = ""; }; + DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = ""; }; + DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PickServerItem.swift; path = Mastodon/Diffiable/Section/PickServerItem.swift; sourceTree = SOURCE_ROOT; }; + DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; }; + DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = ""; }; + DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; @@ -456,11 +468,13 @@ 0FAA102525E1125D0017CCDE /* PickServer */ = { isa = PBXGroup; children = ( - 0FB3D31825E525DE00AAD544 /* CollectionViewCell */, 0FB3D30D25E525C000AAD544 /* View */, + 0FB3D31825E525DE00AAD544 /* CollectionViewCell */, 0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */, 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */, 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */, + DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */, + DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */, ); path = PickServer; sourceTree = ""; @@ -642,6 +656,8 @@ isa = PBXGroup; children = ( 2D76319E25C1521200929FB9 /* StatusSection.swift */, + DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, + DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */, ); path = Section; sourceTree = ""; @@ -683,6 +699,8 @@ isa = PBXGroup; children = ( 2D7631B225C159F700929FB9 /* Item.swift */, + DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */, + DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, ); path = Item; sourceTree = ""; @@ -1464,8 +1482,10 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, + DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, + DB1FD44A25F26CD7004CFCFC /* PickServerItem.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, @@ -1511,12 +1531,14 @@ 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, + DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, + DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */, @@ -1533,10 +1555,12 @@ DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, + DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, + DB1FD46025F278AF004CFCFC /* CategoryPickerSection.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 747fe7df0..60ccd3d87 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -22,7 +22,7 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 12 + 8 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/CategoryPickerItem.swift b/Mastodon/Diffiable/Item/CategoryPickerItem.swift new file mode 100644 index 000000000..9a8f8bd6c --- /dev/null +++ b/Mastodon/Diffiable/Item/CategoryPickerItem.swift @@ -0,0 +1,76 @@ +// +// CategoryPickerItem.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import Foundation +import MastodonSDK + +enum CategoryPickerItem { + case all + case category(category: Mastodon.Entity.Category) +} + +extension CategoryPickerItem { + var title: String { + switch self { + case .all: + return L10n.Scene.ServerPicker.Button.Category.all + case .category(let category): + switch category.category { + case .academia: + return "📚" + case .activism: + return "✊" + case .food: + return "🍕" + case .furry: + return "🦁" + case .games: + return "🕹" + case .general: + return "💬" + case .journalism: + return "📰" + case .lgbt: + return "🏳️‍🌈" + case .regional: + return "📍" + case .art: + return "🎨" + case .music: + return "🎼" + case .tech: + return "📱" + case ._other: + return "❓" + } + } + } +} + +extension CategoryPickerItem: Equatable { + static func == (lhs: CategoryPickerItem, rhs: CategoryPickerItem) -> Bool { + switch (lhs, rhs) { + case (.all, .all): + return true + case (.category(let categoryLeft), .category(let categoryRight)): + return categoryLeft.category.rawValue == categoryRight.category.rawValue + default: + return false + } + } +} + +extension CategoryPickerItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .all: + hasher.combine(String(describing: CategoryPickerItem.all.self)) + case .category(let category): + hasher.combine(category.category.rawValue) + } + } +} diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index c6a182b4d..818c33ea8 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -30,7 +30,7 @@ protocol StatusContentWarningAttribute { } extension Item { - class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { + class StatusTimelineAttribute: Equatable, Hashable, StatusContentWarningAttribute { var isStatusTextSensitive: Bool var isStatusSensitive: Bool @@ -51,7 +51,6 @@ extension Item { hasher.combine(isStatusTextSensitive) hasher.combine(isStatusSensitive) } - } } diff --git a/Mastodon/Diffiable/Section/CategoryPickerSection.swift b/Mastodon/Diffiable/Section/CategoryPickerSection.swift new file mode 100644 index 000000000..5582cb531 --- /dev/null +++ b/Mastodon/Diffiable/Section/CategoryPickerSection.swift @@ -0,0 +1,31 @@ +// +// CategoryPickerSection.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import UIKit + +enum CategoryPickerSection: Equatable, Hashable { + case main +} + +extension CategoryPickerSection { + static func collectionViewDiffableDataSource( + for collectionView: UICollectionView, + dependency: NeedsDependency + ) -> UICollectionViewDiffableDataSource { + UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell + switch item { + case .all: + cell.categoryView.titleLabel.font = .systemFont(ofSize: 17) + case .category: + cell.categoryView.titleLabel.font = .systemFont(ofSize: 28) + } + cell.categoryView.titleLabel.text = item.title + return cell + } + } +} diff --git a/Mastodon/Diffiable/Section/PickServerItem.swift b/Mastodon/Diffiable/Section/PickServerItem.swift new file mode 100644 index 000000000..09ca72c32 --- /dev/null +++ b/Mastodon/Diffiable/Section/PickServerItem.swift @@ -0,0 +1,67 @@ +// +// PickServerItem.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import Foundation +import MastodonSDK + +/// Note: update Equatable when change case +enum PickServerItem { + case header + case categoryPicker(items: [CategoryPickerItem]) + case search + case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute) +} + +extension PickServerItem { + final class ServerItemAttribute: Equatable, Hashable { + var isExpand: Bool + + init(isExpand: Bool) { + self.isExpand = isExpand + } + + static func == (lhs: PickServerItem.ServerItemAttribute, rhs: PickServerItem.ServerItemAttribute) -> Bool { + return lhs.isExpand == rhs.isExpand + } + + func hash(into hasher: inout Hasher) { + hasher.combine(isExpand) + } + } +} + +extension PickServerItem: Equatable { + static func == (lhs: PickServerItem, rhs: PickServerItem) -> Bool { + switch (lhs, rhs) { + case (.header, .header): + return true + case (.categoryPicker(let itemsLeft), .categoryPicker(let itemsRight)): + return itemsLeft == itemsRight + case (.search, .search): + return true + case (.server(let serverLeft, _), .server(let serverRight, _)): + return serverLeft.domain == serverRight.domain + default: + return false + } + } +} + +extension PickServerItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .header: + hasher.combine(String(describing: PickServerItem.header.self)) + case .categoryPicker(let items): + hasher.combine(items) + case .search: + hasher.combine(String(describing: PickServerItem.search.self)) + case .server(let server, _): + hasher.combine(server.domain) + } + } +} diff --git a/Mastodon/Diffiable/Section/PickServerSection.swift b/Mastodon/Diffiable/Section/PickServerSection.swift new file mode 100644 index 000000000..d76cf4c66 --- /dev/null +++ b/Mastodon/Diffiable/Section/PickServerSection.swift @@ -0,0 +1,100 @@ +// +// PickServerSection.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import UIKit +import MastodonSDK +import Kanna + +enum PickServerSection: Equatable, Hashable { + case header + case category + case search + case servers +} + +extension PickServerSection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + pickServerSearchCellDelegate: PickServerSearchCellDelegate, + pickServerCellDelegate: PickServerCellDelegate + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { [weak pickServerSearchCellDelegate, weak pickServerCellDelegate] tableView, indexPath, item -> UITableViewCell? in + switch item { + case .header: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell + return cell + case .categoryPicker(let items): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell + cell.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource( + for: cell.collectionView, + dependency: dependency + ) + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(items, toSection: .main) + cell.diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + return cell + case .search: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell + cell.delegate = pickServerSearchCellDelegate + return cell + case .server(let server, let attribute): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell + PickServerSection.configure(cell: cell, server: server, attribute: attribute) + cell.delegate = pickServerCellDelegate + // cell.server = server + // if expandServerDomainSet.contains(server.domain) { + // cell.mode = .expand + // } else { + // cell.mode = .collapse + // } +// if server == viewModel.selectedServer.value { +// tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) +// } else { +// tableView.deselectRow(at: indexPath, animated: false) +// } +// +// cell.delegate = self + return cell + } + } + } +} + +extension PickServerSection { + + static func configure(cell: PickServerCell, server: Mastodon.Entity.Server, attribute: PickServerItem.ServerItemAttribute) { + cell.domainLabel.text = server.domain + cell.descriptionLabel.text = { + guard let html = try? HTML(html: server.description, encoding: .utf8) else { + return server.description + } + + return html.text ?? server.description + }() + cell.langValueLabel.text = server.language.uppercased() + cell.usersValueLabel.text = parseUsersCount(server.totalUsers) + cell.categoryValueLabel.text = server.category.uppercased() + + cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse) +// UIView.animate(withDuration: 0.33) { +// cell.expandBox.layoutIfNeeded() +// } + } + + private static func parseUsersCount(_ usersCount: Int) -> String { + switch usersCount { + case 0..<1000: + return "\(usersCount)" + default: + let usersCountInThousand = Float(usersCount) / 1000.0 + return String(format: "%.1fK", usersCountInThousand) + } + } + +} diff --git a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift index 1f02baad6..5008ad3a3 100644 --- a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift @@ -9,12 +9,6 @@ import UIKit class PickServerCategoryCollectionViewCell: UICollectionViewCell { - var category: MastodonPickServerViewModel.Category? { - didSet { - categoryView.category = category - } - } - var categoryView: PickServerCategoryView = { let view = PickServerCategoryView() view.translatesAutoresizingMaskIntoConstraints = false diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 909d6ec7b..06e75c0cd 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -5,10 +5,9 @@ // Created by BradGao on 2021/2/20. // +import os.log import UIKit import Combine -import OSLog -import MastodonSDK final class MastodonPickServerViewController: UIViewController, NeedsDependency { @@ -22,13 +21,6 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency private var isAuthenticating = CurrentValueSubject(false) private var expandServerDomainSet = Set() - - enum Section: CaseIterable { - case title - case categories - case search - case serverList - } let tableView: UITableView = { let tableView = ControlContainableTableView() @@ -95,31 +87,16 @@ extension MastodonPickServerViewController { nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside) tableView.delegate = self - tableView.dataSource = self - - viewModel - .searchedServers - .receive(on: DispatchQueue.main) - .sink { _ in - - } receiveValue: { [weak self] servers in - self?.tableView.beginUpdates() - self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic) - self?.tableView.endUpdates() - if let selectedServer = self?.viewModel.selectedServer.value, servers.contains(selectedServer) { - // Previously selected server is still in the list, do nothing - } else { - // Previously selected server is not in the updated list, reset the selectedServer's value - self?.viewModel.selectedServer.send(nil) - } - } - .store(in: &disposeBag) + viewModel.setupDiffableDataSource( + for: tableView, + dependency: self, + pickServerSearchCellDelegate: self, + pickServerCellDelegate: self + ) viewModel .selectedServer - .map { - $0 != nil - } + .map { $0 != nil } .assign(to: \.isEnabled, on: nextStepButton) .store(in: &disposeBag) @@ -165,8 +142,6 @@ extension MastodonPickServerViewController { isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading() } .store(in: &disposeBag) - - viewModel.fetchAllServers() } @objc @@ -292,142 +267,150 @@ extension MastodonPickServerViewController { } extension MastodonPickServerViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return UIView() + } + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let category = Section.allCases[section] - switch category { - case .title: + guard let diffableDataSource = viewModel.diffableDataSource else { return 0 } + let sections = diffableDataSource.snapshot().sectionIdentifiers + let section = sections[section] + switch section { + case .header: return 20 - case .categories: + case .category: // Since category view has a blur shadow effect, its height need to be large than the actual height, // Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom) return 10 case .search: // Same reason as above return 10 - case .serverList: + case .servers: return 0 } } func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + guard let diffableDataSource = viewModel.diffableDataSource else { return nil } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } + guard case let .server(server) = item else { return nil } + if tableView.indexPathForSelectedRow == indexPath { tableView.deselectRow(at: indexPath, animated: false) viewModel.selectedServer.send(nil) return nil } + return indexPath } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard case let .server(server, _) = item else { return } tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) - viewModel.selectedServer.send(viewModel.searchedServers.value[indexPath.row]) + viewModel.selectedServer.send(server) } - + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) viewModel.selectedServer.send(nil) } + } -extension MastodonPickServerViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return UIView() - } - - func numberOfSections(in tableView: UITableView) -> Int { - return Self.Section.allCases.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let section = Self.Section.allCases[section] - switch section { - case .title, - .categories, - .search: - return 1 - case .serverList: - return viewModel.searchedServers.value.count - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - - let section = Self.Section.allCases[indexPath.section] - switch section { - case .title: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell - return cell - case .categories: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell - cell.dataSource = self - cell.delegate = self - return cell - case .search: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell - cell.delegate = self - return cell - case .serverList: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell - let server = viewModel.searchedServers.value[indexPath.row] - cell.server = server - if expandServerDomainSet.contains(server.domain) { - cell.mode = .expand - } else { - cell.mode = .collapse - } - if server == viewModel.selectedServer.value { - tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) - } else { - tableView.deselectRow(at: indexPath, animated: false) - } - - cell.delegate = self - return cell - } - } -} +//extension MastodonPickServerViewController: UITableViewDataSource { -extension MastodonPickServerViewController: PickServerCellDelegate { - func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) { - if newMode == .collapse { - expandServerDomainSet.remove(server.domain) - } else { - expandServerDomainSet.insert(server.domain) - } - - tableView.beginUpdates() - updates() - tableView.endUpdates() - - if newMode == .expand, let modeChangeIndex = self.viewModel.searchedServers.value.firstIndex(where: { $0 == server }), self.tableView.indexPathsForVisibleRows?.last?.row == modeChangeIndex { - self.tableView.scrollToRow(at: IndexPath(row: modeChangeIndex, section: 3), at: .bottom, animated: true) - } - } -} +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// +// let section = Self.Section.allCases[indexPath.section] +// switch section { +// case .title: +// +// case .categories: +// +// case .search: +// +// case .serverList: +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell +// let server = viewModel.servers.value[indexPath.row] +// // cell.server = server +//// if expandServerDomainSet.contains(server.domain) { +//// cell.mode = .expand +//// } else { +//// cell.mode = .collapse +//// } +// if server == viewModel.selectedServer.value { +// tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) +// } else { +// tableView.deselectRow(at: indexPath, animated: false) +// } +// +// cell.delegate = self +// return cell +// } +// } +//} +// MARK: - PickServerSearchCellDelegate extension MastodonPickServerViewController: PickServerSearchCellDelegate { - func pickServerSearchCell(didChange searchText: String?) { + func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) { viewModel.searchText.send(searchText) } } -extension MastodonPickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesDelegate { - func numberOfCategories() -> Int { - return viewModel.categories.count - } - - func category(at index: Int) -> MastodonPickServerViewModel.Category { - return viewModel.categories[index] - } - - func selectedIndex() -> Int { - return viewModel.selectCategoryIndex.value - } - - func pickServerCategoriesCell(didSelect index: Int) { - return viewModel.selectCategoryIndex.send(index) +// MARK: - PickServerCellDelegate +extension MastodonPickServerViewController: PickServerCellDelegate { + func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let indexPath = tableView.indexPath(for: cell) else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard case let .server(_, attribute) = item else { return } + + attribute.isExpand.toggle() + tableView.beginUpdates() + cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse) + tableView.endUpdates() + + // expand attribute change do not needs apply snapshot to diffable data source + // but should I block the viewModel data binding during tableView.beginUpdates/endUpdates? } + +// func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) { +// if newMode == .collapse { +// expandServerDomainSet.remove(server.domain) +// } else { +// expandServerDomainSet.insert(server.domain) +// } +// +// tableView.beginUpdates() +// updates() +// tableView.endUpdates() +// +// if newMode == .expand, let modeChangeIndex = self.viewModel.servers.value.firstIndex(where: { $0 == server }), self.tableView.indexPathsForVisibleRows?.last?.row == modeChangeIndex { +// self.tableView.scrollToRow(at: IndexPath(row: modeChangeIndex, section: 3), at: .bottom, animated: true) +// } +// } } +//extension MastodonPickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesCellDelegate { +// func numberOfCategories() -> Int { +// return viewModel.categories.count +// } +// +// func category(at index: Int) -> MastodonPickServerViewModel.Category { +// return viewModel.categories[index] +// } +// +// func selectedIndex() -> Int { +// return viewModel.selectCategoryIndex.value +// } +// +// func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, didSelect index: Int) { +// return viewModel.selectCategoryIndex.send(index) +// } +//} + // MARK: - OnboardingViewControllerAppearance extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift new file mode 100644 index 000000000..506cbbc48 --- /dev/null +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift @@ -0,0 +1,37 @@ +// +// MastodonPickServerViewController+Diffable.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import UIKit + +extension MastodonPickServerViewModel { + + func setupDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + pickServerSearchCellDelegate: PickServerSearchCellDelegate, + pickServerCellDelegate: PickServerCellDelegate + ) { + diffableDataSource = PickServerSection.tableViewDiffableDataSource( + for: tableView, + dependency: dependency, + pickServerSearchCellDelegate: pickServerSearchCellDelegate, + pickServerCellDelegate: pickServerCellDelegate + ) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.header, .category, .search, .servers]) + snapshot.appendItems([.header], toSection: .header) + snapshot.appendItems([.categoryPicker(items: categoryPickerItems)], toSection: .category) + snapshot.appendItems([.search], toSection: .search) + diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + + loadIndexedServerStateMachine.enter(LoadIndexedServerState.Loading.self) + } + +} + + diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift new file mode 100644 index 000000000..172973b5c --- /dev/null +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift @@ -0,0 +1,84 @@ +// +// MastodonPickServerViewModel+LoadIndexedServerState.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021/3/5. +// + +import os.log +import Foundation +import GameplayKit +import MastodonSDK + +extension MastodonPickServerViewModel { + class LoadIndexedServerState: GKState { + weak var viewModel: MastodonPickServerViewModel? + + init(viewModel: MastodonPickServerViewModel) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + } + } +} + +extension MastodonPickServerViewModel.LoadIndexedServerState { + + class Initial: MastodonPickServerViewModel.LoadIndexedServerState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + } + + class Loading: MastodonPickServerViewModel.LoadIndexedServerState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Fail.self || stateClass == Idle.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return } + viewModel.context.apiService.servers(language: nil, category: nil) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + break + case .finished: + break + } + } receiveValue: { [weak self] response in + guard let _ = self else { return } + stateMachine.enter(Idle.self) + viewModel.indexedServers.value = response.value + } + .store(in: &viewModel.disposeBag) + } + } + + class Fail: MastodonPickServerViewModel.LoadIndexedServerState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Loading.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + guard let self = self else { return } + stateMachine.enter(Loading.self) + } + } + } + + class Idle: MastodonPickServerViewModel.LoadIndexedServerState { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return false + } + } + +} diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index a3b2a8768..2e764f9b1 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -5,9 +5,10 @@ // Created by BradGao on 2021/2/23. // +import os.log import UIKit -import OSLog import Combine +import GameplayKit import MastodonSDK import CoreDataStack @@ -17,69 +18,41 @@ class MastodonPickServerViewModel: NSObject { case signIn } - enum Category { - // `all` means search for all categories - case all - // `some` means search for specific category - case some(Mastodon.Entity.Category) - - var title: String { - switch self { - case .all: - return L10n.Scene.ServerPicker.Button.Category.all - case .some(let masCategory): - // TODO: Use emoji as placeholders - switch masCategory.category { - case .academia: - return "📚" - case .activism: - return "✊" - case .food: - return "🍕" - case .furry: - return "🦁" - case .games: - return "🕹" - case .general: - return "GE" - case .journalism: - return "📰" - case .lgbt: - return "🏳️‍🌈" - case .regional: - return "📍" - case .art: - return "🎨" - case .music: - return "🎼" - case .tech: - return "📱" - case ._other: - return "❓" - } - } - } - } - + var disposeBag = Set() + + // input let mode: PickServerMode let context: AppContext - - var categories = [Category]() + var categoryPickerItems: [CategoryPickerItem] = { + var items: [CategoryPickerItem] = [] + items.append(.all) + items.append(contentsOf: APIService.stubCategories().map { CategoryPickerItem.category(category: $0) }) + return items + }() let selectCategoryIndex = CurrentValueSubject(0) - let searchText = CurrentValueSubject(nil) + let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) + let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([]) - let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) - let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) + // output + var diffableDataSource: UITableViewDiffableDataSource? + private(set) lazy var loadIndexedServerStateMachine: GKStateMachine = { + // exclude timeline middle fetcher state + let stateMachine = GKStateMachine(states: [ + LoadIndexedServerState.Initial(viewModel: self), + LoadIndexedServerState.Loading(viewModel: self), + LoadIndexedServerState.Fail(viewModel: self), + LoadIndexedServerState.Idle(viewModel: self), + ]) + stateMachine.enter(LoadIndexedServerState.Initial.self) + return stateMachine + }() + let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let selectedServer = CurrentValueSubject(nil) let error = PassthroughSubject() let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() - - private var disposeBag = Set() - - weak var tableView: UITableView? - + var mastodonPinBasedAuthenticationViewController: UIViewController? init(context: AppContext, mode: PickServerMode) { @@ -91,83 +64,115 @@ class MastodonPickServerViewModel: NSObject { } private func configure() { - let masCategories = context.apiService.stubCategories() - categories.append(.all) - categories.append(contentsOf: masCategories.map { Category.some($0) }) - Publishers.CombineLatest3( - selectCategoryIndex, - searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), - allServers + indexedServers, + unindexedServers, + searchText ) - .flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher, Never> in - guard let self = self else { return Just(Result.success([])).eraseToAnyPublisher() } + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] indexedServers, unindexedServers, searchText in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } - // 1. Search from the servers recorded in joinmastodon.org - let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers) - if !searchedServersFromAPI.isEmpty { - // If found servers, just return - return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher() - } - // 2. No server found in the recorded list, check if searchText is a valid mastodon server domain - if let toSearchText = searchText, !toSearchText.isEmpty, let _ = URL(string: "https://\(toSearchText)") { - return self.context.apiService.instance(domain: toSearchText) - .map { return Result.success([Mastodon.Entity.Server(instance: $0.value)]) } - .catch({ error -> Just> in - return Just(Result.failure(error)) - }) - .eraseToAnyPublisher() - } - return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher() - } - .sink { _ in - - } receiveValue: { [weak self] result in - switch result { - case .success(let servers): - self?.searchedServers.send(servers) - case .failure(let error): - // TODO: What should be presented when user inputs invalid search text? - self?.searchedServers.send([]) + let oldSnapshot = diffableDataSource.snapshot() + var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:] + for item in oldSnapshot.itemIdentifiers { + guard case let .server(server, attribute) = item else { continue } + oldSnapshotServerItemAttributeDict[server.domain] = attribute } - } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.header, .category, .search, .servers]) + snapshot.appendItems([.header], toSection: .header) + snapshot.appendItems([.categoryPicker(items: self.categoryPickerItems)], toSection: .category) + snapshot.appendItems([.search], toSection: .search) + // TODO: handle filter + var serverItems: [PickServerItem] = [] + for server in indexedServers { + let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isExpand: false) + let item = PickServerItem.server(server: server, attribute: attribute) + serverItems.append(item) + } + snapshot.appendItems(serverItems, toSection: .servers) + + diffableDataSource.apply(snapshot) + }) .store(in: &disposeBag) + + +// Publishers.CombineLatest3( +// selectCategoryIndex, +// searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), +// indexedServers +// ) +// .flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher, Never> in +// guard let self = self else { return Just(Result.success([])).eraseToAnyPublisher() } +// +// // 1. Search from the servers recorded in joinmastodon.org +// let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers) +// if !searchedServersFromAPI.isEmpty { +// // If found servers, just return +// return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher() +// } +// // 2. No server found in the recorded list, check if searchText is a valid mastodon server domain +// if let toSearchText = searchText, !toSearchText.isEmpty, let _ = URL(string: "https://\(toSearchText)") { +// return self.context.apiService.instance(domain: toSearchText) +// .map { return Result.success([Mastodon.Entity.Server(instance: $0.value)]) } +// .catch({ error -> Just> in +// return Just(Result.failure(error)) +// }) +// .eraseToAnyPublisher() +// } +// return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher() +// } +// .sink { _ in +// +// } receiveValue: { [weak self] result in +// switch result { +// case .success(let servers): +// self?.servers.send(servers) +// case .failure(let error): +// // TODO: What should be presented when user inputs invalid search text? +// self?.servers.send([]) +// } +// +// } +// .store(in: &disposeBag) } - func fetchAllServers() { - context.apiService.servers(language: nil, category: nil) - .sink { completion in - // TODO: Add a reload button when fails to fetch servers initially - } receiveValue: { [weak self] result in - self?.allServers.send(result.value) - } - .store(in: &disposeBag) - - } - - private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] { - return allServers - // 1. Filter the category - .filter { - switch category { - case .all: - return true - case .some(let masCategory): - return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame - } - } - // 2. Filter the searchText - .filter { - if let searchText = searchText, !searchText.isEmpty { - return $0.domain.lowercased().contains(searchText.lowercased()) - } else { - return true - } - } - } +// func fetchAllServers() { +// context.apiService.servers(language: nil, category: nil) +// .sink { completion in +// // TODO: Add a reload button when fails to fetch servers initially +// } receiveValue: { [weak self] result in +// self?.indexedServers.send(result.value) +// } +// .store(in: &disposeBag) +// +// } +// +// private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] { +// return allServers +// // 1. Filter the category +// .filter { +// switch category { +// case .all: +// return true +// case .some(let masCategory): +// return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame +// } +// } +// // 2. Filter the searchText +// .filter { +// if let searchText = searchText, !searchText.isEmpty { +// return $0.domain.lowercased().contains(searchText.lowercased()) +// } else { +// return true +// } +// } +// } } // MARK: - SignIn methods & structs diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 8f66e9847..1fd366555 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -5,24 +5,20 @@ // Created by BradGao on 2021/2/23. // +import os.log import UIKit import MastodonSDK -protocol PickServerCategoriesDataSource: class { - func numberOfCategories() -> Int - func category(at index: Int) -> MastodonPickServerViewModel.Category - func selectedIndex() -> Int -} - -protocol PickServerCategoriesDelegate: class { - func pickServerCategoriesCell(didSelect index: Int) +protocol PickServerCategoriesCellDelegate: class { + func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) } final class PickServerCategoriesCell: UITableViewCell { - weak var dataSource: PickServerCategoriesDataSource! - weak var delegate: PickServerCategoriesDelegate! + weak var delegate: PickServerCategoriesCellDelegate? + var diffableDataSource: UICollectionViewDiffableDataSource? + let metricView = UIView() let collectionView: UICollectionView = { @@ -38,6 +34,12 @@ final class PickServerCategoriesCell: UITableViewCell { return view }() + override func prepareForReuse() { + super.prepareForReuse() + + delegate = nil + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() @@ -75,7 +77,6 @@ extension PickServerCategoriesCell { ]) collectionView.delegate = self - collectionView.dataSource = self } override func layoutSubviews() { @@ -86,45 +87,46 @@ extension PickServerCategoriesCell { } +// MARK: - UICollectionViewDelegateFlowLayout extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) - delegate.pickServerCategoriesCell(didSelect: indexPath.row) +// delegate.pickServerCategoriesCell(self, didSelect: indexPath.row) } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { layoutIfNeeded() return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX) } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return 16 } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: 60, height: 80) } } -extension PickServerCategoriesCell: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return dataSource.numberOfCategories() - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let category = dataSource.category(at: indexPath.row) - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell - cell.category = category - - // Select the default category by default - if indexPath.row == dataSource.selectedIndex() { - // Use `[]` as the scrollPosition to avoid contentOffset change - collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) - cell.isSelected = true - } - return cell - } - - -} +//extension PickServerCategoriesCell: UICollectionViewDataSource { +// func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +// return dataSource.numberOfCategories() +// } +// +// func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { +// let category = dataSource.category(at: indexPath.row) +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell +// cell.category = category +// +// // Select the default category by default +// if indexPath.row == dataSource.selectedIndex() { +// // Use `[]` as the scrollPosition to avoid contentOffset change +// collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) +// cell.isSelected = true +// } +// return cell +// } +// +// +//} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 52133c4ba..cadfc74ba 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -5,25 +5,21 @@ // Created by BradGao on 2021/2/24. // +import os.log import UIKit import MastodonSDK import AlamofireImage import Kanna protocol PickServerCellDelegate: class { - func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) + func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) } class PickServerCell: UITableViewCell { weak var delegate: PickServerCellDelegate? - enum Mode { - case collapse - case expand - } - - private var containerView: UIView = { + let containerView: UIView = { let view = UIView() view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16) view.backgroundColor = Asset.Colors.lightWhite.color @@ -31,7 +27,7 @@ class PickServerCell: UITableViewCell { return view }() - private var domainLabel: UILabel = { + let domainLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .headline) label.textColor = Asset.Colors.lightDarkGray.color @@ -40,7 +36,7 @@ class PickServerCell: UITableViewCell { return label }() - private var checkbox: UIImageView = { + let checkbox: UIImageView = { let imageView = UIImageView() imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body) imageView.tintColor = Asset.Colors.lightSecondaryText.color @@ -49,7 +45,7 @@ class PickServerCell: UITableViewCell { return imageView }() - private var descriptionLabel: UILabel = { + let descriptionLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .subheadline) label.numberOfLines = 0 @@ -59,9 +55,9 @@ class PickServerCell: UITableViewCell { return label }() - private let thumbnailActivityIdicator = UIActivityIndicatorView(style: .medium) + let thumbnailActivityIdicator = UIActivityIndicatorView(style: .medium) - private var thumbnailImageView: UIImageView = { + let thumbnailImageView: UIImageView = { let imageView = UIImageView() imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill @@ -69,7 +65,7 @@ class PickServerCell: UITableViewCell { return imageView }() - private var infoStackView: UIStackView = { + let infoStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.alignment = .fill @@ -78,14 +74,14 @@ class PickServerCell: UITableViewCell { return stackView }() - private var expandBox: UIView = { + let expandBox: UIView = { let view = UIView() view.backgroundColor = .clear view.translatesAutoresizingMaskIntoConstraints = false return view }() - private var expandButton: UIButton = { + let expandButton: UIButton = { let button = UIButton(type: .custom) button.setTitle(L10n.Scene.ServerPicker.Button.seemore, for: .normal) button.setTitle(L10n.Scene.ServerPicker.Button.seeless, for: .selected) @@ -95,14 +91,14 @@ class PickServerCell: UITableViewCell { return button }() - private var seperator: UIView = { + let seperator: UIView = { let view = UIView() view.backgroundColor = Asset.Colors.lightBackground.color view.translatesAutoresizingMaskIntoConstraints = false return view }() - private var langValueLabel: UILabel = { + let langValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) @@ -112,7 +108,7 @@ class PickServerCell: UITableViewCell { return label }() - private var usersValueLabel: UILabel = { + let usersValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) @@ -122,7 +118,7 @@ class PickServerCell: UITableViewCell { return label }() - private var categoryValueLabel: UILabel = { + let categoryValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) @@ -132,7 +128,7 @@ class PickServerCell: UITableViewCell { return label }() - private var langTitleLabel: UILabel = { + let langTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = .preferredFont(forTextStyle: .caption2) @@ -143,7 +139,7 @@ class PickServerCell: UITableViewCell { return label }() - private var usersTitleLabel: UILabel = { + let usersTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = .preferredFont(forTextStyle: .caption2) @@ -154,7 +150,7 @@ class PickServerCell: UITableViewCell { return label }() - private var categoryTitleLabel: UILabel = { + let categoryTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.lightDarkGray.color label.font = .preferredFont(forTextStyle: .caption2) @@ -168,22 +164,12 @@ class PickServerCell: UITableViewCell { private var collapseConstraints: [NSLayoutConstraint] = [] private var expandConstraints: [NSLayoutConstraint] = [] - var mode: PickServerCell.Mode = .collapse { - didSet { - updateMode() - } - } - - var server: Mastodon.Entity.Server? { - didSet { - updateServerInfo() - } - } - override func prepareForReuse() { super.prepareForReuse() + thumbnailImageView.isHidden = false thumbnailImageView.af.cancelImageRequest() + thumbnailActivityIdicator.stopAnimating() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -195,6 +181,7 @@ class PickServerCell: UITableViewCell { super.init(coder: coder) _init() } + } // MARK: - Methods to configure appearance @@ -224,7 +211,7 @@ extension PickServerCell { infoStackView.addArrangedSubview(verticalInfoStackViewUsers) infoStackView.addArrangedSubview(verticalInfoStackViewCategory) - let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12).priority(.required) + let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12).priority(.required - 1) collapseConstraints.append(expandButtonTopConstraintInCollapse) let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.defaultHigh) @@ -292,7 +279,7 @@ extension PickServerCell { descriptionLabel.setContentHuggingPriority(.required - 2, for: .vertical) descriptionLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical) - expandButton.addTarget(self, action: #selector(expandButtonDidClicked(_:)), for: .touchUpInside) + expandButton.addTarget(self, action: #selector(expandButtonDidPressed(_:)), for: .touchUpInside) } private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView { @@ -305,8 +292,31 @@ extension PickServerCell { arrangedView.forEach { stackView.addArrangedSubview($0) } return stackView } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + if selected { + checkbox.image = UIImage(systemName: "checkmark.circle.fill") + } else { + checkbox.image = UIImage(systemName: "circle") + } + } - private func updateMode() { + @objc + private func expandButtonDidPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.pickServerCell(self, expandButtonPressed: sender) + } +} + +extension PickServerCell { + + enum ExpandMode { + case collapse + case expand + } + + func updateExpandMode(mode: ExpandMode) { switch mode { case .collapse: expandBox.isHidden = true @@ -318,73 +328,35 @@ extension PickServerCell { expandButton.isSelected = true NSLayoutConstraint.activate(expandConstraints) NSLayoutConstraint.deactivate(collapseConstraints) - - updateThumbnail() } } - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - if selected { - checkbox.image = UIImage(systemName: "checkmark.circle.fill") - } else { - checkbox.image = UIImage(systemName: "circle") - } - } +// private func updateThumbnail() { +// guard let serverInfo = server, +// let proxiedThumbnail = serverInfo.proxiedThumbnail, +// let url = URL(string: proxiedThumbnail) else { +// thumbnailImageView.isHidden = true +// thumbnailActivityIdicator.stopAnimating() +// return +// } +// +// thumbnailImageView.isHidden = false +// thumbnailActivityIdicator.startAnimating() +// +// let placeholderImage = UIImage.placeholder(color: .systemFill).af.imageRounded(withCornerRadius: 3.0, divideRadiusByImageScale: true) +// thumbnailImageView.af.setImage( +// withURL: url, +// placeholderImage: placeholderImage, +// filter: AspectScaledToFillSizeWithRoundedCornersFilter(size: thumbnailImageView.frame.size, radius: 3), +// imageTransition: .crossDissolve(0.33), +// completion: { [weak self] response in +// guard let self = self else { return } +// switch response.result { +// case .success, .failure: +// self.thumbnailActivityIdicator.stopAnimating() +// } +// } +// ) +// } - @objc - private func expandButtonDidClicked(_ sender: UIButton) { - let newMode: Mode = mode == .collapse ? .expand : .collapse - delegate?.pickServerCell(modeChange: server!, newMode: newMode, updates: { [weak self] in - self?.mode = newMode - }) - } -} - -// MARK: - Methods to update data -extension PickServerCell { - private func updateServerInfo() { - guard let serverInfo = server else { return } - domainLabel.text = serverInfo.domain - descriptionLabel.text = { - guard let html = try? HTML(html: serverInfo.description, encoding: .utf8) else { - return serverInfo.description - } - - return html.text ?? serverInfo.description - }() - langValueLabel.text = serverInfo.language.uppercased() - usersValueLabel.text = parseUsersCount(serverInfo.totalUsers) - categoryValueLabel.text = serverInfo.category.uppercased() - } - - private func updateThumbnail() { - guard let serverInfo = server else { return } - - thumbnailActivityIdicator.startAnimating() - let placeholderImage = UIImage.placeholder(color: .systemFill).af.imageRounded(withCornerRadius: 3.0, divideRadiusByImageScale: true) - thumbnailImageView.af.setImage( - withURL: URL(string: serverInfo.proxiedThumbnail ?? "")!, - placeholderImage: placeholderImage, - filter: AspectScaledToFillSizeWithRoundedCornersFilter(size: thumbnailImageView.frame.size, radius: 3), - imageTransition: .crossDissolve(0.33), - completion: { [weak self] response in - guard let self = self else { return } - switch response.result { - case .success, .failure: - self.thumbnailActivityIdicator.stopAnimating() - } - } - ) - } - - private func parseUsersCount(_ usersCount: Int) -> String { - switch usersCount { - case 0..<1000: - return "\(usersCount)" - default: - let usersCountInThousand = Float(usersCount) / 1000.0 - return String(format: "%.1fK", usersCountInThousand) - } - } } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift index 6df8affa2..2de66fa65 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift @@ -8,7 +8,7 @@ import UIKit protocol PickServerSearchCellDelegate: class { - func pickServerSearchCell(didChange searchText: String?) + func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) } class PickServerSearchCell: UITableViewCell { @@ -55,6 +55,12 @@ class PickServerSearchCell: UITableViewCell { return textField }() + override func prepareForReuse() { + super.prepareForReuse() + + delegate = nil + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() @@ -97,7 +103,7 @@ extension PickServerSearchCell { } extension PickServerSearchCell { - @objc func textFieldDidChange(_ textField: UITextField) { - delegate?.pickServerSearchCell(didChange: textField.text) + @objc private func textFieldDidChange(_ textField: UITextField) { + delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text) } } diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index 30fcbc1f9..2c9bd240f 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -9,14 +9,14 @@ import UIKit import MastodonSDK class PickServerCategoryView: UIView { - var category: MastodonPickServerViewModel.Category? { - didSet { - updateCategory() - } - } +// var category: MastodonPickServerViewModel.Category? { +// didSet { +// updateCategory() +// } +// } var selected: Bool = false { didSet { - updateSelectStatus() +// updateSelectStatus() } } @@ -56,44 +56,56 @@ extension PickServerCategoryView { private func configure() { addSubview(bgView) addSubview(titleLabel) - + bgView.backgroundColor = Asset.Colors.lightWhite.color - + NSLayoutConstraint.activate([ bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor), bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor), bgView.topAnchor.constraint(equalTo: self.topAnchor), bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - + titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor), titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } - - private func updateCategory() { - guard let category = category else { return } - titleLabel.text = category.title - switch category { - case .all: - titleLabel.font = UIFont.systemFont(ofSize: 17) - case .some: - titleLabel.font = UIFont.systemFont(ofSize: 28) - } - } - - private func updateSelectStatus() { - if selected { - bgView.backgroundColor = Asset.Colors.lightBrandBlue.color - bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0) - if case .all = category { - titleLabel.textColor = Asset.Colors.lightWhite.color - } - } else { - bgView.backgroundColor = Asset.Colors.lightWhite.color - bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0) - if case .all = category { - titleLabel.textColor = Asset.Colors.lightBrandBlue.color - } + +// private func updateCategory() { +// guard let category = category else { return } +// titleLabel.text = category.title +// switch category { +// case .all: +// titleLabel.font = UIFont.systemFont(ofSize: 17) +// case .some: +// titleLabel.font = UIFont.systemFont(ofSize: 28) +// } +// } +// +// private func updateSelectStatus() { +// if selected { +// bgView.backgroundColor = Asset.Colors.lightBrandBlue.color +// bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0) +// if case .all = category { +// titleLabel.textColor = Asset.Colors.lightWhite.color +// } +// } else { +// bgView.backgroundColor = Asset.Colors.lightWhite.color +// bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0) +// if case .all = category { +// titleLabel.textColor = Asset.Colors.lightBrandBlue.color +// } +// } +// } +} + +#if DEBUG && canImport(SwiftUI) +import SwiftUI + +struct PickServerCategoryView_Previews: PreviewProvider { + static var previews: some View { + UIViewPreview { + PickServerCategoryView() } } } +#endif diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift index fa57ddfd4..d566da4c3 100644 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift +++ b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift @@ -39,8 +39,6 @@ final class MastodonPinBasedAuthenticationViewController: UIViewController, Need } - - extension MastodonPinBasedAuthenticationViewController { override func viewDidLoad() { diff --git a/Mastodon/Service/APIService/APIService+Onboarding.swift b/Mastodon/Service/APIService/APIService+Onboarding.swift index 450dc141c..5cbf455a0 100644 --- a/Mastodon/Service/APIService/APIService+Onboarding.swift +++ b/Mastodon/Service/APIService/APIService+Onboarding.swift @@ -23,7 +23,7 @@ extension APIService { return Mastodon.API.Onboarding.categories(session: session) } - func stubCategories() -> [Mastodon.Entity.Category] { + static func stubCategories() -> [Mastodon.Entity.Category] { return Mastodon.Entity.Category.Kind.allCases.map { kind in return Mastodon.Entity.Category(category: kind.rawValue, serversCount: 0) }