feat: implement pick server view search cell & server list cell

This commit is contained in:
jk234ert 2021-02-24 22:47:42 +08:00
parent eb7a33932e
commit 027fec1cc9
9 changed files with 596 additions and 29 deletions

View File

@ -16,6 +16,8 @@
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; }; 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; };
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; }; 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; };
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; }; 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; };
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; };
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; }; 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
@ -214,6 +216,8 @@
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = "<group>"; }; 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = "<group>"; };
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; }; 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; };
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; }; 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; };
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; }; 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; }; 2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; }; 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
@ -438,6 +442,8 @@
children = ( children = (
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */, 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */,
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
); );
path = TableViewCell; path = TableViewCell;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1370,6 +1376,7 @@
0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
@ -1417,6 +1424,7 @@
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */, DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,

View File

@ -111,6 +111,10 @@ internal enum L10n {
/// Pick a Server,\nany server. /// Pick a Server,\nany server.
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title") internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
internal enum Button { internal enum Button {
/// See less
internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess")
/// See More
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
internal enum Category { internal enum Category {
/// All /// All
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All") internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
@ -120,6 +124,14 @@ internal enum L10n {
/// Find a server or join your own... /// Find a server or join your own...
internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder") internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder")
} }
internal enum Label {
/// CATEGORY
internal static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category")
/// LANGUAGE
internal static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language")
/// USERS
internal static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users")
}
} }
internal enum ServerRules { internal enum ServerRules {
/// By continuing, you're subject to the terms of service and privacy policy for %@. /// By continuing, you're subject to the terms of service and privacy policy for %@.

View File

@ -33,6 +33,12 @@
"Scene.ServerPicker.Title" = "Pick a Server, "Scene.ServerPicker.Title" = "Pick a Server,
any server."; any server.";
"Scene.ServerPicker.Button.Category.All" = "All"; "Scene.ServerPicker.Button.Category.All" = "All";
"Scene.ServerPicker.Button.SeeLess" = "See less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerRules.Button.Confirm" = "I Agree"; "Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; "Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";

View File

@ -32,7 +32,8 @@ final class PickServerViewController: UIViewController, NeedsDependency {
let tableView = ControlContainableTableView() let tableView = ControlContainableTableView()
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self)) tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self)) tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
// tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
@ -83,7 +84,20 @@ extension PickServerViewController {
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
} }
viewModel.tableView = tableView
tableView.delegate = viewModel tableView.delegate = viewModel
tableView.dataSource = viewModel tableView.dataSource = viewModel
viewModel.searchedServers
.receive(on: DispatchQueue.main)
.sink { completion in
print("22")
} receiveValue: { [weak self] servers in
self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic)
}
.store(in: &disposeBag)
viewModel.fetchAllServers()
} }
} }

View File

@ -18,6 +18,7 @@ class PickServerViewModel: NSObject {
enum Section: CaseIterable { enum Section: CaseIterable {
case title case title
case categories case categories
case search
case serverList case serverList
} }
@ -32,23 +33,24 @@ class PickServerViewModel: NSObject {
case .All: case .All:
return L10n.Scene.ServerPicker.Button.Category.all return L10n.Scene.ServerPicker.Button.Category.all
case .Some(let masCategory): case .Some(let masCategory):
// TODO: Use emoji as placeholders
switch masCategory.category { switch masCategory.category {
case .academia: case .academia:
return "AC" return "📚"
case .activism: case .activism:
return "AT" return ""
case .food: case .food:
return "F" return "🍕"
case .furry: case .furry:
return "FU" return "🦁"
case .games: case .games:
return "G" return "🕹"
case .general: case .general:
return "GE" return "GE"
case .journalism: case .journalism:
return "JO" return "📰"
case .lgbt: case .lgbt:
return "LG" return "🏳️‍🌈"
case .regional: case .regional:
return "📍" return "📍"
case .art: case .art:
@ -58,7 +60,7 @@ class PickServerViewModel: NSObject {
case .tech: case .tech:
return "📱" return "📱"
case ._other: case ._other:
return "UN" return ""
} }
} }
} }
@ -72,11 +74,15 @@ class PickServerViewModel: NSObject {
let searchText = CurrentValueSubject<String?, Never>(nil) let searchText = CurrentValueSubject<String?, Never>(nil)
let allServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([]) let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Error>([]) let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
let nextButtonEnable = CurrentValueSubject<Bool, Never>(false) let nextButtonEnable = CurrentValueSubject<Bool, Never>(false)
private var disposeBag = Set<AnyCancellable>()
weak var tableView: UITableView?
init(context: AppContext, mode: PickServerMode) { init(context: AppContext, mode: PickServerMode) {
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -89,20 +95,89 @@ class PickServerViewModel: NSObject {
let masCategories = context.apiService.stubCategories() let masCategories = context.apiService.stubCategories()
categories.append(.All) categories.append(.All)
categories.append(contentsOf: masCategories.map { Category.Some($0) }) categories.append(contentsOf: masCategories.map { Category.Some($0) })
Publishers.CombineLatest3(
selectCategoryIndex,
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
allServers
)
.flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<[Mastodon.Entity.Server], Error> in
guard let self = self else { return Just([]).setFailureType(to: Error.self).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(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
}
// 2. No server found in the recorded list, check if searchText is a valid mastodon server domain
if let toSearchText = searchText, !toSearchText.isEmpty {
return self.context.apiService.instance(domain: toSearchText)
.map { return [Mastodon.Entity.Server(instance: $0.value)] }.eraseToAnyPublisher()
}
return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
}
.sink { completion in
print("1")
} receiveValue: { [weak self] servers in
self?.searchedServers.send(servers)
}
.store(in: &disposeBag)
}
func fetchAllServers() {
context.apiService.servers(language: nil, category: nil)
.receive(on: DispatchQueue.main)
.sink { error in
print("11")
} 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 {
return $0.domain.contains(searchText)
} else {
return true
}
}
} }
} }
extension PickServerViewModel: UITableViewDelegate { extension PickServerViewModel: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 { let category = Section.allCases[section]
switch category {
case .title:
return 20 return 20
} case .categories:
else if section == 1 { // 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 return 10
} case .search:
else { // Same reason as above
return 10 return 10
case .serverList:
// Header with 1 height as the separator
return 1
} }
} }
@ -121,7 +196,8 @@ extension PickServerViewModel: UITableViewDataSource {
let section = Self.Section.allCases[section] let section = Self.Section.allCases[section]
switch section { switch section {
case .title, case .title,
.categories: .categories,
.search:
return 1 return 1
case .serverList: case .serverList:
return searchedServers.value.count return searchedServers.value.count
@ -140,8 +216,15 @@ extension PickServerViewModel: UITableViewDataSource {
cell.dataSource = self cell.dataSource = self
cell.delegate = self cell.delegate = self
return cell return cell
case .search:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell
cell.delegate = self
return cell
case .serverList: case .serverList:
return UITableViewCell(style: .default, reuseIdentifier: "1") let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
cell.server = searchedServers.value[indexPath.row]
cell.delegate = self
return cell
} }
} }
} }
@ -163,3 +246,17 @@ extension PickServerViewModel: PickServerCategoriesDataSource, PickServerCategor
selectCategoryIndex.send(index) selectCategoryIndex.send(index)
} }
} }
extension PickServerViewModel: PickServerSearchCellDelegate {
func pickServerSearchCell(didChange searchText: String?) {
self.searchText.send(searchText)
}
}
extension PickServerViewModel: PickServerCellDelegate {
func pickServerCell(modeChange updates: (() -> Void)) {
tableView?.beginUpdates()
tableView?.performBatchUpdates(updates, completion: nil)
tableView?.endUpdates()
}
}

View File

@ -0,0 +1,322 @@
//
// PickServerCell.swift
// Mastodon
//
// Created by BradGao on 2021/2/24.
//
import UIKit
import MastodonSDK
import Kingfisher
protocol PickServerCellDelegate: class {
func pickServerCell(modeChange updates: (() -> Void))
}
class PickServerCell: UITableViewCell {
weak var delegate: PickServerCellDelegate?
enum Mode {
case collapse
case expand
}
private var bgView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.lightWhite.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private var domainLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline)
label.textColor = Asset.Colors.lightDarkGray.color
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var checkbox: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private var descriptionLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 0
label.textColor = Asset.Colors.lightDarkGray.color
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var thumbImageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private var infoStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
private var expandBox: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private var expandButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var seperator: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.lightBackground.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private var langValueLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var usersValueLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var categoryValueLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var langTitleLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2)
label.text = L10n.Scene.ServerPicker.Label.language
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var usersTitleLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2)
label.text = L10n.Scene.ServerPicker.Label.users
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var categoryTitleLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.lightDarkGray.color
label.font = .preferredFont(forTextStyle: .caption2)
label.text = L10n.Scene.ServerPicker.Label.category
label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private var collapseConstraints: [NSLayoutConstraint] = []
private var expandConstraints: [NSLayoutConstraint] = []
var mode: PickServerCell.Mode = .collapse {
didSet {
updateMode()
}
}
var server: Mastodon.Entity.Server? {
didSet {
updateServerInfo()
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
// MARK: - Methods to configure appearance
extension PickServerCell {
private func _init() {
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(bgView)
contentView.addSubview(domainLabel)
contentView.addSubview(checkbox)
contentView.addSubview(descriptionLabel)
contentView.addSubview(seperator)
contentView.addSubview(expandButton)
// Always add the expandbox which contains elements only visible in expand mode
contentView.addSubview(expandBox)
expandBox.addSubview(thumbImageView)
expandBox.addSubview(infoStackView)
expandBox.isHidden = true
let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel)
let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel)
let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel)
infoStackView.addArrangedSubview(verticalInfoStackViewLang)
infoStackView.addArrangedSubview(verticalInfoStackViewUsers)
infoStackView.addArrangedSubview(verticalInfoStackViewCategory)
let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12)
collapseConstraints.append(expandButtonTopConstraintInCollapse)
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8)
expandConstraints.append(expandButtonTopConstraintInExpand)
NSLayoutConstraint.activate([
// Set background view
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: bgView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: bgView.bottomAnchor, constant: 1),
// Set bottom separator
seperator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: seperator.bottomAnchor),
seperator.heightAnchor.constraint(equalToConstant: 1),
domainLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
domainLabel.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 16),
checkbox.widthAnchor.constraint(equalToConstant: 23),
checkbox.heightAnchor.constraint(equalToConstant: 22),
bgView.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 16),
checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16),
descriptionLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
descriptionLabel.topAnchor.constraint(equalTo: domainLabel.firstBaselineAnchor, constant: 8),
bgView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor, constant: 16),
// Set expandBox constraints
expandBox.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
bgView.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor, constant: 16),
expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8),
expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor),
thumbImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
expandBox.trailingAnchor.constraint(equalTo: thumbImageView.trailingAnchor),
thumbImageView.topAnchor.constraint(equalTo: expandBox.topAnchor),
thumbImageView.heightAnchor.constraint(equalTo: thumbImageView.widthAnchor, multiplier: 151.0 / 303.0),
infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor),
infoStackView.topAnchor.constraint(equalTo: thumbImageView.bottomAnchor, constant: 16),
expandButton.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
bgView.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor, constant: 16),
bgView.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor, constant: 8),
])
NSLayoutConstraint.activate(collapseConstraints)
expandButton.addTarget(self, action: #selector(expandButtonDidClicked(_:)), for: .touchUpInside)
}
private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .equalCentering
stackView.spacing = 2
arrangedView.forEach { stackView.addArrangedSubview($0) }
return stackView
}
private func updateMode() {
switch mode {
case .collapse:
expandBox.isHidden = true
NSLayoutConstraint.deactivate(expandConstraints)
NSLayoutConstraint.activate(collapseConstraints)
case .expand:
expandBox.isHidden = false
NSLayoutConstraint.activate(expandConstraints)
NSLayoutConstraint.deactivate(collapseConstraints)
}
}
@objc
private func expandButtonDidClicked(_ sender: UIButton) {
delegate?.pickServerCell(modeChange: {
let newMode: Mode = mode == .collapse ? .expand : .collapse
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 = serverInfo.description
let processor = RoundCornerImageProcessor(cornerRadius: 3)
thumbImageView.kf.indicatorType = .activity
thumbImageView.kf.setImage(with: URL(string: serverInfo.proxiedThumbnail ?? "")!, placeholder: UIImage.placeholder(color: .yellow), options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(1))
])
langValueLabel.text = serverInfo.language.uppercased()
usersValueLabel.text = "\(serverInfo.totalUsers)"
categoryValueLabel.text = serverInfo.category.uppercased()
}
}

View File

@ -0,0 +1,101 @@
//
// PickServerSearchCell.swift
// Mastodon
//
// Created by BradGao on 2021/2/24.
//
import UIKit
protocol PickServerSearchCellDelegate: class {
func pickServerSearchCell(didChange searchText: String?)
}
class PickServerSearchCell: UITableViewCell {
weak var delegate: PickServerSearchCellDelegate?
private var bgView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.lightWhite.color
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.maskedCorners = [
.layerMinXMinYCorner,
.layerMaxXMinYCorner
]
view.layer.cornerCurve = .continuous
view.layer.cornerRadius = 10
return view
}()
private var textFieldBgView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.lightBackground.color.withAlphaComponent(0.6)
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.masksToBounds = true
view.layer.cornerRadius = 6
view.layer.cornerCurve = .continuous
return view
}()
private var searchTextField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.font = .preferredFont(forTextStyle: .headline)
textField.tintColor = Asset.Colors.lightDarkGray.color
textField.textColor = Asset.Colors.lightDarkGray.color
textField.adjustsFontForContentSizeCategory = true
textField.attributedPlaceholder =
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
attributes: [.font: UIFont.preferredFont(forTextStyle: .headline),
.foregroundColor: Asset.Colors.lightSecondaryText.color.withAlphaComponent(0.6)])
textField.clearButtonMode = .whileEditing
return textField
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension PickServerSearchCell {
private func _init() {
self.selectionStyle = .none
backgroundColor = .clear
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
contentView.addSubview(bgView)
contentView.addSubview(textFieldBgView)
contentView.addSubview(searchTextField)
NSLayoutConstraint.activate([
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
bgView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
])
}
}
extension PickServerSearchCell {
@objc func textFieldDidChange(_ textField: UITextField) {
delegate?.pickServerSearchCell(didChange: textField.text)
}
}

View File

@ -54,20 +54,12 @@ class PickServerCategoryView: UIView {
extension PickServerCategoryView { extension PickServerCategoryView {
private func configure() { private func configure() {
// bgShadowView.backgroundColor = nil
// addSubview(bgShadowView)
// bgShadowView.addSubview(bgView)
addSubview(bgView) addSubview(bgView)
addSubview(titleLabel) addSubview(titleLabel)
bgView.backgroundColor = .white bgView.backgroundColor = Asset.Colors.lightWhite.color
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
// bgShadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
// bgShadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
// bgShadowView.topAnchor.constraint(equalTo: self.topAnchor),
// bgShadowView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
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),
@ -94,10 +86,10 @@ extension PickServerCategoryView {
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 = .white titleLabel.textColor = Asset.Colors.lightWhite.color
} }
} else { } else {
bgView.backgroundColor = .white 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.lightBackground.color titleLabel.textColor = Asset.Colors.lightBackground.color

View File

@ -37,6 +37,21 @@ extension Mastodon.Entity {
case language case language
case category case category
} }
public init(instance: Instance) {
self.domain = instance.title
self.version = "\(instance.version)"
self.description = instance.description
self.language = instance.languages?.first ?? ""
self.languages = instance.languages ?? []
self.region = "Unknown" // TODO: how to handle properties not in an instance
self.categories = []
self.category = "Unknown"
self.proxiedThumbnail = instance.thumbnail
self.totalUsers = instance.statistics?.userCount ?? 0
self.lastWeekUsers = 0
self.approvalRequired = instance.approvalRequired ?? false
}
} }
} }