forked from zelo72/mastodon-ios
feat: update server pick scene UI
This commit is contained in:
parent
cff048c2a3
commit
223049a3f5
|
@ -198,7 +198,9 @@
|
|||
"log_in": "Log In"
|
||||
},
|
||||
"server_picker": {
|
||||
"title": "Pick a server,\nany server.",
|
||||
"title": "Mastodon is made of users in different communities.",
|
||||
"subtitle": "Pick a community based on your interests, region, or a general purpose one.",
|
||||
"subtitle_extend": "Pick a community based on your interests, region, or a general purpose one. Each community is operated by an entirely independent organization or individual.",
|
||||
"button": {
|
||||
"category": {
|
||||
"all": "All",
|
||||
|
@ -225,7 +227,7 @@
|
|||
"category": "CATEGORY"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Find a server or join your own..."
|
||||
"placeholder": "Search communities"
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
|
@ -234,7 +236,7 @@
|
|||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "Tell us about you.",
|
||||
"title": "Let’s get you set up on %s",
|
||||
"input": {
|
||||
"avatar": {
|
||||
"delete": "Delete"
|
||||
|
@ -288,7 +290,7 @@
|
|||
},
|
||||
"server_rules": {
|
||||
"title": "Some ground rules.",
|
||||
"subtitle": "These rules are set by the admins of %s.",
|
||||
"subtitle": "These are set and enforced by the %s moderators.",
|
||||
"prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.",
|
||||
"terms_of_service": "terms of service",
|
||||
"privacy_policy": "privacy policy",
|
||||
|
@ -298,7 +300,7 @@
|
|||
},
|
||||
"confirm_email": {
|
||||
"title": "One last thing.",
|
||||
"subtitle": "We just sent an email to %s,\ntap the link to confirm your account.",
|
||||
"subtitle": "Tap the link we emailed to you to verify your account.",
|
||||
"button": {
|
||||
"open_email_app": "Open Email App",
|
||||
"dont_receive_email": "I never got an email"
|
||||
|
|
|
@ -193,6 +193,8 @@
|
|||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
|
||||
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EA277EF3820030EE79 /* GradientBorderView.swift */; };
|
||||
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */; };
|
||||
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EE277F12720030EE79 /* NavigationActionView.swift */; };
|
||||
DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */; };
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB0C946426A6FD4D0088FB11 /* AlamofireImage */; };
|
||||
|
@ -973,6 +975,8 @@
|
|||
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||
DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = "<group>"; };
|
||||
DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = "<group>"; };
|
||||
DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = "<group>"; };
|
||||
DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerServerSectionTableHeaderView.swift; sourceTree = "<group>"; };
|
||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1595,8 +1599,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */,
|
||||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */,
|
||||
);
|
||||
|
@ -1608,6 +1610,7 @@
|
|||
children = (
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */,
|
||||
DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2097,6 +2100,15 @@
|
|||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0617F3278436360030EE79 /* Deprecated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
);
|
||||
path = Deprecated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2228,6 +2240,7 @@
|
|||
children = (
|
||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
DB0617F3278436360030EE79 /* Deprecated */,
|
||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||
DB8AF52A25C13561002E6C99 /* State */,
|
||||
2D61335525C1886800CAE157 /* Service */,
|
||||
|
@ -2524,6 +2537,7 @@
|
|||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */,
|
||||
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */,
|
||||
DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */,
|
||||
DB0617EE277F12720030EE79 /* NavigationActionView.swift */,
|
||||
);
|
||||
path = Share;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3972,6 +3986,7 @@
|
|||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */,
|
||||
DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */,
|
||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */,
|
||||
|
@ -4132,6 +4147,7 @@
|
|||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
||||
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */,
|
||||
|
|
|
@ -216,6 +216,15 @@
|
|||
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
||||
"version": "2.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
//
|
||||
// PickServerCategoriesCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
//import os.log
|
||||
//import UIKit
|
||||
//import MastodonSDK
|
||||
//
|
||||
//protocol PickServerCategoriesCellDelegate: AnyObject {
|
||||
// func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
//}
|
||||
//
|
||||
//final class PickServerCategoriesCell: UITableViewCell {
|
||||
//
|
||||
// weak var delegate: PickServerCategoriesCellDelegate?
|
||||
//
|
||||
// var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
|
||||
//
|
||||
// let metricView = UIView()
|
||||
//
|
||||
// let collectionView: UICollectionView = {
|
||||
// let flowLayout = UICollectionViewFlowLayout()
|
||||
// flowLayout.scrollDirection = .horizontal
|
||||
// let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
// view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
|
||||
// view.backgroundColor = .clear
|
||||
// view.showsHorizontalScrollIndicator = false
|
||||
// view.showsVerticalScrollIndicator = false
|
||||
// view.layer.masksToBounds = false
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// override func prepareForReuse() {
|
||||
// super.prepareForReuse()
|
||||
//
|
||||
// delegate = nil
|
||||
// }
|
||||
//
|
||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
//
|
||||
// private func _init() {
|
||||
// selectionStyle = .none
|
||||
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
// configureMargin()
|
||||
//
|
||||
// metricView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// contentView.addSubview(metricView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
// metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
// metricView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
// metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
// metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// contentView.addSubview(collectionView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
// contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20),
|
||||
// collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
// ])
|
||||
//
|
||||
// collectionView.delegate = self
|
||||
// }
|
||||
//
|
||||
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// super.traitCollectionDidChange(previousTraitCollection)
|
||||
//
|
||||
// configureMargin()
|
||||
// }
|
||||
//
|
||||
// override func layoutSubviews() {
|
||||
// super.layoutSubviews()
|
||||
//
|
||||
// collectionView.collectionViewLayout.invalidateLayout()
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
// private func configureMargin() {
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
// default:
|
||||
// contentView.layoutMargins = .zero
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - UICollectionViewDelegateFlowLayout
|
||||
//extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
// collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
// delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
// layoutIfNeeded()
|
||||
// return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX)
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
// return 16
|
||||
// }
|
||||
//
|
||||
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
// return CGSize(width: 60, height: 80)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension PickServerCategoriesCell {
|
||||
//
|
||||
// override func accessibilityElementCount() -> Int {
|
||||
// guard let diffableDataSource = diffableDataSource else { return 0 }
|
||||
// return diffableDataSource.snapshot().itemIdentifiers.count
|
||||
// }
|
||||
//
|
||||
// override func accessibilityElement(at index: Int) -> Any? {
|
||||
// guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
|
||||
// return item
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -0,0 +1,171 @@
|
|||
//
|
||||
// PickServerSearchCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
//protocol PickServerSearchCellDelegate: AnyObject {
|
||||
// func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?)
|
||||
//}
|
||||
//
|
||||
//class PickServerSearchCell: UITableViewCell {
|
||||
//
|
||||
// weak var delegate: PickServerSearchCellDelegate?
|
||||
//
|
||||
// private var bgView: UIView = {
|
||||
// let view = UIView()
|
||||
// view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.layer.maskedCorners = [
|
||||
// .layerMinXMinYCorner,
|
||||
// .layerMaxXMinYCorner
|
||||
// ]
|
||||
// view.layer.cornerCurve = .continuous
|
||||
// view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// private var textFieldBgView: UIView = {
|
||||
// let view = UIView()
|
||||
// view.backgroundColor = Asset.Colors.TextField.background.color
|
||||
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.layer.masksToBounds = true
|
||||
// view.layer.cornerRadius = 6
|
||||
// view.layer.cornerCurve = .continuous
|
||||
// return view
|
||||
// }()
|
||||
//
|
||||
// let searchTextField: UITextField = {
|
||||
// let textField = UITextField()
|
||||
// textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
// textField.leftView = {
|
||||
// let imageView = UIImageView(
|
||||
// image: UIImage(
|
||||
// systemName: "magnifyingglass",
|
||||
// withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
// )
|
||||
// )
|
||||
// imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
//
|
||||
// let containerView = UIView()
|
||||
// imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// containerView.addSubview(imageView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// let paddingView = UIView()
|
||||
// paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// containerView.addSubview(paddingView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
// paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||
// paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
// paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
// paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
// ])
|
||||
// return containerView
|
||||
// }()
|
||||
// textField.leftViewMode = .always
|
||||
// textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
// textField.tintColor = Asset.Colors.Label.primary.color
|
||||
// textField.textColor = Asset.Colors.Label.primary.color
|
||||
// textField.adjustsFontForContentSizeCategory = true
|
||||
// textField.attributedPlaceholder =
|
||||
// NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
// attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||
// .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
||||
// textField.clearButtonMode = .whileEditing
|
||||
// textField.autocapitalizationType = .none
|
||||
// textField.autocorrectionType = .no
|
||||
// textField.returnKeyType = .done
|
||||
// textField.keyboardType = .URL
|
||||
// return textField
|
||||
// }()
|
||||
//
|
||||
// override func prepareForReuse() {
|
||||
// super.prepareForReuse()
|
||||
//
|
||||
// delegate = nil
|
||||
// }
|
||||
//
|
||||
// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
// super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
// _init()
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// super.init(coder: coder)
|
||||
// _init()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// private func _init() {
|
||||
// selectionStyle = .none
|
||||
// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
// configureMargin()
|
||||
//
|
||||
// searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
// searchTextField.delegate = self
|
||||
//
|
||||
// contentView.addSubview(bgView)
|
||||
// contentView.addSubview(textFieldBgView)
|
||||
// contentView.addSubview(searchTextField)
|
||||
//
|
||||
// NSLayoutConstraint.activate([
|
||||
// bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
// bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
// bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
// bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
//
|
||||
// textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
|
||||
// textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
|
||||
// bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
|
||||
// bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
|
||||
//
|
||||
// searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
|
||||
// searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
|
||||
// textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
|
||||
// textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
|
||||
// ])
|
||||
// }
|
||||
//
|
||||
// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
// super.traitCollectionDidChange(previousTraitCollection)
|
||||
//
|
||||
// configureMargin()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// private func configureMargin() {
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
// default:
|
||||
// contentView.layoutMargins = .zero
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension PickServerSearchCell {
|
||||
// @objc private func textFieldDidChange(_ textField: UITextField) {
|
||||
// delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - UITextFieldDelegate
|
||||
//extension PickServerSearchCell: UITextFieldDelegate {
|
||||
//
|
||||
// func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
// textField.resignFirstResponder()
|
||||
// return false
|
||||
// }
|
||||
//}
|
|
@ -15,10 +15,11 @@ enum CategoryPickerItem {
|
|||
}
|
||||
|
||||
extension CategoryPickerItem {
|
||||
var title: String {
|
||||
|
||||
var emoji: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return L10n.Scene.ServerPicker.Button.Category.all
|
||||
return "💬"
|
||||
case .category(let category):
|
||||
switch category.category {
|
||||
case .academia:
|
||||
|
@ -32,7 +33,7 @@ extension CategoryPickerItem {
|
|||
case .games:
|
||||
return "🕹"
|
||||
case .general:
|
||||
return "💬"
|
||||
return "🐘"
|
||||
case .journalism:
|
||||
return "📰"
|
||||
case .lgbt:
|
||||
|
@ -50,6 +51,41 @@ extension CategoryPickerItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return L10n.Scene.ServerPicker.Button.Category.all
|
||||
case .category(let category):
|
||||
switch category.category {
|
||||
case .academia:
|
||||
return L10n.Scene.ServerPicker.Button.Category.academia
|
||||
case .activism:
|
||||
return L10n.Scene.ServerPicker.Button.Category.activism
|
||||
case .food:
|
||||
return L10n.Scene.ServerPicker.Button.Category.food
|
||||
case .furry:
|
||||
return L10n.Scene.ServerPicker.Button.Category.furry
|
||||
case .games:
|
||||
return L10n.Scene.ServerPicker.Button.Category.games
|
||||
case .general:
|
||||
return L10n.Scene.ServerPicker.Button.Category.general
|
||||
case .journalism:
|
||||
return L10n.Scene.ServerPicker.Button.Category.journalism
|
||||
case .lgbt:
|
||||
return L10n.Scene.ServerPicker.Button.Category.lgbt
|
||||
case .regional:
|
||||
return L10n.Scene.ServerPicker.Button.Category.regional
|
||||
case .art:
|
||||
return L10n.Scene.ServerPicker.Button.Category.art
|
||||
case .music:
|
||||
return L10n.Scene.ServerPicker.Button.Category.music
|
||||
case .tech:
|
||||
return L10n.Scene.ServerPicker.Button.Category.tech
|
||||
case ._other:
|
||||
return "-" // FIXME:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityDescription: String {
|
||||
switch self {
|
||||
|
@ -82,7 +118,7 @@ extension CategoryPickerItem {
|
|||
case .tech:
|
||||
return L10n.Scene.ServerPicker.Button.Category.tech
|
||||
case ._other:
|
||||
return "❓" // FIXME:
|
||||
return "-" // FIXME:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import MastodonSDK
|
|||
/// Note: update Equatable when change case
|
||||
enum PickServerItem {
|
||||
case header
|
||||
case categoryPicker(items: [CategoryPickerItem])
|
||||
case search
|
||||
case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute)
|
||||
case loader(attribute: LoaderItemAttribute)
|
||||
}
|
||||
|
@ -63,10 +61,6 @@ extension PickServerItem: Equatable {
|
|||
switch (lhs, rhs) {
|
||||
case (.header, .header):
|
||||
return true
|
||||
case (.categoryPicker(let itemsLeft), .categoryPicker(let itemsRight)):
|
||||
return itemsLeft == itemsRight
|
||||
case (.search, .search):
|
||||
return true
|
||||
case (.server(let serverLeft, _), .server(let serverRight, _)):
|
||||
return serverLeft.domain == serverRight.domain
|
||||
case (.loader(let attributeLeft), loader(let attributeRight)):
|
||||
|
@ -82,10 +76,6 @@ extension PickServerItem: Hashable {
|
|||
switch self {
|
||||
case .header:
|
||||
hasher.combine(String(describing: PickServerItem.header.self))
|
||||
case .categoryPicker(let items):
|
||||
hasher.combine(items)
|
||||
case .search:
|
||||
hasher.combine(String(describing: PickServerItem.search.self))
|
||||
case .server(let server, _):
|
||||
hasher.combine(server.domain)
|
||||
case .loader(let attribute):
|
||||
|
|
|
@ -19,27 +19,11 @@ extension CategoryPickerSection {
|
|||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell
|
||||
switch item {
|
||||
case .all:
|
||||
cell.categoryView.titleLabel.font = .systemFont(ofSize: 17)
|
||||
case .category:
|
||||
cell.categoryView.titleLabel.font = .systemFont(ofSize: 28)
|
||||
}
|
||||
cell.categoryView.emojiLabel.text = item.emoji
|
||||
cell.categoryView.titleLabel.text = item.title
|
||||
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
||||
if cell.isSelected {
|
||||
cell.categoryView.bgView.backgroundColor = Asset.Colors.brandBlue.color
|
||||
cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
|
||||
if case .all = item {
|
||||
cell.categoryView.titleLabel.textColor = .white
|
||||
}
|
||||
} else {
|
||||
cell.categoryView.bgView.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
|
||||
if case .all = item {
|
||||
cell.categoryView.titleLabel.textColor = Asset.Colors.brandBlue.color
|
||||
}
|
||||
}
|
||||
cell.categoryView.highlightedIndicatorView.alpha = cell.isSelected ? 1 : 0
|
||||
cell.categoryView.titleLabel.textColor = cell.isSelected ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color
|
||||
}
|
||||
.store(in: &cell.observations)
|
||||
|
||||
|
|
|
@ -12,8 +12,6 @@ import AlamofireImage
|
|||
|
||||
enum PickServerSection: Equatable, Hashable {
|
||||
case header
|
||||
case category
|
||||
case search
|
||||
case servers
|
||||
}
|
||||
|
||||
|
@ -21,14 +19,10 @@ extension PickServerSection {
|
|||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
pickServerCategoriesCellDelegate: PickServerCategoriesCellDelegate,
|
||||
pickServerSearchCellDelegate: PickServerSearchCellDelegate,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency,
|
||||
weak pickServerCategoriesCellDelegate,
|
||||
weak pickServerSearchCellDelegate,
|
||||
weak pickServerCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let dependency = dependency else { return nil }
|
||||
|
@ -36,22 +30,6 @@ extension PickServerSection {
|
|||
case .header:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell
|
||||
return cell
|
||||
case .categoryPicker(let items):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell
|
||||
cell.delegate = pickServerCategoriesCellDelegate
|
||||
cell.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource(
|
||||
for: cell.collectionView,
|
||||
dependency: dependency
|
||||
)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<CategoryPickerSection, CategoryPickerItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
cell.diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
return cell
|
||||
case .search:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell
|
||||
cell.delegate = pickServerSearchCellDelegate
|
||||
return cell
|
||||
case .server(let server, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
|
||||
PickServerSection.configure(cell: cell, server: server, attribute: attribute)
|
||||
|
@ -70,19 +48,63 @@ extension PickServerSection {
|
|||
|
||||
static func configure(cell: PickServerCell, server: Mastodon.Entity.Server, attribute: PickServerItem.ServerItemAttribute) {
|
||||
cell.domainLabel.text = server.domain
|
||||
cell.descriptionLabel.text = {
|
||||
guard let html = try? HTML(html: server.description, encoding: .utf8) else {
|
||||
return server.description
|
||||
}
|
||||
cell.descriptionLabel.attributedText = {
|
||||
let content: String = {
|
||||
guard let html = try? HTML(html: server.description, encoding: .utf8) else {
|
||||
return server.description
|
||||
}
|
||||
return html.text ?? server.description
|
||||
}()
|
||||
|
||||
return html.text ?? server.description
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineHeightMultiple = 1.16
|
||||
|
||||
return NSAttributedString(
|
||||
string: content,
|
||||
attributes: [
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
}()
|
||||
cell.langValueLabel.text = server.language.uppercased()
|
||||
cell.usersValueLabel.text = parseUsersCount(server.totalUsers)
|
||||
cell.categoryValueLabel.text = server.category.uppercased()
|
||||
|
||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
||||
|
||||
cell.usersValueLabel.attributedText = {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let attachment = NSTextAttachment(image: UIImage(systemName: "person.2.fill")!)
|
||||
let attachmentAttributedString = NSAttributedString(attachment: attachment)
|
||||
attributedString.append(attachmentAttributedString)
|
||||
attributedString.append(NSAttributedString(string: " "))
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineHeightMultiple = 1.12
|
||||
let valueAttributedString = NSAttributedString(
|
||||
string: parseUsersCount(server.totalUsers),
|
||||
attributes: [
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
attributedString.append(valueAttributedString)
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
cell.langValueLabel.attributedText = {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let attachment = NSTextAttachment(image: UIImage(systemName: "text.bubble.fill")!)
|
||||
let attachmentAttributedString = NSAttributedString(attachment: attachment)
|
||||
attributedString.append(attachmentAttributedString)
|
||||
attributedString.append(NSAttributedString(string: " "))
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineHeightMultiple = 1.12
|
||||
let valueAttributedString = NSAttributedString(
|
||||
string: server.language.uppercased(),
|
||||
attributes: [
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
)
|
||||
attributedString.append(valueAttributedString)
|
||||
|
||||
return attributedString
|
||||
}()
|
||||
|
||||
attribute.isLast
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] isLast in
|
||||
|
@ -101,41 +123,6 @@ extension PickServerSection {
|
|||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
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.thumbnailActivityIndicator.stopAnimating()
|
||||
return
|
||||
}
|
||||
cell.thumbnailImageView.isHidden = false
|
||||
cell.thumbnailActivityIndicator.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?.thumbnailActivityIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
private static func parseUsersCount(_ usersCount: Int) -> String {
|
||||
|
|
|
@ -47,6 +47,7 @@ internal enum Asset {
|
|||
}
|
||||
internal enum Label {
|
||||
internal static let primary = ColorAsset(name: "Colors/Label/primary")
|
||||
internal static let primaryReverse = ColorAsset(name: "Colors/Label/primary.reverse")
|
||||
internal static let secondary = ColorAsset(name: "Colors/Label/secondary")
|
||||
internal static let tertiary = ColorAsset(name: "Colors/Label/tertiary")
|
||||
}
|
||||
|
@ -89,6 +90,14 @@ internal enum Asset {
|
|||
internal static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive")
|
||||
}
|
||||
internal enum Scene {
|
||||
internal enum Onboarding {
|
||||
internal static let navigationBackButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background")
|
||||
internal static let navigationBackButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background.highlighted")
|
||||
internal static let navigationNextButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background")
|
||||
internal static let navigationNextButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background.highlighted")
|
||||
internal static let onboardingBackground = ColorAsset(name: "Scene/Onboarding/onboarding.background")
|
||||
internal static let searchBarBackground = ColorAsset(name: "Scene/Onboarding/search.bar.background")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
internal static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray")
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
"blue" : "0.216",
|
||||
"green" : "0.173",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
"blue" : "0xEE",
|
||||
"green" : "0xEE",
|
||||
"red" : "0xEE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.933",
|
||||
"green" : "0.933",
|
||||
"red" : "0.933"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.216",
|
||||
"green" : "0.173",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -22,10 +22,10 @@
|
|||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.600",
|
||||
"blue" : "0xF5",
|
||||
"green" : "0xEB",
|
||||
"red" : "0xEB"
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xAD",
|
||||
"green" : "0x9D",
|
||||
"red" : "0x97"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.200",
|
||||
"blue" : "0x80",
|
||||
"green" : "0x78",
|
||||
"red" : "0x78"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE5",
|
||||
"green" : "0xE5",
|
||||
"red" : "0xE5"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.400",
|
||||
"blue" : "0x80",
|
||||
"green" : "0x78",
|
||||
"red" : "0x78"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xEE",
|
||||
"green" : "0xEE",
|
||||
"red" : "0xEE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1B",
|
||||
"green" : "0x15",
|
||||
"red" : "0x13"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xBA",
|
||||
"green" : "0xBA",
|
||||
"red" : "0xBA"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF7",
|
||||
"green" : "0xF2",
|
||||
"red" : "0xF2"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x21",
|
||||
"green" : "0x1B",
|
||||
"red" : "0x19"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.200",
|
||||
"blue" : "0x80",
|
||||
"green" : "0x78",
|
||||
"red" : "0x78"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.240",
|
||||
"blue" : "0x80",
|
||||
"green" : "0x76",
|
||||
"red" : "0x76"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.922",
|
||||
"green" : "0.898",
|
||||
"red" : "0.867"
|
||||
"blue" : "0xEB",
|
||||
"green" : "0xE4",
|
||||
"red" : "0xDD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.910",
|
||||
"green" : "0.882",
|
||||
"red" : "0.851"
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE0",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.910",
|
||||
"green" : "0.882",
|
||||
"red" : "0.851"
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE0",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -8,14 +8,10 @@
|
|||
import UIKit
|
||||
|
||||
class PickServerCategoryCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
var categoryView: PickServerCategoryView = {
|
||||
let view = PickServerCategoryView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
var categoryView = PickServerCategoryView()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
@ -35,13 +31,15 @@ class PickServerCategoryCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
extension PickServerCategoryCollectionViewCell {
|
||||
private func configure() {
|
||||
contentView.addSubview(categoryView)
|
||||
backgroundColor = .clear
|
||||
|
||||
categoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(categoryView)
|
||||
NSLayoutConstraint.activate([
|
||||
categoryView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
categoryView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
categoryView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
categoryView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor, constant: 10),
|
||||
contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import AuthenticationServices
|
|||
final class MastodonPickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
private var tableViewObservation: NSKeyValueObservation?
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -31,21 +32,16 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
private let emptyStateView = PickServerEmptyStateView()
|
||||
private var emptyStateViewLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
private var emptyStateViewTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
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))
|
||||
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
|
||||
tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
||||
} else {
|
||||
|
@ -54,14 +50,11 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
return tableView
|
||||
}()
|
||||
|
||||
let buttonContainer = UIView()
|
||||
let nextStepButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton()
|
||||
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
return navigationActionView
|
||||
}()
|
||||
var buttonContainerBottomLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
var mastodonAuthenticationController: MastodonAuthenticationController?
|
||||
|
||||
|
@ -72,16 +65,15 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
||||
|
||||
setupOnboardingAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
configureTitleLabel()
|
||||
configureMargin()
|
||||
|
||||
#if DEBUG
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
|
||||
|
@ -94,26 +86,36 @@ extension MastodonPickServerViewController {
|
|||
navigationItem.rightBarButtonItem?.menu = UIMenu(title: "Debug Tool", image: nil, identifier: nil, options: [], children: children)
|
||||
#endif
|
||||
|
||||
buttonContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.preservesSuperviewLayoutMargins = true
|
||||
view.addSubview(buttonContainer)
|
||||
buttonContainerBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: 0).priority(.defaultHigh)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
buttonContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
buttonContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: buttonContainer.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
|
||||
buttonContainerBottomLayoutConstraint,
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
view.addSubview(nextStepButton)
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
nextStepButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor),
|
||||
nextStepButton.leadingAnchor.constraint(equalTo: buttonContainer.layoutMarginsGuide.leadingAnchor),
|
||||
buttonContainer.layoutMarginsGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor),
|
||||
nextStepButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor),
|
||||
nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh),
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
print("*** \(inset) ***")
|
||||
self.tableView.contentInset.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
|
||||
// fix AutoLayout warning when observe before view appear
|
||||
viewModel.viewWillAppear
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -125,26 +127,7 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
}
|
||||
.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.Theme.Mastodon.systemGroupedBackground.color
|
||||
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
buttonContainer.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 7),
|
||||
])
|
||||
|
||||
|
||||
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(emptyStateView)
|
||||
emptyStateViewLeadingLayoutConstraint = emptyStateView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor)
|
||||
|
@ -153,64 +136,24 @@ extension MastodonPickServerViewController {
|
|||
emptyStateView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
emptyStateViewLeadingLayoutConstraint,
|
||||
emptyStateViewTrailingLayoutConstraint,
|
||||
buttonContainer.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21),
|
||||
navigationActionView.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21),
|
||||
])
|
||||
view.sendSubviewToBack(emptyStateView)
|
||||
|
||||
// update layout when keyboard show/dismiss
|
||||
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||
KeyboardResponderService.shared.isShow,
|
||||
KeyboardResponderService.shared.state,
|
||||
KeyboardResponderService.shared.endFrame
|
||||
)
|
||||
|
||||
keyboardEventPublishers
|
||||
.sink { [weak self] keyboardEvents in
|
||||
guard let self = self else { return }
|
||||
let (isShow, state, endFrame) = keyboardEvents
|
||||
|
||||
// guard external keyboard connected
|
||||
guard isShow, state == .dock, GCKeyboard.coalesced != nil else {
|
||||
self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight
|
||||
return
|
||||
}
|
||||
|
||||
let externalKeyboardToolbarHeight = self.view.frame.maxY - endFrame.minY
|
||||
guard externalKeyboardToolbarHeight > 0 else {
|
||||
self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.buttonContainerBottomLayoutConstraint.constant = externalKeyboardToolbarHeight + 16
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
switch viewModel.mode {
|
||||
case .signIn:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||
case .signUp:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||
}
|
||||
nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
pickServerCategoriesCellDelegate: self,
|
||||
pickServerSearchCellDelegate: self,
|
||||
pickServerServerSectionTableHeaderViewDelegate: self,
|
||||
pickServerCellDelegate: self
|
||||
)
|
||||
|
||||
|
||||
viewModel
|
||||
.selectedServer
|
||||
.map { $0 != nil }
|
||||
.assign(to: \.isEnabled, on: nextStepButton)
|
||||
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
Publishers.Merge(
|
||||
viewModel.error,
|
||||
authenticationViewModel.error
|
||||
|
@ -229,7 +172,7 @@ extension MastodonPickServerViewController {
|
|||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
authenticationViewModel
|
||||
.authenticated
|
||||
.flatMap { [weak self] (domain, user) -> AnyPublisher<Result<Bool, Error>, Never> in
|
||||
|
@ -249,17 +192,17 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
authenticationViewModel.isAuthenticating
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isAuthenticating in
|
||||
guard let self = self else { return }
|
||||
isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading()
|
||||
isAuthenticating ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel.emptyStateViewState
|
||||
.receive(on: RunLoop.main)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] state in
|
||||
guard let self = self else { return }
|
||||
switch state {
|
||||
|
@ -284,6 +227,9 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(MastodonPickServerViewController.backButtonDidPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonPickServerViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -296,38 +242,20 @@ extension MastodonPickServerViewController {
|
|||
|
||||
setupNavigationBarAppearance()
|
||||
updateEmptyStateViewLayout()
|
||||
configureTitleLabel()
|
||||
configureMargin()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
private func configureTitleLabel() {
|
||||
guard UIDevice.current.userInterfaceIdiom == .pad else {
|
||||
return
|
||||
}
|
||||
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
navigationItem.title = L10n.Scene.ServerPicker.title.replacingOccurrences(of: "\n", with: " ")
|
||||
default:
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
navigationItem.title = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonPickServerViewController {
|
||||
|
||||
@objc
|
||||
private func nextStepButtonDidClicked(_ sender: UIButton) {
|
||||
@objc private func backButtonDidPressed(_ sender: UIButton) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func nextButtonDidPressed(_ sender: UIButton) {
|
||||
switch viewModel.mode {
|
||||
case .signIn:
|
||||
doSignIn()
|
||||
case .signUp:
|
||||
doSignUp()
|
||||
case .signIn: doSignIn()
|
||||
case .signUp: doSignUp()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,16 +386,6 @@ 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, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||
|
@ -500,87 +418,89 @@ extension MastodonPickServerViewController: UITableViewDelegate {
|
|||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
switch item {
|
||||
case .categoryPicker:
|
||||
guard let cell = cell as? PickServerCategoriesCell else { return }
|
||||
guard let diffableDataSource = cell.diffableDataSource else { return }
|
||||
let snapshot = diffableDataSource.snapshot()
|
||||
|
||||
let item = viewModel.selectCategoryItem.value
|
||||
guard let section = snapshot.indexOfSection(.main),
|
||||
let row = snapshot.indexOfItem(item) else { return }
|
||||
cell.collectionView.selectItem(at: IndexPath(item: row, section: section), animated: false, scrollPosition: .centeredHorizontally)
|
||||
case .search:
|
||||
guard let cell = cell as? PickServerSearchCell else { return }
|
||||
cell.searchTextField.text = viewModel.searchText.value
|
||||
// case .categoryPicker:
|
||||
// guard let cell = cell as? PickServerCategoriesCell else { return }
|
||||
// guard let diffableDataSource = cell.diffableDataSource else { return }
|
||||
// let snapshot = diffableDataSource.snapshot()
|
||||
//
|
||||
// let item = viewModel.selectCategoryItem.value
|
||||
// guard let section = snapshot.indexOfSection(.main),
|
||||
// let row = snapshot.indexOfItem(item) else { return }
|
||||
// cell.collectionView.selectItem(at: IndexPath(item: row, section: section), animated: false, scrollPosition: .centeredHorizontally)
|
||||
// case .search:
|
||||
// guard let cell = cell as? PickServerSearchCell else { return }
|
||||
// cell.searchTextField.text = viewModel.searchText.value
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
let snapshot = diffableDataSource.snapshot()
|
||||
guard section < snapshot.numberOfSections else { return nil }
|
||||
let section = snapshot.sectionIdentifiers[section]
|
||||
|
||||
switch section {
|
||||
case .servers:
|
||||
return viewModel.serverSectionHeaderView
|
||||
default:
|
||||
return UIView()
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return .leastNonzeroMagnitude }
|
||||
let snapshot = diffableDataSource.snapshot()
|
||||
guard section < snapshot.numberOfSections else { return .leastNonzeroMagnitude }
|
||||
let section = snapshot.sectionIdentifiers[section]
|
||||
|
||||
switch section {
|
||||
case .servers:
|
||||
return PickServerServerSectionTableHeaderView.height
|
||||
default:
|
||||
return .leastNonzeroMagnitude
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
default:
|
||||
let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x
|
||||
emptyStateViewLeadingLayoutConstraint.constant = margin
|
||||
emptyStateViewTrailingLayoutConstraint.constant = margin
|
||||
}
|
||||
}
|
||||
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
buttonContainer.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
default:
|
||||
buttonContainer.layoutMargins = .zero
|
||||
}
|
||||
// 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
|
||||
//
|
||||
// switch traitCollection.horizontalSizeClass {
|
||||
// case .regular:
|
||||
// emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
// emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin
|
||||
// default:
|
||||
// let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x
|
||||
// emptyStateViewLeadingLayoutConstraint.constant = margin
|
||||
// emptyStateViewTrailingLayoutConstraint.constant = margin
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PickServerCategoriesCellDelegate
|
||||
extension MastodonPickServerViewController: PickServerCategoriesCellDelegate {
|
||||
func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = cell.diffableDataSource else { return }
|
||||
// MARK: - PickServerServerSectionTableHeaderViewDelegate
|
||||
extension MastodonPickServerViewController: PickServerServerSectionTableHeaderViewDelegate {
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = headerView.diffableDataSource else { return }
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||
viewModel.selectCategoryItem.value = item ?? .all
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PickServerSearchCellDelegate
|
||||
extension MastodonPickServerViewController: PickServerSearchCellDelegate {
|
||||
func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) {
|
||||
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) {
|
||||
viewModel.searchText.send(searchText ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PickServerCellDelegate
|
||||
extension MastodonPickServerViewController: PickServerCellDelegate {
|
||||
func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
guard case let .server(_, attribute) = item else { return }
|
||||
|
||||
attribute.isExpand.toggle()
|
||||
tableView.beginUpdates()
|
||||
cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse)
|
||||
tableView.endUpdates()
|
||||
|
||||
// expand attribute change do not needs apply snapshot to diffable data source
|
||||
// but should I block the viewModel data binding during tableView.beginUpdates/endUpdates?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
|
|
|
@ -6,32 +6,101 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
extension MastodonPickServerViewModel {
|
||||
|
||||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
pickServerCategoriesCellDelegate: PickServerCategoriesCellDelegate,
|
||||
pickServerSearchCellDelegate: PickServerSearchCellDelegate,
|
||||
pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
) {
|
||||
// set section header
|
||||
serverSectionHeaderView.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource(
|
||||
for: serverSectionHeaderView.collectionView,
|
||||
dependency: dependency
|
||||
)
|
||||
var sectionHeaderSnapshot = NSDiffableDataSourceSnapshot<CategoryPickerSection, CategoryPickerItem>()
|
||||
sectionHeaderSnapshot.appendSections([.main])
|
||||
sectionHeaderSnapshot.appendItems(categoryPickerItems, toSection: .main)
|
||||
serverSectionHeaderView.delegate = pickServerServerSectionTableHeaderViewDelegate
|
||||
serverSectionHeaderView.diffableDataSource?.applySnapshot(sectionHeaderSnapshot, animated: false)
|
||||
|
||||
// set tableView
|
||||
diffableDataSource = PickServerSection.tableViewDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: dependency,
|
||||
pickServerCategoriesCellDelegate: pickServerCategoriesCellDelegate,
|
||||
pickServerSearchCellDelegate: pickServerSearchCellDelegate,
|
||||
pickServerCellDelegate: pickServerCellDelegate
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
snapshot.appendSections([.header, .category, .search, .servers])
|
||||
snapshot.appendSections([.header, .servers])
|
||||
snapshot.appendItems([.header], toSection: .header)
|
||||
snapshot.appendItems([.categoryPicker(items: categoryPickerItems)], toSection: .category)
|
||||
snapshot.appendItems([.search], toSection: .search)
|
||||
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
|
||||
loadIndexedServerStateMachine.enter(LoadIndexedServerState.Loading.self)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
filteredIndexedServers,
|
||||
unindexedServers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] indexedServers, unindexedServers in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:]
|
||||
for item in oldSnapshot.itemIdentifiers {
|
||||
guard case let .server(server, attribute) = item else { continue }
|
||||
oldSnapshotServerItemAttributeDict[server.domain] = attribute
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
snapshot.appendSections([.header, .servers])
|
||||
snapshot.appendItems([.header], toSection: .header)
|
||||
|
||||
// TODO: handle filter
|
||||
var serverItems: [PickServerItem] = []
|
||||
for server in indexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
|
||||
if let unindexedServers = unindexedServers {
|
||||
if !unindexedServers.isEmpty {
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
} else {
|
||||
if indexedServers.isEmpty && !self.isLoadingIndexedServers.value {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false)))
|
||||
}
|
||||
|
||||
if case let .server(_, attribute) = serverItems.last {
|
||||
attribute.isLast.value = true
|
||||
}
|
||||
if case let .loader(attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
}
|
||||
snapshot.appendItems(serverItems, toSection: .servers)
|
||||
|
||||
diffableDataSource.defaultRowAnimation = .fade
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: true, completion: nil)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import GameplayKit
|
|||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
import OrderedCollections
|
||||
import Tabman
|
||||
|
||||
class MastodonPickServerViewModel: NSObject {
|
||||
|
||||
|
@ -27,6 +28,8 @@ class MastodonPickServerViewModel: NSObject {
|
|||
}
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let serverSectionHeaderView = PickServerServerSectionTableHeaderView()
|
||||
|
||||
// input
|
||||
let mode: PickServerMode
|
||||
|
@ -82,68 +85,6 @@ class MastodonPickServerViewModel: NSObject {
|
|||
extension MastodonPickServerViewModel {
|
||||
|
||||
private func configure() {
|
||||
Publishers.CombineLatest(
|
||||
filteredIndexedServers,
|
||||
unindexedServers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] indexedServers, unindexedServers in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:]
|
||||
for item in oldSnapshot.itemIdentifiers {
|
||||
guard case let .server(server, attribute) = item else { continue }
|
||||
oldSnapshotServerItemAttributeDict[server.domain] = attribute
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
snapshot.appendSections([.header, .category, .search, .servers])
|
||||
snapshot.appendItems([.header], toSection: .header)
|
||||
snapshot.appendItems([.categoryPicker(items: self.categoryPickerItems)], toSection: .category)
|
||||
snapshot.appendItems([.search], toSection: .search)
|
||||
// TODO: handle filter
|
||||
var serverItems: [PickServerItem] = []
|
||||
for server in indexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
|
||||
if let unindexedServers = unindexedServers {
|
||||
if !unindexedServers.isEmpty {
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast.value = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
} else {
|
||||
if indexedServers.isEmpty && !self.isLoadingIndexedServers.value {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false)))
|
||||
}
|
||||
|
||||
if case let .server(_, attribute) = serverItems.last {
|
||||
attribute.isLast.value = true
|
||||
}
|
||||
if case let .loader(attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
}
|
||||
snapshot.appendItems(serverItems, toSection: .servers)
|
||||
|
||||
diffableDataSource.defaultRowAnimation = .fade
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: true, completion: nil)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isLoadingIndexedServers,
|
||||
loadingIndexedServersError
|
||||
|
@ -301,3 +242,12 @@ extension MastodonPickServerViewModel {
|
|||
let applicationToken: Mastodon.Response.Content<Mastodon.Entity.Token>
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TMBarDataSource
|
||||
extension MastodonPickServerViewModel: TMBarDataSource {
|
||||
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
|
||||
let item = categoryPickerItems[index]
|
||||
let barItem = TMBarItem(title: item.title)
|
||||
return barItem
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// PickServerCategoriesCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
protocol PickServerCategoriesCellDelegate: AnyObject {
|
||||
func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
}
|
||||
|
||||
final class PickServerCategoriesCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerCategoriesCellDelegate?
|
||||
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
|
||||
|
||||
let metricView = UIView()
|
||||
|
||||
let collectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
|
||||
view.backgroundColor = .clear
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.layer.masksToBounds = false
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
delegate = nil
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
configureMargin()
|
||||
|
||||
metricView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(metricView)
|
||||
NSLayoutConstraint.activate([
|
||||
metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
metricView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
contentView.addSubview(collectionView)
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20),
|
||||
collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
collectionView.delegate = self
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureMargin()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell {
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
default:
|
||||
contentView.layoutMargins = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegateFlowLayout
|
||||
extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
layoutIfNeeded()
|
||||
return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return 16
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: 60, height: 80)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell {
|
||||
|
||||
override func accessibilityElementCount() -> Int {
|
||||
guard let diffableDataSource = diffableDataSource else { return 0 }
|
||||
return diffableDataSource.snapshot().itemIdentifiers.count
|
||||
}
|
||||
|
||||
override func accessibilityElement(at index: Int) -> Any? {
|
||||
guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,7 @@ import AlamofireImage
|
|||
import Kanna
|
||||
|
||||
protocol PickServerCellDelegate: AnyObject {
|
||||
func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton)
|
||||
// func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton)
|
||||
}
|
||||
|
||||
class PickServerCell: UITableViewCell {
|
||||
|
@ -21,20 +21,17 @@ 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)
|
||||
view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let containerView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 4
|
||||
return view
|
||||
}()
|
||||
|
||||
let domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -52,7 +49,7 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let descriptionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
label.numberOfLines = 0
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
@ -60,112 +57,33 @@ class PickServerCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let thumbnailActivityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
let thumbnailImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let infoStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = 16
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let expandBox: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let expandButton: UIButton = {
|
||||
let button = HitTestExpandedButton(type: .custom)
|
||||
button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.imageView?.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
button.titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
button.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
return button
|
||||
}()
|
||||
|
||||
let separator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = Asset.Theme.System.separator.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let langValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let usersValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.textAlignment = .center
|
||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let categoryValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let langTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.language
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let usersTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.users
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
let categoryTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.category
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -175,9 +93,6 @@ class PickServerCell: UITableViewCell {
|
|||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
thumbnailImageView.isHidden = false
|
||||
thumbnailImageView.af.cancelImageRequest()
|
||||
thumbnailActivityIndicator.stopAnimating()
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
|
@ -197,172 +112,55 @@ class PickServerCell: UITableViewCell {
|
|||
extension PickServerCell {
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
configureMargin()
|
||||
backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(checkbox)
|
||||
NSLayoutConstraint.activate([
|
||||
checkbox.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: 1),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
checkbox.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
])
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
containerView.addSubview(domainLabel)
|
||||
containerView.addSubview(checkbox)
|
||||
containerView.addSubview(descriptionLabel)
|
||||
containerView.addSubview(separator)
|
||||
|
||||
containerView.addSubview(expandButton)
|
||||
|
||||
// Always add the expandbox which contains elements only visible in expand mode
|
||||
containerView.addSubview(expandBox)
|
||||
expandBox.addSubview(thumbnailImageView)
|
||||
expandBox.addSubview(infoStackView)
|
||||
expandBox.isHidden = true
|
||||
|
||||
let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel)
|
||||
let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel)
|
||||
let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewLang)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewUsers)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewCategory)
|
||||
|
||||
let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12).priority(.required - 1)
|
||||
collapseConstraints.append(expandButtonTopConstraintInCollapse)
|
||||
|
||||
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.defaultHigh)
|
||||
expandConstraints.append(expandButtonTopConstraintInExpand)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Set background view
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
|
||||
// Set bottom separator
|
||||
separator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: separator.trailingAnchor),
|
||||
containerView.topAnchor.constraint(equalTo: separator.topAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh),
|
||||
|
||||
domainLabel.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor),
|
||||
domainLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
||||
|
||||
checkbox.widthAnchor.constraint(equalToConstant: 23),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 22),
|
||||
containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor),
|
||||
checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16),
|
||||
checkbox.centerYAnchor.constraint(equalTo: domainLabel.centerYAnchor),
|
||||
|
||||
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
||||
descriptionLabel.topAnchor.constraint(equalTo: domainLabel.bottomAnchor, constant: 8),
|
||||
containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor),
|
||||
|
||||
// Set expandBox constraints
|
||||
expandBox.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
||||
containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor),
|
||||
expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8),
|
||||
expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor).priority(.defaultHigh),
|
||||
|
||||
thumbnailImageView.topAnchor.constraint(equalTo: expandBox.topAnchor),
|
||||
thumbnailImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
|
||||
expandBox.trailingAnchor.constraint(equalTo: thumbnailImageView.trailingAnchor),
|
||||
thumbnailImageView.heightAnchor.constraint(equalTo: thumbnailImageView.widthAnchor, multiplier: 151.0 / 303.0).priority(.defaultHigh),
|
||||
|
||||
infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
|
||||
expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor),
|
||||
infoStackView.topAnchor.constraint(equalTo: thumbnailImageView.bottomAnchor, constant: 16),
|
||||
|
||||
expandButton.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor),
|
||||
containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor),
|
||||
containerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor),
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
|
||||
containerView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 22),
|
||||
containerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 11),
|
||||
checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||
])
|
||||
|
||||
thumbnailActivityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
thumbnailImageView.addSubview(thumbnailActivityIndicator)
|
||||
containerView.addArrangedSubview(domainLabel)
|
||||
containerView.addArrangedSubview(descriptionLabel)
|
||||
containerView.setCustomSpacing(6, after: descriptionLabel)
|
||||
containerView.addArrangedSubview(infoStackView)
|
||||
|
||||
infoStackView.addArrangedSubview(usersValueLabel)
|
||||
infoStackView.addArrangedSubview(langValueLabel)
|
||||
infoStackView.addArrangedSubview(UIView())
|
||||
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separator)
|
||||
NSLayoutConstraint.activate([
|
||||
thumbnailActivityIndicator.centerXAnchor.constraint(equalTo: thumbnailImageView.centerXAnchor),
|
||||
thumbnailActivityIndicator.centerYAnchor.constraint(equalTo: thumbnailImageView.centerYAnchor),
|
||||
separator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: separator.trailingAnchor),
|
||||
separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1),
|
||||
])
|
||||
thumbnailActivityIndicator.hidesWhenStopped = true
|
||||
thumbnailActivityIndicator.stopAnimating()
|
||||
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
|
||||
domainLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
domainLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
descriptionLabel.setContentHuggingPriority(.required - 2, for: .vertical)
|
||||
descriptionLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
|
||||
expandButton.addTarget(self, action: #selector(expandButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureMargin()
|
||||
}
|
||||
|
||||
private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView {
|
||||
let stackView = UIStackView()
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.spacing = 2
|
||||
arrangedView.forEach { stackView.addArrangedSubview($0) }
|
||||
return stackView
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if selected {
|
||||
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
checkbox.tintColor = Asset.Colors.Label.primary.color
|
||||
} else {
|
||||
checkbox.image = UIImage(systemName: "circle")
|
||||
checkbox.tintColor = Asset.Colors.Label.secondary.color
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func expandButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.pickServerCell(self, expandButtonPressed: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerCell {
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
default:
|
||||
contentView.layoutMargins = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCell {
|
||||
|
||||
enum ExpandMode {
|
||||
case collapse
|
||||
case expand
|
||||
}
|
||||
|
||||
func updateExpandMode(mode: ExpandMode) {
|
||||
switch mode {
|
||||
case .collapse:
|
||||
expandButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
expandBox.isHidden = true
|
||||
expandButton.isSelected = false
|
||||
NSLayoutConstraint.deactivate(expandConstraints)
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
case .expand:
|
||||
expandButton.setImage(UIImage(systemName: "chevron.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .normal)
|
||||
expandBox.isHidden = false
|
||||
expandButton.isSelected = true
|
||||
NSLayoutConstraint.activate(expandConstraints)
|
||||
NSLayoutConstraint.deactivate(collapseConstraints)
|
||||
}
|
||||
|
||||
expandMode.value = mode
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,15 +13,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell {
|
|||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16)
|
||||
view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let seperator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
|
@ -30,30 +22,22 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell {
|
|||
label.text = L10n.Scene.ServerPicker.EmptyState.noResults
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.textAlignment = .center
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold), maximumPointSize: 19)
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
|
||||
return label
|
||||
}()
|
||||
|
||||
override func _init() {
|
||||
super._init()
|
||||
|
||||
configureMargin()
|
||||
|
||||
contentView.addSubview(containerView)
|
||||
contentView.addSubview(seperator)
|
||||
|
||||
|
||||
// Set background view
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
// Set background view
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1),
|
||||
|
||||
// Set bottom separator
|
||||
seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
|
||||
containerView.topAnchor.constraint(equalTo: seperator.topAnchor),
|
||||
seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
emptyStatusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -69,24 +53,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell {
|
|||
activityIndicatorView.isHidden = false
|
||||
startAnimating()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureMargin()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerLoaderTableViewCell {
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
default:
|
||||
contentView.layoutMargins = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
//
|
||||
// PickServerSearchCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol PickServerSearchCellDelegate: AnyObject {
|
||||
func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?)
|
||||
}
|
||||
|
||||
class PickServerSearchCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerSearchCellDelegate?
|
||||
|
||||
private var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.maskedCorners = [
|
||||
.layerMinXMinYCorner,
|
||||
.layerMaxXMinYCorner
|
||||
]
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
return view
|
||||
}()
|
||||
|
||||
private var textFieldBgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.TextField.background.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 6
|
||||
view.layer.cornerCurve = .continuous
|
||||
return view
|
||||
}()
|
||||
|
||||
let searchTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.leftView = {
|
||||
let imageView = UIImageView(
|
||||
image: UIImage(
|
||||
systemName: "magnifyingglass",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
)
|
||||
)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
|
||||
let containerView = UIView()
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
let paddingView = UIView()
|
||||
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(paddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||
paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
])
|
||||
return containerView
|
||||
}()
|
||||
textField.leftViewMode = .always
|
||||
textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
textField.tintColor = Asset.Colors.Label.primary.color
|
||||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.adjustsFontForContentSizeCategory = true
|
||||
textField.attributedPlaceholder =
|
||||
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .done
|
||||
textField.keyboardType = .URL
|
||||
return textField
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
delegate = nil
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
configureMargin()
|
||||
|
||||
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
searchTextField.delegate = self
|
||||
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(textFieldBgView)
|
||||
contentView.addSubview(searchTextField)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
|
||||
textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
|
||||
textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
|
||||
bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
|
||||
bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
|
||||
|
||||
searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
|
||||
searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
|
||||
textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
|
||||
textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
|
||||
])
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureMargin()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
private func configureMargin() {
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
let margin = MastodonPickServerViewController.viewEdgeMargin
|
||||
contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
|
||||
default:
|
||||
contentView.layoutMargins = .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
@objc private func textFieldDidChange(_ textField: UITextField) {
|
||||
delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
extension PickServerSearchCell: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -11,17 +11,24 @@ final class PickServerTitleCell: UITableViewCell {
|
|||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 28, weight: .bold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.ServerPicker.title
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let subTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "Pick a community based on your interests, region, or a general purpose one. Each community is operated by an entirely independent organization or individual."
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
|
@ -37,46 +44,22 @@ extension PickServerTitleCell {
|
|||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 16
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: .leastNonzeroMagnitude)
|
||||
contentView.addSubview(container)
|
||||
NSLayoutConstraint.activate([
|
||||
container.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(titleLabel)
|
||||
|
||||
configureTitleLabelDisplay()
|
||||
container.addArrangedSubview(subTitleLabel)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
configureTitleLabelDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerTitleCell {
|
||||
private func configureTitleLabelDisplay() {
|
||||
guard traitCollection.userInterfaceIdiom == .pad else {
|
||||
titleLabel.isHidden = false
|
||||
return
|
||||
}
|
||||
|
||||
switch traitCollection.horizontalSizeClass {
|
||||
case .regular:
|
||||
titleLabel.isHidden = true
|
||||
containerHeightLayoutConstraint.isActive = true
|
||||
default:
|
||||
titleLabel.isHidden = false
|
||||
containerHeightLayoutConstraint.isActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,24 +10,24 @@ import MastodonSDK
|
|||
|
||||
class PickServerCategoryView: UIView {
|
||||
|
||||
var bgShadowView: UIView = {
|
||||
let highlightedIndicatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = Asset.Colors.Label.primary.color
|
||||
return view
|
||||
}()
|
||||
|
||||
var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 30
|
||||
return view
|
||||
}()
|
||||
|
||||
var titleLabel: UILabel = {
|
||||
|
||||
let emojiLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = .systemFont(ofSize: 34, weight: .regular)
|
||||
return label
|
||||
}()
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -45,20 +45,27 @@ class PickServerCategoryView: UIView {
|
|||
extension PickServerCategoryView {
|
||||
|
||||
private func configure() {
|
||||
addSubview(bgView)
|
||||
addSubview(titleLabel)
|
||||
|
||||
bgView.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.distribution = .fillProportionally
|
||||
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(container)
|
||||
NSLayoutConstraint.activate([
|
||||
bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
|
||||
titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
container.topAnchor.constraint(equalTo: topAnchor),
|
||||
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
container.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
container.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(emojiLabel)
|
||||
container.addArrangedSubview(titleLabel)
|
||||
highlightedIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addArrangedSubview(highlightedIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
highlightedIndicatorView.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self) * 3).priority(.required - 1),
|
||||
])
|
||||
titleLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -44,13 +44,7 @@ final class PickServerEmptyStateView: UIView {
|
|||
extension PickServerEmptyStateView {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
layer.maskedCorners = [
|
||||
.layerMinXMaxYCorner,
|
||||
.layerMaxXMaxYCorner
|
||||
]
|
||||
layer.cornerCurve = .continuous
|
||||
layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
backgroundColor = .clear
|
||||
|
||||
let topPaddingView = UIView()
|
||||
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -101,7 +95,7 @@ extension PickServerEmptyStateView {
|
|||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0).priority(.defaultHigh),
|
||||
topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 2.5).priority(.defaultHigh), // magic scale
|
||||
])
|
||||
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
//
|
||||
// PickServerServerSectionTableHeaderView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-4.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Tabman
|
||||
|
||||
protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject {
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||
func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?)
|
||||
}
|
||||
|
||||
final class PickServerServerSectionTableHeaderView: UIView {
|
||||
|
||||
static let collectionViewHeight: CGFloat = 88
|
||||
static let searchTextFieldHeight: CGFloat = 38
|
||||
static let spacing: CGFloat = 11
|
||||
|
||||
static let height: CGFloat = collectionViewHeight + spacing + searchTextFieldHeight + spacing
|
||||
|
||||
weak var delegate: PickServerServerSectionTableHeaderViewDelegate?
|
||||
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<CategoryPickerSection, CategoryPickerItem>?
|
||||
|
||||
static func createCollectionViewLayout() -> UICollectionViewLayout {
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(88), heightDimension: .absolute(PickServerServerSectionTableHeaderView.collectionViewHeight))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: itemSize.widthDimension, heightDimension: itemSize.heightDimension)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.orthogonalScrollingBehavior = .continuous
|
||||
section.contentInsetsReference = .readableContent
|
||||
section.interGroupSpacing = 16
|
||||
|
||||
return UICollectionViewCompositionalLayout(section: section)
|
||||
}
|
||||
|
||||
let collectionView: UICollectionView = {
|
||||
let collectionViewLayout = PickServerServerSectionTableHeaderView.createCollectionViewLayout()
|
||||
let view = ControlContainableCollectionView(
|
||||
frame: CGRect(origin: .zero, size: CGSize(width: 100, height: PickServerServerSectionTableHeaderView.collectionViewHeight)),
|
||||
collectionViewLayout: collectionViewLayout
|
||||
)
|
||||
view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
|
||||
view.backgroundColor = .clear
|
||||
view.alwaysBounceVertical = false
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.layer.masksToBounds = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let searchTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.backgroundColor = Asset.Scene.Onboarding.searchBarBackground.color
|
||||
textField.leftView = {
|
||||
let imageView = UIImageView(
|
||||
image: UIImage(
|
||||
systemName: "magnifyingglass",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
)
|
||||
)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
|
||||
let containerView = UIView()
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(imageView)
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
|
||||
imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
let paddingView = UIView()
|
||||
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(paddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
||||
paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
])
|
||||
return containerView
|
||||
}()
|
||||
textField.leftViewMode = .always
|
||||
textField.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
textField.tintColor = Asset.Colors.Label.primary.color
|
||||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.adjustsFontForContentSizeCategory = true
|
||||
textField.attributedPlaceholder =
|
||||
NSAttributedString(
|
||||
string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular),
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)])
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .done
|
||||
textField.keyboardType = .URL
|
||||
textField.borderStyle = .none
|
||||
|
||||
textField.layer.masksToBounds = true
|
||||
textField.layer.cornerRadius = 10
|
||||
textField.layer.cornerCurve = .continuous
|
||||
|
||||
return textField
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
collectionView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
private func _init() {
|
||||
preservesSuperviewLayoutMargins = true
|
||||
backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.preservesSuperviewLayoutMargins = true
|
||||
addSubview(collectionView)
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.topAnchor.constraint(equalTo: topAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
collectionView.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.collectionViewHeight).priority(.required - 1),
|
||||
])
|
||||
|
||||
searchTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(searchTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
searchTextField.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing),
|
||||
searchTextField.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
searchTextField.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing),
|
||||
searchTextField.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.searchTextFieldHeight).priority(.required - 1),
|
||||
])
|
||||
|
||||
collectionView.delegate = self
|
||||
searchTextField.delegate = self
|
||||
searchTextField.addTarget(self, action: #selector(PickServerServerSectionTableHeaderView.textFieldDidChange(_:)), for: .editingChanged)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
@objc private func textFieldDidChange(_ textField: UITextField) {
|
||||
delegate?.pickServerServerSectionTableHeaderView(self, searchTextDidChange: textField.text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate
|
||||
extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerServerSectionTableHeaderView {
|
||||
|
||||
override func accessibilityElementCount() -> Int {
|
||||
guard let diffableDataSource = diffableDataSource else { return 0 }
|
||||
return diffableDataSource.snapshot().itemIdentifiers.count
|
||||
}
|
||||
|
||||
override func accessibilityElement(at index: Int) -> Any? {
|
||||
guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
extension PickServerServerSectionTableHeaderView: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// NavigationActionView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2021-12-31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class NavigationActionView: UIView {
|
||||
|
||||
static let buttonHeight: CGFloat = 50
|
||||
|
||||
let buttonContainer: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 18
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let backButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton()
|
||||
button.action = .back
|
||||
button.setTitle(L10n.Common.Controls.Actions.back, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let nextButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton()
|
||||
button.action = .next
|
||||
button.setTitle(L10n.Common.Controls.Actions.next, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NavigationActionView {
|
||||
private func _init() {
|
||||
buttonContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.preservesSuperviewLayoutMargins = true
|
||||
addSubview(buttonContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
buttonContainer.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
||||
buttonContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
buttonContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: 8),
|
||||
])
|
||||
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.addArrangedSubview(backButton)
|
||||
nextButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.addArrangedSubview(nextButton)
|
||||
NSLayoutConstraint.activate([
|
||||
backButton.heightAnchor.constraint(equalToConstant: NavigationActionView.buttonHeight).priority(.required - 1),
|
||||
nextButton.heightAnchor.constraint(equalToConstant: NavigationActionView.buttonHeight).priority(.required - 1),
|
||||
nextButton.widthAnchor.constraint(equalTo: backButton.widthAnchor, multiplier: 2).priority(.required - 1),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ extension OnboardingNavigationController {
|
|||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension OnboardingNavigationController {
|
||||
|
@ -47,4 +47,5 @@ extension OnboardingNavigationController {
|
|||
gradientBorderView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ extension OnboardingViewControllerAppearance {
|
|||
static var viewBottomPaddingHeightExtend: CGFloat { return 22 }
|
||||
|
||||
func setupOnboardingAppearance() {
|
||||
view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
|
||||
setupNavigationBarAppearance()
|
||||
|
||||
|
@ -39,31 +39,22 @@ extension OnboardingViewControllerAppearance {
|
|||
// use TransparentBackground so view push / dismiss will be more visual nature
|
||||
// please add opaque background for status bar manually if needs
|
||||
|
||||
switch traitCollection.userInterfaceIdiom {
|
||||
case .pad:
|
||||
if traitCollection.horizontalSizeClass == .regular {
|
||||
// do nothing
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
default:
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
if #available(iOS 15.0, *) {
|
||||
navigationItem.compactScrollEdgeAppearance = barAppearance
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
if #available(iOS 15.0, *) {
|
||||
navigationItem.compactScrollEdgeAppearance = barAppearance
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
|
||||
func setupNavigationBarBackgroundView() {
|
||||
let navigationBarBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color
|
||||
view.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
return view
|
||||
}()
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ final class GradientBorderView: UIView {
|
|||
let gradientLayer = CAGradientLayer()
|
||||
let maskLayer = CAShapeLayer()
|
||||
|
||||
var cornerRadius: CGFloat = 9 {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -48,7 +52,7 @@ extension GradientBorderView {
|
|||
super.layoutSubviews()
|
||||
|
||||
let bezierPath = UIBezierPath(rect: bounds)
|
||||
bezierPath.append(UIBezierPath(roundedRect: bounds.insetBy(dx: 3, dy: 3), cornerRadius: 10))
|
||||
bezierPath.append(UIBezierPath(roundedRect: bounds.insetBy(dx: 3, dy: 3), cornerRadius: cornerRadius))
|
||||
|
||||
maskLayer.fillRule = .evenOdd
|
||||
maskLayer.path = bezierPath.cgPath
|
||||
|
|
|
@ -21,6 +21,12 @@ class PrimaryActionButton: UIButton {
|
|||
|
||||
private var originalButtonTitle: String?
|
||||
|
||||
var action: Action = .next {
|
||||
didSet {
|
||||
setupAppearance(action: action)
|
||||
}
|
||||
}
|
||||
|
||||
var adjustsBackgroundImageWhenUserInterfaceStyleChanges = true
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -35,26 +41,44 @@ class PrimaryActionButton: UIButton {
|
|||
|
||||
}
|
||||
|
||||
extension PrimaryActionButton {
|
||||
|
||||
public enum Action {
|
||||
case back
|
||||
case next
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PrimaryActionButton {
|
||||
|
||||
private func _init() {
|
||||
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||
setTitleColor(.white, for: .normal)
|
||||
setupBackgroundAppearance()
|
||||
setupAppearance(action: action)
|
||||
applyCornerRadius(radius: 10)
|
||||
}
|
||||
|
||||
func setupBackgroundAppearance() {
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color), for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlueDarken20.color), for: .highlighted)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled)
|
||||
func setupAppearance(action: Action) {
|
||||
switch action {
|
||||
case .back:
|
||||
setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationBackButtonBackground.color), for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationBackButtonBackgroundHighlighted.color), for: .highlighted)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled)
|
||||
case .next:
|
||||
setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationNextButtonBackground.color), for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationNextButtonBackgroundHighlighted.color), for: .highlighted)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled)
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
if adjustsBackgroundImageWhenUserInterfaceStyleChanges {
|
||||
setupBackgroundAppearance()
|
||||
setupAppearance(action: action)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
final class TouchBlockingView: UIView {
|
||||
class TouchBlockingView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
|
@ -126,10 +126,10 @@ struct TimelineHeaderView_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let headerView = TimelineHeaderView()
|
||||
headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage
|
||||
headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message
|
||||
return headerView
|
||||
let serverSectionHeaderView = TimelineHeaderView()
|
||||
serverSectionHeaderView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage
|
||||
serverSectionHeaderView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message
|
||||
return serverSectionHeaderView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 400))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue