feat: [WIP] display empty state when fetching server list
This commit is contained in:
parent
54c7610c7f
commit
e70fd532c4
|
@ -64,6 +64,10 @@
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": "Find a server or join your own..."
|
"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": {
|
"register": {
|
||||||
|
@ -150,4 +154,4 @@
|
||||||
"title": "Public"
|
"title": "Public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,7 @@
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.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 */; };
|
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 */; };
|
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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -494,6 +496,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
||||||
|
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1554,6 +1557,7 @@
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||||
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
|
||||||
|
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import Kanna
|
import Kanna
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
enum PickServerSection: Equatable, Hashable {
|
enum PickServerSection: Equatable, Hashable {
|
||||||
case header
|
case header
|
||||||
|
@ -82,9 +83,41 @@ extension PickServerSection {
|
||||||
cell.categoryValueLabel.text = server.category.uppercased()
|
cell.categoryValueLabel.text = server.category.uppercased()
|
||||||
|
|
||||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
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 {
|
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 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 {
|
internal enum Input {
|
||||||
/// 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")
|
||||||
|
|
|
@ -68,6 +68,8 @@ tap the link to confirm your account.";
|
||||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||||
"Scene.ServerPicker.Button.Seeless" = "See Less";
|
"Scene.ServerPicker.Button.Seeless" = "See Less";
|
||||||
"Scene.ServerPicker.Button.Seemore" = "See More";
|
"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.Input.Placeholder" = "Find a server or join your own...";
|
||||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||||
|
|
|
@ -12,16 +12,19 @@ import Combine
|
||||||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
private var disposeBag = Set<AnyCancellable>()
|
private var disposeBag = Set<AnyCancellable>()
|
||||||
|
private var tableViewObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
var viewModel: MastodonPickServerViewModel!
|
var viewModel: MastodonPickServerViewModel!
|
||||||
|
|
||||||
private var isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
|
||||||
|
|
||||||
private var expandServerDomainSet = Set<String>()
|
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: UITableView = {
|
||||||
let tableView = ControlContainableTableView()
|
let tableView = ControlContainableTableView()
|
||||||
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
|
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
|
||||||
|
@ -45,6 +48,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
||||||
}()
|
}()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
tableViewObservation = nil
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
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 {
|
extension MastodonPickServerViewController {
|
||||||
|
|
||||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
||||||
return .darkContent
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -70,6 +70,38 @@ extension MastodonPickServerViewController {
|
||||||
view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
|
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)
|
view.addSubview(tableView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
@ -135,15 +167,50 @@ extension MastodonPickServerViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
isAuthenticating
|
viewModel.isAuthenticating
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] isAuthenticating in
|
.sink { [weak self] isAuthenticating in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading()
|
isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading()
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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
|
@objc
|
||||||
private func nextStepButtonDidClicked(_ sender: UIButton) {
|
private func nextStepButtonDidClicked(_ sender: UIButton) {
|
||||||
switch viewModel.mode {
|
switch viewModel.mode {
|
||||||
|
@ -156,7 +223,7 @@ extension MastodonPickServerViewController {
|
||||||
|
|
||||||
private func doSignIn() {
|
private func doSignIn() {
|
||||||
guard let server = viewModel.selectedServer.value else { return }
|
guard let server = viewModel.selectedServer.value else { return }
|
||||||
isAuthenticating.send(true)
|
viewModel.isAuthenticating.send(true)
|
||||||
context.apiService.createApplication(domain: server.domain)
|
context.apiService.createApplication(domain: server.domain)
|
||||||
.tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in
|
.tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in
|
||||||
let application = response.value
|
let application = response.value
|
||||||
|
@ -168,7 +235,7 @@ extension MastodonPickServerViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.isAuthenticating.send(false)
|
self.viewModel.isAuthenticating.send(false)
|
||||||
|
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -196,7 +263,7 @@ extension MastodonPickServerViewController {
|
||||||
private func doSignUp() {
|
private func doSignUp() {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
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 }
|
guard let server = viewModel.selectedServer.value else { return }
|
||||||
isAuthenticating.send(true)
|
viewModel.isAuthenticating.send(true)
|
||||||
|
|
||||||
context.apiService.instance(domain: server.domain)
|
context.apiService.instance(domain: server.domain)
|
||||||
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
|
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
|
||||||
|
@ -232,7 +299,7 @@ extension MastodonPickServerViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.isAuthenticating.send(false)
|
self.viewModel.isAuthenticating.send(false)
|
||||||
|
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -266,10 +333,23 @@ extension MastodonPickServerViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
extension MastodonPickServerViewController: 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? {
|
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 {
|
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 {
|
//extension MastodonPickServerViewController: UITableViewDataSource {
|
||||||
|
|
||||||
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
|
|
@ -41,6 +41,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
||||||
super.didEnter(from: previousState)
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
||||||
|
viewModel.isLoadingIndexedServers.value = true
|
||||||
viewModel.context.apiService.servers(language: nil, category: nil)
|
viewModel.context.apiService.servers(language: nil, category: nil)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
|
@ -67,9 +68,9 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
||||||
override func didEnter(from previousState: GKState?) {
|
override func didEnter(from previousState: GKState?) {
|
||||||
super.didEnter(from: previousState)
|
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
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let _ = self else { return }
|
||||||
stateMachine.enter(Loading.self)
|
stateMachine.enter(Loading.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,6 +80,13 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
||||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
return false
|
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
|
import CoreDataStack
|
||||||
|
|
||||||
class MastodonPickServerViewModel: NSObject {
|
class MastodonPickServerViewModel: NSObject {
|
||||||
|
|
||||||
enum PickServerMode {
|
enum PickServerMode {
|
||||||
case signUp
|
case signUp
|
||||||
case signIn
|
case signIn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EmptyStateViewState {
|
||||||
|
case none
|
||||||
|
case loading
|
||||||
|
case badNetwork
|
||||||
|
}
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
|
@ -33,6 +40,7 @@ class MastodonPickServerViewModel: NSObject {
|
||||||
let searchText = CurrentValueSubject<String?, Never>(nil)
|
let searchText = CurrentValueSubject<String?, Never>(nil)
|
||||||
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||||
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([])
|
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([])
|
||||||
|
let viewWillAppear = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<PickServerSection, PickServerItem>?
|
var diffableDataSource: UITableViewDiffableDataSource<PickServerSection, PickServerItem>?
|
||||||
|
@ -47,12 +55,15 @@ class MastodonPickServerViewModel: NSObject {
|
||||||
stateMachine.enter(LoadIndexedServerState.Initial.self)
|
stateMachine.enter(LoadIndexedServerState.Initial.self)
|
||||||
return stateMachine
|
return stateMachine
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
|
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>()
|
||||||
|
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
let isLoadingIndexedServers = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
|
||||||
|
|
||||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||||
|
|
||||||
init(context: AppContext, mode: PickServerMode) {
|
init(context: AppContext, mode: PickServerMode) {
|
||||||
|
@ -99,6 +110,16 @@ class MastodonPickServerViewModel: NSObject {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.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(
|
// Publishers.CombineLatest3(
|
||||||
// selectCategoryIndex,
|
// selectCategoryIndex,
|
||||||
|
|
|
@ -54,8 +54,8 @@ final class PickServerCategoriesCell: UITableViewCell {
|
||||||
extension PickServerCategoriesCell {
|
extension PickServerCategoriesCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
self.selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
metricView.translatesAutoresizingMaskIntoConstraints = false
|
metricView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(metricView)
|
contentView.addSubview(metricView)
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import Kanna
|
import Kanna
|
||||||
|
@ -19,6 +20,10 @@ class PickServerCell: UITableViewCell {
|
||||||
|
|
||||||
weak var delegate: PickServerCellDelegate?
|
weak var delegate: PickServerCellDelegate?
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let expandMode = CurrentValueSubject<ExpandMode, Never>(.collapse)
|
||||||
|
|
||||||
let containerView: UIView = {
|
let 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)
|
||||||
|
@ -170,6 +175,7 @@ class PickServerCell: UITableViewCell {
|
||||||
thumbnailImageView.isHidden = false
|
thumbnailImageView.isHidden = false
|
||||||
thumbnailImageView.af.cancelImageRequest()
|
thumbnailImageView.af.cancelImageRequest()
|
||||||
thumbnailActivityIdicator.stopAnimating()
|
thumbnailActivityIdicator.stopAnimating()
|
||||||
|
disposeBag.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
@ -329,34 +335,8 @@ extension PickServerCell {
|
||||||
NSLayoutConstraint.activate(expandConstraints)
|
NSLayoutConstraint.activate(expandConstraints)
|
||||||
NSLayoutConstraint.deactivate(collapseConstraints)
|
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 {
|
extension PickServerSearchCell {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
self.selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,8 @@ final class PickServerTitleCell: UITableViewCell {
|
||||||
extension PickServerTitleCell {
|
extension PickServerTitleCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
self.selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
contentView.addSubview(titleLabel)
|
contentView.addSubview(titleLabel)
|
||||||
NSLayoutConstraint.activate([
|
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