chore: [WIP] refactor pick server scene with diffable data source

This commit is contained in:
CMK 2021-03-05 22:50:20 +08:00
parent 652c286c71
commit 54c7610c7f
18 changed files with 830 additions and 440 deletions

View File

@ -96,6 +96,12 @@
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; 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 */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; 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 */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.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 = "<group>"; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
@ -456,11 +468,13 @@
0FAA102525E1125D0017CCDE /* PickServer */ = { 0FAA102525E1125D0017CCDE /* PickServer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0FB3D31825E525DE00AAD544 /* CollectionViewCell */,
0FB3D30D25E525C000AAD544 /* View */, 0FB3D30D25E525C000AAD544 /* View */,
0FB3D31825E525DE00AAD544 /* CollectionViewCell */,
0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */, 0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */,
0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */, 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */,
0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */, 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */,
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */,
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */,
); );
path = PickServer; path = PickServer;
sourceTree = "<group>"; sourceTree = "<group>";
@ -642,6 +656,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2D76319E25C1521200929FB9 /* StatusSection.swift */, 2D76319E25C1521200929FB9 /* StatusSection.swift */,
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */,
); );
path = Section; path = Section;
sourceTree = "<group>"; sourceTree = "<group>";
@ -683,6 +699,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2D7631B225C159F700929FB9 /* Item.swift */, 2D7631B225C159F700929FB9 /* Item.swift */,
DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */,
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
); );
path = Item; path = Item;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1464,8 +1482,10 @@
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
DB1FD44A25F26CD7004CFCFC /* PickServerItem.swift in Sources */,
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
@ -1511,12 +1531,14 @@
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */, DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */,
@ -1533,10 +1555,12 @@
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
DB1FD46025F278AF004CFCFC /* CategoryPickerSection.swift in Sources */,
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,

View File

@ -22,7 +22,7 @@
<key>Mastodon.xcscheme_^#shared#^_</key> <key>Mastodon.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>12</integer> <integer>8</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -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)
}
}
}

View File

@ -30,7 +30,7 @@ protocol StatusContentWarningAttribute {
} }
extension Item { extension Item {
class StatusTimelineAttribute: Hashable, StatusContentWarningAttribute { class StatusTimelineAttribute: Equatable, Hashable, StatusContentWarningAttribute {
var isStatusTextSensitive: Bool var isStatusTextSensitive: Bool
var isStatusSensitive: Bool var isStatusSensitive: Bool
@ -51,7 +51,6 @@ extension Item {
hasher.combine(isStatusTextSensitive) hasher.combine(isStatusTextSensitive)
hasher.combine(isStatusSensitive) hasher.combine(isStatusSensitive)
} }
} }
} }

View File

@ -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<CategoryPickerSection, CategoryPickerItem> {
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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<PickServerSection, PickServerItem> {
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<CategoryPickerSection, CategoryPickerItem>()
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)
}
}
}

View File

@ -9,12 +9,6 @@ import UIKit
class PickServerCategoryCollectionViewCell: UICollectionViewCell { class PickServerCategoryCollectionViewCell: UICollectionViewCell {
var category: MastodonPickServerViewModel.Category? {
didSet {
categoryView.category = category
}
}
var categoryView: PickServerCategoryView = { var categoryView: PickServerCategoryView = {
let view = PickServerCategoryView() let view = PickServerCategoryView()
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false

View File

@ -5,10 +5,9 @@
// Created by BradGao on 2021/2/20. // Created by BradGao on 2021/2/20.
// //
import os.log
import UIKit import UIKit
import Combine import Combine
import OSLog
import MastodonSDK
final class MastodonPickServerViewController: UIViewController, NeedsDependency { final class MastodonPickServerViewController: UIViewController, NeedsDependency {
@ -22,13 +21,6 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
private var isAuthenticating = CurrentValueSubject<Bool, Never>(false) private var isAuthenticating = CurrentValueSubject<Bool, Never>(false)
private var expandServerDomainSet = Set<String>() private var expandServerDomainSet = Set<String>()
enum Section: CaseIterable {
case title
case categories
case search
case serverList
}
let tableView: UITableView = { let tableView: UITableView = {
let tableView = ControlContainableTableView() let tableView = ControlContainableTableView()
@ -95,31 +87,16 @@ extension MastodonPickServerViewController {
nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside) nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside)
tableView.delegate = self tableView.delegate = self
tableView.dataSource = self viewModel.setupDiffableDataSource(
for: tableView,
viewModel dependency: self,
.searchedServers pickServerSearchCellDelegate: self,
.receive(on: DispatchQueue.main) pickServerCellDelegate: self
.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 viewModel
.selectedServer .selectedServer
.map { .map { $0 != nil }
$0 != nil
}
.assign(to: \.isEnabled, on: nextStepButton) .assign(to: \.isEnabled, on: nextStepButton)
.store(in: &disposeBag) .store(in: &disposeBag)
@ -165,8 +142,6 @@ extension MastodonPickServerViewController {
isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading() isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading()
} }
.store(in: &disposeBag) .store(in: &disposeBag)
viewModel.fetchAllServers()
} }
@objc @objc
@ -292,142 +267,150 @@ extension MastodonPickServerViewController {
} }
extension MastodonPickServerViewController: UITableViewDelegate { extension MastodonPickServerViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return UIView()
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let category = Section.allCases[section] guard let diffableDataSource = viewModel.diffableDataSource else { return 0 }
switch category { let sections = diffableDataSource.snapshot().sectionIdentifiers
case .title: let section = sections[section]
switch section {
case .header:
return 20 return 20
case .categories: case .category:
// Since category view has a blur shadow effect, its height need to be large than the actual height, // 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) // 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 return 10
case .search: case .search:
// Same reason as above // Same reason as above
return 10 return 10
case .serverList: case .servers:
return 0 return 0
} }
} }
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 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 { if tableView.indexPathForSelectedRow == indexPath {
tableView.deselectRow(at: indexPath, animated: false) tableView.deselectRow(at: indexPath, animated: false)
viewModel.selectedServer.send(nil) viewModel.selectedServer.send(nil)
return nil return nil
} }
return indexPath return indexPath
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: 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) 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) { func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false) tableView.deselectRow(at: indexPath, animated: false)
viewModel.selectedServer.send(nil) viewModel.selectedServer.send(nil)
} }
} }
extension MastodonPickServerViewController: UITableViewDataSource { //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: PickServerCellDelegate { // func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) { //
if newMode == .collapse { // let section = Self.Section.allCases[indexPath.section]
expandServerDomainSet.remove(server.domain) // switch section {
} else { // case .title:
expandServerDomainSet.insert(server.domain) //
} // case .categories:
//
tableView.beginUpdates() // case .search:
updates() //
tableView.endUpdates() // case .serverList:
// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
if newMode == .expand, let modeChangeIndex = self.viewModel.searchedServers.value.firstIndex(where: { $0 == server }), self.tableView.indexPathsForVisibleRows?.last?.row == modeChangeIndex { // let server = viewModel.servers.value[indexPath.row]
self.tableView.scrollToRow(at: IndexPath(row: modeChangeIndex, section: 3), at: .bottom, animated: true) // // 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 { extension MastodonPickServerViewController: PickServerSearchCellDelegate {
func pickServerSearchCell(didChange searchText: String?) { func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) {
viewModel.searchText.send(searchText) viewModel.searchText.send(searchText)
} }
} }
extension MastodonPickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesDelegate { // MARK: - PickServerCellDelegate
func numberOfCategories() -> Int { extension MastodonPickServerViewController: PickServerCellDelegate {
return viewModel.categories.count func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) {
} guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
func category(at index: Int) -> MastodonPickServerViewModel.Category { guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
return viewModel.categories[index] guard case let .server(_, attribute) = item else { return }
}
attribute.isExpand.toggle()
func selectedIndex() -> Int { tableView.beginUpdates()
return viewModel.selectCategoryIndex.value cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
} tableView.endUpdates()
func pickServerCategoriesCell(didSelect index: Int) { // expand attribute change do not needs apply snapshot to diffable data source
return viewModel.selectCategoryIndex.send(index) // 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 // MARK: - OnboardingViewControllerAppearance
extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } extension MastodonPickServerViewController: OnboardingViewControllerAppearance { }

View File

@ -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<PickServerSection, PickServerItem>()
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)
}
}

View File

@ -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
}
}
}

View File

@ -5,9 +5,10 @@
// Created by BradGao on 2021/2/23. // Created by BradGao on 2021/2/23.
// //
import os.log
import UIKit import UIKit
import OSLog
import Combine import Combine
import GameplayKit
import MastodonSDK import MastodonSDK
import CoreDataStack import CoreDataStack
@ -17,69 +18,41 @@ class MastodonPickServerViewModel: NSObject {
case signIn case signIn
} }
enum Category { var disposeBag = Set<AnyCancellable>()
// `all` means search for all categories
case all // input
// `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 ""
}
}
}
}
let mode: PickServerMode let mode: PickServerMode
let context: AppContext let context: AppContext
var categoryPickerItems: [CategoryPickerItem] = {
var categories = [Category]() var items: [CategoryPickerItem] = []
items.append(.all)
items.append(contentsOf: APIService.stubCategories().map { CategoryPickerItem.category(category: $0) })
return items
}()
let selectCategoryIndex = CurrentValueSubject<Int, Never>(0) let selectCategoryIndex = CurrentValueSubject<Int, Never>(0)
let searchText = CurrentValueSubject<String?, Never>(nil) let searchText = CurrentValueSubject<String?, Never>(nil)
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([])
let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) // output
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) var diffableDataSource: UITableViewDiffableDataSource<PickServerSection, PickServerItem>?
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<Mastodon.Entity.Server?, Never>(nil) let selectedServer = CurrentValueSubject<Mastodon.Entity.Server?, Never>(nil)
let error = PassthroughSubject<Error, Never>() let error = PassthroughSubject<Error, Never>()
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
private var disposeBag = Set<AnyCancellable>()
weak var tableView: UITableView?
var mastodonPinBasedAuthenticationViewController: UIViewController? var mastodonPinBasedAuthenticationViewController: UIViewController?
init(context: AppContext, mode: PickServerMode) { init(context: AppContext, mode: PickServerMode) {
@ -91,83 +64,115 @@ class MastodonPickServerViewModel: NSObject {
} }
private func configure() { private func configure() {
let masCategories = context.apiService.stubCategories()
categories.append(.all)
categories.append(contentsOf: masCategories.map { Category.some($0) })
Publishers.CombineLatest3( Publishers.CombineLatest3(
selectCategoryIndex, indexedServers,
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), unindexedServers,
allServers searchText
) )
.flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<Result<[Mastodon.Entity.Server], Error>, Never> in .receive(on: DispatchQueue.main)
guard let self = self else { return Just(Result.success([])).eraseToAnyPublisher() } .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 oldSnapshot = diffableDataSource.snapshot()
let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers) var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:]
if !searchedServersFromAPI.isEmpty { for item in oldSnapshot.itemIdentifiers {
// If found servers, just return guard case let .server(server, attribute) = item else { continue }
return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher() oldSnapshotServerItemAttributeDict[server.domain] = attribute
}
// 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<Result<[Mastodon.Entity.Server], Error>> 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([])
} }
} var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
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) .store(in: &disposeBag)
// Publishers.CombineLatest3(
// selectCategoryIndex,
// searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
// indexedServers
// )
// .flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<Result<[Mastodon.Entity.Server], Error>, 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<Result<[Mastodon.Entity.Server], Error>> 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() { // func fetchAllServers() {
context.apiService.servers(language: nil, category: nil) // context.apiService.servers(language: nil, category: nil)
.sink { completion in // .sink { completion in
// TODO: Add a reload button when fails to fetch servers initially // // TODO: Add a reload button when fails to fetch servers initially
} receiveValue: { [weak self] result in // } receiveValue: { [weak self] result in
self?.allServers.send(result.value) // self?.indexedServers.send(result.value)
} // }
.store(in: &disposeBag) // .store(in: &disposeBag)
//
} // }
//
private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] { // private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] {
return allServers // return allServers
// 1. Filter the category // // 1. Filter the category
.filter { // .filter {
switch category { // switch category {
case .all: // case .all:
return true // return true
case .some(let masCategory): // case .some(let masCategory):
return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame // return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame
} // }
} // }
// 2. Filter the searchText // // 2. Filter the searchText
.filter { // .filter {
if let searchText = searchText, !searchText.isEmpty { // if let searchText = searchText, !searchText.isEmpty {
return $0.domain.lowercased().contains(searchText.lowercased()) // return $0.domain.lowercased().contains(searchText.lowercased())
} else { // } else {
return true // return true
} // }
} // }
} // }
} }
// MARK: - SignIn methods & structs // MARK: - SignIn methods & structs

View File

@ -5,24 +5,20 @@
// Created by BradGao on 2021/2/23. // Created by BradGao on 2021/2/23.
// //
import os.log
import UIKit import UIKit
import MastodonSDK import MastodonSDK
protocol PickServerCategoriesDataSource: class { protocol PickServerCategoriesCellDelegate: class {
func numberOfCategories() -> Int func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
func category(at index: Int) -> MastodonPickServerViewModel.Category
func selectedIndex() -> Int
}
protocol PickServerCategoriesDelegate: class {
func pickServerCategoriesCell(didSelect index: Int)
} }
final class PickServerCategoriesCell: UITableViewCell { final class PickServerCategoriesCell: UITableViewCell {
weak var dataSource: PickServerCategoriesDataSource! weak var delegate: PickServerCategoriesCellDelegate?
weak var delegate: PickServerCategoriesDelegate!
var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
let metricView = UIView() let metricView = UIView()
let collectionView: UICollectionView = { let collectionView: UICollectionView = {
@ -38,6 +34,12 @@ final class PickServerCategoriesCell: UITableViewCell {
return view return view
}() }()
override func prepareForReuse() {
super.prepareForReuse()
delegate = nil
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
_init() _init()
@ -75,7 +77,6 @@ extension PickServerCategoriesCell {
]) ])
collectionView.delegate = self collectionView.delegate = self
collectionView.dataSource = self
} }
override func layoutSubviews() { override func layoutSubviews() {
@ -86,45 +87,46 @@ extension PickServerCategoriesCell {
} }
// MARK: - UICollectionViewDelegateFlowLayout
extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) 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 { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
layoutIfNeeded() layoutIfNeeded()
return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX) 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 { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 16 return 16
} }
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 60, height: 80) return CGSize(width: 60, height: 80)
} }
} }
extension PickServerCategoriesCell: UICollectionViewDataSource { //extension PickServerCategoriesCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.numberOfCategories() // return dataSource.numberOfCategories()
} // }
//
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { // func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let category = dataSource.category(at: indexPath.row) // let category = dataSource.category(at: indexPath.row)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell // let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell
cell.category = category // cell.category = category
//
// Select the default category by default // // Select the default category by default
if indexPath.row == dataSource.selectedIndex() { // if indexPath.row == dataSource.selectedIndex() {
// Use `[]` as the scrollPosition to avoid contentOffset change // // Use `[]` as the scrollPosition to avoid contentOffset change
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) // collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
cell.isSelected = true // cell.isSelected = true
} // }
return cell // return cell
} // }
//
//
} //}

View File

@ -5,25 +5,21 @@
// Created by BradGao on 2021/2/24. // Created by BradGao on 2021/2/24.
// //
import os.log
import UIKit import UIKit
import MastodonSDK import MastodonSDK
import AlamofireImage import AlamofireImage
import Kanna import Kanna
protocol PickServerCellDelegate: class { 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 { class PickServerCell: UITableViewCell {
weak var delegate: PickServerCellDelegate? weak var delegate: PickServerCellDelegate?
enum Mode { let containerView: UIView = {
case collapse
case expand
}
private var containerView: UIView = {
let view = UIView() let view = UIView()
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16) view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16)
view.backgroundColor = Asset.Colors.lightWhite.color view.backgroundColor = Asset.Colors.lightWhite.color
@ -31,7 +27,7 @@ class PickServerCell: UITableViewCell {
return view return view
}() }()
private var domainLabel: UILabel = { let domainLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline) label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
@ -40,7 +36,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var checkbox: UIImageView = { let checkbox: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body) imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
imageView.tintColor = Asset.Colors.lightSecondaryText.color imageView.tintColor = Asset.Colors.lightSecondaryText.color
@ -49,7 +45,7 @@ class PickServerCell: UITableViewCell {
return imageView return imageView
}() }()
private var descriptionLabel: UILabel = { let descriptionLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .subheadline) label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 0 label.numberOfLines = 0
@ -59,9 +55,9 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private let thumbnailActivityIdicator = UIActivityIndicatorView(style: .medium) let thumbnailActivityIdicator = UIActivityIndicatorView(style: .medium)
private var thumbnailImageView: UIImageView = { let thumbnailImageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.clipsToBounds = true imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
@ -69,7 +65,7 @@ class PickServerCell: UITableViewCell {
return imageView return imageView
}() }()
private var infoStackView: UIStackView = { let infoStackView: UIStackView = {
let stackView = UIStackView() let stackView = UIStackView()
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.alignment = .fill stackView.alignment = .fill
@ -78,14 +74,14 @@ class PickServerCell: UITableViewCell {
return stackView return stackView
}() }()
private var expandBox: UIView = { let expandBox: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = .clear view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
return view return view
}() }()
private var expandButton: UIButton = { let expandButton: UIButton = {
let button = UIButton(type: .custom) let button = UIButton(type: .custom)
button.setTitle(L10n.Scene.ServerPicker.Button.seemore, for: .normal) button.setTitle(L10n.Scene.ServerPicker.Button.seemore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeless, for: .selected) button.setTitle(L10n.Scene.ServerPicker.Button.seeless, for: .selected)
@ -95,14 +91,14 @@ class PickServerCell: UITableViewCell {
return button return button
}() }()
private var seperator: UIView = { let seperator: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = Asset.Colors.lightBackground.color view.backgroundColor = Asset.Colors.lightBackground.color
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
return view return view
}() }()
private var langValueLabel: UILabel = { let langValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
@ -112,7 +108,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var usersValueLabel: UILabel = { let usersValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
@ -122,7 +118,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var categoryValueLabel: UILabel = { let categoryValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
@ -132,7 +128,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var langTitleLabel: UILabel = { let langTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2) label.font = .preferredFont(forTextStyle: .caption2)
@ -143,7 +139,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var usersTitleLabel: UILabel = { let usersTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2) label.font = .preferredFont(forTextStyle: .caption2)
@ -154,7 +150,7 @@ class PickServerCell: UITableViewCell {
return label return label
}() }()
private var categoryTitleLabel: UILabel = { let categoryTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2) label.font = .preferredFont(forTextStyle: .caption2)
@ -168,22 +164,12 @@ class PickServerCell: UITableViewCell {
private var collapseConstraints: [NSLayoutConstraint] = [] private var collapseConstraints: [NSLayoutConstraint] = []
private var expandConstraints: [NSLayoutConstraint] = [] private var expandConstraints: [NSLayoutConstraint] = []
var mode: PickServerCell.Mode = .collapse {
didSet {
updateMode()
}
}
var server: Mastodon.Entity.Server? {
didSet {
updateServerInfo()
}
}
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
thumbnailImageView.isHidden = false
thumbnailImageView.af.cancelImageRequest() thumbnailImageView.af.cancelImageRequest()
thumbnailActivityIdicator.stopAnimating()
} }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -195,6 +181,7 @@ class PickServerCell: UITableViewCell {
super.init(coder: coder) super.init(coder: coder)
_init() _init()
} }
} }
// MARK: - Methods to configure appearance // MARK: - Methods to configure appearance
@ -224,7 +211,7 @@ extension PickServerCell {
infoStackView.addArrangedSubview(verticalInfoStackViewUsers) infoStackView.addArrangedSubview(verticalInfoStackViewUsers)
infoStackView.addArrangedSubview(verticalInfoStackViewCategory) 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) collapseConstraints.append(expandButtonTopConstraintInCollapse)
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.defaultHigh) 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.setContentHuggingPriority(.required - 2, for: .vertical)
descriptionLabel.setContentCompressionResistancePriority(.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 { private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView {
@ -305,8 +292,31 @@ extension PickServerCell {
arrangedView.forEach { stackView.addArrangedSubview($0) } arrangedView.forEach { stackView.addArrangedSubview($0) }
return stackView 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 { switch mode {
case .collapse: case .collapse:
expandBox.isHidden = true expandBox.isHidden = true
@ -318,73 +328,35 @@ extension PickServerCell {
expandButton.isSelected = true expandButton.isSelected = true
NSLayoutConstraint.activate(expandConstraints) NSLayoutConstraint.activate(expandConstraints)
NSLayoutConstraint.deactivate(collapseConstraints) NSLayoutConstraint.deactivate(collapseConstraints)
updateThumbnail()
} }
} }
override func setSelected(_ selected: Bool, animated: Bool) { // private func updateThumbnail() {
super.setSelected(selected, animated: animated) // guard let serverInfo = server,
if selected { // let proxiedThumbnail = serverInfo.proxiedThumbnail,
checkbox.image = UIImage(systemName: "checkmark.circle.fill") // let url = URL(string: proxiedThumbnail) else {
} else { // thumbnailImageView.isHidden = true
checkbox.image = UIImage(systemName: "circle") // 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)
}
}
} }

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol PickServerSearchCellDelegate: class { protocol PickServerSearchCellDelegate: class {
func pickServerSearchCell(didChange searchText: String?) func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?)
} }
class PickServerSearchCell: UITableViewCell { class PickServerSearchCell: UITableViewCell {
@ -55,6 +55,12 @@ class PickServerSearchCell: UITableViewCell {
return textField return textField
}() }()
override func prepareForReuse() {
super.prepareForReuse()
delegate = nil
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
_init() _init()
@ -97,7 +103,7 @@ extension PickServerSearchCell {
} }
extension PickServerSearchCell { extension PickServerSearchCell {
@objc func textFieldDidChange(_ textField: UITextField) { @objc private func textFieldDidChange(_ textField: UITextField) {
delegate?.pickServerSearchCell(didChange: textField.text) delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
} }
} }

View File

@ -9,14 +9,14 @@ import UIKit
import MastodonSDK import MastodonSDK
class PickServerCategoryView: UIView { class PickServerCategoryView: UIView {
var category: MastodonPickServerViewModel.Category? { // var category: MastodonPickServerViewModel.Category? {
didSet { // didSet {
updateCategory() // updateCategory()
} // }
} // }
var selected: Bool = false { var selected: Bool = false {
didSet { didSet {
updateSelectStatus() // updateSelectStatus()
} }
} }
@ -56,44 +56,56 @@ extension PickServerCategoryView {
private func configure() { private func configure() {
addSubview(bgView) addSubview(bgView)
addSubview(titleLabel) addSubview(titleLabel)
bgView.backgroundColor = Asset.Colors.lightWhite.color bgView.backgroundColor = Asset.Colors.lightWhite.color
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor), bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor), bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
bgView.topAnchor.constraint(equalTo: self.topAnchor), bgView.topAnchor.constraint(equalTo: self.topAnchor),
bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor), bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor), titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
]) ])
} }
private func updateCategory() { // private func updateCategory() {
guard let category = category else { return } // guard let category = category else { return }
titleLabel.text = category.title // titleLabel.text = category.title
switch category { // switch category {
case .all: // case .all:
titleLabel.font = UIFont.systemFont(ofSize: 17) // titleLabel.font = UIFont.systemFont(ofSize: 17)
case .some: // case .some:
titleLabel.font = UIFont.systemFont(ofSize: 28) // titleLabel.font = UIFont.systemFont(ofSize: 28)
} // }
} // }
//
private func updateSelectStatus() { // private func updateSelectStatus() {
if selected { // if selected {
bgView.backgroundColor = Asset.Colors.lightBrandBlue.color // bgView.backgroundColor = Asset.Colors.lightBrandBlue.color
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0) // bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
if case .all = category { // if case .all = category {
titleLabel.textColor = Asset.Colors.lightWhite.color // titleLabel.textColor = Asset.Colors.lightWhite.color
} // }
} else { // } else {
bgView.backgroundColor = Asset.Colors.lightWhite.color // bgView.backgroundColor = Asset.Colors.lightWhite.color
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0) // bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
if case .all = category { // if case .all = category {
titleLabel.textColor = Asset.Colors.lightBrandBlue.color // titleLabel.textColor = Asset.Colors.lightBrandBlue.color
} // }
// }
// }
}
#if DEBUG && canImport(SwiftUI)
import SwiftUI
struct PickServerCategoryView_Previews: PreviewProvider {
static var previews: some View {
UIViewPreview {
PickServerCategoryView()
} }
} }
} }
#endif

View File

@ -39,8 +39,6 @@ final class MastodonPinBasedAuthenticationViewController: UIViewController, Need
} }
extension MastodonPinBasedAuthenticationViewController { extension MastodonPinBasedAuthenticationViewController {
override func viewDidLoad() { override func viewDidLoad() {

View File

@ -23,7 +23,7 @@ extension APIService {
return Mastodon.API.Onboarding.categories(session: session) 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.Kind.allCases.map { kind in
return Mastodon.Entity.Category(category: kind.rawValue, serversCount: 0) return Mastodon.Entity.Category(category: kind.rawValue, serversCount: 0)
} }