feat: [WIP] display empty state when fetching server list
This commit is contained in:
parent
54c7610c7f
commit
e70fd532c4
|
@ -64,6 +64,10 @@
|
|||
},
|
||||
"input": {
|
||||
"placeholder": "Find a server or join your own..."
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
"bad_network": "Something went wrong while loading data. Check your internet connection."
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
|
@ -150,4 +154,4 @@
|
|||
"title": "Public"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@
|
|||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; };
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||
|
@ -383,6 +384,7 @@
|
|||
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = "<group>"; };
|
||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = "<group>"; };
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||
|
@ -494,6 +496,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1554,6 +1557,7 @@
|
|||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import MastodonSDK
|
||||
import Kanna
|
||||
import AlamofireImage
|
||||
|
||||
enum PickServerSection: Equatable, Hashable {
|
||||
case header
|
||||
|
@ -82,9 +83,41 @@ extension PickServerSection {
|
|||
cell.categoryValueLabel.text = server.category.uppercased()
|
||||
|
||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
||||
// UIView.animate(withDuration: 0.33) {
|
||||
// cell.expandBox.layoutIfNeeded()
|
||||
// }
|
||||
|
||||
cell.expandMode
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { mode in
|
||||
switch mode {
|
||||
case .collapse:
|
||||
// do nothing
|
||||
break
|
||||
case .expand:
|
||||
let placeholderImage = UIImage.placeholder(size: cell.thumbnailImageView.frame.size, color: .systemFill)
|
||||
.af.imageRounded(withCornerRadius: 3.0, divideRadiusByImageScale: false)
|
||||
guard let proxiedThumbnail = server.proxiedThumbnail,
|
||||
let url = URL(string: proxiedThumbnail) else {
|
||||
cell.thumbnailImageView.image = placeholderImage
|
||||
cell.thumbnailActivityIdicator.stopAnimating()
|
||||
return
|
||||
}
|
||||
cell.thumbnailImageView.isHidden = false
|
||||
cell.thumbnailActivityIdicator.startAnimating()
|
||||
|
||||
cell.thumbnailImageView.af.setImage(
|
||||
withURL: url,
|
||||
placeholderImage: placeholderImage,
|
||||
filter: AspectScaledToFillSizeWithRoundedCornersFilter(size: cell.thumbnailImageView.frame.size, radius: 3),
|
||||
imageTransition: .crossDissolve(0.33),
|
||||
completion: { [weak cell] response in
|
||||
switch response.result {
|
||||
case .success, .failure:
|
||||
cell?.thumbnailActivityIdicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
private static func parseUsersCount(_ usersCount: Int) -> String {
|
||||
|
|
|
@ -236,6 +236,12 @@ internal enum L10n {
|
|||
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
|
||||
}
|
||||
}
|
||||
internal enum EmptyState {
|
||||
/// Something went wrong while loading data. Check your internet connection.
|
||||
internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork")
|
||||
/// Finding available servers...
|
||||
internal static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers")
|
||||
}
|
||||
internal enum Input {
|
||||
/// Find a server or join your own...
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder")
|
||||
|
|
|
@ -68,6 +68,8 @@ tap the link to confirm your account.";
|
|||
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||
"Scene.ServerPicker.Button.Seeless" = "See Less";
|
||||
"Scene.ServerPicker.Button.Seemore" = "See More";
|
||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
|
|
|
@ -12,16 +12,19 @@ import Combine
|
|||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
private var tableViewObservation: NSKeyValueObservation?
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: MastodonPickServerViewModel!
|
||||
|
||||
private var isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
private var expandServerDomainSet = Set<String>()
|
||||
|
||||
|
||||
let emptyStateView = PickServerEmptyStateView()
|
||||
let tableViewTopPaddingView = UIView() // fix empty state view background display when tableView bounce scrolling
|
||||
var tableViewTopPaddingViewHeightLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
|
||||
|
@ -45,6 +48,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
}()
|
||||
|
||||
deinit {
|
||||
tableViewObservation = nil
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
|
@ -52,10 +56,6 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .darkContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -70,6 +70,38 @@ extension MastodonPickServerViewController {
|
|||
view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
|
||||
])
|
||||
|
||||
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(emptyStateView)
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStateView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
emptyStateView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
emptyStateView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
nextStepButton.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 7)
|
||||
])
|
||||
|
||||
// fix AutoLayout warning when observe before view appear
|
||||
viewModel.viewWillAppear
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.tableViewObservation = self.tableView.observe(\.contentSize, options: [.initial, .new]) { [weak self] tableView, _ in
|
||||
guard let self = self else { return }
|
||||
self.updateEmptyStateViewLayout()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
tableViewTopPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableViewTopPaddingView)
|
||||
tableViewTopPaddingViewHeightLayoutConstraint = tableViewTopPaddingView.heightAnchor.constraint(equalToConstant: 0.0).priority(.defaultHigh)
|
||||
NSLayoutConstraint.activate([
|
||||
tableViewTopPaddingView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
tableViewTopPaddingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableViewTopPaddingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableViewTopPaddingViewHeightLayoutConstraint,
|
||||
])
|
||||
tableViewTopPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
|
@ -135,15 +167,50 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
isAuthenticating
|
||||
viewModel.isAuthenticating
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isAuthenticating in
|
||||
guard let self = self else { return }
|
||||
isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.emptyStateViewState
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] state in
|
||||
guard let self = self else { return }
|
||||
switch state {
|
||||
case .none:
|
||||
self.emptyStateView.networkIndicatorImageView.isHidden = true
|
||||
self.emptyStateView.activityIndicatorView.stopAnimating()
|
||||
self.emptyStateView.infoLabel.isHidden = true
|
||||
case .loading:
|
||||
self.emptyStateView.networkIndicatorImageView.isHidden = true
|
||||
self.emptyStateView.activityIndicatorView.startAnimating()
|
||||
self.emptyStateView.infoLabel.isHidden = false
|
||||
self.emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
||||
self.emptyStateView.infoLabel.textAlignment = self.traitCollection.layoutDirection == .rightToLeft ? .right : .left
|
||||
case .badNetwork:
|
||||
self.emptyStateView.networkIndicatorImageView.isHidden = false
|
||||
self.emptyStateView.activityIndicatorView.stopAnimating()
|
||||
self.emptyStateView.infoLabel.isHidden = false
|
||||
self.emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.badNetwork
|
||||
self.emptyStateView.infoLabel.textAlignment = .center
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
viewModel.viewWillAppear.send()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
@objc
|
||||
private func nextStepButtonDidClicked(_ sender: UIButton) {
|
||||
switch viewModel.mode {
|
||||
|
@ -156,7 +223,7 @@ extension MastodonPickServerViewController {
|
|||
|
||||
private func doSignIn() {
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
isAuthenticating.send(true)
|
||||
viewModel.isAuthenticating.send(true)
|
||||
context.apiService.createApplication(domain: server.domain)
|
||||
.tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
|
@ -168,7 +235,7 @@ extension MastodonPickServerViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.isAuthenticating.send(false)
|
||||
self.viewModel.isAuthenticating.send(false)
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
|
@ -196,7 +263,7 @@ extension MastodonPickServerViewController {
|
|||
private func doSignUp() {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
isAuthenticating.send(true)
|
||||
viewModel.isAuthenticating.send(true)
|
||||
|
||||
context.apiService.instance(domain: server.domain)
|
||||
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
|
||||
|
@ -232,7 +299,7 @@ extension MastodonPickServerViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.isAuthenticating.send(false)
|
||||
self.viewModel.isAuthenticating.send(false)
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
|
@ -266,10 +333,23 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension MastodonPickServerViewController: UITableViewDelegate {
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard scrollView === tableView else { return }
|
||||
let offsetY = scrollView.contentOffset.y + scrollView.safeAreaInsets.top
|
||||
if offsetY < 0 {
|
||||
tableViewTopPaddingViewHeightLayoutConstraint.constant = abs(offsetY)
|
||||
} else {
|
||||
tableViewTopPaddingViewHeightLayoutConstraint.constant = 0
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
return UIView()
|
||||
let headerView = UIView()
|
||||
headerView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
return headerView
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
@ -320,6 +400,15 @@ extension MastodonPickServerViewController: UITableViewDelegate {
|
|||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
private func updateEmptyStateViewLayout() {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return }
|
||||
guard let indexPath = diffableDataSource.indexPath(for: .search) else { return }
|
||||
let rectInTableView = tableView.rectForRow(at: indexPath)
|
||||
|
||||
emptyStateView.topPaddingViewTopLayoutConstraint.constant = rectInTableView.maxY
|
||||
}
|
||||
}
|
||||
//extension MastodonPickServerViewController: UITableViewDataSource {
|
||||
|
||||
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
|
|
@ -41,6 +41,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
|||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
||||
viewModel.isLoadingIndexedServers.value = true
|
||||
viewModel.context.apiService.servers(language: nil, category: nil)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
|
@ -67,9 +68,9 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
|||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
||||
guard let stateMachine = self.stateMachine else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let _ = self else { return }
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +80,13 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
|||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
||||
viewModel.isLoadingIndexedServers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,11 +13,18 @@ import MastodonSDK
|
|||
import CoreDataStack
|
||||
|
||||
class MastodonPickServerViewModel: NSObject {
|
||||
|
||||
enum PickServerMode {
|
||||
case signUp
|
||||
case signIn
|
||||
}
|
||||
|
||||
enum EmptyStateViewState {
|
||||
case none
|
||||
case loading
|
||||
case badNetwork
|
||||
}
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
|
@ -33,6 +40,7 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let searchText = CurrentValueSubject<String?, Never>(nil)
|
||||
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([])
|
||||
let viewWillAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<PickServerSection, PickServerItem>?
|
||||
|
@ -47,12 +55,15 @@ class MastodonPickServerViewModel: NSObject {
|
|||
stateMachine.enter(LoadIndexedServerState.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
|
||||
let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
|
||||
let selectedServer = CurrentValueSubject<Mastodon.Entity.Server?, Never>(nil)
|
||||
let error = PassthroughSubject<Error, Never>()
|
||||
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
|
||||
|
||||
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false)
|
||||
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
|
||||
|
||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
|
||||
init(context: AppContext, mode: PickServerMode) {
|
||||
|
@ -99,6 +110,16 @@ class MastodonPickServerViewModel: NSObject {
|
|||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
isLoadingIndexedServers
|
||||
.map { isLoadingIndexedServers -> EmptyStateViewState in
|
||||
if isLoadingIndexedServers {
|
||||
return .loading
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
.assign(to: \.value, on: emptyStateViewState)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// Publishers.CombineLatest3(
|
||||
// selectCategoryIndex,
|
||||
|
|
|
@ -54,8 +54,8 @@ final class PickServerCategoriesCell: UITableViewCell {
|
|||
extension PickServerCategoriesCell {
|
||||
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
metricView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(metricView)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import AlamofireImage
|
||||
import Kanna
|
||||
|
@ -19,6 +20,10 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
weak var delegate: PickServerCellDelegate?
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let expandMode = CurrentValueSubject<ExpandMode, Never>(.collapse)
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16)
|
||||
|
@ -170,6 +175,7 @@ class PickServerCell: UITableViewCell {
|
|||
thumbnailImageView.isHidden = false
|
||||
thumbnailImageView.af.cancelImageRequest()
|
||||
thumbnailActivityIdicator.stopAnimating()
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
@ -329,34 +335,8 @@ extension PickServerCell {
|
|||
NSLayoutConstraint.activate(expandConstraints)
|
||||
NSLayoutConstraint.deactivate(collapseConstraints)
|
||||
}
|
||||
|
||||
expandMode.value = mode
|
||||
}
|
||||
|
||||
// private func updateThumbnail() {
|
||||
// guard let serverInfo = server,
|
||||
// let proxiedThumbnail = serverInfo.proxiedThumbnail,
|
||||
// let url = URL(string: proxiedThumbnail) else {
|
||||
// thumbnailImageView.isHidden = true
|
||||
// thumbnailActivityIdicator.stopAnimating()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// thumbnailImageView.isHidden = false
|
||||
// thumbnailActivityIdicator.startAnimating()
|
||||
//
|
||||
// let placeholderImage = UIImage.placeholder(color: .systemFill).af.imageRounded(withCornerRadius: 3.0, divideRadiusByImageScale: true)
|
||||
// thumbnailImageView.af.setImage(
|
||||
// withURL: url,
|
||||
// placeholderImage: placeholderImage,
|
||||
// filter: AspectScaledToFillSizeWithRoundedCornersFilter(size: thumbnailImageView.frame.size, radius: 3),
|
||||
// imageTransition: .crossDissolve(0.33),
|
||||
// completion: { [weak self] response in
|
||||
// guard let self = self else { return }
|
||||
// switch response.result {
|
||||
// case .success, .failure:
|
||||
// self.thumbnailActivityIdicator.stopAnimating()
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ class PickServerSearchCell: UITableViewCell {
|
|||
|
||||
extension PickServerSearchCell {
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
|
|
|
@ -34,8 +34,8 @@ final class PickServerTitleCell: UITableViewCell {
|
|||
extension PickServerTitleCell {
|
||||
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// PickServerEmptyStateView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021/3/6.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class PickServerEmptyStateView: UIView {
|
||||
|
||||
var topPaddingViewTopLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let networkIndicatorImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let configuration = UIImage.SymbolConfiguration(pointSize: 64, weight: .regular)
|
||||
imageView.image = UIImage(systemName: "wifi.exclamationmark", withConfiguration: configuration)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
return imageView
|
||||
}()
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
let infoLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 17)
|
||||
label.textAlignment = .center
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "info"
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerEmptyStateView {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||
|
||||
let topPaddingView = UIView()
|
||||
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(topPaddingView)
|
||||
topPaddingViewTopLayoutConstraint = topPaddingView.topAnchor.constraint(equalTo: topAnchor, constant: 0)
|
||||
NSLayoutConstraint.activate([
|
||||
topPaddingViewTopLayoutConstraint,
|
||||
topPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
topPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
|
||||
let containerStackView = UIStackView()
|
||||
containerStackView.axis = .vertical
|
||||
containerStackView.alignment = .center
|
||||
containerStackView.distribution = .fill
|
||||
containerStackView.spacing = 16
|
||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(containerStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerStackView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
containerStackView.addArrangedSubview(networkIndicatorImageView)
|
||||
|
||||
let infoContainerView = UIView()
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
infoContainerView.addSubview(activityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
activityIndicatorView.leadingAnchor.constraint(equalTo: infoContainerView.leadingAnchor),
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: infoContainerView.centerYAnchor),
|
||||
activityIndicatorView.bottomAnchor.constraint(equalTo: infoContainerView.bottomAnchor),
|
||||
])
|
||||
infoLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
infoContainerView.addSubview(infoLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
infoLabel.leadingAnchor.constraint(equalTo: activityIndicatorView.trailingAnchor, constant: 4),
|
||||
infoLabel.centerYAnchor.constraint(equalTo: infoContainerView.centerYAnchor),
|
||||
infoLabel.trailingAnchor.constraint(equalTo: infoContainerView.trailingAnchor),
|
||||
])
|
||||
containerStackView.addArrangedSubview(infoContainerView)
|
||||
|
||||
let bottomPaddingView = UIView()
|
||||
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(bottomPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
bottomPaddingView.topAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||
bottomPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
bottomPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomPaddingView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 2.0),
|
||||
])
|
||||
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG && canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
struct PickServerEmptyStateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let emptyStateView = PickServerEmptyStateView()
|
||||
emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.badNetwork
|
||||
emptyStateView.infoLabel.textAlignment = .center
|
||||
emptyStateView.activityIndicatorView.stopAnimating()
|
||||
return emptyStateView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 400))
|
||||
UIViewPreview(width: 375) {
|
||||
let emptyStateView = PickServerEmptyStateView()
|
||||
emptyStateView.infoLabel.text = L10n.Scene.ServerPicker.EmptyState.findingServers
|
||||
emptyStateView.infoLabel.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .right : .left
|
||||
emptyStateView.activityIndicatorView.startAnimating()
|
||||
return emptyStateView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 400))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue