feat: [WIP] display empty state when fetching server list

This commit is contained in:
CMK 2021-03-06 12:55:52 +08:00
parent 54c7610c7f
commit e70fd532c4
13 changed files with 337 additions and 55 deletions

View File

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

View File

@ -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 */,

View File

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

View File

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

View File

@ -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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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([

View File

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