diff --git a/Localization/app.json b/Localization/app.json index 9c438b568..58807fc2f 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -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" } } -} \ No newline at end of file +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index cfe6a3ed0..ecec29164 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; }; + DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = ""; }; DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; @@ -494,6 +496,7 @@ isa = PBXGroup; children = ( 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */, + DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */, ); path = View; sourceTree = ""; @@ -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 */, diff --git a/Mastodon/Diffiable/Section/PickServerSection.swift b/Mastodon/Diffiable/Section/PickServerSection.swift index d76cf4c66..813ef9b20 100644 --- a/Mastodon/Diffiable/Section/PickServerSection.swift +++ b/Mastodon/Diffiable/Section/PickServerSection.swift @@ -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 { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 75a661fad..035c0011a 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -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") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 0ab1c70e4..4cf8ea52e 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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"; diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 06e75c0cd..d803e0054 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -12,16 +12,19 @@ import Combine final class MastodonPickServerViewController: UIViewController, NeedsDependency { private var disposeBag = Set() + 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(false) - private var expandServerDomainSet = Set() - + + 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? 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 { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift index 172973b5c..85c6ab289 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift @@ -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 + } } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 2e764f9b1..8953e2ed9 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -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() // input @@ -33,6 +40,7 @@ class MastodonPickServerViewModel: NSObject { let searchText = CurrentValueSubject(nil) let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Instance], Never>([]) + let viewWillAppear = PassthroughSubject() // output var diffableDataSource: UITableViewDiffableDataSource? @@ -47,12 +55,15 @@ class MastodonPickServerViewModel: NSObject { stateMachine.enter(LoadIndexedServerState.Initial.self) return stateMachine }() - let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let selectedServer = CurrentValueSubject(nil) let error = PassthroughSubject() let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() - + let isAuthenticating = CurrentValueSubject(false) + + let isLoadingIndexedServers = CurrentValueSubject(false) + let emptyStateViewState = CurrentValueSubject(.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, diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 1fd366555..66f8caac1 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -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) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index cadfc74ba..95a5491cc 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -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() + + let expandMode = CurrentValueSubject(.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() -// } -// } -// ) -// } - } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift index 2de66fa65..510df3a0b 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift @@ -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) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift index 82d155535..30d24ddc0 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift @@ -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([ diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift new file mode 100644 index 000000000..c553b51f6 --- /dev/null +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift @@ -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